diff --git a/client/package.json b/client/package.json index 888495c56..69021c86a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.3.0", + "version": "0.4.0", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -21,7 +21,7 @@ "preview": "vite preview" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.3", + "@modelcontextprotocol/sdk": "^1.4.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.1.2", @@ -30,6 +30,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.447.0", + "pkce-challenge": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-toastify": "^10.0.6", diff --git a/client/src/App.tsx b/client/src/App.tsx index f3791b2d0..246e035f4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,3 @@ -import { useDraggablePane } from "./lib/hooks/useDraggablePane"; -import { useConnection } from "./lib/hooks/useConnection"; import { ClientRequest, CompatibilityCallToolResult, @@ -10,15 +8,17 @@ import { ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, - ReadResourceResultSchema, ListToolsResultSchema, + ReadResourceResultSchema, Resource, ResourceTemplate, Root, ServerNotification, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { useEffect, useRef, useState } from "react"; +import React, { Suspense, useEffect, useRef, useState } from "react"; +import { useConnection } from "./lib/hooks/useConnection"; +import { useDraggablePane } from "./lib/hooks/useDraggablePane"; import { StdErrNotification } from "./lib/notificationTypes"; @@ -32,6 +32,7 @@ import { MessageSquare, } from "lucide-react"; +import { toast } from "react-toastify"; import { z } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; @@ -49,6 +50,17 @@ const PROXY_PORT = params.get("proxyPort") ?? "3000"; const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`; const App = () => { + // Handle OAuth callback route + if (window.location.pathname === "/oauth/callback") { + const OAuthCallback = React.lazy( + () => import("./components/OAuthCallback"), + ); + return ( + Loading...}> + + + ); + } const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< ResourceTemplate[] @@ -71,8 +83,14 @@ const App = () => { return localStorage.getItem("lastArgs") || ""; }); - const [sseUrl, setSseUrl] = useState("http://localhost:3001/sse"); - const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); + const [sseUrl, setSseUrl] = useState(() => { + return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse"; + }); + const [transportType, setTransportType] = useState<"stdio" | "sse">(() => { + return ( + (localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio" + ); + }); const [notifications, setNotifications] = useState([]); const [stdErrNotifications, setStdErrNotifications] = useState< StdErrNotification[] @@ -190,6 +208,31 @@ const App = () => { localStorage.setItem("lastArgs", args); }, [args]); + useEffect(() => { + localStorage.setItem("lastSseUrl", sseUrl); + }, [sseUrl]); + + useEffect(() => { + localStorage.setItem("lastTransportType", transportType); + }, [transportType]); + + // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) + useEffect(() => { + const serverUrl = params.get("serverUrl"); + if (serverUrl) { + setSseUrl(serverUrl); + setTransportType("sse"); + // Remove serverUrl from URL without reloading the page + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete("serverUrl"); + window.history.replaceState({}, "", newUrl.toString()); + // Show success toast for OAuth + toast.success("Successfully authenticated with OAuth"); + // Connect to the server + connectMcpServer(); + } + }, []); + useEffect(() => { fetch(`${PROXY_SERVER_URL}/config`) .then((response) => response.json()) diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx new file mode 100644 index 000000000..a7439df94 --- /dev/null +++ b/client/src/components/OAuthCallback.tsx @@ -0,0 +1,48 @@ +import { useEffect, useRef } from "react"; +import { handleOAuthCallback } from "../lib/auth"; +import { SESSION_KEYS } from "../lib/constants"; + +const OAuthCallback = () => { + const hasProcessedRef = useRef(false); + + useEffect(() => { + const handleCallback = async () => { + // Skip if we've already processed this callback + if (hasProcessedRef.current) { + return; + } + hasProcessedRef.current = true; + + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL); + + if (!code || !serverUrl) { + console.error("Missing code or server URL"); + window.location.href = "/"; + return; + } + + try { + const accessToken = await handleOAuthCallback(serverUrl, code); + // Store the access token for future use + sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken); + // Redirect back to the main app with server URL to trigger auto-connect + window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`; + } catch (error) { + console.error("OAuth callback error:", error); + window.location.href = "/"; + } + }; + + void handleCallback(); + }, []); + + return ( +
+

Processing OAuth callback...

+
+ ); +}; + +export default OAuthCallback; diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts new file mode 100644 index 000000000..0417731d9 --- /dev/null +++ b/client/src/lib/auth.ts @@ -0,0 +1,93 @@ +import pkceChallenge from "pkce-challenge"; +import { SESSION_KEYS } from "./constants"; + +export interface OAuthMetadata { + authorization_endpoint: string; + token_endpoint: string; +} + +export async function discoverOAuthMetadata( + serverUrl: string, +): Promise { + try { + const url = new URL("/.well-known/oauth-authorization-server", serverUrl); + const response = await fetch(url.toString()); + + if (response.ok) { + const metadata = await response.json(); + return { + authorization_endpoint: metadata.authorization_endpoint, + token_endpoint: metadata.token_endpoint, + }; + } + } catch (error) { + console.warn("OAuth metadata discovery failed:", error); + } + + // Fall back to default endpoints + const baseUrl = new URL(serverUrl); + return { + authorization_endpoint: new URL("/authorize", baseUrl).toString(), + token_endpoint: new URL("/token", baseUrl).toString(), + }; +} + +export async function startOAuthFlow(serverUrl: string): Promise { + // Generate PKCE challenge + const challenge = await pkceChallenge(); + const codeVerifier = challenge.code_verifier; + const codeChallenge = challenge.code_challenge; + + // Store code verifier for later use + sessionStorage.setItem(SESSION_KEYS.CODE_VERIFIER, codeVerifier); + + // Discover OAuth endpoints + const metadata = await discoverOAuthMetadata(serverUrl); + + // Build authorization URL + const authUrl = new URL(metadata.authorization_endpoint); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("code_challenge", codeChallenge); + authUrl.searchParams.set("code_challenge_method", "S256"); + authUrl.searchParams.set( + "redirect_uri", + window.location.origin + "/oauth/callback", + ); + + return authUrl.toString(); +} + +export async function handleOAuthCallback( + serverUrl: string, + code: string, +): Promise { + // Get stored code verifier + const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER); + if (!codeVerifier) { + throw new Error("No code verifier found"); + } + + // Discover OAuth endpoints + const metadata = await discoverOAuthMetadata(serverUrl); + + // Exchange code for tokens + const response = await fetch(metadata.token_endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "authorization_code", + code, + code_verifier: codeVerifier, + redirect_uri: window.location.origin + "/oauth/callback", + }), + }); + + if (!response.ok) { + throw new Error("Token exchange failed"); + } + + const data = await response.json(); + return data.access_token; +} diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts new file mode 100644 index 000000000..e302b52fe --- /dev/null +++ b/client/src/lib/constants.ts @@ -0,0 +1,6 @@ +// OAuth-related session storage keys +export const SESSION_KEYS = { + CODE_VERIFIER: "mcp_code_verifier", + SERVER_URL: "mcp_server_url", + ACCESS_TOKEN: "mcp_access_token", +} as const; diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index ef937d173..de2d29ecc 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -1,5 +1,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + SSEClientTransport, + SseError, +} from "@modelcontextprotocol/sdk/client/sse.js"; import { ClientNotification, ClientRequest, @@ -12,8 +15,10 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { useState } from "react"; import { toast } from "react-toastify"; -import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { z } from "zod"; +import { startOAuthFlow } from "../auth"; +import { SESSION_KEYS } from "../constants"; +import { Notification, StdErrNotificationSchema } from "../notificationTypes"; const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; @@ -144,7 +149,20 @@ export function useConnection({ backendUrl.searchParams.append("url", sseUrl); } - const clientTransport = new SSEClientTransport(backendUrl); + const headers: HeadersInit = {}; + const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN); + if (accessToken) { + headers["Authorization"] = `Bearer ${accessToken}`; + } + + const clientTransport = new SSEClientTransport(backendUrl, { + eventSourceInit: { + fetch: (url, init) => fetch(url, { ...init, headers }), + }, + requestInit: { + headers, + }, + }); if (onNotification) { client.setNotificationHandler( @@ -160,7 +178,20 @@ export function useConnection({ ); } - await client.connect(clientTransport); + try { + await client.connect(clientTransport); + } catch (error) { + console.error("Failed to connect to MCP server:", error); + if (error instanceof SseError && error.code === 401) { + // Store the server URL for the callback handler + sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl); + const redirectUrl = await startOAuthFlow(sseUrl); + window.location.href = redirectUrl; + return; + } + + throw error; + } const capabilities = client.getServerCapabilities(); setServerCapabilities(capabilities ?? null); diff --git a/client/vite.config.ts b/client/vite.config.ts index dd3bd0164..b3d0f4577 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,10 +1,11 @@ +import react from "@vitejs/plugin-react"; import path from "path"; import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: {}, resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/package-lock.json b/package-lock.json index c5e4d8825..0260f1798 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "workspaces": [ "client", @@ -31,10 +31,10 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.3", + "@modelcontextprotocol/sdk": "^1.4.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.1.2", @@ -43,6 +43,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.447.0", + "pkce-challenge": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-toastify": "^10.0.6", @@ -1205,13 +1206,31 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.3.tgz", - "integrity": "sha512-2as3cX/VJ0YBHGmdv3GFyTpoM8q2gqE98zh3Vf1NwnsSY0h3mvoO07MUzfygCKkWsFjcZm4otIiqD6Xh7kiSBQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.4.1.tgz", + "integrity": "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", + "eventsource": "^3.0.2", "raw-body": "^3.0.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz", + "integrity": "sha512-YolzkJNxsTL3tCJMWFxpxtG2sCjbZ4LQUBUrkdaJK0ub0p6lmJt+2+1SwhKjLc652lpH9L/79Ptez972H9tphw==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -2257,13 +2276,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eventsource": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", - "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -3688,13 +3700,13 @@ "node": ">= 0.6" } }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/express": { @@ -4935,6 +4947,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", + "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -6927,22 +6948,30 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.3", + "@modelcontextprotocol/sdk": "^1.4.1", "cors": "^2.8.5", - "eventsource": "^2.0.2", "express": "^4.21.0", "ws": "^8.18.0", "zod": "^3.23.8" @@ -6952,7 +6981,6 @@ }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/eventsource": "^1.1.15", "@types/express": "^4.17.21", "@types/ws": "^8.5.12", "tsx": "^4.19.0", diff --git a/package.json b/package.json index c2625881c..cf06f708d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.3.0", + "version": "0.4.0", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/server/package.json b/server/package.json index c79e7d2e0..eb711f56c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.3.0", + "version": "0.4.0", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -20,16 +20,14 @@ }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/eventsource": "^1.1.15", "@types/express": "^4.17.21", "@types/ws": "^8.5.12", "tsx": "^4.19.0", "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.3", + "@modelcontextprotocol/sdk": "^1.4.1", "cors": "^2.8.5", - "eventsource": "^2.0.2", "express": "^4.21.0", "ws": "^8.18.0", "zod": "^3.23.8" diff --git a/server/src/index.ts b/server/src/index.ts index 96620090d..4d8ac4212 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,29 +1,29 @@ #!/usr/bin/env node import cors from "cors"; -import EventSource from "eventsource"; import { parseArgs } from "node:util"; import { parse as shellParseArgs } from "shell-quote"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + SSEClientTransport, + SseError, +} from "@modelcontextprotocol/sdk/client/sse.js"; import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; -import mcpProxy from "./mcpProxy.js"; import { findActualExecutable } from "spawn-rx"; +import mcpProxy from "./mcpProxy.js"; + +const SSE_HEADERS_PASSTHROUGH = ["authorization"]; const defaultEnvironment = { ...getDefaultEnvironment(), ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}), }; -// Polyfill EventSource for an SSE client in Node.js -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).EventSource = EventSource; - const { values } = parseArgs({ args: process.argv.slice(2), options: { @@ -37,7 +37,8 @@ app.use(cors()); let webAppTransports: SSEServerTransport[] = []; -const createTransport = async (query: express.Request["query"]) => { +const createTransport = async (req: express.Request) => { + const query = req.query; console.log("Query parameters:", query); const transportType = query.transportType as string; @@ -65,9 +66,26 @@ const createTransport = async (query: express.Request["query"]) => { return transport; } else if (transportType === "sse") { const url = query.url as string; - console.log(`SSE transport: url=${url}`); + const headers: HeadersInit = {}; + for (const key of SSE_HEADERS_PASSTHROUGH) { + if (req.headers[key] === undefined) { + continue; + } + + const value = req.headers[key]; + headers[key] = Array.isArray(value) ? value[value.length - 1] : value; + } - const transport = new SSEClientTransport(new URL(url)); + console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`); + + const transport = new SSEClientTransport(new URL(url), { + eventSourceInit: { + fetch: (url, init) => fetch(url, { ...init, headers }), + }, + requestInit: { + headers, + }, + }); await transport.start(); console.log("Connected to SSE transport"); @@ -82,7 +100,21 @@ app.get("/sse", async (req, res) => { try { console.log("New SSE connection"); - const backingServerTransport = await createTransport(req.query); + let backingServerTransport; + try { + backingServerTransport = await createTransport(req); + } catch (error) { + if (error instanceof SseError && error.code === 401) { + console.error( + "Received 401 Unauthorized from MCP server:", + error.message, + ); + res.status(401).json(error); + return; + } + + throw error; + } console.log("Connected MCP client to backing server transport"); @@ -109,9 +141,6 @@ app.get("/sse", async (req, res) => { mcpProxy({ transportToClient: webAppTransport, transportToServer: backingServerTransport, - onerror: (error) => { - console.error(error); - }, }); console.log("Set up MCP proxy"); diff --git a/server/src/mcpProxy.ts b/server/src/mcpProxy.ts index 7932845f7..b93c0cdfd 100644 --- a/server/src/mcpProxy.ts +++ b/server/src/mcpProxy.ts @@ -1,23 +1,29 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +function onClientError(error: Error) { + console.error("Error from inspector client:", error); +} + +function onServerError(error: Error) { + console.error("Error from MCP server:", error); +} + export default function mcpProxy({ transportToClient, transportToServer, - onerror, }: { transportToClient: Transport; transportToServer: Transport; - onerror: (error: Error) => void; }) { let transportToClientClosed = false; let transportToServerClosed = false; transportToClient.onmessage = (message) => { - transportToServer.send(message).catch(onerror); + transportToServer.send(message).catch(onServerError); }; transportToServer.onmessage = (message) => { - transportToClient.send(message).catch(onerror); + transportToClient.send(message).catch(onClientError); }; transportToClient.onclose = () => { @@ -26,7 +32,7 @@ export default function mcpProxy({ } transportToClientClosed = true; - transportToServer.close().catch(onerror); + transportToServer.close().catch(onServerError); }; transportToServer.onclose = () => { @@ -34,10 +40,9 @@ export default function mcpProxy({ return; } transportToServerClosed = true; - - transportToClient.close().catch(onerror); + transportToClient.close().catch(onClientError); }; - transportToClient.onerror = onerror; - transportToServer.onerror = onerror; + transportToClient.onerror = onClientError; + transportToServer.onerror = onServerError; }