Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions client/e2e/fixtures/unsupported-protocol-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import http from "node:http";
import { once } from "node:events";

const ERROR_MESSAGE =
"Unsupported protocol version: 2025-11-25 - supported versions: 2025-06-18,2025-03-26,2024-11-05,2024-10-07";

const applyCors = (res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Headers",
"content-type, accept, mcp-session-id, mcp-protocol-version, authorization",
);
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
};

export async function startUnsupportedProtocolServer() {
const server = http.createServer(async (req, res) => {
applyCors(res);

if (req.method === "OPTIONS") {
res.statusCode = 204;
res.end();
return;
}

// Streamable HTTP transport does an optional GET to establish an SSE stream.
// Returning 405 is an expected case handled by the SDK.
if (req.method === "GET") {
res.statusCode = 405;
res.end();
return;
}

// Accepts any path; the Inspector URL field can include arbitrary endpoints.
if (req.method !== "POST") {
res.statusCode = 404;
res.end();
return;
}

let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
await once(req, "end");

let parsed;
try {
parsed = JSON.parse(body);
} catch {
res.statusCode = 400;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ error: "Invalid JSON" }));
return;
}

res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(
JSON.stringify({
jsonrpc: "2.0",
id: parsed?.id ?? null,
error: {
code: -32602,
message: ERROR_MESSAGE,
},
}),
);
});

server.listen(0, "127.0.0.1");
await once(server, "listening");

const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to start unsupported protocol fixture server");
}

const baseUrl = `http://127.0.0.1:${address.port}`;

return {
baseUrl,
close: () =>
new Promise((resolve, reject) =>
server.close((e) => (e ? reject(e) : resolve())),
),
};
}
43 changes: 43 additions & 0 deletions client/e2e/protocol-version-error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { test, expect } from "@playwright/test";
import { startUnsupportedProtocolServer } from "./fixtures/unsupported-protocol-server.js";

const APP_URL = "http://localhost:6274/";

