diff --git a/README.md b/README.md index 6a671f1c..a4270500 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,37 @@ The MCP Inspector supports the following configuration settings. To change them, These settings can be adjusted in real-time through the UI and will persist across sessions. +#### OAuth Callback Configuration + +The MCP Inspector supports configurable OAuth callback URLs through environment variables. This allows you to run the inspector with custom OAuth callback endpoints that OAuth providers can redirect to: + +| Environment Variable | Description | Default | +| ------------------------------------ | --------------------------------------------------- | ----------------------------------------------- | +| `OAUTH_MCP_INSPECTOR_CALLBACK` | OAuth callback URL for standard authentication flow | `{window.location.origin}/oauth/callback` | +| `OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK` | OAuth callback URL for debug authentication flow | `{window.location.origin}/oauth/callback/debug` | + +**How it works:** +1. MCP Inspector automatically starts HTTP servers on the ports specified in your OAuth callback URLs +2. When an OAuth provider redirects to your callback URL, these servers capture the authorization code +3. The servers then redirect the browser to the MCP Inspector UI to complete the OAuth flow + +**Example usage:** + +```bash +export OAUTH_MCP_INSPECTOR_CALLBACK="http://localhost:3060" +export OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK="http://localhost:3061" +npx @modelcontextprotocol/inspector +``` + +**What happens:** +- MCP Inspector starts on `http://localhost:6274` (default) +- OAuth callback server starts on `http://localhost:3060` +- OAuth debug callback server starts on `http://localhost:3061` +- OAuth providers redirect to your configured URLs (3060/3061) +- These servers capture the authorization code and redirect to MCP Inspector (6274) + +**Note:** The OAuth callback URLs are configured at runtime, so no rebuild is required when changing them. + The inspector also supports configuration files to store settings for different MCP servers. This is useful when working with multiple servers or complex configurations: ```bash diff --git a/client/src/App.tsx b/client/src/App.tsx index cff51b4f..cbc04c1c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -234,6 +234,36 @@ const App = () => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); + // Load OAuth configuration from server + useEffect(() => { + const loadOAuthConfig = async () => { + try { + const proxyUrl = getMCPProxyAddress(config); + const authConfig = getMCPProxyAuthToken(config); + const headers: Record = {}; + + if (authConfig.token) { + headers[authConfig.header] = `Bearer ${authConfig.token}`; + } + + const response = await fetch(`${proxyUrl}/config`, { headers }); + if (response.ok) { + const serverConfig = await response.json(); + if (serverConfig.oauthCallback) { + sessionStorage.setItem('OAUTH_MCP_INSPECTOR_CALLBACK', serverConfig.oauthCallback); + } + if (serverConfig.oauthDebugCallback) { + sessionStorage.setItem('OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK', serverConfig.oauthDebugCallback); + } + } + } catch (error) { + // Silently fail - OAuth config is optional + console.debug('Failed to load OAuth configuration:', error); + } + }; + loadOAuthConfig(); + }, [config]); + // Auto-connect to previously saved serverURL after OAuth callback const onOAuthConnect = useCallback( (serverUrl: string) => { diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 3e3516e0..f16c8c8f 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -16,6 +16,11 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } get redirectUrl() { + // Check for runtime configuration + const configuredCallback = sessionStorage.getItem('OAUTH_MCP_INSPECTOR_CALLBACK'); + if (configuredCallback) { + return configuredCallback; + } return window.location.origin + "/oauth/callback"; } @@ -108,6 +113,11 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { // display in debug UI. export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider { get redirectUrl(): string { + // Check for runtime configuration + const configuredDebugCallback = sessionStorage.getItem('OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK'); + if (configuredDebugCallback) { + return configuredDebugCallback; + } return `${window.location.origin}/oauth/callback/debug`; } diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index 418f0007..d8deaeb8 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -142,6 +142,12 @@ export const initializeInspectorConfig = ( // Ensure all config items have the latest labels/descriptions from defaults for (const [key, value] of Object.entries(baseConfig)) { + // Skip if key doesn't exist in DEFAULT_INSPECTOR_CONFIG + if (!(key in DEFAULT_INSPECTOR_CONFIG)) { + delete baseConfig[key as keyof InspectorConfig]; + continue; + } + baseConfig[key as keyof InspectorConfig] = { ...value, label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, diff --git a/package-lock.json b/package-lock.json index 709ce621..f05bc427 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "mcp-inspector": "cli/build/cli.js" }, "devDependencies": { + "@modelcontextprotocol/inspector": "^0.15.0", "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", @@ -1996,6 +1997,33 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/inspector": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector/-/inspector-0.15.0.tgz", + "integrity": "sha512-PN1R7InR48Y6wU8s/vHWc0KOYAjlYQkgCpjUQsNFB078ebdv+empkMI6d1Gg+UIRx8mTrwtbBgv0A6ookGG+0w==", + "dev": true, + "license": "MIT", + "workspaces": [ + "client", + "server", + "cli" + ], + "dependencies": { + "@modelcontextprotocol/inspector-cli": "^0.15.0", + "@modelcontextprotocol/inspector-client": "^0.15.0", + "@modelcontextprotocol/inspector-server": "^0.15.0", + "@modelcontextprotocol/sdk": "^1.13.1", + "concurrently": "^9.0.1", + "open": "^10.1.0", + "shell-quote": "^1.8.2", + "spawn-rx": "^5.1.2", + "ts-node": "^10.9.2", + "zod": "^3.23.8" + }, + "bin": { + "mcp-inspector": "cli/build/cli.js" + } + }, "node_modules/@modelcontextprotocol/inspector-cli": { "resolved": "cli", "link": true diff --git a/package.json b/package.json index b0a2e98e..c5af9ad6 100644 --- a/package.json +++ b/package.json @@ -71,4 +71,4 @@ "engines": { "node": ">=22.7.5" } -} +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index dafd187c..3ba0201d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -20,6 +20,7 @@ import express from "express"; import { findActualExecutable } from "spawn-rx"; import mcpProxy from "./mcpProxy.js"; import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto"; +import { OAuthCallbackManager } from "./oauthCallbacks.js"; const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277"; const SSE_HEADERS_PASSTHROUGH = ["authorization"]; @@ -522,6 +523,8 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { defaultEnvironment, defaultCommand: values.env, defaultArgs: values.args, + oauthCallback: process.env.OAUTH_MCP_INSPECTOR_CALLBACK || null, + oauthDebugCallback: process.env.OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK || null, }); } catch (error) { console.error("Error in /config route:", error); @@ -534,6 +537,11 @@ const PORT = parseInt( 10, ); const HOST = process.env.HOST || "localhost"; +const CLIENT_PORT = process.env.CLIENT_PORT || "6274"; + +// Initialize OAuth callback manager +const mcpInspectorUrl = `http://${HOST}:${CLIENT_PORT}`; +const oauthCallbackManager = new OAuthCallbackManager(mcpInspectorUrl); const server = app.listen(PORT, HOST); server.on("listening", () => { @@ -548,6 +556,9 @@ server.on("listening", () => { `⚠️ WARNING: Authentication is disabled. This is not recommended.`, ); } + + // Start OAuth callback servers + oauthCallbackManager.start(); }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { @@ -557,3 +568,22 @@ server.on("error", (err) => { } process.exit(1); }); + +// Graceful shutdown +process.on("SIGINT", () => { + console.log("\nShutting down MCP Inspector..."); + oauthCallbackManager.stop(); + server.close(() => { + console.log("Server closed"); + process.exit(0); + }); +}); + +process.on("SIGTERM", () => { + console.log("\nShutting down MCP Inspector..."); + oauthCallbackManager.stop(); + server.close(() => { + console.log("Server closed"); + process.exit(0); + }); +}); diff --git a/server/src/oauthCallbacks.ts b/server/src/oauthCallbacks.ts new file mode 100644 index 00000000..10b7d8e6 --- /dev/null +++ b/server/src/oauthCallbacks.ts @@ -0,0 +1,145 @@ +import http from "http"; +import { URL } from "url"; + +interface OAuthCallbackServer { + server: http.Server; + port: number; + url: string; +} + +export class OAuthCallbackManager { + private servers: OAuthCallbackServer[] = []; + private mcpInspectorUrl: string; + + constructor(mcpInspectorUrl: string) { + this.mcpInspectorUrl = mcpInspectorUrl; + } + + private createCallbackServer(callbackUrl: string, isDebug: boolean = false): OAuthCallbackServer | null { + try { + const parsedUrl = new URL(callbackUrl); + const port = parseInt(parsedUrl.port, 10); + + if (!port || isNaN(port)) { + console.warn(`Invalid port in OAuth callback URL: ${callbackUrl}`); + return null; + } + + const server = http.createServer((req, res) => { + const reqUrl = new URL(req.url || "", `http://${req.headers.host}`); + + // Get OAuth parameters from the query string + const code = reqUrl.searchParams.get("code"); + const state = reqUrl.searchParams.get("state"); + const error = reqUrl.searchParams.get("error"); + const errorDescription = reqUrl.searchParams.get("error_description"); + + // Build redirect URL to MCP Inspector + const inspectorPath = isDebug ? "/oauth/callback/debug" : "/oauth/callback"; + const redirectUrl = new URL(inspectorPath, this.mcpInspectorUrl); + + // Forward all query parameters + reqUrl.searchParams.forEach((value, key) => { + redirectUrl.searchParams.set(key, value); + }); + + // Send redirect response + res.writeHead(302, { + "Location": redirectUrl.toString(), + "Content-Type": "text/html", + }); + + const redirectHtml = ` + + + + OAuth Redirect + + + +

