UNPKG

mcp-use

Version:

Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents, Clients and Servers with support for ChatGPT Apps, Code Mode, OAuth, Notifications, Sampling, Observability and more.

561 lines (556 loc) 20.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/auth/index.ts var auth_exports = {}; __export(auth_exports, { BrowserOAuthClientProvider: () => BrowserOAuthClientProvider, onMcpAuthorization: () => onMcpAuthorization }); module.exports = __toCommonJS(auth_exports); // src/utils/url-sanitize.ts function sanitizeUrl(raw) { const abort = /* @__PURE__ */ __name(() => { throw new Error(`Invalid url to pass to open(): ${raw}`); }, "abort"); let url; try { url = new URL(raw); } catch (_) { abort(); } if (url.protocol !== "https:" && url.protocol !== "http:") abort(); if (url.hostname !== encodeURIComponent(url.hostname)) abort(); if (url.username) url.username = encodeURIComponent(url.username); if (url.password) url.password = encodeURIComponent(url.password); url.pathname = url.pathname.slice(0, 1) + encodeURIComponent(url.pathname.slice(1)).replace(/%2f/gi, "/"); url.search = url.search.slice(0, 1) + Array.from(url.searchParams.entries()).map(sanitizeParam).join("&"); url.hash = url.hash.slice(0, 1) + encodeURIComponent(url.hash.slice(1)); return url.href; } __name(sanitizeUrl, "sanitizeUrl"); function sanitizeParam([k, v]) { return `${encodeURIComponent(k)}${v.length > 0 ? `=${encodeURIComponent(v)}` : ""}`; } __name(sanitizeParam, "sanitizeParam"); // src/auth/browser-provider.ts async function serializeBody(body) { if (typeof body === "string") return body; if (body instanceof URLSearchParams || body instanceof FormData) { return Object.fromEntries(body.entries()); } if (body instanceof Blob) return await body.text(); return body; } __name(serializeBody, "serializeBody"); var BrowserOAuthClientProvider = class { static { __name(this, "BrowserOAuthClientProvider"); } serverUrl; storageKeyPrefix; serverUrlHash; clientName; clientUri; logoUri; callbackUrl; preventAutoAuth; useRedirectFlow; oauthProxyUrl; connectionUrl; // MCP proxy URL that client connected to originalFetch; onPopupWindow; constructor(serverUrl, options = {}) { this.serverUrl = serverUrl; this.storageKeyPrefix = options.storageKeyPrefix || "mcp:auth"; this.serverUrlHash = this.hashString(serverUrl); this.clientName = options.clientName || "mcp-use"; this.clientUri = options.clientUri || (typeof window !== "undefined" ? window.location.origin : ""); this.logoUri = options.logoUri || "https://mcp-use.com/logo.png"; this.callbackUrl = sanitizeUrl( options.callbackUrl || (typeof window !== "undefined" ? new URL("/oauth/callback", window.location.origin).toString() : "/oauth/callback") ); this.preventAutoAuth = options.preventAutoAuth; this.useRedirectFlow = options.useRedirectFlow; this.oauthProxyUrl = options.oauthProxyUrl; this.connectionUrl = options.connectionUrl; this.onPopupWindow = options.onPopupWindow; } /** * Install fetch interceptor to proxy OAuth requests through the backend */ installFetchInterceptor() { if (!this.oauthProxyUrl) { console.warn( "[BrowserOAuthProvider] No OAuth proxy URL configured, skipping fetch interceptor installation" ); return; } if (!this.originalFetch) { this.originalFetch = window.fetch; } else { console.warn( "[BrowserOAuthProvider] Fetch interceptor already installed" ); return; } const oauthProxyUrl = this.oauthProxyUrl; const connectionUrl = this.connectionUrl; const originalFetch = this.originalFetch; console.log( `[BrowserOAuthProvider] Installing fetch interceptor with proxy: ${oauthProxyUrl}` ); window.fetch = /* @__PURE__ */ __name(async function interceptedFetch(input, init) { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const isOAuthRequest = url.includes("/.well-known/") || url.match(/\/(register|token|authorize)$/); if (!isOAuthRequest) { return await originalFetch(input, init); } try { const urlObj = new URL(url); const proxyUrlObj = new URL(oauthProxyUrl); if (urlObj.origin === proxyUrlObj.origin && (urlObj.pathname.startsWith(proxyUrlObj.pathname) || url.includes("/inspector/api/oauth"))) { return await originalFetch(input, init); } } catch { } try { const isMetadata = url.includes("/.well-known/"); const proxyEndpoint = isMetadata ? `${oauthProxyUrl}/metadata?url=${encodeURIComponent(url)}` : `${oauthProxyUrl}/proxy`; console.log( `[OAuth Proxy] Routing ${isMetadata ? "metadata" : "request"} through: ${proxyEndpoint}` ); if (isMetadata) { const headers = { ...init?.headers ? Object.fromEntries(new Headers(init.headers)) : {} }; if (connectionUrl) { headers["X-Connection-URL"] = connectionUrl; } return await originalFetch(proxyEndpoint, { ...init, method: "GET", headers }); } const body = init?.body ? await serializeBody(init.body) : void 0; const response = await originalFetch(proxyEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, method: init?.method || "POST", headers: init?.headers ? Object.fromEntries(new Headers(init.headers)) : {}, body }) }); const data = await response.json(); return new Response(JSON.stringify(data.body), { status: data.status, statusText: data.statusText, headers: new Headers(data.headers) }); } catch (error) { console.error( "[OAuth Proxy] Request failed, falling back to direct fetch:", error ); return await originalFetch(input, init); } }, "interceptedFetch"); } /** * Restore original fetch after OAuth flow completes */ restoreFetch() { if (this.originalFetch) { console.log("[BrowserOAuthProvider] Restoring original fetch"); window.fetch = this.originalFetch; this.originalFetch = void 0; } } // --- SDK Interface Methods --- get redirectUrl() { return sanitizeUrl(this.callbackUrl); } get clientMetadata() { return { redirect_uris: [this.redirectUrl], token_endpoint_auth_method: "none", // Public client grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], client_name: this.clientName, client_uri: this.clientUri, logo_uri: this.logoUri // scope: 'openid profile email mcp', // Example scopes, adjust as needed }; } async clientInformation() { const key = this.getKey("client_info"); const data = localStorage.getItem(key); if (!data) return void 0; try { return JSON.parse(data); } catch (e) { console.warn( `[${this.storageKeyPrefix}] Failed to parse client information:`, e ); localStorage.removeItem(key); return void 0; } } // NOTE: The SDK's auth() function uses this if dynamic registration is needed. // Ensure your OAuthClientInformationFull matches the expected structure if DCR is used. async saveClientInformation(clientInformation) { const key = this.getKey("client_info"); localStorage.setItem(key, JSON.stringify(clientInformation)); } async tokens() { const key = this.getKey("tokens"); const data = localStorage.getItem(key); if (!data) return void 0; try { return JSON.parse(data); } catch (e) { console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e); localStorage.removeItem(key); return void 0; } } async saveTokens(tokens) { const key = this.getKey("tokens"); localStorage.setItem(key, JSON.stringify(tokens)); localStorage.removeItem(this.getKey("code_verifier")); localStorage.removeItem(this.getKey("last_auth_url")); } async saveCodeVerifier(codeVerifier) { const key = this.getKey("code_verifier"); localStorage.setItem(key, codeVerifier); } async codeVerifier() { const key = this.getKey("code_verifier"); const verifier = localStorage.getItem(key); if (!verifier) { throw new Error( `[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.` ); } return verifier; } /** * Generates and stores the authorization URL with state, without opening a popup. * Used when preventAutoAuth is enabled to provide the URL for manual navigation. * @param authorizationUrl The fully constructed authorization URL from the SDK. * @returns The full authorization URL with state parameter. */ async prepareAuthorizationUrl(authorizationUrl) { const state = globalThis.crypto.randomUUID(); const stateKey = `${this.storageKeyPrefix}:state_${state}`; const stateData = { serverUrlHash: this.serverUrlHash, expiry: Date.now() + 1e3 * 60 * 10, // State expires in 10 minutes // Store provider options needed to reconstruct on callback providerOptions: { serverUrl: this.serverUrl, storageKeyPrefix: this.storageKeyPrefix, clientName: this.clientName, clientUri: this.clientUri, callbackUrl: this.callbackUrl }, // Store flow type so callback knows how to handle the response flowType: this.useRedirectFlow ? "redirect" : "popup", // Store current URL for redirect flow so we can return to it returnUrl: this.useRedirectFlow && typeof window !== "undefined" ? window.location.href : void 0 }; localStorage.setItem(stateKey, JSON.stringify(stateData)); authorizationUrl.searchParams.set("state", state); const authUrlString = authorizationUrl.toString(); const sanitizedAuthUrl = sanitizeUrl(authUrlString); localStorage.setItem(this.getKey("last_auth_url"), sanitizedAuthUrl); return sanitizedAuthUrl; } /** * Redirects the user agent to the authorization URL, storing necessary state. * This now adheres to the SDK's void return type expectation for the interface. * @param authorizationUrl The fully constructed authorization URL from the SDK. */ async redirectToAuthorization(authorizationUrl) { const sanitizedAuthUrl = await this.prepareAuthorizationUrl(authorizationUrl); if (this.preventAutoAuth) { console.info( `[${this.storageKeyPrefix}] Auto-auth prevented. Authorization URL stored for manual trigger.` ); return; } if (this.useRedirectFlow) { console.info( `[${this.storageKeyPrefix}] Redirecting to authorization URL (full-page redirect).` ); window.location.href = sanitizedAuthUrl; return; } const popupFeatures = "width=600,height=700,resizable=yes,scrollbars=yes,status=yes"; try { const popup = window.open( sanitizedAuthUrl, `mcp_auth_${this.serverUrlHash}`, popupFeatures ); if (this.onPopupWindow) { this.onPopupWindow(sanitizedAuthUrl, popupFeatures, popup); } if (!popup || popup.closed || typeof popup.closed === "undefined") { console.warn( `[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.` ); } else { popup.focus(); console.info( `[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.` ); } } catch (e) { console.error( `[${this.storageKeyPrefix}] Error opening popup window:`, e ); } } // --- Helper Methods --- /** * Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback. */ getLastAttemptedAuthUrl() { const storedUrl = localStorage.getItem(this.getKey("last_auth_url")); return storedUrl ? sanitizeUrl(storedUrl) : null; } clearStorage() { const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`; const statePattern = `${this.storageKeyPrefix}:state_`; const keysToRemove = []; let count = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key) continue; if (key.startsWith(prefixPattern)) { keysToRemove.push(key); } else if (key.startsWith(statePattern)) { try { const item = localStorage.getItem(key); if (item) { const state = JSON.parse(item); if (state.serverUrlHash === this.serverUrlHash) { keysToRemove.push(key); } } } catch (e) { console.warn( `[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e ); } } } const uniqueKeysToRemove = [...new Set(keysToRemove)]; uniqueKeysToRemove.forEach((key) => { localStorage.removeItem(key); count++; }); return count; } hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash).toString(16); } getKey(keySuffix) { return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`; } }; // src/auth/callback.ts var import_auth = require("@modelcontextprotocol/sdk/client/auth.js"); async function onMcpAuthorization() { const queryParams = new URLSearchParams(window.location.search); const code = queryParams.get("code"); const state = queryParams.get("state"); const error = queryParams.get("error"); const errorDescription = queryParams.get("error_description"); const logPrefix = "[mcp-callback]"; console.log(`${logPrefix} Handling callback...`, { code, state, error, errorDescription }); let provider = null; let storedStateData = null; const stateKey = state ? `mcp:auth:state_${state}` : null; try { if (error) { throw new Error( `OAuth error: ${error} - ${errorDescription || "No description provided."}` ); } if (!code) { throw new Error( "Authorization code not found in callback query parameters." ); } if (!state || !stateKey) { throw new Error( "State parameter not found or invalid in callback query parameters." ); } const storedStateJSON = localStorage.getItem(stateKey); if (!storedStateJSON) { throw new Error( `Invalid or expired state parameter "${state}". No matching state found in storage.` ); } try { storedStateData = JSON.parse(storedStateJSON); } catch (e) { throw new Error("Failed to parse stored OAuth state."); } if (!storedStateData.expiry || storedStateData.expiry < Date.now()) { localStorage.removeItem(stateKey); throw new Error( "OAuth state has expired. Please try initiating authentication again." ); } if (!storedStateData.providerOptions) { throw new Error("Stored state is missing required provider options."); } const { serverUrl, ...providerOptions } = storedStateData.providerOptions; console.log( `${logPrefix} Re-instantiating provider for server: ${serverUrl}` ); provider = new BrowserOAuthClientProvider(serverUrl, providerOptions); console.log(`${logPrefix} Calling SDK auth() to exchange code...`); const baseUrl = new URL(serverUrl).origin; const authResult = await (0, import_auth.auth)(provider, { serverUrl: baseUrl, authorizationCode: code }); if (authResult === "AUTHORIZED") { console.log(`${logPrefix} Authorization successful via SDK auth().`); const isRedirectFlow = storedStateData.flowType === "redirect"; if (isRedirectFlow && storedStateData.returnUrl) { console.log( `${logPrefix} Redirect flow complete. Returning to: ${storedStateData.returnUrl}` ); localStorage.removeItem(stateKey); window.location.href = storedStateData.returnUrl; } else if (window.opener && !window.opener.closed) { console.log(`${logPrefix} Popup flow complete. Notifying opener...`); window.opener.postMessage( { type: "mcp_auth_callback", success: true }, window.location.origin ); localStorage.removeItem(stateKey); window.close(); } else { console.warn( `${logPrefix} No opener window or return URL detected. Redirecting to root.` ); localStorage.removeItem(stateKey); const pathParts = window.location.pathname.split("/").filter(Boolean); const basePath = pathParts.length > 0 && pathParts[pathParts.length - 1] === "callback" ? "/" + pathParts.slice(0, -2).join("/") : "/"; window.location.href = basePath || "/"; } } else { console.warn( `${logPrefix} SDK auth() returned unexpected status: ${authResult}` ); throw new Error( `Unexpected result from authentication library: ${authResult}` ); } } catch (err) { console.error(`${logPrefix} Error during OAuth callback handling:`, err); const errorMessage = err instanceof Error ? err.message : String(err); if (window.opener && !window.opener.closed) { window.opener.postMessage( { type: "mcp_auth_callback", success: false, error: errorMessage }, window.location.origin ); } try { document.body.innerHTML = ""; const container = document.createElement("div"); container.style.fontFamily = "sans-serif"; container.style.padding = "20px"; const heading = document.createElement("h1"); heading.textContent = "Authentication Error"; container.appendChild(heading); const errorPara = document.createElement("p"); errorPara.style.color = "red"; errorPara.style.backgroundColor = "#ffebeb"; errorPara.style.border = "1px solid red"; errorPara.style.padding = "10px"; errorPara.style.borderRadius = "4px"; errorPara.textContent = errorMessage; container.appendChild(errorPara); const closePara = document.createElement("p"); closePara.textContent = "You can close this window or "; const closeLink = document.createElement("a"); closeLink.href = "#"; closeLink.textContent = "click here to close"; closeLink.onclick = (e) => { e.preventDefault(); window.close(); return false; }; closePara.appendChild(closeLink); closePara.appendChild(document.createTextNode(".")); container.appendChild(closePara); if (err instanceof Error && err.stack) { const stackPre = document.createElement("pre"); stackPre.style.fontSize = "0.8em"; stackPre.style.color = "#555"; stackPre.style.marginTop = "20px"; stackPre.style.whiteSpace = "pre-wrap"; stackPre.textContent = err.stack; container.appendChild(stackPre); } document.body.appendChild(container); } catch (displayError) { console.error( `${logPrefix} Could not display error in callback window:`, displayError ); } if (stateKey) { localStorage.removeItem(stateKey); } if (provider) { localStorage.removeItem(provider.getKey("code_verifier")); localStorage.removeItem(provider.getKey("last_auth_url")); } } } __name(onMcpAuthorization, "onMcpAuthorization");