test.describe("Protocol version negotiation errors", () => {
test("surfaces -32602 initialize error and hides proxy token hint", async ({
page,
}) => {
const fixture = await startUnsupportedProtocolServer();
try {
await page.goto(APP_URL);

const transportSelect = page.getByLabel("Transport Type");
await expect(transportSelect).toBeVisible();
await transportSelect.click();
await page.getByRole("option", { name: "Streamable HTTP" }).click();

const connectionTypeSelect = page.getByLabel("Connection Type");
await expect(connectionTypeSelect).toBeVisible();
await connectionTypeSelect.click();
await page.getByRole("option", { name: "Direct" }).click();

await page.locator("#sse-url-input").fill(fixture.baseUrl);

await page.getByRole("button", { name: "Connect" }).click();

await expect(page.getByText(/MCP error\s*-32602/i).first()).toBeVisible({
timeout: 10000,
});

await expect(
page.getByText(/Did you add the proxy session token in Configuration/i),
).toHaveCount(0);

await expect(
page.locator('[data-testid="connection-error-details"]'),
).toBeVisible();
} finally {
await fixture.close();
}
});
});
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ const App = () => {

const {
connectionStatus,
connectionError,
serverCapabilities,
serverImplementation,
mcpClient,
Expand Down Expand Up @@ -956,6 +957,7 @@ const App = () => {
>
<Sidebar
connectionStatus={connectionStatus}
connectionError={connectionError}
transportType={transportType}
setTransportType={setTransportType}
command={command}
Expand Down
82 changes: 79 additions & 3 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import { InspectorConfig } from "@/lib/configurationTypes";
import { ConnectionStatus } from "@/lib/constants";
import useTheme from "../lib/hooks/useTheme";
import { version } from "../../../package.json";
import {
getMcpErrorInfo,
parseUnsupportedProtocolVersionError,
} from "@/utils/mcpErrorUtils";
import {
Tooltip,
TooltipTrigger,
Expand All @@ -45,6 +49,7 @@ import IconDisplay, { WithIcons } from "./IconDisplay";

interface SidebarProps {
connectionStatus: ConnectionStatus;
connectionError?: unknown | null;
transportType: "stdio" | "sse" | "streamable-http";
setTransportType: (type: "stdio" | "sse" | "streamable-http") => void;
command: string;
Expand Down Expand Up @@ -80,6 +85,7 @@ interface SidebarProps {

const Sidebar = ({
connectionStatus,
connectionError,
transportType,
setTransportType,
command,
Expand Down Expand Up @@ -119,6 +125,22 @@ const Sidebar = ({
const [copiedServerFile, setCopiedServerFile] = useState(false);
const { toast } = useToast();

const mcpError = connectionError ? getMcpErrorInfo(connectionError) : null;
const mcpErrorDisplayMessage = mcpError
? mcpError.message
.replace(/^McpError:\s*/i, "")
.replace(/^MCP error\s+-?\d+:\s*/i, "")
: "";
const protocolVersionDetails = mcpError
? parseUnsupportedProtocolVersionError(mcpError.message)
: null;

const shouldShowProxyTokenHint =
connectionStatus === "error" &&
connectionType === "proxy" &&
!mcpError &&
!config.MCP_PROXY_AUTH_TOKEN?.value;

const connectionTypeTip =
"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
// Reusable error reporter for copy actions
Expand Down Expand Up @@ -753,7 +775,6 @@ const Sidebar = ({
case "connected":
return "bg-green-500";
case "error":
return "bg-red-500";
case "error-connecting-to-proxy":
return "bg-red-500";
default:
Expand All @@ -767,10 +788,18 @@ const Sidebar = ({
case "connected":
return "Connected";
case "error": {
const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value;
if (!hasProxyToken) {
if (mcpError && typeof mcpError.code === "number") {
return `MCP error ${mcpError.code}: ${mcpErrorDisplayMessage}`;
}

if (mcpError) {
return `MCP error: ${mcpErrorDisplayMessage}`;
}

if (shouldShowProxyTokenHint) {
return "Connection Error - Did you add the proxy session token in Configuration?";
}

return "Connection Error - Check if your MCP server is running and proxy token is correct";
}
case "error-connecting-to-proxy":
Expand All @@ -782,6 +811,53 @@ const Sidebar = ({
</span>
</div>

{connectionStatus === "error" && mcpError && (
<details
className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4 text-sm"
data-testid="connection-error-details"
>
<summary className="cursor-pointer font-medium text-gray-800 dark:text-gray-200">
Error details
</summary>
<div className="mt-2 space-y-2 text-xs text-gray-700 dark:text-gray-300">
<div className="font-mono break-words">
MCP error
{typeof mcpError.code === "number"
? ` ${mcpError.code}`
: ""}
: {mcpErrorDisplayMessage}
</div>

{protocolVersionDetails?.supportedProtocolVersions?.length ? (
<div>
Supported versions:{" "}
<span className="font-mono">
{protocolVersionDetails.supportedProtocolVersions.join(
", ",
)}
</span>
</div>
) : null}

{mcpError.code === -32602 ? (
<div className="text-gray-600 dark:text-gray-400">
The server returned an error for{" "}
<span className="font-mono">initialize</span> instead of
negotiating a compatible protocol version.{" "}
<a
className="text-blue-600 dark:text-blue-400 hover:underline"
href="https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation"
target="_blank"
rel="noopener noreferrer"
>
Spec: version negotiation
</a>
</div>
) : null}
</div>
</details>
)}

{connectionStatus === "connected" && serverImplementation && (
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4">
<div className="flex items-center gap-2 mb-1">
Expand Down
68 changes: 67 additions & 1 deletion client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { render, screen, fireEvent, act } from "@testing-library/react";
import { render, screen, fireEvent, act, within } from "@testing-library/react";
import "@testing-library/jest-dom";
import { describe, it, beforeEach, jest } from "@jest/globals";
import Sidebar from "../Sidebar";
import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
import { InspectorConfig } from "@/lib/configurationTypes";
import { TooltipProvider } from "@/components/ui/tooltip";
import { McpError } from "@modelcontextprotocol/sdk/types.js";

// Mock theme hook
jest.mock("../../lib/hooks/useTheme", () => ({
Expand Down Expand Up @@ -1046,4 +1047,69 @@ describe("Sidebar", () => {
);
});
});

describe("Connection status errors", () => {
it("shows MCP error details and hides proxy token hint", () => {
const mcpError = new McpError(
-32602,
"Unsupported protocol version: 2025-11-25 - supported versions: 2025-06-18,2025-03-26,2024-11-05,2024-10-07",
);

renderSidebar({
connectionStatus: "error",
connectionError: mcpError,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "",
},
},
});

expect(
screen.getAllByText(
/MCP error -32602: Unsupported protocol version: 2025-11-25/i,
).length,
).toBeGreaterThan(0);

expect(
within(screen.getByTestId("connection-error-details")).getAllByText(
/Supported versions:/i,
).length,
).toBeGreaterThan(0);

const details = within(screen.getByTestId("connection-error-details"));
expect(details.getAllByText(/2025-06-18/).length).toBeGreaterThan(0);
expect(details.getAllByText(/2025-03-26/).length).toBeGreaterThan(0);
expect(details.getAllByText(/2024-11-05/).length).toBeGreaterThan(0);
expect(details.getAllByText(/2024-10-07/).length).toBeGreaterThan(0);

expect(
screen.queryByText(
/Did you add the proxy session token in Configuration\?/i,
),
).not.toBeInTheDocument();
});

it("does not show proxy token hint for direct connections", () => {
renderSidebar({
connectionStatus: "error",
connectionType: "direct",
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "",
},
},
});

expect(
screen.queryByText(
/Did you add the proxy session token in Configuration\?/i,
),
).not.toBeInTheDocument();
});
});
});
Loading