OAuth Authentication

+

Redirecting to MCP Inspector...

+

If you are not redirected automatically, click here.

+ + + + `; + + res.end(redirectHtml); + + console.log(`OAuth ${isDebug ? "debug " : ""}callback received on port ${port}`); + if (code) { + console.log(` Authorization code: ${code.substring(0, 10)}...`); + } + if (error) { + console.log(` Error: ${error} - ${errorDescription || "No description"}`); + } + console.log(` Redirecting to: ${redirectUrl.toString()}`); + }); + + return { server, port, url: callbackUrl }; + } catch (error) { + console.error(`Failed to create OAuth callback server for ${callbackUrl}:`, error); + return null; + } + } + + start(): void { + const oauthCallback = process.env.OAUTH_MCP_INSPECTOR_CALLBACK; + const oauthDebugCallback = process.env.OAUTH_MCP_INSPECTOR_DEBUG_CALLBACK; + + if (oauthCallback) { + const callbackServer = this.createCallbackServer(oauthCallback, false); + if (callbackServer) { + callbackServer.server.listen(callbackServer.port, () => { + console.log(`🔗 OAuth callback server listening on ${callbackServer.url}`); + }); + + callbackServer.server.on("error", (err) => { + if ((err as any).code === "EADDRINUSE") { + console.warn(`⚠️ OAuth callback port ${callbackServer.port} is in use`); + } else { + console.error(`OAuth callback server error:`, err); + } + }); + + this.servers.push(callbackServer); + } + } + + if (oauthDebugCallback) { + const debugCallbackServer = this.createCallbackServer(oauthDebugCallback, true); + if (debugCallbackServer) { + debugCallbackServer.server.listen(debugCallbackServer.port, () => { + console.log(`🔗 OAuth debug callback server listening on ${debugCallbackServer.url}`); + }); + + debugCallbackServer.server.on("error", (err) => { + if ((err as any).code === "EADDRINUSE") { + console.warn(`⚠️ OAuth debug callback port ${debugCallbackServer.port} is in use`); + } else { + console.error(`OAuth debug callback server error:`, err); + } + }); + + this.servers.push(debugCallbackServer); + } + } + + if (this.servers.length === 0) { + console.log("No OAuth callback URLs configured"); + } + } + + stop(): void { + this.servers.forEach(({ server, port }) => { + server.close(() => { + console.log(`OAuth callback server on port ${port} stopped`); + }); + }); + this.servers = []; + } +} \ No newline at end of file