UNPKG

mcp-use

Version:

Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents and Clients + MCP Servers with support for MCP-UI.

700 lines (691 loc) 20 kB
import { createReadableStreamFromGenerator, streamEventsToAISDK, streamEventsToAISDKWithTools } from "./chunk-EW4MJSHA.js"; import { useMcp, useWidget, useWidgetProps, useWidgetState, useWidgetTheme } from "./chunk-X2HJQBDI.js"; import { BaseConnector, BaseMCPClient, BrowserOAuthClientProvider, ConnectionManager, HttpConnector, MCPSession, WebSocketConnector, onMcpAuthorization } from "./chunk-37MPZ3D6.js"; import { AcquireActiveMCPServerTool, AddMCPServerFromConfigTool, BaseAdapter, ConnectMCPServerTool, LangChainAdapter, ListMCPServersTool, MCPAgent, ObservabilityManager, ReleaseMCPServerConnectionTool, RemoteAgent, ServerManager, Telemetry, setTelemetrySource } from "./chunk-73SH52J2.js"; import "./chunk-YURRUCIM.js"; import { Logger, logger } from "./chunk-34R6SIER.js"; import { __name } from "./chunk-3GQAWCBQ.js"; // src/client.ts import fs from "fs"; import path from "path"; // src/config.ts import { readFileSync } from "fs"; // src/connectors/stdio.ts import process2 from "process"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; // src/task_managers/stdio.ts import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; var StdioConnectionManager = class extends ConnectionManager { static { __name(this, "StdioConnectionManager"); } serverParams; errlog; _transport = null; /** * Create a new stdio connection manager. * * @param serverParams Parameters for the stdio server process. * @param errlog Stream to which the server's stderr should be piped. * Defaults to `process.stderr`. */ constructor(serverParams, errlog = process.stderr) { super(); this.serverParams = serverParams; this.errlog = errlog; } /** * Establish the stdio connection by spawning the server process and starting * the SDK's transport. Returns the live `StdioClientTransport` instance. */ async establishConnection() { this._transport = new StdioClientTransport(this.serverParams); if (this._transport.stderr && typeof this._transport.stderr.pipe === "function") { this._transport.stderr.pipe( this.errlog ); } logger.debug(`${this.constructor.name} connected successfully`); return this._transport; } /** * Close the stdio connection, making sure the transport cleans up the child * process and associated resources. */ async closeConnection(_connection) { if (this._transport) { try { await this._transport.close(); } catch (e) { logger.warn(`Error closing stdio transport: ${e}`); } finally { this._transport = null; } } } }; // src/connectors/stdio.ts var StdioConnector = class extends BaseConnector { static { __name(this, "StdioConnector"); } command; args; env; errlog; clientInfo; constructor({ command = "npx", args = [], env, errlog = process2.stderr, ...rest } = {}) { super(rest); this.command = command; this.args = args; this.env = env; this.errlog = errlog; this.clientInfo = rest.clientInfo ?? { name: "stdio-connector", version: "1.0.0" }; } /** Establish connection to the MCP implementation. */ async connect() { if (this.connected) { logger.debug("Already connected to MCP implementation"); return; } logger.debug(`Connecting to MCP implementation via stdio: ${this.command}`); try { let mergedEnv; if (this.env) { mergedEnv = {}; for (const [key, value] of Object.entries(process2.env)) { if (value !== void 0) { mergedEnv[key] = value; } } Object.assign(mergedEnv, this.env); } const serverParams = { command: this.command, args: this.args, env: mergedEnv }; this.connectionManager = new StdioConnectionManager( serverParams, this.errlog ); const transport = await this.connectionManager.start(); this.client = new Client(this.clientInfo, this.opts.clientOptions); await this.client.connect(transport); this.connected = true; logger.debug( `Successfully connected to MCP implementation: ${this.command}` ); } catch (err) { logger.error(`Failed to connect to MCP implementation: ${err}`); await this.cleanupResources(); throw err; } } get publicIdentifier() { return { type: "stdio", "command&args": `${this.command} ${this.args.join(" ")}` }; } }; // src/config.ts function loadConfigFile(filepath) { const raw = readFileSync(filepath, "utf-8"); return JSON.parse(raw); } __name(loadConfigFile, "loadConfigFile"); function createConnectorFromConfig(serverConfig) { if ("command" in serverConfig && "args" in serverConfig) { return new StdioConnector({ command: serverConfig.command, args: serverConfig.args, env: serverConfig.env }); } if ("url" in serverConfig) { const transport = serverConfig.transport || "http"; return new HttpConnector(serverConfig.url, { headers: serverConfig.headers, authToken: serverConfig.auth_token || serverConfig.authToken, // Only force SSE if explicitly requested preferSse: serverConfig.preferSse || transport === "sse" }); } if ("ws_url" in serverConfig) { return new WebSocketConnector(serverConfig.ws_url, { headers: serverConfig.headers, authToken: serverConfig.auth_token }); } throw new Error("Cannot determine connector type from config"); } __name(createConnectorFromConfig, "createConnectorFromConfig"); // src/client.ts var MCPClient = class _MCPClient extends BaseMCPClient { static { __name(this, "MCPClient"); } constructor(config) { if (config) { if (typeof config === "string") { super(loadConfigFile(config)); } else { super(config); } } else { super(); } } static fromDict(cfg) { return new _MCPClient(cfg); } static fromConfigFile(path2) { return new _MCPClient(loadConfigFile(path2)); } /** * Save configuration to a file (Node.js only) */ saveConfig(filepath) { const dir = path.dirname(filepath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filepath, JSON.stringify(this.config, null, 2), "utf-8"); } /** * Create a connector from server configuration (Node.js version) * Supports all connector types including StdioConnector */ createConnectorFromConfig(serverConfig) { return createConnectorFromConfig(serverConfig); } }; // src/oauth-helper.ts var OAuthHelper = class { static { __name(this, "OAuthHelper"); } config; discovery; state; clientRegistration; constructor(config) { this.config = config; this.state = { isRequired: false, isAuthenticated: false, isAuthenticating: false, isCompletingOAuth: false, authError: null, oauthTokens: null }; } /** * Get current OAuth state */ getState() { return { ...this.state }; } /** * Check if a server requires authentication by pinging the URL */ async checkAuthRequired(serverUrl) { console.log("\u{1F50D} [OAuthHelper] Checking auth requirement for:", serverUrl); try { const response = await fetch(serverUrl, { method: "GET", headers: { Accept: "text/event-stream", "Cache-Control": "no-cache" }, redirect: "manual", signal: AbortSignal.timeout(1e4) // 10 second timeout }); console.log("\u{1F50D} [OAuthHelper] Auth check response:", { status: response.status, statusText: response.statusText, url: serverUrl }); if (response.status === 401 || response.status === 403 || response.status === 400) { console.log("\u{1F510} [OAuthHelper] Authentication required for:", serverUrl); return true; } console.log( "\u2705 [OAuthHelper] No authentication required for:", serverUrl ); return false; } catch (error) { console.warn( "\u26A0\uFE0F [OAuthHelper] Could not check auth requirement for:", serverUrl, error ); if (error.name === "TypeError" && (error.message?.includes("CORS") || error.message?.includes("Failed to fetch"))) { console.log( "\u{1F50D} [OAuthHelper] CORS blocked direct check, using heuristics for:", serverUrl ); return this.checkAuthByHeuristics(serverUrl); } if (error.name === "AbortError") { console.log( "\u23F0 [OAuthHelper] Request timeout, assuming no auth required for:", serverUrl ); return false; } return this.checkAuthByHeuristics(serverUrl); } } /** * Fallback heuristics for determining auth requirements when direct checking fails */ checkAuthByHeuristics(serverUrl) { console.log( "\u{1F50D} [OAuthHelper] Using heuristics to determine auth for:", serverUrl ); const authRequiredPatterns = [ /api\.githubcopilot\.com/i, // GitHub Copilot /api\.github\.com/i, // GitHub API /.*\.googleapis\.com/i, // Google APIs /api\.openai\.com/i, // OpenAI /api\.anthropic\.com/i, // Anthropic /.*\.atlassian\.net/i, // Atlassian (Jira, Confluence) /.*\.slack\.com/i, // Slack /api\.notion\.com/i, // Notion /api\.linear\.app/i // Linear ]; const noAuthPatterns = [ /localhost/i, // Local development /127\.0\.0\.1/, // Local development /\.local/i, // Local development /mcp\..*\.com/i // Generic MCP server pattern (often public) ]; for (const pattern of noAuthPatterns) { if (pattern.test(serverUrl)) { console.log( "\u2705 [OAuthHelper] Heuristic: No auth required (matches no-auth pattern):", serverUrl ); return false; } } for (const pattern of authRequiredPatterns) { if (pattern.test(serverUrl)) { console.log( "\u{1F510} [OAuthHelper] Heuristic: Auth required (matches auth pattern):", serverUrl ); return true; } } console.log( "\u2753 [OAuthHelper] Heuristic: Unknown pattern, assuming no auth required:", serverUrl ); return false; } /** * Discover OAuth configuration from a server */ async discoverOAuthConfig(serverUrl) { try { const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server`; console.log( "\u{1F50D} [OAuthHelper] Attempting OAuth discovery at:", discoveryUrl ); const response = await fetch(discoveryUrl); if (!response.ok) { console.error("\u274C [OAuthHelper] OAuth discovery failed:", { status: response.status, statusText: response.statusText, url: discoveryUrl }); throw new Error( `OAuth discovery failed: ${response.status} ${response.statusText}` ); } this.discovery = await response.json(); console.log("\u2705 [OAuthHelper] OAuth discovery successful:", { authorization_endpoint: this.discovery?.authorization_endpoint, token_endpoint: this.discovery?.token_endpoint, registration_endpoint: this.discovery?.registration_endpoint }); return this.discovery; } catch (error) { console.error("\u274C [OAuthHelper] OAuth discovery error:", error); throw new Error(`Failed to discover OAuth configuration: ${error}`); } } /** * Register a new OAuth client dynamically */ async registerClient(_serverUrl) { if (!this.discovery) { throw new Error( "OAuth discovery not performed. Call discoverOAuthConfig first." ); } if (!this.discovery.registration_endpoint) { throw new Error("Server does not support dynamic client registration"); } try { const registrationData = { client_name: this.config.clientName || "MCP Use Example", redirect_uris: [this.config.redirectUri], grant_types: ["authorization_code"], response_types: ["code"], token_endpoint_auth_method: "none", // Use public client (no secret) scope: this.config.scope || "read write" }; console.log("\u{1F510} [OAuthHelper] Registering OAuth client dynamically:", { registration_endpoint: this.discovery.registration_endpoint, client_name: registrationData.client_name, redirect_uri: this.config.redirectUri }); const response = await fetch(this.discovery.registration_endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(registrationData) }); if (!response.ok) { const errorText = await response.text(); throw new Error( `Client registration failed: ${response.status} ${response.statusText} - ${errorText}` ); } this.clientRegistration = await response.json(); console.log("\u2705 [OAuthHelper] Client registered successfully:", { client_id: this.clientRegistration?.client_id, client_secret: this.clientRegistration?.client_secret ? "***" : "none" }); return this.clientRegistration; } catch (error) { console.error("\u274C [OAuthHelper] Client registration failed:", error); throw new Error(`Failed to register OAuth client: ${error}`); } } /** * Generate authorization URL for OAuth flow */ generateAuthUrl(serverUrl, additionalParams) { if (!this.discovery) { throw new Error( "OAuth discovery not performed. Call discoverOAuthConfig first." ); } if (!this.clientRegistration) { throw new Error("Client not registered. Call registerClient first."); } const params = new URLSearchParams({ client_id: this.clientRegistration.client_id, redirect_uri: this.config.redirectUri, response_type: "code", scope: this.config.scope || "read", state: this.config.state || this.generateState(), ...additionalParams }); return `${this.discovery.authorization_endpoint}?${params.toString()}`; } /** * Exchange authorization code for access token */ async exchangeCodeForToken(serverUrl, code, codeVerifier) { if (!this.discovery) { throw new Error( "OAuth discovery not performed. Call discoverOAuthConfig first." ); } if (!this.clientRegistration) { throw new Error("Client not registered. Call registerClient first."); } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: this.clientRegistration.client_id, code, redirect_uri: this.config.redirectUri }); if (codeVerifier) { body.append("code_verifier", codeVerifier); } const response = await fetch(this.discovery.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString() }); if (!response.ok) { const error = await response.text(); throw new Error( `Token exchange failed: ${response.status} ${response.statusText} - ${error}` ); } return await response.json(); } /** * Handle OAuth callback and extract authorization code */ handleCallback() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); const state = urlParams.get("state"); if (!code || !state) { return null; } const url = new URL(window.location.href); url.searchParams.delete("code"); url.searchParams.delete("state"); window.history.replaceState({}, "", url.toString()); return { code, state }; } /** * Start OAuth flow by opening popup window (similar to your implementation) */ async startOAuthFlow(serverUrl) { this.setState({ isAuthenticating: true, authError: null }); try { await this.discoverOAuthConfig(serverUrl); await this.registerClient(serverUrl); const authUrl = this.generateAuthUrl(serverUrl); const authWindow = window.open( authUrl, "mcp-oauth", "width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes" ); if (!authWindow) { throw new Error( "Failed to open authentication window. Please allow popups for this site and try again." ); } console.log("\u2705 [OAuthHelper] OAuth popup opened successfully"); } catch (error) { console.error("\u274C [OAuthHelper] Failed to start OAuth flow:", error); this.setState({ isAuthenticating: false, authError: error instanceof Error ? error.message : "Failed to start authentication" }); throw error; } } /** * Complete OAuth flow by exchanging code for token */ async completeOAuthFlow(serverUrl, code) { this.setState({ isCompletingOAuth: true, authError: null }); try { const tokenResponse = await this.exchangeCodeForToken(serverUrl, code); this.setState({ isAuthenticating: false, isAuthenticated: true, isCompletingOAuth: false, authError: null, oauthTokens: tokenResponse }); console.log("\u2705 [OAuthHelper] OAuth flow completed successfully"); return tokenResponse; } catch (error) { console.error("\u274C [OAuthHelper] Failed to complete OAuth flow:", error); this.setState({ isAuthenticating: false, isCompletingOAuth: false, authError: error instanceof Error ? error.message : "Failed to complete authentication" }); throw error; } } /** * Reset authentication state */ resetAuth() { this.setState({ isRequired: false, isAuthenticated: false, isAuthenticating: false, isCompletingOAuth: false, authError: null, oauthTokens: null }); } /** * Set OAuth state (internal method) */ setState(newState) { this.state = { ...this.state, ...newState }; } /** * Generate a random state parameter for CSRF protection */ generateState() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } }; var LINEAR_OAUTH_CONFIG = { // No clientId needed - will use dynamic client registration redirectUri: typeof window !== "undefined" ? window.location.origin + window.location.pathname : "http://localhost:5173", scope: "read write", clientName: "MCP Use Example" }; function createOAuthMCPConfig(serverUrl, accessToken) { return { mcpServers: { linear: { url: serverUrl, authToken: accessToken, transport: "sse" } } }; } __name(createOAuthMCPConfig, "createOAuthMCPConfig"); export { AcquireActiveMCPServerTool, AddMCPServerFromConfigTool, BaseAdapter, BaseConnector, BrowserOAuthClientProvider, ConnectMCPServerTool, HttpConnector, LINEAR_OAUTH_CONFIG, LangChainAdapter, ListMCPServersTool, Logger, MCPAgent, MCPClient, MCPSession, OAuthHelper, ObservabilityManager, ReleaseMCPServerConnectionTool, RemoteAgent, ServerManager, StdioConnector, Telemetry, WebSocketConnector, createOAuthMCPConfig, createReadableStreamFromGenerator, loadConfigFile, logger, onMcpAuthorization, setTelemetrySource, streamEventsToAISDK, streamEventsToAISDKWithTools, useMcp, useWidget, useWidgetProps, useWidgetState, useWidgetTheme };