Skip to content

Commit 11df5d7

Browse files
fix: surface MCP initialize errors in UI
1 parent 1977e2a commit 11df5d7

File tree

8 files changed

+414
-5
lines changed

8 files changed

+414
-5
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import http from "node:http";
2+
import { once } from "node:events";
3+
4+
const ERROR_MESSAGE =
5+
"Unsupported protocol version: 2025-11-25 - supported versions: 2025-06-18,2025-03-26,2024-11-05,2024-10-07";
6+
7+
const applyCors = (res) => {
8+
res.setHeader("Access-Control-Allow-Origin", "*");
9+
res.setHeader(
10+
"Access-Control-Allow-Headers",
11+
"content-type, accept, mcp-session-id, mcp-protocol-version, authorization",
12+
);
13+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
14+
};
15+
16+
export async function startUnsupportedProtocolServer() {
17+
const server = http.createServer(async (req, res) => {
18+
applyCors(res);
19+
20+
if (req.method === "OPTIONS") {
21+
res.statusCode = 204;
22+
res.end();
23+
return;
24+
}
25+
26+
// Streamable HTTP transport does an optional GET to establish an SSE stream.
27+
// Returning 405 is an expected case handled by the SDK.
28+
if (req.method === "GET") {
29+
res.statusCode = 405;
30+
res.end();
31+
return;
32+
}
33+
34+
// Accepts any path; the Inspector URL field can include arbitrary endpoints.
35+
if (req.method !== "POST") {
36+
res.statusCode = 404;
37+
res.end();
38+
return;
39+
}
40+
41+
let body = "";
42+
req.setEncoding("utf8");
43+
req.on("data", (chunk) => {
44+
body += chunk;
45+
});
46+
await once(req, "end");
47+
48+
let parsed;
49+
try {
50+
parsed = JSON.parse(body);
51+
} catch {
52+
res.statusCode = 400;
53+
res.setHeader("content-type", "application/json");
54+
res.end(JSON.stringify({ error: "Invalid JSON" }));
55+
return;
56+
}
57+
58+
res.statusCode = 200;
59+
res.setHeader("content-type", "application/json");
60+
res.end(
61+
JSON.stringify({
62+
jsonrpc: "2.0",
63+
id: parsed?.id ?? null,
64+
error: {
65+
code: -32602,
66+
message: ERROR_MESSAGE,
67+
},
68+
}),
69+
);
70+
});
71+
72+
server.listen(0, "127.0.0.1");
73+
await once(server, "listening");
74+
75+
const address = server.address();
76+
if (!address || typeof address === "string") {
77+
throw new Error("Failed to start unsupported protocol fixture server");
78+
}
79+
80+
const baseUrl = `http://127.0.0.1:${address.port}`;
81+
82+
return {
83+
baseUrl,
84+
close: () =>
85+
new Promise((resolve, reject) =>
86+
server.close((e) => (e ? reject(e) : resolve())),
87+
),
88+
};
89+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { test, expect } from "@playwright/test";
2+
import { startUnsupportedProtocolServer } from "./fixtures/unsupported-protocol-server.js";
3+
4+
const APP_URL = "http://localhost:6274/";
5+
6+
test.describe("Protocol version negotiation errors", () => {
7+
test("surfaces -32602 initialize error and hides proxy token hint", async ({
8+
page,
9+
}) => {
10+
const fixture = await startUnsupportedProtocolServer();
11+
try {
12+
await page.goto(APP_URL);
13+
14+
const transportSelect = page.getByLabel("Transport Type");
15+
await expect(transportSelect).toBeVisible();
16+
await transportSelect.click();
17+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
18+
19+
const connectionTypeSelect = page.getByLabel("Connection Type");
20+
await expect(connectionTypeSelect).toBeVisible();
21+
await connectionTypeSelect.click();
22+
await page.getByRole("option", { name: "Direct" }).click();
23+
24+
await page.locator("#sse-url-input").fill(fixture.baseUrl);
25+
26+
await page.getByRole("button", { name: "Connect" }).click();
27+
28+
await expect(page.getByText(/MCP error\s*-32602/i).first()).toBeVisible({
29+
timeout: 10000,
30+
});
31+
32+
await expect(
33+
page.getByText(/Did you add the proxy session token in Configuration/i),
34+
).toHaveCount(0);
35+
36+
await expect(
37+
page.locator('[data-testid="connection-error-details"]'),
38+
).toBeVisible();
39+
} finally {
40+
await fixture.close();
41+
}
42+
});
43+
});

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ const App = () => {
299299

300300
const {
301301
connectionStatus,
302+
connectionError,
302303
serverCapabilities,
303304
serverImplementation,
304305
mcpClient,
@@ -956,6 +957,7 @@ const App = () => {
956957
>
957958
<Sidebar
958959
connectionStatus={connectionStatus}
960+
connectionError={connectionError}
959961
transportType={transportType}
960962
setTransportType={setTransportType}
961963
command={command}

client/src/components/Sidebar.tsx

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import { InspectorConfig } from "@/lib/configurationTypes";
3333
import { ConnectionStatus } from "@/lib/constants";
3434
import useTheme from "../lib/hooks/useTheme";
3535
import { version } from "../../../package.json";
36+
import {
37+
getMcpErrorInfo,
38+
parseUnsupportedProtocolVersionError,
39+
} from "@/utils/mcpErrorUtils";
3640
import {
3741
Tooltip,
3842
TooltipTrigger,
@@ -45,6 +49,7 @@ import IconDisplay, { WithIcons } from "./IconDisplay";
4549

4650
interface SidebarProps {
4751
connectionStatus: ConnectionStatus;
52+
connectionError?: unknown | null;
4853
transportType: "stdio" | "sse" | "streamable-http";
4954
setTransportType: (type: "stdio" | "sse" | "streamable-http") => void;
5055
command: string;
@@ -80,6 +85,7 @@ interface SidebarProps {
8085

8186
const Sidebar = ({
8287
connectionStatus,
88+
connectionError,
8389
transportType,
8490
setTransportType,
8591
command,
@@ -119,6 +125,22 @@ const Sidebar = ({
119125
const [copiedServerFile, setCopiedServerFile] = useState(false);
120126
const { toast } = useToast();
121127

128+
const mcpError = connectionError ? getMcpErrorInfo(connectionError) : null;
129+
const mcpErrorDisplayMessage = mcpError
130+
? mcpError.message
131+
.replace(/^McpError:\s*/i, "")
132+
.replace(/^MCP error\s+-?\d+:\s*/i, "")
133+
: "";
134+
const protocolVersionDetails = mcpError
135+
? parseUnsupportedProtocolVersionError(mcpError.message)
136+
: null;
137+
138+
const shouldShowProxyTokenHint =
139+
connectionStatus === "error" &&
140+
connectionType === "proxy" &&
141+
!mcpError &&
142+
!config.MCP_PROXY_AUTH_TOKEN?.value;
143+
122144
const connectionTypeTip =
123145
"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
124146
// Reusable error reporter for copy actions
@@ -753,7 +775,6 @@ const Sidebar = ({
753775
case "connected":
754776
return "bg-green-500";
755777
case "error":
756-
return "bg-red-500";
757778
case "error-connecting-to-proxy":
758779
return "bg-red-500";
759780
default:
@@ -767,10 +788,18 @@ const Sidebar = ({
767788
case "connected":
768789
return "Connected";
769790
case "error": {
770-
const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value;
771-
if (!hasProxyToken) {
791+
if (mcpError && typeof mcpError.code === "number") {
792+
return `MCP error ${mcpError.code}: ${mcpErrorDisplayMessage}`;
793+
}
794+
795+
if (mcpError) {
796+
return `MCP error: ${mcpErrorDisplayMessage}`;
797+
}
798+
799+
if (shouldShowProxyTokenHint) {
772800
return "Connection Error - Did you add the proxy session token in Configuration?";
773801
}
802+
774803
return "Connection Error - Check if your MCP server is running and proxy token is correct";
775804
}
776805
case "error-connecting-to-proxy":
@@ -782,6 +811,53 @@ const Sidebar = ({
782811
</span>
783812
</div>
784813

814+
{connectionStatus === "error" && mcpError && (
815+
<details
816+
className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4 text-sm"
817+
data-testid="connection-error-details"
818+
>
819+
<summary className="cursor-pointer font-medium text-gray-800 dark:text-gray-200">
820+
Error details
821+
</summary>
822+
<div className="mt-2 space-y-2 text-xs text-gray-700 dark:text-gray-300">
823+
<div className="font-mono break-words">
824+
MCP error
825+
{typeof mcpError.code === "number"
826+
? ` ${mcpError.code}`
827+
: ""}
828+
: {mcpErrorDisplayMessage}
829+
</div>
830+
831+
{protocolVersionDetails?.supportedProtocolVersions?.length ? (
832+
<div>
833+
Supported versions:{" "}
834+
<span className="font-mono">
835+
{protocolVersionDetails.supportedProtocolVersions.join(
836+
", ",
837+
)}
838+
</span>
839+
</div>
840+
) : null}
841+
842+
{mcpError.code === -32602 ? (
843+
<div className="text-gray-600 dark:text-gray-400">
844+
The server returned an error for{" "}
845+
<span className="font-mono">initialize</span> instead of
846+
negotiating a compatible protocol version.{" "}
847+
<a
848+
className="text-blue-600 dark:text-blue-400 hover:underline"
849+
href="https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation"
850+
target="_blank"
851+
rel="noopener noreferrer"
852+
>
853+
Spec: version negotiation
854+
</a>
855+
</div>
856+
) : null}
857+
</div>
858+
</details>
859+
)}
860+
785861
{connectionStatus === "connected" && serverImplementation && (
786862
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4">
787863
<div className="flex items-center gap-2 mb-1">

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { render, screen, fireEvent, act } from "@testing-library/react";
1+
import { render, screen, fireEvent, act, within } from "@testing-library/react";
22
import "@testing-library/jest-dom";
33
import { describe, it, beforeEach, jest } from "@jest/globals";
44
import Sidebar from "../Sidebar";
55
import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
66
import { InspectorConfig } from "@/lib/configurationTypes";
77
import { TooltipProvider } from "@/components/ui/tooltip";
8+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
89

910
// Mock theme hook
1011
jest.mock("../../lib/hooks/useTheme", () => ({
@@ -1046,4 +1047,69 @@ describe("Sidebar", () => {
10461047
);
10471048
});
10481049
});
1050+
1051+
describe("Connection status errors", () => {
1052+
it("shows MCP error details and hides proxy token hint", () => {
1053+
const mcpError = new McpError(
1054+
-32602,
1055+
"Unsupported protocol version: 2025-11-25 - supported versions: 2025-06-18,2025-03-26,2024-11-05,2024-10-07",
1056+
);
1057+
1058+
renderSidebar({
1059+
connectionStatus: "error",
1060+
connectionError: mcpError,
1061+
config: {
1062+
...DEFAULT_INSPECTOR_CONFIG,
1063+
MCP_PROXY_AUTH_TOKEN: {
1064+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
1065+
value: "",
1066+
},
1067+
},
1068+
});
1069+
1070+
expect(
1071+
screen.getAllByText(
1072+
/MCP error -32602: Unsupported protocol version: 2025-11-25/i,
1073+
).length,
1074+
).toBeGreaterThan(0);
1075+
1076+
expect(
1077+
within(screen.getByTestId("connection-error-details")).getAllByText(
1078+
/Supported versions:/i,
1079+
).length,
1080+
).toBeGreaterThan(0);
1081+
1082+
const details = within(screen.getByTestId("connection-error-details"));
1083+
expect(details.getAllByText(/2025-06-18/).length).toBeGreaterThan(0);
1084+
expect(details.getAllByText(/2025-03-26/).length).toBeGreaterThan(0);
1085+
expect(details.getAllByText(/2024-11-05/).length).toBeGreaterThan(0);
1086+
expect(details.getAllByText(/2024-10-07/).length).toBeGreaterThan(0);
1087+
1088+
expect(
1089+
screen.queryByText(
1090+
/Did you add the proxy session token in Configuration\?/i,
1091+
),
1092+
).not.toBeInTheDocument();
1093+
});
1094+
1095+
it("does not show proxy token hint for direct connections", () => {
1096+
renderSidebar({
1097+
connectionStatus: "error",
1098+
connectionType: "direct",
1099+
config: {
1100+
...DEFAULT_INSPECTOR_CONFIG,
1101+
MCP_PROXY_AUTH_TOKEN: {
1102+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
1103+
value: "",
1104+
},
1105+
},
1106+
});
1107+
1108+
expect(
1109+
screen.queryByText(
1110+
/Did you add the proxy session token in Configuration\?/i,
1111+
),
1112+
).not.toBeInTheDocument();
1113+
});
1114+
});
10491115
});

0 commit comments

Comments
 (0)