-
Server Notifications
+
+
Server Notifications
+
+ Clear
+
+
{serverNotifications.length === 0 ? (
No notifications yet
diff --git a/client/src/components/__tests__/HistoryAndNotifications.test.tsx b/client/src/components/__tests__/HistoryAndNotifications.test.tsx
index 42c58519..a813db8d 100644
--- a/client/src/components/__tests__/HistoryAndNotifications.test.tsx
+++ b/client/src/components/__tests__/HistoryAndNotifications.test.tsx
@@ -1,4 +1,5 @@
-import { render, screen, fireEvent } from "@testing-library/react";
+import { render, screen, fireEvent, within } from "@testing-library/react";
+import { useState } from "react";
import { describe, it, expect, jest } from "@jest/globals";
import HistoryAndNotifications from "../HistoryAndNotifications";
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
@@ -223,4 +224,66 @@ describe("HistoryAndNotifications", () => {
expect(screen.getByText("No history yet")).toBeTruthy();
expect(screen.getByText("No notifications yet")).toBeTruthy();
});
+
+ it("clears request history when Clear is clicked", () => {
+ const Wrapper = () => {
+ const [history, setHistory] = useState(mockRequestHistory);
+ return (
+ setHistory([])}
+ />
+ );
+ };
+
+ render( );
+
+ // Verify items are present initially
+ expect(screen.getByText("2. test/method2")).toBeTruthy();
+ expect(screen.getByText("1. test/method1")).toBeTruthy();
+
+ // Click Clear in History header (scoped by the History heading's container)
+ const historyHeader = screen.getByText("History");
+ const historyHeaderContainer = historyHeader.parentElement as HTMLElement;
+ const historyClearButton = within(historyHeaderContainer).getByRole(
+ "button",
+ { name: "Clear" },
+ );
+ fireEvent.click(historyClearButton);
+
+ // History should now be empty
+ expect(screen.getByText("No history yet")).toBeTruthy();
+ });
+
+ it("clears server notifications when Clear is clicked", () => {
+ const Wrapper = () => {
+ const [notifications, setNotifications] =
+ useState(mockNotifications);
+ return (
+ setNotifications([])}
+ />
+ );
+ };
+
+ render( );
+
+ // Verify items are present initially
+ expect(screen.getByText("2. notifications/progress")).toBeTruthy();
+ expect(screen.getByText("1. notifications/message")).toBeTruthy();
+
+ // Click Clear in Server Notifications header (scoped by its heading's container)
+ const notifHeader = screen.getByText("Server Notifications");
+ const notifHeaderContainer = notifHeader.parentElement as HTMLElement;
+ const notifClearButton = within(notifHeaderContainer).getByRole("button", {
+ name: "Clear",
+ });
+ fireEvent.click(notifClearButton);
+
+ // Notifications should now be empty
+ expect(screen.getByText("No notifications yet")).toBeTruthy();
+ });
});
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index 8f457910..bfb72941 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -653,11 +653,16 @@ export function useConnection({
setServerCapabilities(null);
};
+ const clearRequestHistory = () => {
+ setRequestHistory([]);
+ };
+
return {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
+ clearRequestHistory,
makeRequest,
sendNotification,
handleCompletion,
From 71583caf3bd72aad73e4424e0c638588a13a35db Mon Sep 17 00:00:00 2001
From: Max Gerber
Date: Fri, 5 Sep 2025 18:40:24 -0700
Subject: [PATCH 117/140] fix: correct authState.authorizationUrl type to URL
---
client/src/components/AuthDebugger.tsx | 2 +-
client/src/components/OAuthFlowProgress.tsx | 2 +-
client/src/components/__tests__/AuthDebugger.test.tsx | 8 ++++++--
client/src/lib/auth-types.ts | 2 +-
client/src/lib/oauth-state-machine.ts | 2 +-
client/src/utils/urlValidation.ts | 2 +-
6 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx
index e7acbd80..6252c116 100644
--- a/client/src/components/AuthDebugger.tsx
+++ b/client/src/components/AuthDebugger.tsx
@@ -187,7 +187,7 @@ const AuthDebugger = ({
JSON.stringify(currentState),
);
// Open the authorization URL automatically
- window.location.href = currentState.authorizationUrl;
+ window.location.href = currentState.authorizationUrl.toString();
break;
}
}
diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx
index e50b7df4..5f44a4f5 100644
--- a/client/src/components/OAuthFlowProgress.tsx
+++ b/client/src/components/OAuthFlowProgress.tsx
@@ -240,7 +240,7 @@ export const OAuthFlowProgress = ({
Authorization URL:
- {authState.authorizationUrl}
+ {String(authState.authorizationUrl)}
{
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index b3da23fd..5d5042ea 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -482,7 +482,9 @@ describe("AuthDebugger", () => {
await waitFor(() => {
expect(updateAuthState).toHaveBeenCalledWith(
expect.objectContaining({
- authorizationUrl: expect.stringContaining("scope="),
+ authorizationUrl: expect.objectContaining({
+ href: "https://oauth.example.com/authorize?scope=read+write",
+ }),
}),
);
});
@@ -496,7 +498,9 @@ describe("AuthDebugger", () => {
await waitFor(() => {
expect(updateAuthState).toHaveBeenCalledWith(
expect.objectContaining({
- authorizationUrl: expect.stringContaining("scope="),
+ authorizationUrl: expect.objectContaining({
+ href: "https://oauth.example.com/authorize?scope=read+write",
+ }),
}),
);
});
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts
index f8f927c6..aaa83428 100644
--- a/client/src/lib/auth-types.ts
+++ b/client/src/lib/auth-types.ts
@@ -34,7 +34,7 @@ export interface AuthDebuggerState {
authServerUrl: URL | null;
oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
- authorizationUrl: string | null;
+ authorizationUrl: URL | null;
authorizationCode: string;
latestError: Error | null;
statusMessage: StatusMessage | null;
diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts
index 91465853..c505f698 100644
--- a/client/src/lib/oauth-state-machine.ts
+++ b/client/src/lib/oauth-state-machine.ts
@@ -132,7 +132,7 @@ export const oauthTransitions: Record = {
context.provider.saveCodeVerifier(codeVerifier);
context.updateState({
- authorizationUrl: authorizationUrl.toString(),
+ authorizationUrl: authorizationUrl,
oauthStep: "authorization_code",
});
},
diff --git a/client/src/utils/urlValidation.ts b/client/src/utils/urlValidation.ts
index 70bc458b..f6811e04 100644
--- a/client/src/utils/urlValidation.ts
+++ b/client/src/utils/urlValidation.ts
@@ -5,7 +5,7 @@
* @param url - The URL string to validate
* @throws Error if the URL has an unsafe protocol
*/
-export function validateRedirectUrl(url: string): void {
+export function validateRedirectUrl(url: string | URL): void {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
From 1896f2001ed68b06db1f64611a05e91861d54fa5 Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Sat, 6 Sep 2025 14:05:24 -0400
Subject: [PATCH 118/140] Implement dynamic header injection for proxy
transports. Fixes #679
Adds a mechanism to dynamically inject and update HTTP headers for ongoing proxy sessions, resolving an issue where headers introduced after the initial handshake (e.g, Mcp-Protocol-Version) were not being forwarded to the server.
The core problem was that the `transportToServer` instance was created once with a static set of headers. This meant that critical, dynamically added headers like `mcp-protocol-version` and `last-event-id` were being dropped in subsequent requests.
The solution implemented here is to wrap the `fetch` function used by the HTTP-based transports (`StreamableHttp` and `SSE`). This custom wrapper merges the latest headers from the client with the headers of each outgoing request from the proxy, ensuring all headers are preserved for the lifetime of the session.
Changes:
In `src/index.ts`:
- Added `sessionHeaderHolders`, a `Map` to store the most recent set of headers for each active session ID.
- Added `createCustomFetch`, a new helper function that creates a `fetch` wrapper. This wrapper merges the session headers from `sessionHeaderHolders` with the request-specific headers from the SDK (like `Content-Type`)
- In `createTransport`, use `createCustomFetch` for both the `StreamableHTTPClientTransport` and `SSEClientTransport`. This injects the dynamic header logic into the transport layer.
- In `/mcp`, `/sse`, `/message` route handlers, added logic to update the `sessionHeaderHolders` map with the latest headers from every incoming client request.
- Added cleanup logic to the `onsessionclosed` callback and the `/mcp` DELETE handler to remove session data from `sessionHeaderHolders`.
- In `getHttpHeaders`, more robust handling of `string[]` and `undefined` values from `req.headers`, satisfying the `"strict": true` TypeScript configuration.
---
server/src/index.ts | 340 +++++++++++++++++++++++++-------------------
1 file changed, 195 insertions(+), 145 deletions(-)
diff --git a/server/src/index.ts b/server/src/index.ts
index c0fb3797..5d28acca 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -22,12 +22,6 @@ import mcpProxy from "./mcpProxy.js";
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
-const SSE_HEADERS_PASSTHROUGH = ["authorization"];
-const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
- "authorization",
- "mcp-session-id",
- "last-event-id",
-];
const defaultEnvironment = {
...getDefaultEnvironment(),
@@ -46,40 +40,56 @@ const { values } = parseArgs({
});
// Function to get HTTP headers.
-// Supports only "sse" and "streamable-http" transport types.
-const getHttpHeaders = (
- req: express.Request,
- transportType: string,
-): HeadersInit => {
- const headers: HeadersInit = {
- Accept:
- transportType === "sse"
- ? "text/event-stream"
- : "text/event-stream, application/json",
- };
- const defaultHeaders =
- transportType === "sse"
- ? SSE_HEADERS_PASSTHROUGH
- : STREAMABLE_HTTP_HEADERS_PASSTHROUGH;
+const getHttpHeaders = (req: express.Request): Record => {
+ const headers: Record = {};
- for (const key of defaultHeaders) {
- if (req.headers[key] === undefined) {
- continue;
- }
+ // Iterate over all headers in the request
+ for (const key in req.headers) {
+ const lowerKey = key.toLowerCase();
- const value = req.headers[key];
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
- }
+ // Check if the header is one we want to forward
+ if (
+ lowerKey.startsWith("mcp-") ||
+ lowerKey === "authorization" ||
+ lowerKey === "last-event-id"
+ ) {
+ // Exclude the proxy's own authentication header
+ if (lowerKey !== "x-mcp-proxy-auth") {
+ const value = req.headers[key];
- // If the header "x-custom-auth-header" is present, use its value as the custom header name.
- if (req.headers["x-custom-auth-header"] !== undefined) {
- const customHeaderName = req.headers["x-custom-auth-header"] as string;
- const lowerCaseHeaderName = customHeaderName.toLowerCase();
- if (req.headers[lowerCaseHeaderName] !== undefined) {
- const value = req.headers[lowerCaseHeaderName];
- headers[customHeaderName] = value as string;
+ if (typeof value === "string") {
+ // If the value is a string, use it directly
+ headers[key] = value;
+ } else if (Array.isArray(value)) {
+ // If the value is an array, use the last element
+ const lastValue = value.at(-1);
+ if (lastValue !== undefined) {
+ headers[key] = lastValue;
+ }
+ }
+ // If value is undefined, it's skipped, which is correct.
+ }
}
}
+
+ // Handle the custom auth header separately. We expect `x-custom-auth-header`
+ // to be a string containing the name of the actual authentication header.
+ const customAuthHeaderName = req.headers["x-custom-auth-header"];
+ if (typeof customAuthHeaderName === "string") {
+ const lowerCaseHeaderName = customAuthHeaderName.toLowerCase();
+ const value = req.headers[lowerCaseHeaderName];
+
+ if (typeof value === "string") {
+ headers[customAuthHeaderName] = value;
+ } else if (Array.isArray(value)) {
+ // If the actual auth header was sent multiple times, use the last value.
+ const lastValue = value.at(-1);
+ if (lastValue !== undefined) {
+ headers[customAuthHeaderName] = lastValue;
+ }
+ }
+ }
+
return headers;
};
@@ -92,6 +102,7 @@ app.use((req, res, next) => {
const webAppTransports: Map = new Map(); // Web app transports by web app sessionId
const serverTransports: Map = new Map(); // Server Transports by web app sessionId
+const sessionHeaderHolders: Map = new Map(); // For dynamic header updates
// Use provided token from environment or generate a new one
const sessionToken =
@@ -174,7 +185,38 @@ const authMiddleware = (
next();
};
-const createTransport = async (req: express.Request): Promise => {
+/**
+ * Creates a `fetch` function that merges dynamic session headers with the
+ * headers from the actual request, ensuring that request-specific headers like
+ * `Content-Type` are preserved.
+ */
+const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
+ return (input: RequestInfo | URL, init?: RequestInit): Promise => {
+ // Determine the headers from the original request/init.
+ // The SDK may pass a Request object or a URL and an init object.
+ const originalHeaders =
+ input instanceof Request ? input.headers : init?.headers;
+
+ // Start with our dynamic session headers.
+ const finalHeaders = new Headers(headerHolder.headers);
+
+ // Merge the SDK's request-specific headers, letting them overwrite.
+ // This is crucial for preserving Content-Type on POST requests.
+ new Headers(originalHeaders).forEach((value, key) => {
+ finalHeaders.set(key, value);
+ });
+
+ // This works for both `fetch(url, init)` and `fetch(request)` style calls.
+ return fetch(input, { ...init, headers: finalHeaders });
+ };
+};
+
+const createTransport = async (
+ req: express.Request,
+): Promise<{
+ transport: Transport;
+ headerHolder?: { headers: HeadersInit };
+}> => {
const query = req.query;
console.log("Query parameters:", JSON.stringify(query));
@@ -198,11 +240,13 @@ const createTransport = async (req: express.Request): Promise => {
});
await transport.start();
- return transport;
+ return { transport };
} else if (transportType === "sse") {
const url = query.url as string;
- const headers = getHttpHeaders(req, transportType);
+ const headers = getHttpHeaders(req);
+ headers["Accept"] = "text/event-stream";
+ const headerHolder = { headers };
console.log(
`SSE transport: url=${url}, headers=${JSON.stringify(headers)}`,
@@ -210,27 +254,28 @@ const createTransport = async (req: express.Request): Promise => {
const transport = new SSEClientTransport(new URL(url), {
eventSourceInit: {
- fetch: (url, init) => fetch(url, { ...init, headers }),
+ fetch: createCustomFetch(headerHolder),
},
requestInit: {
- headers,
+ headers: headerHolder.headers,
},
});
await transport.start();
- return transport;
+ return { transport, headerHolder };
} else if (transportType === "streamable-http") {
- const headers = getHttpHeaders(req, transportType);
+ const headers = getHttpHeaders(req);
+ headers["Accept"] = "text/event-stream, application/json";
+ const headerHolder = { headers };
const transport = new StreamableHTTPClientTransport(
new URL(query.url as string),
{
- requestInit: {
- headers,
- },
+ // Pass a custom fetch to inject the latest headers on each request
+ fetch: createCustomFetch(headerHolder),
},
);
await transport.start();
- return transport;
+ return { transport, headerHolder };
} else {
console.error(`Invalid transport type: ${transportType}`);
throw new Error("Invalid transport type specified");
@@ -244,6 +289,12 @@ app.get(
async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
console.log(`Received GET message for sessionId ${sessionId}`);
+
+ const headerHolder = sessionHeaderHolders.get(sessionId);
+ if (headerHolder) {
+ headerHolder.headers = getHttpHeaders(req);
+ }
+
try {
const transport = webAppTransports.get(
sessionId,
@@ -267,34 +318,51 @@ app.post(
authMiddleware,
async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
- let serverTransport: Transport | undefined;
- if (!sessionId) {
+
+ if (sessionId) {
+ console.log(`Received POST message for sessionId ${sessionId}`);
+ const headerHolder = sessionHeaderHolders.get(sessionId);
+ if (headerHolder) {
+ headerHolder.headers = getHttpHeaders(req);
+ }
+
try {
- console.log("New StreamableHttp connection request");
- try {
- serverTransport = 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;
+ const transport = webAppTransports.get(
+ sessionId,
+ ) as StreamableHTTPServerTransport;
+ if (!transport) {
+ res.status(404).end("Transport not found for sessionId " + sessionId);
+ } else {
+ await (transport as StreamableHTTPServerTransport).handleRequest(
+ req,
+ res,
+ );
}
-
- console.log("Created StreamableHttp server transport");
+ } catch (error) {
+ console.error("Error in /mcp route:", error);
+ res.status(500).json(error);
+ }
+ } else {
+ console.log("New StreamableHttp connection request");
+ try {
+ const { transport: serverTransport, headerHolder } =
+ await createTransport(req);
const webAppTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: randomUUID,
onsessioninitialized: (sessionId) => {
webAppTransports.set(sessionId, webAppTransport);
- serverTransports.set(sessionId, serverTransport!);
+ serverTransports.set(sessionId, serverTransport!); // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ if (headerHolder) {
+ sessionHeaderHolders.set(sessionId, headerHolder);
+ }
console.log("Client <-> Proxy sessionId: " + sessionId);
},
+ onsessionclosed: (sessionId) => {
+ webAppTransports.delete(sessionId);
+ serverTransports.delete(sessionId);
+ sessionHeaderHolders.delete(sessionId);
+ },
});
console.log("Created StreamableHttp client transport");
@@ -311,25 +379,15 @@ app.post(
req.body,
);
} catch (error) {
- console.error("Error in /mcp POST route:", error);
- res.status(500).json(error);
- }
- } else {
- console.log(`Received POST message for sessionId ${sessionId}`);
- try {
- const transport = webAppTransports.get(
- sessionId,
- ) as StreamableHTTPServerTransport;
- if (!transport) {
- res.status(404).end("Transport not found for sessionId " + sessionId);
- } else {
- await (transport as StreamableHTTPServerTransport).handleRequest(
- req,
- res,
+ if (error instanceof SseError && error.code === 401) {
+ console.error(
+ "Received 401 Unauthorized from MCP server:",
+ error.message,
);
+ res.status(401).json(error);
+ return;
}
- } catch (error) {
- console.error("Error in /mcp route:", error);
+ console.error("Error in /mcp POST route:", error);
res.status(500).json(error);
}
}
@@ -343,20 +401,18 @@ app.delete(
async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
console.log(`Received DELETE message for sessionId ${sessionId}`);
- let serverTransport: Transport | undefined;
if (sessionId) {
try {
- serverTransport = serverTransports.get(
+ const serverTransport = serverTransports.get(
sessionId,
) as StreamableHTTPClientTransport;
if (!serverTransport) {
res.status(404).end("Transport not found for sessionId " + sessionId);
} else {
- await (
- serverTransport as StreamableHTTPClientTransport
- ).terminateSession();
+ await serverTransport.terminateSession();
webAppTransports.delete(sessionId);
serverTransports.delete(sessionId);
+ sessionHeaderHolders.delete(sessionId);
console.log(`Transports removed for sessionId ${sessionId}`);
}
res.status(200).end();
@@ -375,20 +431,7 @@ app.get(
async (req, res) => {
try {
console.log("New STDIO connection request");
- let serverTransport: Transport | undefined;
- try {
- serverTransport = await createTransport(req);
- } catch (error) {
- if (error instanceof SseError && error.code === 401) {
- console.error(
- "Received 401 Unauthorized from MCP server. Authentication failure.",
- );
- res.status(401).json(error);
- return;
- }
-
- throw error;
- }
+ const { transport: serverTransport } = await createTransport(req);
const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
const prefix = proxyFullAddress || "";
@@ -422,6 +465,7 @@ app.get(
serverTransport.close();
webAppTransports.delete(webAppTransport.sessionId);
serverTransports.delete(webAppTransport.sessionId);
+ sessionHeaderHolders.delete(webAppTransport.sessionId);
console.error(message);
} else {
// Inspect message and attempt to assign a RFC 5424 Syslog Protocol level
@@ -475,6 +519,13 @@ app.get(
transportToServer: serverTransport,
});
} catch (error) {
+ if (error instanceof SseError && error.code === 401) {
+ console.error(
+ "Received 401 Unauthorized from MCP server. Authentication failure.",
+ );
+ res.status(401).json(error);
+ return;
+ }
console.error("Error in /stdio route:", error);
res.status(500).json(error);
}
@@ -490,50 +541,46 @@ app.get(
console.log(
"New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp",
);
- let serverTransport: Transport | undefined;
- try {
- serverTransport = await createTransport(req);
- } catch (error) {
- if (error instanceof SseError && error.code === 401) {
- console.error(
- "Received 401 Unauthorized from MCP server. Authentication failure.",
- );
- res.status(401).json(error);
- return;
- } else if (error instanceof SseError && error.code === 404) {
- console.error(
- "Received 404 not found from MCP server. Does the MCP server support SSE?",
- );
- res.status(404).json(error);
- return;
- } else if (JSON.stringify(error).includes("ECONNREFUSED")) {
- console.error("Connection refused. Is the MCP server running?");
- res.status(500).json(error);
- } else {
- throw error;
- }
+ const { transport: serverTransport, headerHolder } =
+ await createTransport(req);
+
+ const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
+ const prefix = proxyFullAddress || "";
+ const endpoint = `${prefix}/message`;
+
+ const webAppTransport = new SSEServerTransport(endpoint, res);
+ webAppTransports.set(webAppTransport.sessionId, webAppTransport);
+ console.log("Created client transport");
+
+ serverTransports.set(webAppTransport.sessionId, serverTransport!); // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ if (headerHolder) {
+ sessionHeaderHolders.set(webAppTransport.sessionId, headerHolder);
}
+ console.log("Created server transport");
- if (serverTransport) {
- const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
- const prefix = proxyFullAddress || "";
- const endpoint = `${prefix}/message`;
+ await webAppTransport.start();
- const webAppTransport = new SSEServerTransport(endpoint, res);
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
- console.log("Created client transport");
-
- serverTransports.set(webAppTransport.sessionId, serverTransport!);
- console.log("Created server transport");
-
- await webAppTransport.start();
-
- mcpProxy({
- transportToClient: webAppTransport,
- transportToServer: serverTransport,
- });
- }
+ mcpProxy({
+ transportToClient: webAppTransport,
+ transportToServer: serverTransport,
+ });
} catch (error) {
+ if (error instanceof SseError && error.code === 401) {
+ console.error(
+ "Received 401 Unauthorized from MCP server. Authentication failure.",
+ );
+ res.status(401).json(error);
+ return;
+ } else if (error instanceof SseError && error.code === 404) {
+ console.error(
+ "Received 404 not found from MCP server. Does the MCP server support SSE?",
+ );
+ res.status(404).json(error);
+ return;
+ } else if (JSON.stringify(error).includes("ECONNREFUSED")) {
+ console.error("Connection refused. Is the MCP server running?");
+ res.status(500).json(error);
+ }
console.error("Error in /sse route:", error);
res.status(500).json(error);
}
@@ -546,12 +593,15 @@ app.post(
authMiddleware,
async (req, res) => {
try {
- const sessionId = req.query.sessionId;
+ const sessionId = req.query.sessionId as string;
console.log(`Received POST message for sessionId ${sessionId}`);
- const transport = webAppTransports.get(
- sessionId as string,
- ) as SSEServerTransport;
+ const headerHolder = sessionHeaderHolders.get(sessionId);
+ if (headerHolder) {
+ headerHolder.headers = getHttpHeaders(req);
+ }
+
+ const transport = webAppTransports.get(sessionId) as SSEServerTransport;
if (!transport) {
res.status(404).end("Session not found");
return;
From 2f420c4edf8edd43b9fecae82be24fae72162545 Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Sat, 6 Sep 2025 15:51:43 -0400
Subject: [PATCH 119/140] A new `updateHeadersInPlace` function *mutates* the
original header object instead of replacing it. This ensures that transports
holding a static reference, like `SSEClientTransport`, always see the latest
headers.
* In `src/server/index.ts`,
- Added `updateHeadersInPlace` helper function to solve the stale header reference issue in `SSEClientTransport`. It mutates the header object in-place, ensuring the transport sees all updates, while carefully preserving the original `Accept` header required by the transport.
- in `/mcp GET`, `/mcp POST`, and `/message POST`, use `updateHeadersInPlace` to replace current headers with new headers, retaining the Accept header.
---
server/src/index.ts | 40 +++++++++++++++++++++++++++++++++++++---
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/server/src/index.ts b/server/src/index.ts
index 5d28acca..3f739f39 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -93,6 +93,31 @@ const getHttpHeaders = (req: express.Request): Record => {
return headers;
};
+/**
+ * Updates a headers object in-place, preserving the original Accept header.
+ * This is necessary to ensure that transports holding a reference to the headers
+ * object see the updates.
+ * @param currentHeaders The headers object to update.
+ * @param newHeaders The new headers to apply.
+ */
+const updateHeadersInPlace = (
+ currentHeaders: Record,
+ newHeaders: Record,
+) => {
+ // Preserve the Accept header, which is set at transport creation and
+ // is not present in subsequent client requests.
+ const accept = currentHeaders["Accept"];
+
+ // Clear the old headers and apply the new ones.
+ Object.keys(currentHeaders).forEach((key) => delete currentHeaders[key]);
+ Object.assign(currentHeaders, newHeaders);
+
+ // Restore the Accept header.
+ if (accept) {
+ currentHeaders["Accept"] = accept;
+ }
+};
+
const app = express();
app.use(cors());
app.use((req, res, next) => {
@@ -292,7 +317,10 @@ app.get(
const headerHolder = sessionHeaderHolders.get(sessionId);
if (headerHolder) {
- headerHolder.headers = getHttpHeaders(req);
+ updateHeadersInPlace(
+ headerHolder.headers as Record,
+ getHttpHeaders(req),
+ );
}
try {
@@ -323,7 +351,10 @@ app.post(
console.log(`Received POST message for sessionId ${sessionId}`);
const headerHolder = sessionHeaderHolders.get(sessionId);
if (headerHolder) {
- headerHolder.headers = getHttpHeaders(req);
+ updateHeadersInPlace(
+ headerHolder.headers as Record,
+ getHttpHeaders(req),
+ );
}
try {
@@ -598,7 +629,10 @@ app.post(
const headerHolder = sessionHeaderHolders.get(sessionId);
if (headerHolder) {
- headerHolder.headers = getHttpHeaders(req);
+ updateHeadersInPlace(
+ headerHolder.headers as Record,
+ getHttpHeaders(req),
+ );
}
const transport = webAppTransports.get(sessionId) as SSEServerTransport;
From d728d546148eea27ac77c9e7e3ed8525b6ae78ea Mon Sep 17 00:00:00 2001
From: Wei He
Date: Sat, 6 Sep 2025 23:47:54 -0400
Subject: [PATCH 120/140] fix: toast message when copying server entry for a
Streamable HTTP transport URL
---
client/src/components/Sidebar.tsx | 4 +++-
client/src/components/__tests__/Sidebar.test.tsx | 15 +++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx
index a44c6e29..2f53b3aa 100644
--- a/client/src/components/Sidebar.tsx
+++ b/client/src/components/Sidebar.tsx
@@ -175,7 +175,9 @@ const Sidebar = ({
description:
transportType === "stdio"
? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name."
- : "SSE URL has been copied. Use this URL directly in your MCP Client.",
+ : transportType === "streamable-http"
+ ? "Streamable HTTP URL has been copied. Use this URL directly in your MCP Client."
+ : "SSE URL has been copied. Use this URL directly in your MCP Client.",
});
setTimeout(() => {
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx
index 8f693717..786584d2 100644
--- a/client/src/components/__tests__/Sidebar.test.tsx
+++ b/client/src/components/__tests__/Sidebar.test.tsx
@@ -446,6 +446,11 @@ describe("Sidebar", () => {
4,
);
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
+ expect(mockToast).toHaveBeenCalledWith({
+ title: "Config entry copied",
+ description:
+ "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name.",
+ });
});
it("should copy servers file configuration to clipboard for STDIO transport", async () => {
@@ -504,6 +509,11 @@ describe("Sidebar", () => {
4,
);
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
+ expect(mockToast).toHaveBeenCalledWith({
+ title: "Config entry copied",
+ description:
+ "SSE URL has been copied. Use this URL directly in your MCP Client.",
+ });
});
it("should copy servers file configuration to clipboard for SSE transport", async () => {
@@ -554,6 +564,11 @@ describe("Sidebar", () => {
4,
);
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
+ expect(mockToast).toHaveBeenCalledWith({
+ title: "Config entry copied",
+ description:
+ "Streamable HTTP URL has been copied. Use this URL directly in your MCP Client.",
+ });
});
it("should copy servers file configuration to clipboard for streamable-http transport", async () => {
From 0caaeed9ce49f0f9d106643fc8bb99ddec57a6af Mon Sep 17 00:00:00 2001
From: Wei He
Date: Sat, 6 Sep 2025 23:57:16 -0400
Subject: [PATCH 121/140] docs: add streamable transport examples
---
README.md | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index fc563f91..d1922f94 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,16 @@ The MCP Inspector provides convenient buttons to export server launch configurat
}
```
+ **Streamable HTTP transport example:**
+
+ ```json
+ {
+ "type": "streamable-http",
+ "url": "http://localhost:3000/mcp",
+ "note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
+ }
+ ```
+
- **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`.
**STDIO transport example:**
@@ -131,9 +141,22 @@ The MCP Inspector provides convenient buttons to export server launch configurat
}
```
+ **Streamable HTTP transport example:**
+ ```json
+ {
+ "mcpServers": {
+ "default-server": {
+ "type": "streamable-http",
+ "url": "http://localhost:3000/mcp",
+ "note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
+ }
+ }
+ }
+ ```
+
These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.
-For SSE transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the SSE URL configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the SSE URL for direct use in clients.
+For SSE and Streamable HTTP transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the URL for direct use in clients.
You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file.
From 1207834b7eccd56692fd071de8be5d5f8a9f1492 Mon Sep 17 00:00:00 2001
From: Wei He
Date: Sun, 7 Sep 2025 00:27:56 -0400
Subject: [PATCH 122/140] docs: add streamable transport examples
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index d1922f94..29037550 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ The MCP Inspector provides convenient buttons to export server launch configurat
```
**Streamable HTTP transport example:**
+
```json
{
"mcpServers": {
From b476ece67eafe2d30bd8ba604cdb392b7a4e2bda Mon Sep 17 00:00:00 2001
From: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
Date: Mon, 8 Sep 2025 11:50:36 +0100
Subject: [PATCH 123/140] Update issue templates
---
.github/ISSUE_TEMPLATE/bug_report.md | 34 ++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..28e0bbd5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,34 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Inspector Version**
+- [e.g. 0.16.5)
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Environment (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+
+**Additional context**
+Add any other context about the problem here.
From c5acc6312921fdd9009796113488400c2ac1d14c Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Tue, 26 Aug 2025 18:51:06 +0530
Subject: [PATCH 124/140] Add support for custom headers and migrate from
legacy auth
- Introduced CustomHeaders component for managing custom headers.
- Updated Sidebar to include CustomHeaders component.
- Enhanced useConnection hook to handle custom headers and legacy auth migration.
- Added tests for custom headers functionality.
- Updated package.json and package-lock.json for new dependencies.
---
client/package.json | 2 +
client/src/App.tsx | 43 ++++
client/src/components/CustomHeaders.tsx | 215 ++++++++++++++++++
client/src/components/Sidebar.tsx | 54 ++---
.../src/components/__tests__/Sidebar.test.tsx | 4 +
client/src/components/ui/switch.tsx | 27 +++
.../hooks/__tests__/useConnection.test.tsx | 130 +++++++++++
client/src/lib/hooks/useConnection.ts | 58 ++++-
client/src/lib/types/customHeaders.ts | 59 +++++
package-lock.json | 40 ++++
server/package.json | 3 +
server/src/index.ts | 17 ++
12 files changed, 605 insertions(+), 47 deletions(-)
create mode 100644 client/src/components/CustomHeaders.tsx
create mode 100644 client/src/components/ui/switch.tsx
create mode 100644 client/src/lib/types/customHeaders.ts
diff --git a/client/package.json b/client/package.json
index 61f65b62..6b84b106 100644
--- a/client/package.json
+++ b/client/package.json
@@ -33,6 +33,7 @@
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
@@ -69,6 +70,7 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
+ "jest-fixed-jsdom": "^0.0.9",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 60ce90c5..ff44759e 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -74,6 +74,7 @@ import ElicitationTab, {
PendingElicitationRequest,
ElicitationResponse,
} from "./components/ElicitationTab";
+import { CustomHeaders, migrateFromLegacyAuth } from "./lib/types/customHeaders";
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
@@ -127,6 +128,28 @@ const App = () => {
return localStorage.getItem("lastOauthScope") || "";
});
+ // Custom headers state with migration from legacy auth
+ const [customHeaders, setCustomHeaders] = useState(() => {
+ const savedHeaders = localStorage.getItem("lastCustomHeaders");
+ if (savedHeaders) {
+ try {
+ return JSON.parse(savedHeaders);
+ } catch {
+ // Fall back to migration if JSON parsing fails
+ }
+ }
+
+ // Migrate from legacy auth if available
+ const legacyToken = localStorage.getItem("lastBearerToken") || "";
+ const legacyHeaderName = localStorage.getItem("lastHeaderName") || "";
+
+ if (legacyToken) {
+ return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
+ }
+
+ return [];
+ });
+
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
PendingRequest & {
@@ -215,6 +238,7 @@ const App = () => {
env,
bearerToken,
headerName,
+ customHeaders,
oauthClientId,
oauthScope,
config,
@@ -310,6 +334,23 @@ const App = () => {
localStorage.setItem("lastHeaderName", headerName);
}, [headerName]);
+ useEffect(() => {
+ localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders));
+ }, [customHeaders]);
+
+ // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists
+ useEffect(() => {
+ if (customHeaders.length === 0 && (bearerToken || headerName)) {
+ const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName);
+ if (migratedHeaders.length > 0) {
+ setCustomHeaders(migratedHeaders);
+ // Clear legacy auth after migration
+ setBearerToken("");
+ setHeaderName("");
+ }
+ }
+ }, [bearerToken, headerName, customHeaders, setCustomHeaders]);
+
useEffect(() => {
localStorage.setItem("lastOauthClientId", oauthClientId);
}, [oauthClientId]);
@@ -814,6 +855,8 @@ const App = () => {
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
+ customHeaders={customHeaders}
+ setCustomHeaders={setCustomHeaders}
oauthClientId={oauthClientId}
setOauthClientId={setOauthClientId}
oauthScope={oauthScope}
diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx
new file mode 100644
index 00000000..9196480e
--- /dev/null
+++ b/client/src/components/CustomHeaders.tsx
@@ -0,0 +1,215 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Switch } from "@/components/ui/switch";
+import { Plus, Trash2, Eye, EyeOff } from "lucide-react";
+import { CustomHeaders as CustomHeadersType, CustomHeader, createEmptyHeader } from "@/lib/types/customHeaders";
+
+interface CustomHeadersProps {
+ headers: CustomHeadersType;
+ onChange: (headers: CustomHeadersType) => void;
+ className?: string;
+}
+
+const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) => {
+ const [isJsonMode, setIsJsonMode] = useState(false);
+ const [jsonValue, setJsonValue] = useState("");
+ const [jsonError, setJsonError] = useState(null);
+ const [visibleValues, setVisibleValues] = useState>(new Set());
+
+ const updateHeader = (index: number, field: keyof CustomHeader, value: string | boolean) => {
+ const newHeaders = [...headers];
+ newHeaders[index] = { ...newHeaders[index], [field]: value };
+ onChange(newHeaders);
+ };
+
+ const addHeader = () => {
+ onChange([...headers, createEmptyHeader()]);
+ };
+
+ const removeHeader = (index: number) => {
+ const newHeaders = headers.filter((_, i) => i !== index);
+ onChange(newHeaders);
+ };
+
+ const toggleValueVisibility = (index: number) => {
+ const newVisible = new Set(visibleValues);
+ if (newVisible.has(index)) {
+ newVisible.delete(index);
+ } else {
+ newVisible.add(index);
+ }
+ setVisibleValues(newVisible);
+ };
+
+ const switchToJsonMode = () => {
+ const jsonObject: Record = {};
+ headers.forEach((header) => {
+ if (header.enabled && header.name.trim() && header.value.trim()) {
+ jsonObject[header.name.trim()] = header.value.trim();
+ }
+ });
+ setJsonValue(JSON.stringify(jsonObject, null, 2));
+ setJsonError(null);
+ setIsJsonMode(true);
+ };
+
+ const switchToFormMode = () => {
+ try {
+ const parsed = JSON.parse(jsonValue);
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+ setJsonError("JSON must be an object with string key-value pairs");
+ return;
+ }
+
+ const newHeaders: CustomHeadersType = Object.entries(parsed).map(([name, value]) => ({
+ name,
+ value: String(value),
+ enabled: true,
+ }));
+
+ onChange(newHeaders);
+ setJsonError(null);
+ setIsJsonMode(false);
+ } catch (error) {
+ setJsonError("Invalid JSON format");
+ }
+ };
+
+ const handleJsonChange = (value: string) => {
+ setJsonValue(value);
+ setJsonError(null);
+ };
+
+ if (isJsonMode) {
+ return (
+
+
+
Custom Headers (JSON)
+
+ Switch to Form
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {headers.length === 0 ? (
+
+
No custom headers configured
+
Click "Add" to get started
+
+ ) : (
+
+ {headers.map((header, index) => (
+
+
updateHeader(index, "enabled", enabled)}
+ className="shrink-0 mt-2"
+ />
+
+ removeHeader(index)}
+ className="shrink-0 text-red-600 hover:text-red-700 hover:bg-red-50 h-6 w-6 p-0 mt-2"
+ >
+
+
+
+ ))}
+
+ )}
+
+ {headers.length > 0 && (
+
+ Use the toggle to enable/disable headers. Only enabled headers with both name and value will be sent.
+
+ )}
+
+ );
+};
+
+export default CustomHeaders;
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx
index a44c6e29..e7b101ec 100644
--- a/client/src/components/Sidebar.tsx
+++ b/client/src/components/Sidebar.tsx
@@ -37,6 +37,8 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
+import CustomHeaders from "./CustomHeaders";
+import { CustomHeaders as CustomHeadersType } from "@/lib/types/customHeaders";
import { useToast } from "../lib/hooks/useToast";
interface SidebarProps {
@@ -51,10 +53,14 @@ interface SidebarProps {
setSseUrl: (url: string) => void;
env: Record;
setEnv: (env: Record) => void;
+ // Legacy auth props (for backward compatibility)
bearerToken: string;
setBearerToken: (token: string) => void;
headerName?: string;
setHeaderName?: (name: string) => void;
+ // New custom headers support
+ customHeaders: CustomHeadersType;
+ setCustomHeaders: (headers: CustomHeadersType) => void;
oauthClientId: string;
setOauthClientId: (id: string) => void;
oauthScope: string;
@@ -80,10 +86,12 @@ const Sidebar = ({
setSseUrl,
env,
setEnv,
- bearerToken,
- setBearerToken,
- headerName,
- setHeaderName,
+ bearerToken: _bearerToken,
+ setBearerToken: _setBearerToken,
+ headerName: _headerName,
+ setHeaderName: _setHeaderName,
+ customHeaders,
+ setCustomHeaders,
oauthClientId,
setOauthClientId,
oauthScope,
@@ -497,38 +505,12 @@ const Sidebar = ({
{showAuthConfig && (
<>
- {/* Bearer Token Section */}
-
-
- API Token Authentication
-
-
- Header Name
-
- setHeaderName && setHeaderName(e.target.value)
- }
- data-testid="header-input"
- className="font-mono"
- value={headerName}
- />
-
- Bearer Token
-
- setBearerToken(e.target.value)}
- data-testid="bearer-token-input"
- className="font-mono"
- type="password"
- />
-
+ {/* Custom Headers Section */}
+
+
{transportType !== "stdio" && (
// OAuth Configuration
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx
index 8f693717..1cb221c5 100644
--- a/client/src/components/__tests__/Sidebar.test.tsx
+++ b/client/src/components/__tests__/Sidebar.test.tsx
@@ -50,6 +50,10 @@ describe("Sidebar", () => {
setEnv: jest.fn(),
bearerToken: "",
setBearerToken: jest.fn(),
+ headerName: "",
+ setHeaderName: jest.fn(),
+ customHeaders: [],
+ setCustomHeaders: jest.fn(),
onConnect: jest.fn(),
onDisconnect: jest.fn(),
stdErrNotifications: [],
diff --git a/client/src/components/ui/switch.tsx b/client/src/components/ui/switch.tsx
new file mode 100644
index 00000000..f15014bd
--- /dev/null
+++ b/client/src/components/ui/switch.tsx
@@ -0,0 +1,27 @@
+import * as React from "react";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/utils";
+
+const Switch = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx
index 624583dc..c2db0cde 100644
--- a/client/src/lib/hooks/__tests__/useConnection.test.tsx
+++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx
@@ -13,6 +13,7 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { discoverScopes } from "../../auth";
+import { CustomHeaders } from "../../types/customHeaders";
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
@@ -740,6 +741,135 @@ describe("useConnection", () => {
});
});
+ describe("Custom Headers", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset the mock transport objects
+ mockSSETransport.url = undefined;
+ mockSSETransport.options = undefined;
+ mockStreamableHTTPTransport.url = undefined;
+ mockStreamableHTTPTransport.options = undefined;
+ });
+
+ test("sends multiple custom headers correctly", async () => {
+ const customHeaders: CustomHeaders = [
+ { name: "Authorization", value: "Bearer token123", enabled: true },
+ { name: "X-Tenant-ID", value: "acme-inc", enabled: true },
+ { name: "X-Environment", value: "staging", enabled: true },
+ ];
+
+ const propsWithCustomHeaders = {
+ ...defaultProps,
+ customHeaders,
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithCustomHeaders),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that the transport was created with the correct headers
+ expect(mockSSETransport.options).toBeDefined();
+ expect(mockSSETransport.options?.requestInit?.headers).toBeDefined();
+
+ const headers = mockSSETransport.options?.requestInit?.headers;
+ expect(headers).toHaveProperty("Authorization", "Bearer token123");
+ expect(headers).toHaveProperty("X-Tenant-ID", "acme-inc");
+ expect(headers).toHaveProperty("X-Environment", "staging");
+ expect(headers).toHaveProperty("x-custom-auth-headers", JSON.stringify(["X-Tenant-ID", "X-Environment"]));
+ });
+
+ test("ignores disabled custom headers", async () => {
+ const customHeaders: CustomHeaders = [
+ { name: "Authorization", value: "Bearer token123", enabled: true },
+ { name: "X-Disabled", value: "should-not-appear", enabled: false },
+ { name: "X-Enabled", value: "should-appear", enabled: true },
+ ];
+
+ const propsWithCustomHeaders = {
+ ...defaultProps,
+ customHeaders,
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithCustomHeaders),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ const headers = mockSSETransport.options?.requestInit?.headers;
+ expect(headers).toHaveProperty("Authorization", "Bearer token123");
+ expect(headers).toHaveProperty("X-Enabled", "should-appear");
+ expect(headers).not.toHaveProperty("X-Disabled");
+ });
+
+ test("migrates from legacy bearerToken and headerName", async () => {
+ const propsWithLegacyAuth = {
+ ...defaultProps,
+ bearerToken: "legacy-token",
+ headerName: "X-Custom-Auth",
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithLegacyAuth),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ const headers = mockSSETransport.options?.requestInit?.headers;
+ expect(headers).toHaveProperty("X-Custom-Auth", "legacy-token");
+ expect(headers).toHaveProperty("x-custom-auth-headers", JSON.stringify(["X-Custom-Auth"]));
+ });
+
+ test("uses OAuth token when no custom headers or legacy auth provided", async () => {
+ const propsWithoutAuth = {
+ ...defaultProps,
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithoutAuth),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ const headers = mockSSETransport.options?.requestInit?.headers;
+ expect(headers).toHaveProperty("Authorization", "Bearer mock-token");
+ });
+
+ test("prioritizes custom headers over legacy auth", async () => {
+ const customHeaders: CustomHeaders = [
+ { name: "Authorization", value: "Bearer custom-token", enabled: true },
+ ];
+
+ const propsWithBothAuth = {
+ ...defaultProps,
+ customHeaders,
+ bearerToken: "legacy-token",
+ headerName: "Authorization",
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithBothAuth),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ const headers = mockSSETransport.options?.requestInit?.headers;
+ expect(headers).toHaveProperty("Authorization", "Bearer custom-token");
+ });
+ });
+
describe("OAuth Error Handling with Scope Discovery", () => {
beforeEach(() => {
jest.clearAllMocks();
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index bfb72941..04778157 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -57,6 +57,7 @@ import {
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
+import { CustomHeaders, migrateFromLegacyAuth } from "../types/customHeaders";
interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
@@ -64,8 +65,11 @@ interface UseConnectionOptions {
args: string;
sseUrl: string;
env: Record;
+ // Legacy auth support (for backward compatibility)
bearerToken?: string;
headerName?: string;
+ // New custom headers support
+ customHeaders?: CustomHeaders;
oauthClientId?: string;
oauthScope?: string;
config: InspectorConfig;
@@ -88,6 +92,7 @@ export function useConnection({
env,
bearerToken,
headerName,
+ customHeaders,
oauthClientId,
oauthScope,
config,
@@ -379,21 +384,52 @@ export function useConnection({
// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
- // Use manually provided bearer token if available, otherwise use OAuth tokens
- const token =
- bearerToken || (await serverAuthProvider.tokens())?.access_token;
- if (token) {
- const authHeaderName = headerName || "Authorization";
+ // Handle custom headers (new approach) or legacy auth (backward compatibility)
+ let finalHeaders: CustomHeaders = [];
- // Add custom header name as a special request header to let the server know which header to pass through
- if (authHeaderName.toLowerCase() !== "authorization") {
- headers[authHeaderName] = token;
- headers["x-custom-auth-header"] = authHeaderName;
- } else {
- headers[authHeaderName] = `Bearer ${token}`;
+ if (customHeaders && customHeaders.length > 0) {
+ // Use new custom headers approach
+ finalHeaders = customHeaders;
+ } else if (bearerToken || headerName) {
+ // Migrate from legacy auth approach
+ finalHeaders = migrateFromLegacyAuth(bearerToken, headerName);
+ }
+
+ // Add OAuth token if available and no custom headers are set
+ if (finalHeaders.length === 0) {
+ const oauthToken = (await serverAuthProvider.tokens())?.access_token;
+ if (oauthToken) {
+ finalHeaders = [
+ {
+ name: "Authorization",
+ value: `Bearer ${oauthToken}`,
+ enabled: true,
+ },
+ ];
}
}
+ // Process all enabled custom headers
+ const customHeaderNames: string[] = [];
+ finalHeaders.forEach((header) => {
+ if (header.enabled && header.name.trim() && header.value.trim()) {
+ const headerName = header.name.trim();
+ const headerValue = header.value.trim();
+
+ headers[headerName] = headerValue;
+
+ // Track custom header names for server processing
+ if (headerName.toLowerCase() !== "authorization") {
+ customHeaderNames.push(headerName);
+ }
+ }
+ });
+
+ // Add custom header names as a special request header for server processing
+ if (customHeaderNames.length > 0) {
+ headers["x-custom-auth-headers"] = JSON.stringify(customHeaderNames);
+ }
+
// Add proxy authentication
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
getMCPProxyAuthToken(config);
diff --git a/client/src/lib/types/customHeaders.ts b/client/src/lib/types/customHeaders.ts
new file mode 100644
index 00000000..0c82d38f
--- /dev/null
+++ b/client/src/lib/types/customHeaders.ts
@@ -0,0 +1,59 @@
+export interface CustomHeader {
+ name: string;
+ value: string;
+ enabled: boolean;
+}
+
+export type CustomHeaders = CustomHeader[];
+
+export const createEmptyHeader = (): CustomHeader => ({
+ name: "",
+ value: "",
+ enabled: true,
+});
+
+export const createHeaderFromBearerToken = (
+ bearerToken: string,
+ headerName?: string,
+): CustomHeader => ({
+ name: headerName || "Authorization",
+ value: headerName?.toLowerCase() === "authorization" || !headerName
+ ? `Bearer ${bearerToken}`
+ : bearerToken,
+ enabled: true,
+});
+
+export const getEnabledHeaders = (headers: CustomHeaders): CustomHeaders => {
+ return headers.filter(header => header.enabled && header.name.trim() && header.value.trim());
+};
+
+export const headersToRecord = (headers: CustomHeaders): Record => {
+ const enabledHeaders = getEnabledHeaders(headers);
+ const record: Record = {};
+
+ enabledHeaders.forEach(header => {
+ record[header.name.trim()] = header.value.trim();
+ });
+
+ return record;
+};
+
+export const recordToHeaders = (record: Record): CustomHeaders => {
+ return Object.entries(record).map(([name, value]) => ({
+ name,
+ value,
+ enabled: true,
+ }));
+};
+
+// Migration helper for backward compatibility
+export const migrateFromLegacyAuth = (
+ bearerToken?: string,
+ headerName?: string,
+): CustomHeaders => {
+ if (!bearerToken) {
+ return [];
+ }
+
+ return [createHeaderFromBearerToken(bearerToken, headerName)];
+};
diff --git a/package-lock.json b/package-lock.json
index 91385dd4..f5452d65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -80,6 +80,7 @@
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
@@ -119,6 +120,7 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
+ "jest-fixed-jsdom": "^0.0.9",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7",
@@ -2619,6 +2621,41 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
@@ -11752,6 +11789,8 @@
"@modelcontextprotocol/sdk": "^1.17.5",
"cors": "^2.8.5",
"express": "^5.1.0",
+ "shell-quote": "^1.8.3",
+ "spawn-rx": "^5.1.2",
"ws": "^8.18.0",
"zod": "^3.25.76"
},
@@ -11761,6 +11800,7 @@
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.23",
+ "@types/shell-quote": "^1.7.5",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
diff --git a/server/package.json b/server/package.json
index 39cc08f1..52c8c25a 100644
--- a/server/package.json
+++ b/server/package.json
@@ -22,6 +22,7 @@
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.23",
+ "@types/shell-quote": "^1.7.5",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
@@ -30,6 +31,8 @@
"@modelcontextprotocol/sdk": "^1.17.5",
"cors": "^2.8.5",
"express": "^5.1.0",
+ "shell-quote": "^1.8.3",
+ "spawn-rx": "^5.1.2",
"ws": "^8.18.0",
"zod": "^3.25.76"
}
diff --git a/server/src/index.ts b/server/src/index.ts
index 3f739f39..27515c19 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -90,6 +90,23 @@ const getHttpHeaders = (req: express.Request): Record => {
}
}
+ // Handle multiple custom headers (new approach)
+ if (req.headers["x-custom-auth-headers"] !== undefined) {
+ try {
+ const customHeaderNames = JSON.parse(req.headers["x-custom-auth-headers"] as string) as string[];
+ if (Array.isArray(customHeaderNames)) {
+ customHeaderNames.forEach((headerName) => {
+ const lowerCaseHeaderName = headerName.toLowerCase();
+ if (req.headers[lowerCaseHeaderName] !== undefined) {
+ const value = req.headers[lowerCaseHeaderName];
+ headers[headerName] = Array.isArray(value) ? value[value.length - 1] : value;
+ }
+ });
+ }
+ } catch (error) {
+ console.warn("Failed to parse x-custom-auth-headers:", error);
+ }
+ }
return headers;
};
From cb8c45456b5a81f9aa582ef05e2d760a861ec46b Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Mon, 1 Sep 2025 12:15:37 +0530
Subject: [PATCH 125/140] refactor: improve code formatting and readability
across multiple files
---
client/src/App.tsx | 5 +-
client/src/components/CustomHeaders.tsx | 55 +++++++++++++------
client/src/components/ui/switch.tsx | 4 +-
.../hooks/__tests__/useConnection.test.tsx | 22 ++++----
client/src/lib/types/customHeaders.ts | 27 +++++----
server/src/index.ts | 8 ++-
6 files changed, 79 insertions(+), 42 deletions(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index ff44759e..17c50435 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -74,7 +74,10 @@ import ElicitationTab, {
PendingElicitationRequest,
ElicitationResponse,
} from "./components/ElicitationTab";
-import { CustomHeaders, migrateFromLegacyAuth } from "./lib/types/customHeaders";
+import {
+ CustomHeaders,
+ migrateFromLegacyAuth,
+} from "./lib/types/customHeaders";
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx
index 9196480e..35b12212 100644
--- a/client/src/components/CustomHeaders.tsx
+++ b/client/src/components/CustomHeaders.tsx
@@ -4,7 +4,11 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Plus, Trash2, Eye, EyeOff } from "lucide-react";
-import { CustomHeaders as CustomHeadersType, CustomHeader, createEmptyHeader } from "@/lib/types/customHeaders";
+import {
+ CustomHeaders as CustomHeadersType,
+ CustomHeader,
+ createEmptyHeader,
+} from "@/lib/types/customHeaders";
interface CustomHeadersProps {
headers: CustomHeadersType;
@@ -12,13 +16,21 @@ interface CustomHeadersProps {
className?: string;
}
-const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) => {
+const CustomHeaders = ({
+ headers,
+ onChange,
+ className,
+}: CustomHeadersProps) => {
const [isJsonMode, setIsJsonMode] = useState(false);
const [jsonValue, setJsonValue] = useState("");
const [jsonError, setJsonError] = useState(null);
const [visibleValues, setVisibleValues] = useState>(new Set());
- const updateHeader = (index: number, field: keyof CustomHeader, value: string | boolean) => {
+ const updateHeader = (
+ index: number,
+ field: keyof CustomHeader,
+ value: string | boolean,
+ ) => {
const newHeaders = [...headers];
newHeaders[index] = { ...newHeaders[index], [field]: value };
onChange(newHeaders);
@@ -58,16 +70,22 @@ const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) =>
const switchToFormMode = () => {
try {
const parsed = JSON.parse(jsonValue);
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+ if (
+ typeof parsed !== "object" ||
+ parsed === null ||
+ Array.isArray(parsed)
+ ) {
setJsonError("JSON must be an object with string key-value pairs");
return;
}
- const newHeaders: CustomHeadersType = Object.entries(parsed).map(([name, value]) => ({
- name,
- value: String(value),
- enabled: true,
- }));
+ const newHeaders: CustomHeadersType = Object.entries(parsed).map(
+ ([name, value]) => ({
+ name,
+ value: String(value),
+ enabled: true,
+ }),
+ );
onChange(newHeaders);
setJsonError(null);
@@ -86,7 +104,9 @@ const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) =>
return (
-
Custom Headers (JSON)
+
+ Custom Headers (JSON)
+
placeholder='{\n "Authorization": "Bearer token123",\n "X-Tenant-ID": "acme-inc",\n "X-Environment": "staging"\n}'
className="font-mono text-sm min-h-[100px] resize-none"
/>
- {jsonError && (
- {jsonError}
- )}
+ {jsonError && {jsonError}
}
Enter headers as a JSON object with string key-value pairs.
@@ -156,7 +174,9 @@ const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) =>
>
updateHeader(index, "enabled", enabled)}
+ onCheckedChange={(enabled) =>
+ updateHeader(index, "enabled", enabled)
+ }
className="shrink-0 mt-2"
/>
@@ -170,7 +190,9 @@ const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) =>
updateHeader(index, "value", e.target.value)}
+ onChange={(e) =>
+ updateHeader(index, "value", e.target.value)
+ }
type={visibleValues.has(index) ? "text" : "password"}
className="font-mono text-xs pr-8"
/>
@@ -205,7 +227,8 @@ const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) =>
{headers.length > 0 && (
- Use the toggle to enable/disable headers. Only enabled headers with both name and value will be sent.
+ Use the toggle to enable/disable headers. Only enabled headers with
+ both name and value will be sent.
)}
diff --git a/client/src/components/ui/switch.tsx b/client/src/components/ui/switch.tsx
index f15014bd..6338184e 100644
--- a/client/src/components/ui/switch.tsx
+++ b/client/src/components/ui/switch.tsx
@@ -10,14 +10,14 @@ const Switch = React.forwardRef<
diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx
index c2db0cde..8047bed8 100644
--- a/client/src/lib/hooks/__tests__/useConnection.test.tsx
+++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx
@@ -779,7 +779,10 @@ describe("useConnection", () => {
expect(headers).toHaveProperty("Authorization", "Bearer token123");
expect(headers).toHaveProperty("X-Tenant-ID", "acme-inc");
expect(headers).toHaveProperty("X-Environment", "staging");
- expect(headers).toHaveProperty("x-custom-auth-headers", JSON.stringify(["X-Tenant-ID", "X-Environment"]));
+ expect(headers).toHaveProperty(
+ "x-custom-auth-headers",
+ JSON.stringify(["X-Tenant-ID", "X-Environment"]),
+ );
});
test("ignores disabled custom headers", async () => {
@@ -815,9 +818,7 @@ describe("useConnection", () => {
headerName: "X-Custom-Auth",
};
- const { result } = renderHook(() =>
- useConnection(propsWithLegacyAuth),
- );
+ const { result } = renderHook(() => useConnection(propsWithLegacyAuth));
await act(async () => {
await result.current.connect();
@@ -825,7 +826,10 @@ describe("useConnection", () => {
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("X-Custom-Auth", "legacy-token");
- expect(headers).toHaveProperty("x-custom-auth-headers", JSON.stringify(["X-Custom-Auth"]));
+ expect(headers).toHaveProperty(
+ "x-custom-auth-headers",
+ JSON.stringify(["X-Custom-Auth"]),
+ );
});
test("uses OAuth token when no custom headers or legacy auth provided", async () => {
@@ -833,9 +837,7 @@ describe("useConnection", () => {
...defaultProps,
};
- const { result } = renderHook(() =>
- useConnection(propsWithoutAuth),
- );
+ const { result } = renderHook(() => useConnection(propsWithoutAuth));
await act(async () => {
await result.current.connect();
@@ -857,9 +859,7 @@ describe("useConnection", () => {
headerName: "Authorization",
};
- const { result } = renderHook(() =>
- useConnection(propsWithBothAuth),
- );
+ const { result } = renderHook(() => useConnection(propsWithBothAuth));
await act(async () => {
await result.current.connect();
diff --git a/client/src/lib/types/customHeaders.ts b/client/src/lib/types/customHeaders.ts
index 0c82d38f..3443727a 100644
--- a/client/src/lib/types/customHeaders.ts
+++ b/client/src/lib/types/customHeaders.ts
@@ -17,28 +17,35 @@ export const createHeaderFromBearerToken = (
headerName?: string,
): CustomHeader => ({
name: headerName || "Authorization",
- value: headerName?.toLowerCase() === "authorization" || !headerName
- ? `Bearer ${bearerToken}`
- : bearerToken,
+ value:
+ headerName?.toLowerCase() === "authorization" || !headerName
+ ? `Bearer ${bearerToken}`
+ : bearerToken,
enabled: true,
});
export const getEnabledHeaders = (headers: CustomHeaders): CustomHeaders => {
- return headers.filter(header => header.enabled && header.name.trim() && header.value.trim());
+ return headers.filter(
+ (header) => header.enabled && header.name.trim() && header.value.trim(),
+ );
};
-export const headersToRecord = (headers: CustomHeaders): Record => {
+export const headersToRecord = (
+ headers: CustomHeaders,
+): Record => {
const enabledHeaders = getEnabledHeaders(headers);
const record: Record = {};
-
- enabledHeaders.forEach(header => {
+
+ enabledHeaders.forEach((header) => {
record[header.name.trim()] = header.value.trim();
});
-
+
return record;
};
-export const recordToHeaders = (record: Record): CustomHeaders => {
+export const recordToHeaders = (
+ record: Record,
+): CustomHeaders => {
return Object.entries(record).map(([name, value]) => ({
name,
value,
@@ -54,6 +61,6 @@ export const migrateFromLegacyAuth = (
if (!bearerToken) {
return [];
}
-
+
return [createHeaderFromBearerToken(bearerToken, headerName)];
};
diff --git a/server/src/index.ts b/server/src/index.ts
index 27515c19..af3ea928 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -93,13 +93,17 @@ const getHttpHeaders = (req: express.Request): Record => {
// Handle multiple custom headers (new approach)
if (req.headers["x-custom-auth-headers"] !== undefined) {
try {
- const customHeaderNames = JSON.parse(req.headers["x-custom-auth-headers"] as string) as string[];
+ const customHeaderNames = JSON.parse(
+ req.headers["x-custom-auth-headers"] as string,
+ ) as string[];
if (Array.isArray(customHeaderNames)) {
customHeaderNames.forEach((headerName) => {
const lowerCaseHeaderName = headerName.toLowerCase();
if (req.headers[lowerCaseHeaderName] !== undefined) {
const value = req.headers[lowerCaseHeaderName];
- headers[headerName] = Array.isArray(value) ? value[value.length - 1] : value;
+ headers[headerName] = Array.isArray(value)
+ ? value[value.length - 1]
+ : value;
}
});
}
From 6f727b6662594651b57d1d633377485a750781b5 Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Sun, 7 Sep 2025 14:22:14 +0530
Subject: [PATCH 126/140] fix: update error handling in JSON parsing and add
ESLint rule for unused variables
---
client/eslint.config.js | 4 ++++
client/src/components/CustomHeaders.tsx | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/client/eslint.config.js b/client/eslint.config.js
index 79a552ea..c689c75d 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -23,6 +23,10 @@ export default tseslint.config(
"warn",
{ allowConstantExport: true },
],
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ { argsIgnorePattern: "^_" },
+ ],
},
},
);
diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx
index 35b12212..67aee18d 100644
--- a/client/src/components/CustomHeaders.tsx
+++ b/client/src/components/CustomHeaders.tsx
@@ -90,7 +90,7 @@ const CustomHeaders = ({
onChange(newHeaders);
setJsonError(null);
setIsJsonMode(false);
- } catch (error) {
+ } catch {
setJsonError("Invalid JSON format");
}
};
From fde55efc2f000f76d4b2f244482a4e19ce0104ea Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Wed, 10 Sep 2025 18:50:15 +0530
Subject: [PATCH 127/140] fix: standardize formatting in bug report issue
template
---
.github/ISSUE_TEMPLATE/bug_report.md | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 28e0bbd5..0db5467a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,13 +1,13 @@
---
name: Bug report
about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
+title: ""
+labels: ""
+assignees: ""
---
**Inspector Version**
+
- [e.g. 0.16.5)
**Describe the bug**
@@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -27,8 +28,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here.
From 90ab419d124e1a880d8150fbcf05d9b9d15f764e Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Wed, 10 Sep 2025 21:01:24 +0530
Subject: [PATCH 128/140] fix: enhance error handling for custom headers and
improve test coverage for header management
---
client/src/App.tsx | 6 +-
client/src/components/CustomHeaders.tsx | 3 +
.../src/components/__tests__/Sidebar.test.tsx | 195 ++++++++++++++----
3 files changed, 163 insertions(+), 41 deletions(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 17c50435..1454811a 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -137,7 +137,11 @@ const App = () => {
if (savedHeaders) {
try {
return JSON.parse(savedHeaders);
- } catch {
+ } catch (error) {
+ console.warn(
+ `Failed to parse custom headers: "${savedHeaders}", will try legacy migration`,
+ error,
+ );
// Fall back to migration if JSON parsing fails
}
}
diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx
index 67aee18d..463f7333 100644
--- a/client/src/components/CustomHeaders.tsx
+++ b/client/src/components/CustomHeaders.tsx
@@ -153,6 +153,7 @@ const CustomHeaders = ({
size="sm"
onClick={addHeader}
className="text-xs px-2"
+ data-testid="add-header-button"
>
Add
@@ -185,6 +186,7 @@ const CustomHeaders = ({
value={header.name}
onChange={(e) => updateHeader(index, "name", e.target.value)}
className="font-mono text-xs"
+ data-testid={`header-name-input-${index}`}
/>
{
fireEvent.click(button);
};
- it("should update bearer token", () => {
- const setBearerToken = jest.fn();
+ it("should update bearer token via custom headers", async () => {
+ const setCustomHeaders = jest.fn();
renderSidebar({
- bearerToken: "",
- setBearerToken,
+ customHeaders: [],
+ setCustomHeaders,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
- const tokenInput = screen.getByTestId("bearer-token-input");
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
+ // Add a new header
+ const addButton = screen.getByTestId("add-header-button");
+ fireEvent.click(addButton);
- expect(setBearerToken).toHaveBeenCalledWith("new_token");
+ // Verify that setCustomHeaders was called to add an empty header
+ expect(setCustomHeaders).toHaveBeenCalledWith([
+ {
+ name: "",
+ value: "",
+ enabled: true,
+ },
+ ]);
});
- it("should update header name", () => {
- const setHeaderName = jest.fn();
+ it("should update header name via custom headers", () => {
+ const setCustomHeaders = jest.fn();
renderSidebar({
- headerName: "Authorization",
- setHeaderName,
+ customHeaders: [
+ {
+ name: "Authorization",
+ value: "Bearer token123",
+ enabled: true,
+ },
+ ],
+ setCustomHeaders,
transportType: "sse",
});
openAuthSection();
- const headerInput = screen.getByTestId("header-input");
- fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
+ const headerNameInput = screen.getByTestId("header-name-input-0");
+ fireEvent.change(headerNameInput, { target: { value: "X-Custom-Auth" } });
- expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
+ expect(setCustomHeaders).toHaveBeenCalledWith([
+ {
+ name: "X-Custom-Auth",
+ value: "Bearer token123",
+ enabled: true,
+ },
+ ]);
});
- it("should clear bearer token", () => {
- const setBearerToken = jest.fn();
+ it("should clear bearer token via custom headers", () => {
+ const setCustomHeaders = jest.fn();
renderSidebar({
- bearerToken: "existing_token",
- setBearerToken,
+ customHeaders: [
+ {
+ name: "Authorization",
+ value: "Bearer existing_token",
+ enabled: true,
+ },
+ ],
+ setCustomHeaders,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
- const tokenInput = screen.getByTestId("bearer-token-input");
- fireEvent.change(tokenInput, { target: { value: "" } });
+ const headerValueInput = screen.getByTestId("header-value-input-0");
+ fireEvent.change(headerValueInput, { target: { value: "" } });
- expect(setBearerToken).toHaveBeenCalledWith("");
+ expect(setCustomHeaders).toHaveBeenCalledWith([
+ {
+ name: "Authorization",
+ value: "",
+ enabled: true,
+ },
+ ]);
});
- it("should properly render bearer token input", () => {
+ it("should properly render header value input as password field", () => {
const { rerender } = renderSidebar({
- bearerToken: "existing_token",
+ customHeaders: [
+ {
+ name: "Authorization",
+ value: "Bearer existing_token",
+ enabled: true,
+ },
+ ],
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
- const tokenInput = screen.getByTestId("bearer-token-input");
+ const tokenInput = screen.getByTestId("header-value-input-0");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
+ fireEvent.change(tokenInput, { target: { value: "Bearer new_token" } });
// Rerender with updated token
rerender(
,
);
// Token input should still exist after update
- expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
+ expect(screen.getByTestId("header-value-input-0")).toBeInTheDocument();
});
it("should maintain token visibility state after update", () => {
const { rerender } = renderSidebar({
- bearerToken: "existing_token",
+ customHeaders: [
+ {
+ name: "Authorization",
+ value: "Bearer existing_token",
+ enabled: true,
+ },
+ ],
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
- const tokenInput = screen.getByTestId("bearer-token-input");
+ const tokenInput = screen.getByTestId("header-value-input-0");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
+ fireEvent.change(tokenInput, { target: { value: "Bearer new_token" } });
// Rerender with updated token
rerender(
,
);
// Token input should still exist after update
- expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
+ expect(screen.getByTestId("header-value-input-0")).toBeInTheDocument();
});
it("should maintain header name when toggling auth section", () => {
renderSidebar({
- headerName: "X-API-Key",
+ customHeaders: [
+ {
+ name: "X-API-Key",
+ value: "api-key-123",
+ enabled: true,
+ },
+ ],
transportType: "sse",
});
@@ -741,7 +809,7 @@ describe("Sidebar", () => {
openAuthSection();
// Verify header name is displayed
- const headerInput = screen.getByTestId("header-input");
+ const headerInput = screen.getByTestId("header-name-input-0");
expect(headerInput).toHaveValue("X-API-Key");
// Close auth section
@@ -752,19 +820,66 @@ describe("Sidebar", () => {
fireEvent.click(authButton);
// Verify header name is still preserved
- expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
+ expect(screen.getByTestId("header-name-input-0")).toHaveValue(
+ "X-API-Key",
+ );
});
- it("should display default header name when not specified", () => {
+ it("should display placeholder for header name when no headers exist", () => {
renderSidebar({
- headerName: undefined,
+ customHeaders: [],
transportType: "sse",
});
openAuthSection();
- const headerInput = screen.getByTestId("header-input");
- expect(headerInput).toHaveAttribute("placeholder", "Authorization");
+ // Verify that the "No custom headers configured" message is shown
+ expect(
+ screen.getByText("No custom headers configured"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('Click "Add" to get started'),
+ ).toBeInTheDocument();
+
+ // Verify the Add button is present
+ const addButton = screen.getByTestId("add-header-button");
+ expect(addButton).toBeInTheDocument();
+ });
+
+ it("should allow editing existing headers", () => {
+ const setCustomHeaders = jest.fn();
+ renderSidebar({
+ customHeaders: [
+ {
+ name: "Authorization",
+ value: "Bearer token123",
+ enabled: true,
+ },
+ ],
+ setCustomHeaders,
+ transportType: "sse",
+ });
+
+ openAuthSection();
+
+ // Verify header inputs are rendered
+ const headerNameInput = screen.getByTestId("header-name-input-0");
+ const headerValueInput = screen.getByTestId("header-value-input-0");
+
+ expect(headerNameInput).toHaveValue("Authorization");
+ expect(headerValueInput).toHaveValue("Bearer token123");
+ expect(headerNameInput).toHaveAttribute("placeholder", "Header Name");
+ expect(headerValueInput).toHaveAttribute("placeholder", "Header Value");
+
+ // Update header name
+ fireEvent.change(headerNameInput, { target: { value: "X-API-Key" } });
+ expect(setCustomHeaders).toHaveBeenCalledWith([
+ {
+ name: "X-API-Key",
+ value: "Bearer token123",
+ enabled: true,
+ },
+ ]);
});
});
From f1c78fdde7181a2efb7708481123ba296cfd0c0f Mon Sep 17 00:00:00 2001
From: David Soria Parra
Date: Wed, 10 Sep 2025 20:40:49 +0100
Subject: [PATCH 129/140] fix prettier
---
.github/ISSUE_TEMPLATE/bug_report.md | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 28e0bbd5..0db5467a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,13 +1,13 @@
---
name: Bug report
about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
+title: ""
+labels: ""
+assignees: ""
---
**Inspector Version**
+
- [e.g. 0.16.5)
**Describe the bug**
@@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
+
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -27,8 +28,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
+
+- OS: [e.g. iOS]
+- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here.
From ca26a4f7a9024e4ae69273076b3ae9be7023a89b Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Thu, 11 Sep 2025 16:41:03 -0400
Subject: [PATCH 130/140] Bumped TS SDK in all the packages to ^1.18.0
---
cli/package.json | 2 +-
client/package.json | 2 +-
package-lock.json | 14 +++++++-------
package.json | 2 +-
server/package.json | 2 +-
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/cli/package.json b/cli/package.json
index 506b1856..cd88a46a 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -24,7 +24,7 @@
},
"devDependencies": {},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
}
diff --git a/client/package.json b/client/package.json
index 61f65b62..09f853d7 100644
--- a/client/package.json
+++ b/client/package.json
@@ -25,7 +25,7 @@
"cleanup:e2e": "node e2e/global-teardown.js"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
diff --git a/package-lock.json b/package-lock.json
index 91385dd4..242f0ce5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@modelcontextprotocol/inspector-cli": "^0.16.6",
"@modelcontextprotocol/inspector-client": "^0.16.6",
"@modelcontextprotocol/inspector-server": "^0.16.6",
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
"shell-quote": "^1.8.3",
@@ -49,7 +49,7 @@
"version": "0.16.6",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
},
@@ -72,7 +72,7 @@
"version": "0.16.6",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
@@ -2011,9 +2011,9 @@
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.17.5",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz",
- "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==",
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.0.tgz",
+ "integrity": "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -11749,7 +11749,7 @@
"version": "0.16.6",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"ws": "^8.18.0",
diff --git a/package.json b/package.json
index 860a4bf3..25df40c8 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"@modelcontextprotocol/inspector-cli": "^0.16.6",
"@modelcontextprotocol/inspector-client": "^0.16.6",
"@modelcontextprotocol/inspector-server": "^0.16.6",
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
"shell-quote": "^1.8.3",
diff --git a/server/package.json b/server/package.json
index 39cc08f1..0f7426a5 100644
--- a/server/package.json
+++ b/server/package.json
@@ -27,7 +27,7 @@
"typescript": "^5.6.2"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.5",
+ "@modelcontextprotocol/sdk": "^1.18.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"ws": "^8.18.0",
From 1c5f5e4839b9963e8933936f1acd8e557bdbfae7 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Fri, 12 Sep 2025 14:30:54 +0000
Subject: [PATCH 131/140] chore: bump version to 0.16.7
Co-authored-by: Ola Hungerford
---
cli/package.json | 2 +-
client/package.json | 2 +-
package-lock.json | 16 ++++++++--------
package.json | 8 ++++----
server/package.json | 2 +-
5 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/cli/package.json b/cli/package.json
index cd88a46a..caf21a3e 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.6",
+ "version": "0.16.7",
"description": "CLI for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
diff --git a/client/package.json b/client/package.json
index 09f853d7..fd6fc698 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.6",
+ "version": "0.16.7",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
diff --git a/package-lock.json b/package-lock.json
index 242f0ce5..e2070a9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.6",
+ "version": "0.16.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.6",
+ "version": "0.16.7",
"license": "MIT",
"workspaces": [
"client",
@@ -14,9 +14,9 @@
"cli"
],
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.6",
- "@modelcontextprotocol/inspector-client": "^0.16.6",
- "@modelcontextprotocol/inspector-server": "^0.16.6",
+ "@modelcontextprotocol/inspector-cli": "^0.16.7",
+ "@modelcontextprotocol/inspector-client": "^0.16.7",
+ "@modelcontextprotocol/inspector-server": "^0.16.7",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
@@ -46,7 +46,7 @@
},
"cli": {
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.6",
+ "version": "0.16.7",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
@@ -69,7 +69,7 @@
},
"client": {
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.6",
+ "version": "0.16.7",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
@@ -11746,7 +11746,7 @@
},
"server": {
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.6",
+ "version": "0.16.7",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
diff --git a/package.json b/package.json
index 25df40c8..48609a20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.6",
+ "version": "0.16.7",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -47,9 +47,9 @@
"check-version": "node scripts/check-version-consistency.js"
},
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.6",
- "@modelcontextprotocol/inspector-client": "^0.16.6",
- "@modelcontextprotocol/inspector-server": "^0.16.6",
+ "@modelcontextprotocol/inspector-cli": "^0.16.7",
+ "@modelcontextprotocol/inspector-client": "^0.16.7",
+ "@modelcontextprotocol/inspector-server": "^0.16.7",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
diff --git a/server/package.json b/server/package.json
index 0f7426a5..b46f94fb 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.6",
+ "version": "0.16.7",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
From 70ddef28f2f6255e317cf0c9eeb38842955dcfbd Mon Sep 17 00:00:00 2001
From: Basil Hosmer
Date: Fri, 12 Sep 2025 22:23:32 -0400
Subject: [PATCH 132/140] Fix OAuth redirect URI mismatch in debug flow
Register both normal and debug redirect URIs in client metadata to prevent mismatch errors when using the Auth tab's debug mode versus normal connection flow.
---
client/src/lib/auth.ts | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index c1505398..0a8d26cf 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -117,9 +117,14 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
return window.location.origin + "/oauth/callback";
}
+ get debugRedirectUrl() {
+ return window.location.origin + "/oauth/callback/debug";
+ }
+
get clientMetadata(): OAuthClientMetadata {
+ // Register both redirect URIs to support both normal and debug flows
return {
- redirect_uris: [this.redirectUrl],
+ redirect_uris: [this.redirectUrl, this.debugRedirectUrl],
token_endpoint_auth_method: "none",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
@@ -223,11 +228,13 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
}
}
-// Overrides debug URL and allows saving server OAuth metadata to
+// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to
// display in debug UI.
export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {
get redirectUrl(): string {
- return `${window.location.origin}/oauth/callback/debug`;
+ // We can use the debug redirect URL here because it was already registered
+ // in the parent class's clientMetadata along with the normal redirect URL
+ return this.debugRedirectUrl;
}
saveServerMetadata(metadata: OAuthMetadata) {
From 79fe56237a31f7bd6bdd45191e1db92ca66a6e31 Mon Sep 17 00:00:00 2001
From: Cameron Roberts
Date: Sat, 13 Sep 2025 11:34:13 +0100
Subject: [PATCH 133/140] feat: first pass at stlying fix and searching on
name/description
---
client/src/components/ListPane.tsx | 74 +++++++++++++++---------------
1 file changed, 37 insertions(+), 37 deletions(-)
diff --git a/client/src/components/ListPane.tsx b/client/src/components/ListPane.tsx
index 09c9f57d..e746a0b0 100644
--- a/client/src/components/ListPane.tsx
+++ b/client/src/components/ListPane.tsx
@@ -32,7 +32,13 @@ const ListPane = ({
if (!searchQuery.trim()) return items;
return items.filter((item) => {
- const searchableText = JSON.stringify(item).toLowerCase();
+ console.log('item : ', item);
+ const searchableText = [
+ (item as { name?: string }).name || "",
+ (item as { description?: string }).description || "",
+ ]
+ .join(" ")
+ .toLowerCase();
return searchableText.includes(searchQuery.toLowerCase());
});
}, [items, searchQuery]);
@@ -53,43 +59,37 @@ const ListPane = ({
return (
-
-
{title}
-
-
-
-
-
-
-
-
-
setSearchQuery(e.target.value)}
- onBlur={handleSearchBlur}
- className="pl-10 w-full transition-all duration-300 ease-in-out"
- />
+
+
+ {title}
+
+
+ {!isSearchExpanded ? (
+
+
+
+ ) : (
+
-
+ )}
From 36c38e7b47dbec9b1ce54bd6eeb21b16fd73b33e Mon Sep 17 00:00:00 2001
From: Cameron Roberts <40502796+cameronldroberts@users.noreply.github.com>
Date: Sun, 14 Sep 2025 00:29:54 +0100
Subject: [PATCH 134/140] Update ListPane.tsx
---
client/src/components/ListPane.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/client/src/components/ListPane.tsx b/client/src/components/ListPane.tsx
index e746a0b0..8cc2c1fe 100644
--- a/client/src/components/ListPane.tsx
+++ b/client/src/components/ListPane.tsx
@@ -32,7 +32,6 @@ const ListPane =
({
if (!searchQuery.trim()) return items;
return items.filter((item) => {
- console.log('item : ', item);
const searchableText = [
(item as { name?: string }).name || "",
(item as { description?: string }).description || "",
From e08ce8cee9425588d858b0d0a6dff8729aeed485 Mon Sep 17 00:00:00 2001
From: "jeetul.samaiya"
Date: Mon, 15 Sep 2025 20:19:56 +0530
Subject: [PATCH 135/140] feat: add default Authorization Bearer header and
clean up legacy auth
- Provide "Authorization: Bearer " as default starting header
- Remove legacy auth props from components
- Centralize migration logic and fix localStorage cleanup
- Update tests for new auth architecture
---
client/eslint.config.js | 4 ---
client/src/App.tsx | 27 ++++++++++++-------
client/src/components/Sidebar.tsx | 11 +-------
.../src/components/__tests__/Sidebar.test.tsx | 12 +--------
.../hooks/__tests__/useConnection.test.tsx | 24 ++++++++++++-----
client/src/lib/hooks/useConnection.ts | 21 +++------------
client/src/lib/types/customHeaders.ts | 8 +++---
7 files changed, 45 insertions(+), 62 deletions(-)
diff --git a/client/eslint.config.js b/client/eslint.config.js
index c689c75d..79a552ea 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -23,10 +23,6 @@ export default tseslint.config(
"warn",
{ allowConstantExport: true },
],
- "@typescript-eslint/no-unused-vars": [
- "error",
- { argsIgnorePattern: "^_" },
- ],
},
},
);
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 1454811a..4c35b0ec 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -154,7 +154,14 @@ const App = () => {
return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
}
- return [];
+ // Default to Authorization: Bearer as the most common case
+ return [
+ {
+ name: "Authorization",
+ value: "Bearer ",
+ enabled: true,
+ },
+ ];
});
const [pendingSampleRequests, setPendingSampleRequests] = useState<
@@ -243,8 +250,6 @@ const App = () => {
args,
sseUrl,
env,
- bearerToken,
- headerName,
customHeaders,
oauthClientId,
oauthScope,
@@ -334,11 +339,19 @@ const App = () => {
}, [transportType]);
useEffect(() => {
- localStorage.setItem("lastBearerToken", bearerToken);
+ if (bearerToken) {
+ localStorage.setItem("lastBearerToken", bearerToken);
+ } else {
+ localStorage.removeItem("lastBearerToken");
+ }
}, [bearerToken]);
useEffect(() => {
- localStorage.setItem("lastHeaderName", headerName);
+ if (headerName) {
+ localStorage.setItem("lastHeaderName", headerName);
+ } else {
+ localStorage.removeItem("lastHeaderName");
+ }
}, [headerName]);
useEffect(() => {
@@ -858,10 +871,6 @@ const App = () => {
setEnv={setEnv}
config={config}
setConfig={setConfig}
- bearerToken={bearerToken}
- setBearerToken={setBearerToken}
- headerName={headerName}
- setHeaderName={setHeaderName}
customHeaders={customHeaders}
setCustomHeaders={setCustomHeaders}
oauthClientId={oauthClientId}
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx
index 9806624f..c0c61fbe 100644
--- a/client/src/components/Sidebar.tsx
+++ b/client/src/components/Sidebar.tsx
@@ -53,12 +53,7 @@ interface SidebarProps {
setSseUrl: (url: string) => void;
env: Record;
setEnv: (env: Record) => void;
- // Legacy auth props (for backward compatibility)
- bearerToken: string;
- setBearerToken: (token: string) => void;
- headerName?: string;
- setHeaderName?: (name: string) => void;
- // New custom headers support
+ // Custom headers support
customHeaders: CustomHeadersType;
setCustomHeaders: (headers: CustomHeadersType) => void;
oauthClientId: string;
@@ -86,10 +81,6 @@ const Sidebar = ({
setSseUrl,
env,
setEnv,
- bearerToken: _bearerToken,
- setBearerToken: _setBearerToken,
- headerName: _headerName,
- setHeaderName: _setHeaderName,
customHeaders,
setCustomHeaders,
oauthClientId,
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx
index b6797d38..3a23daaf 100644
--- a/client/src/components/__tests__/Sidebar.test.tsx
+++ b/client/src/components/__tests__/Sidebar.test.tsx
@@ -1,10 +1,4 @@
-import {
- render,
- screen,
- fireEvent,
- act,
- waitFor,
-} from "@testing-library/react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import { describe, it, beforeEach, jest } from "@jest/globals";
import Sidebar from "../Sidebar";
@@ -54,10 +48,6 @@ describe("Sidebar", () => {
setOauthScope: jest.fn(),
env: {},
setEnv: jest.fn(),
- bearerToken: "",
- setBearerToken: jest.fn(),
- headerName: "",
- setHeaderName: jest.fn(),
customHeaders: [],
setCustomHeaders: jest.fn(),
onConnect: jest.fn(),
diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx
index 8047bed8..a94f2386 100644
--- a/client/src/lib/hooks/__tests__/useConnection.test.tsx
+++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx
@@ -649,9 +649,17 @@ describe("useConnection", () => {
});
test("preserves server Authorization header when proxy auth is configured", async () => {
+ const customHeaders: CustomHeaders = [
+ {
+ name: "Authorization",
+ value: "Bearer server-auth-token",
+ enabled: true,
+ },
+ ];
+
const propsWithBothAuth = {
...defaultProps,
- bearerToken: "server-auth-token",
+ customHeaders,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
@@ -811,14 +819,18 @@ describe("useConnection", () => {
expect(headers).not.toHaveProperty("X-Disabled");
});
- test("migrates from legacy bearerToken and headerName", async () => {
- const propsWithLegacyAuth = {
+ test("handles migrated legacy auth via custom headers", async () => {
+ // Simulate what App.tsx would do - migrate legacy auth to custom headers
+ const customHeaders: CustomHeaders = [
+ { name: "X-Custom-Auth", value: "legacy-token", enabled: true },
+ ];
+
+ const propsWithMigratedAuth = {
...defaultProps,
- bearerToken: "legacy-token",
- headerName: "X-Custom-Auth",
+ customHeaders,
};
- const { result } = renderHook(() => useConnection(propsWithLegacyAuth));
+ const { result } = renderHook(() => useConnection(propsWithMigratedAuth));
await act(async () => {
await result.current.connect();
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index 04778157..f47f0d15 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -57,7 +57,7 @@ import {
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
-import { CustomHeaders, migrateFromLegacyAuth } from "../types/customHeaders";
+import { CustomHeaders } from "../types/customHeaders";
interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
@@ -65,10 +65,7 @@ interface UseConnectionOptions {
args: string;
sseUrl: string;
env: Record;
- // Legacy auth support (for backward compatibility)
- bearerToken?: string;
- headerName?: string;
- // New custom headers support
+ // Custom headers support
customHeaders?: CustomHeaders;
oauthClientId?: string;
oauthScope?: string;
@@ -90,8 +87,6 @@ export function useConnection({
args,
sseUrl,
env,
- bearerToken,
- headerName,
customHeaders,
oauthClientId,
oauthScope,
@@ -384,16 +379,8 @@ export function useConnection({
// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
- // Handle custom headers (new approach) or legacy auth (backward compatibility)
- let finalHeaders: CustomHeaders = [];
-
- if (customHeaders && customHeaders.length > 0) {
- // Use new custom headers approach
- finalHeaders = customHeaders;
- } else if (bearerToken || headerName) {
- // Migrate from legacy auth approach
- finalHeaders = migrateFromLegacyAuth(bearerToken, headerName);
- }
+ // Use custom headers (migration is handled in App.tsx)
+ let finalHeaders: CustomHeaders = customHeaders || [];
// Add OAuth token if available and no custom headers are set
if (finalHeaders.length === 0) {
diff --git a/client/src/lib/types/customHeaders.ts b/client/src/lib/types/customHeaders.ts
index 3443727a..46e7237d 100644
--- a/client/src/lib/types/customHeaders.ts
+++ b/client/src/lib/types/customHeaders.ts
@@ -58,9 +58,7 @@ export const migrateFromLegacyAuth = (
bearerToken?: string,
headerName?: string,
): CustomHeaders => {
- if (!bearerToken) {
- return [];
- }
-
- return [createHeaderFromBearerToken(bearerToken, headerName)];
+ return bearerToken
+ ? [createHeaderFromBearerToken(bearerToken, headerName)]
+ : [];
};
From 51d28cd7b4701d715bf3957239682cae2ac9a945 Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Mon, 15 Sep 2025 18:09:44 -0400
Subject: [PATCH 136/140] When the client sends a request, do not pass on the
mcp-session-id header for the Client <-> Proxy connection. This causes a
problem for stateless SSE or StreamableHttp connections.
---
server/src/index.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/src/index.ts b/server/src/index.ts
index af3ea928..7e966d34 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -53,8 +53,8 @@ const getHttpHeaders = (req: express.Request): Record => {
lowerKey === "authorization" ||
lowerKey === "last-event-id"
) {
- // Exclude the proxy's own authentication header
- if (lowerKey !== "x-mcp-proxy-auth") {
+ // Exclude the proxy's own authentication header and the Client <-> Proxy session ID header
+ if (lowerKey !== "x-mcp-proxy-auth" && lowerKey !== "mcp-session-id") {
const value = req.headers[key];
if (typeof value === "string") {
From 7b338b3ea3fa209f3d8bea94241a4c1694cb5180 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Tue, 16 Sep 2025 03:39:40 +0000
Subject: [PATCH 137/140] chore: bump version to 0.16.8
Co-authored-by: Ola Hungerford
---
cli/package.json | 2 +-
client/package.json | 2 +-
package-lock.json | 16 ++++++++--------
package.json | 8 ++++----
server/package.json | 2 +-
5 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/cli/package.json b/cli/package.json
index caf21a3e..065f9bd6 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.7",
+ "version": "0.16.8",
"description": "CLI for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
diff --git a/client/package.json b/client/package.json
index db4358fd..7a4c2cf0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.7",
+ "version": "0.16.8",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
diff --git a/package-lock.json b/package-lock.json
index d422148b..31234e7a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.7",
+ "version": "0.16.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.7",
+ "version": "0.16.8",
"license": "MIT",
"workspaces": [
"client",
@@ -14,9 +14,9 @@
"cli"
],
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.7",
- "@modelcontextprotocol/inspector-client": "^0.16.7",
- "@modelcontextprotocol/inspector-server": "^0.16.7",
+ "@modelcontextprotocol/inspector-cli": "^0.16.8",
+ "@modelcontextprotocol/inspector-client": "^0.16.8",
+ "@modelcontextprotocol/inspector-server": "^0.16.8",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
@@ -46,7 +46,7 @@
},
"cli": {
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.7",
+ "version": "0.16.8",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
@@ -69,7 +69,7 @@
},
"client": {
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.7",
+ "version": "0.16.8",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
@@ -11783,7 +11783,7 @@
},
"server": {
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.7",
+ "version": "0.16.8",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
diff --git a/package.json b/package.json
index 48609a20..918a2222 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.7",
+ "version": "0.16.8",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -47,9 +47,9 @@
"check-version": "node scripts/check-version-consistency.js"
},
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.7",
- "@modelcontextprotocol/inspector-client": "^0.16.7",
- "@modelcontextprotocol/inspector-server": "^0.16.7",
+ "@modelcontextprotocol/inspector-cli": "^0.16.8",
+ "@modelcontextprotocol/inspector-client": "^0.16.8",
+ "@modelcontextprotocol/inspector-server": "^0.16.8",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
"open": "^10.2.0",
diff --git a/server/package.json b/server/package.json
index 528bee70..b6b9a9b2 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.7",
+ "version": "0.16.8",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
From 5ef78470dea707e3cf0f9ddb9ee3246647f49eaf Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Tue, 16 Sep 2025 17:26:29 -0400
Subject: [PATCH 138/140] * In App.tsx - Add state for connectionType -
Fetch connectionType from localstorage if present
* In Sidebar.tsx
- Add dropdown for connectionType, only visible if transport type is not stdio
* In Sidebar.test.tsx
- set connectionType to "proxy"
* In useConnection.ts
- add connectionType param, defaulting to "proxy"
- add state for mcpSessionId
- Only check proxy health if the connection type is proxy
- create a serverUrl variable which will either be the proxy url OR the server url depending upon the connection type
- added captureResponseHeaders function to extract the protocol-related headers from a response.
- when creating transport options and connectionType is "direct" and transportType is not "stdio",
- add the appropriate `Accept` and `Content-Type` headers
- capture the protocol related response headers and store them in the custom fetch handler
- set serverUrl to be the server url, not the proxy url
- if connection type is proxy set serverUrl to the mcpProxyUrl
- use serverUrl instead of mcpProxyUrl for the transport instantiation
* In useConnection.test.tsx
- replaced test "sends X-MCP-Proxy-Auth header when proxy auth token is configured" with two tests:
- "sends X-MCP-Proxy-Auth header when proxy auth token is configured for 'proxy' connectionType"
- "does NOT send X-MCP-Proxy-Auth header when proxy auth token is configured for 'direct' connectionType"
- added Connection URL Verification section with 2 tests
- uses server URL directly when connectionType is 'direct'"
- "uses proxy server URL when connectionType is 'proxy'"
---
client/src/App.tsx | 15 +
client/src/components/Sidebar.tsx | 28 ++
.../src/components/__tests__/Sidebar.test.tsx | 2 +
.../hooks/__tests__/useConnection.test.tsx | 107 ++++++-
client/src/lib/hooks/useConnection.ts | 299 ++++++++++++------
5 files changed, 345 insertions(+), 106 deletions(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 4c35b0ec..6c9ae333 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -107,6 +107,14 @@ const App = () => {
const [transportType, setTransportType] = useState<
"stdio" | "sse" | "streamable-http"
>(getInitialTransportType);
+ const [connectionType, setConnectionType] = useState<"direct" | "proxy">(
+ () => {
+ return (
+ (localStorage.getItem("lastConnectionType") as "direct" | "proxy") ||
+ "proxy"
+ );
+ },
+ );
const [logLevel, setLogLevel] = useState("debug");
const [notifications, setNotifications] = useState([]);
const [roots, setRoots] = useState([]);
@@ -254,6 +262,7 @@ const App = () => {
oauthClientId,
oauthScope,
config,
+ connectionType,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
@@ -338,6 +347,10 @@ const App = () => {
localStorage.setItem("lastTransportType", transportType);
}, [transportType]);
+ useEffect(() => {
+ localStorage.setItem("lastConnectionType", connectionType);
+ }, [connectionType]);
+
useEffect(() => {
if (bearerToken) {
localStorage.setItem("lastBearerToken", bearerToken);
@@ -882,6 +895,8 @@ const App = () => {
logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false}
+ connectionType={connectionType}
+ setConnectionType={setConnectionType}
/>
void;
+ connectionType: "direct" | "proxy";
+ setConnectionType: (type: "direct" | "proxy") => void;
}
const Sidebar = ({
@@ -94,6 +96,8 @@ const Sidebar = ({
loggingSupported,
config,
setConfig,
+ connectionType,
+ setConnectionType,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
@@ -313,6 +317,30 @@ const Sidebar = ({
/>
)}
+
+ {/* Connection Type switch - only visible for non-STDIO transport types */}
+
+
+ Connection Type
+
+
+ setConnectionType(value)
+ }
+ >
+
+
+
+
+ Via Proxy
+ Direct
+
+
+
>
)}
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx
index 3a23daaf..4684eb63 100644
--- a/client/src/components/__tests__/Sidebar.test.tsx
+++ b/client/src/components/__tests__/Sidebar.test.tsx
@@ -59,6 +59,8 @@ describe("Sidebar", () => {
loggingSupported: true,
config: DEFAULT_INSPECTOR_CONFIG,
setConfig: jest.fn(),
+ connectionType: "proxy" as const,
+ setConnectionType: jest.fn(),
};
const renderSidebar = (props = {}) => {
diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx
index a94f2386..b16eaa3d 100644
--- a/client/src/lib/hooks/__tests__/useConnection.test.tsx
+++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx
@@ -18,6 +18,9 @@ import { CustomHeaders } from "../../types/customHeaders";
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ status: "ok" }),
+ headers: {
+ get: jest.fn().mockReturnValue(null),
+ },
});
// Mock the SDK dependencies
@@ -574,9 +577,10 @@ describe("useConnection", () => {
mockStreamableHTTPTransport.options = undefined;
});
- test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
+ test("sends X-MCP-Proxy-Auth header when proxy auth token is configured for proxy connectionType", async () => {
const propsWithProxyAuth = {
...defaultProps,
+ connectionType: "proxy" as const,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
@@ -626,6 +630,56 @@ describe("useConnection", () => {
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
});
+ test("does NOT send X-MCP-Proxy-Auth header when proxy auth token is configured for direct connectionType", async () => {
+ const propsWithProxyAuth = {
+ ...defaultProps,
+ connectionType: "direct" as const,
+ config: {
+ ...DEFAULT_INSPECTOR_CONFIG,
+ MCP_PROXY_AUTH_TOKEN: {
+ ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
+ value: "test-proxy-token",
+ },
+ },
+ };
+
+ const { result } = renderHook(() => useConnection(propsWithProxyAuth));
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that the transport was created with the correct headers
+ expect(mockSSETransport.options).toBeDefined();
+ expect(mockSSETransport.options?.requestInit).toBeDefined();
+
+ // Verify that X-MCP-Proxy-Auth header is NOT present for direct connections
+ expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
+ "X-MCP-Proxy-Auth",
+ );
+ expect(mockSSETransport?.options?.fetch).toBeDefined();
+
+ // Verify the fetch function does NOT include the proxy auth header
+ const mockFetch = mockSSETransport.options?.fetch;
+ const testUrl = "http://test.com";
+ await mockFetch?.(testUrl, {
+ headers: {
+ Accept: "text/event-stream",
+ },
+ cache: "no-store",
+ mode: "cors",
+ signal: new AbortController().signal,
+ redirect: "follow",
+ credentials: "include",
+ });
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(testUrl);
+ expect(
+ (global.fetch as jest.Mock).mock.calls[0][1].headers,
+ ).not.toHaveProperty("X-MCP-Proxy-Auth");
+ });
+
test("does NOT send Authorization header for proxy auth", async () => {
const propsWithProxyAuth = {
...defaultProps,
@@ -882,6 +936,57 @@ describe("useConnection", () => {
});
});
+ describe("Connection URL Verification", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset the mock transport objects
+ mockSSETransport.url = undefined;
+ mockSSETransport.options = undefined;
+ mockStreamableHTTPTransport.url = undefined;
+ mockStreamableHTTPTransport.options = undefined;
+ });
+
+ test("uses server URL directly when connectionType is 'direct'", async () => {
+ const directProps = {
+ ...defaultProps,
+ connectionType: "direct" as const,
+ };
+
+ const { result } = renderHook(() => useConnection(directProps));
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Verify the transport was created with the direct server URL
+ expect(mockSSETransport.url).toBeDefined();
+ expect(mockSSETransport.url?.toString()).toBe("http://localhost:8080/");
+ });
+
+ test("uses proxy server URL when connectionType is 'proxy'", async () => {
+ const proxyProps = {
+ ...defaultProps,
+ connectionType: "proxy" as const,
+ };
+
+ const { result } = renderHook(() => useConnection(proxyProps));
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Verify the transport was created with a proxy server URL
+ expect(mockSSETransport.url).toBeDefined();
+ expect(mockSSETransport.url?.pathname).toBe("/sse");
+ expect(mockSSETransport.url?.searchParams.get("url")).toBe(
+ "http://localhost:8080",
+ );
+ expect(mockSSETransport.url?.searchParams.get("transportType")).toBe(
+ "sse",
+ );
+ });
+ });
+
describe("OAuth Error Handling with Scope Discovery", () => {
beforeEach(() => {
jest.clearAllMocks();
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index f47f0d15..1341b0c2 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -70,6 +70,7 @@ interface UseConnectionOptions {
oauthClientId?: string;
oauthScope?: string;
config: InspectorConfig;
+ connectionType?: "direct" | "proxy";
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -91,6 +92,7 @@ export function useConnection({
oauthClientId,
oauthScope,
config,
+ connectionType = "proxy",
onNotification,
onPendingRequest,
onElicitationRequest,
@@ -110,6 +112,10 @@ export function useConnection({
{ request: string; response?: string }[]
>([]);
const [completionsSupported, setCompletionsSupported] = useState(false);
+ const [mcpSessionId, setMcpSessionId] = useState(null);
+ const [mcpProtocolVersion, setMcpProtocolVersion] = useState(
+ null,
+ );
useEffect(() => {
if (!oauthClientId) {
@@ -346,6 +352,17 @@ export function useConnection({
return false;
};
+ const captureResponseHeaders = (response: Response): void => {
+ const sessionId = response.headers.get("mcp-session-id");
+ const protocolVersion = response.headers.get("mcp-protocol-version");
+ if (sessionId && sessionId !== mcpSessionId) {
+ setMcpSessionId(sessionId);
+ }
+ if (protocolVersion && protocolVersion !== mcpProtocolVersion) {
+ setMcpProtocolVersion(protocolVersion);
+ }
+ };
+
const connect = async (_e?: unknown, retryCount: number = 0) => {
const client = new Client(
{
@@ -363,11 +380,14 @@ export function useConnection({
},
);
- try {
- await checkProxyHealth();
- } catch {
- setConnectionStatus("error-connecting-to-proxy");
- return;
+ // Only check proxy health for proxy connections
+ if (connectionType === "proxy") {
+ try {
+ await checkProxyHealth();
+ } catch {
+ setConnectionStatus("error-connecting-to-proxy");
+ return;
+ }
}
let lastRequest = "";
@@ -417,115 +437,183 @@ export function useConnection({
headers["x-custom-auth-headers"] = JSON.stringify(customHeaderNames);
}
- // Add proxy authentication
- const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
- getMCPProxyAuthToken(config);
- const proxyHeaders: HeadersInit = {};
- if (proxyAuthToken) {
- proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
- }
-
// Create appropriate transport
let transportOptions:
| StreamableHTTPClientTransportOptions
| SSEClientTransportOptions;
- let mcpProxyServerUrl;
- switch (transportType) {
- case "stdio": {
- mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
- mcpProxyServerUrl.searchParams.append("command", command);
- mcpProxyServerUrl.searchParams.append("args", args);
- mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
+ let serverUrl: URL;
- const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS
- .value as string;
- if (proxyFullAddress) {
- mcpProxyServerUrl.searchParams.append(
- "proxyFullAddress",
- proxyFullAddress,
- );
- }
- transportOptions = {
- authProvider: serverAuthProvider,
- eventSourceInit: {
- fetch: (
+ // Determine connection URL based on the connection type
+ if (connectionType === "direct" && transportType !== "stdio") {
+ // Direct connection - use the provided URL directly (not available for STDIO)
+ serverUrl = new URL(sseUrl);
+
+ const requestHeaders = { ...headers };
+ if (mcpSessionId) {
+ requestHeaders["mcp-session-id"] = mcpSessionId;
+ }
+ switch (transportType) {
+ case "sse":
+ requestHeaders["Accept"] = "text/event-stream";
+ requestHeaders["content-type"] = "application/json";
+ transportOptions = {
+ fetch: async (
url: string | URL | globalThis.Request,
init?: RequestInit,
- ) =>
- fetch(url, {
+ ) => {
+ const response = await fetch(url, {
...init,
- headers: { ...headers, ...proxyHeaders },
- }),
- },
- requestInit: {
- headers: { ...headers, ...proxyHeaders },
- },
- };
- break;
+ headers: requestHeaders,
+ });
+
+ // Capture protocol-related headers from response
+ captureResponseHeaders(response);
+ return response;
+ },
+ requestInit: {
+ headers: requestHeaders,
+ },
+ };
+ break;
+
+ case "streamable-http":
+ transportOptions = {
+ fetch: async (
+ url: string | URL | globalThis.Request,
+ init?: RequestInit,
+ ) => {
+ requestHeaders["Accept"] =
+ "text/event-stream, application/json";
+ requestHeaders["Content-Type"] = "application/json";
+ const response = await fetch(url, {
+ headers: requestHeaders,
+ ...init,
+ });
+
+ // Capture protocol-related headers from response
+ captureResponseHeaders(response);
+
+ return response;
+ },
+ requestInit: {
+ headers: requestHeaders,
+ },
+ // TODO these should be configurable...
+ reconnectionOptions: {
+ maxReconnectionDelay: 30000,
+ initialReconnectionDelay: 1000,
+ reconnectionDelayGrowFactor: 1.5,
+ maxRetries: 2,
+ },
+ };
+ break;
+ }
+ } else {
+ // Proxy connection (default behavior)
+ // Add proxy authentication headers for proxy connections only
+ const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
+ getMCPProxyAuthToken(config);
+ const proxyHeaders: HeadersInit = {};
+ if (proxyAuthToken) {
+ proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
}
- case "sse": {
- mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
- mcpProxyServerUrl.searchParams.append("url", sseUrl);
+ let mcpProxyServerUrl;
+ switch (transportType) {
+ case "stdio": {
+ mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
+ mcpProxyServerUrl.searchParams.append("command", command);
+ mcpProxyServerUrl.searchParams.append("args", args);
+ mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
- const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS
- .value as string;
- if (proxyFullAddressSSE) {
- mcpProxyServerUrl.searchParams.append(
- "proxyFullAddress",
- proxyFullAddressSSE,
- );
+ const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS
+ .value as string;
+ if (proxyFullAddress) {
+ mcpProxyServerUrl.searchParams.append(
+ "proxyFullAddress",
+ proxyFullAddress,
+ );
+ }
+ transportOptions = {
+ authProvider: serverAuthProvider,
+ eventSourceInit: {
+ fetch: (
+ url: string | URL | globalThis.Request,
+ init?: RequestInit,
+ ) =>
+ fetch(url, {
+ ...init,
+ headers: { ...headers, ...proxyHeaders },
+ }),
+ },
+ requestInit: {
+ headers: { ...headers, ...proxyHeaders },
+ },
+ };
+ break;
}
- transportOptions = {
- eventSourceInit: {
- fetch: (
- url: string | URL | globalThis.Request,
- init?: RequestInit,
- ) =>
- fetch(url, {
- ...init,
- headers: { ...headers, ...proxyHeaders },
- }),
- },
- requestInit: {
- headers: { ...headers, ...proxyHeaders },
- },
- };
- break;
- }
- case "streamable-http":
- mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
- mcpProxyServerUrl.searchParams.append("url", sseUrl);
- transportOptions = {
- eventSourceInit: {
- fetch: (
- url: string | URL | globalThis.Request,
- init?: RequestInit,
- ) =>
- fetch(url, {
- ...init,
- headers: { ...headers, ...proxyHeaders },
- }),
- },
- requestInit: {
- headers: { ...headers, ...proxyHeaders },
- },
- // TODO these should be configurable...
- reconnectionOptions: {
- maxReconnectionDelay: 30000,
- initialReconnectionDelay: 1000,
- reconnectionDelayGrowFactor: 1.5,
- maxRetries: 2,
- },
- };
- break;
+ case "sse": {
+ mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
+ mcpProxyServerUrl.searchParams.append("url", sseUrl);
+
+ const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS
+ .value as string;
+ if (proxyFullAddressSSE) {
+ mcpProxyServerUrl.searchParams.append(
+ "proxyFullAddress",
+ proxyFullAddressSSE,
+ );
+ }
+ transportOptions = {
+ eventSourceInit: {
+ fetch: (
+ url: string | URL | globalThis.Request,
+ init?: RequestInit,
+ ) =>
+ fetch(url, {
+ ...init,
+ headers: { ...headers, ...proxyHeaders },
+ }),
+ },
+ requestInit: {
+ headers: { ...headers, ...proxyHeaders },
+ },
+ };
+ break;
+ }
+
+ case "streamable-http":
+ mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
+ mcpProxyServerUrl.searchParams.append("url", sseUrl);
+ transportOptions = {
+ eventSourceInit: {
+ fetch: (
+ url: string | URL | globalThis.Request,
+ init?: RequestInit,
+ ) =>
+ fetch(url, {
+ ...init,
+ headers: { ...headers, ...proxyHeaders },
+ }),
+ },
+ requestInit: {
+ headers: { ...headers, ...proxyHeaders },
+ },
+ // TODO these should be configurable...
+ reconnectionOptions: {
+ maxReconnectionDelay: 30000,
+ initialReconnectionDelay: 1000,
+ reconnectionDelayGrowFactor: 1.5,
+ maxRetries: 2,
+ },
+ };
+ break;
+ }
+ serverUrl = mcpProxyServerUrl as URL;
+ serverUrl.searchParams.append("transportType", transportType);
}
- (mcpProxyServerUrl as URL).searchParams.append(
- "transportType",
- transportType,
- );
if (onNotification) {
[
@@ -551,14 +639,11 @@ export function useConnection({
try {
const transport =
transportType === "streamable-http"
- ? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, {
+ ? new StreamableHTTPClientTransport(serverUrl, {
sessionId: undefined,
...transportOptions,
})
- : new SSEClientTransport(
- mcpProxyServerUrl as URL,
- transportOptions,
- );
+ : new SSEClientTransport(serverUrl, transportOptions);
await client.connect(transport as Transport);
@@ -575,7 +660,9 @@ export function useConnection({
});
} catch (error) {
console.error(
- `Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
+ connectionType === "direct"
+ ? `Failed to connect directly to MCP Server at: ${serverUrl}:`
+ : `Failed to connect to MCP Server via the MCP Inspector Proxy: ${serverUrl}:`,
error,
);
@@ -674,6 +761,8 @@ export function useConnection({
setConnectionStatus("disconnected");
setCompletionsSupported(false);
setServerCapabilities(null);
+ setMcpSessionId(null);
+ setMcpProtocolVersion(null);
};
const clearRequestHistory = () => {
From f1e93e0475aeac2ebd2c841dcac42be1f0054abf Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Wed, 17 Sep 2025 14:45:58 -0400
Subject: [PATCH 139/140] * In server/index.ts - in createCustomFetch
function - convert finalHeaders to headers object for node-fetch
compatibility - for SSE streams, convert node stream to response body to
web ReadableStream since EventSource polyfill expects web-compatible stream
* In package-lock.json and package.json
- add node-fetch as a dependency
This fixes #600
---
package-lock.json | 92 +++++++++++++++++++++++++++++++++++++++++++++
package.json | 1 +
server/src/index.ts | 72 +++++++++++++++++++++++++++++++++--
3 files changed, 161 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d422148b..92a40ccf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@modelcontextprotocol/inspector-server": "^0.16.7",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
+ "node-fetch": "^3.3.2",
"open": "^10.2.0",
"shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2",
@@ -5103,6 +5104,15 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -5970,6 +5980,29 @@
"bser": "2.1.1"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -6154,6 +6187,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -8716,6 +8761,44 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -11504,6 +11587,15 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
diff --git a/package.json b/package.json
index 48609a20..a1f704a2 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"@modelcontextprotocol/inspector-server": "^0.16.7",
"@modelcontextprotocol/sdk": "^1.18.0",
"concurrently": "^9.2.0",
+ "node-fetch": "^3.3.2",
"open": "^10.2.0",
"shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2",
diff --git a/server/src/index.ts b/server/src/index.ts
index 7e966d34..88954ebc 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -3,6 +3,11 @@
import cors from "cors";
import { parseArgs } from "node:util";
import { parse as shellParseArgs } from "shell-quote";
+import nodeFetch, { Headers as NodeHeaders } from "node-fetch";
+
+// Type-compatible wrappers for node-fetch to work with browser-style types
+const fetch = nodeFetch;
+const Headers = NodeHeaders;
import {
SSEClientTransport,
@@ -231,13 +236,37 @@ const authMiddleware = (
next();
};
+/**
+ * Converts a Node.js ReadableStream to a web-compatible ReadableStream
+ * This is necessary for the EventSource polyfill which expects web streams
+ */
+const createWebReadableStream = (nodeStream: any): ReadableStream => {
+ return new ReadableStream({
+ start(controller) {
+ nodeStream.on("data", (chunk: any) => {
+ controller.enqueue(chunk);
+ });
+ nodeStream.on("end", () => {
+ controller.close();
+ });
+ nodeStream.on("error", (err: any) => {
+ controller.error(err);
+ });
+ },
+ });
+};
+
/**
* Creates a `fetch` function that merges dynamic session headers with the
* headers from the actual request, ensuring that request-specific headers like
- * `Content-Type` are preserved.
+ * `Content-Type` are preserved. For SSE requests, it also converts Node.js
+ * streams to web-compatible streams.
*/
const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
- return (input: RequestInfo | URL, init?: RequestInit): Promise => {
+ return async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ): Promise => {
// Determine the headers from the original request/init.
// The SDK may pass a Request object or a URL and an init object.
const originalHeaders =
@@ -252,8 +281,43 @@ const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
finalHeaders.set(key, value);
});
- // This works for both `fetch(url, init)` and `fetch(request)` style calls.
- return fetch(input, { ...init, headers: finalHeaders });
+ // Convert Headers to a plain object for node-fetch compatibility
+ const headersObject: Record = {};
+ finalHeaders.forEach((value, key) => {
+ headersObject[key] = value;
+ });
+
+ // Get the response from node-fetch (cast input and init to handle type differences)
+ const response = await fetch(
+ input as any,
+ { ...init, headers: headersObject } as any,
+ );
+
+ // Check if this is an SSE request by looking at the Accept header
+ const acceptHeader = finalHeaders.get("Accept");
+ const isSSE = acceptHeader?.includes("text/event-stream");
+
+ if (isSSE && response.body) {
+ // For SSE requests, we need to convert the Node.js stream to a web ReadableStream
+ // because the EventSource polyfill expects web-compatible streams
+ const webStream = createWebReadableStream(response.body);
+
+ // Create a new response with the web-compatible stream
+ // Convert node-fetch headers to plain object for web Response compatibility
+ const responseHeaders: Record = {};
+ response.headers.forEach((value: string, key: string) => {
+ responseHeaders[key] = value;
+ });
+
+ return new Response(webStream, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: responseHeaders,
+ }) as Response;
+ }
+
+ // For non-SSE requests, return the response as-is (cast to handle type differences)
+ return response as unknown as Response;
};
};
From 6b7c7526841ca7c94dbd84473b3e0e7d52f95107 Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Thu, 18 Sep 2025 17:42:37 -0400
Subject: [PATCH 140/140] * In Sidebar.tsx - Add tooltip over the Connection
Type field
---
client/src/components/Sidebar.tsx | 51 ++++++++++++++++++-------------
1 file changed, 29 insertions(+), 22 deletions(-)
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx
index e054fe8b..889380d8 100644
--- a/client/src/components/Sidebar.tsx
+++ b/client/src/components/Sidebar.tsx
@@ -108,6 +108,8 @@ const Sidebar = ({
const [copiedServerFile, setCopiedServerFile] = useState(false);
const { toast } = useToast();
+ const connectionTypeTip =
+ "Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
// Reusable error reporter for copy actions
const reportError = useCallback(
(error: unknown) => {
@@ -319,28 +321,33 @@ const Sidebar = ({
{/* Connection Type switch - only visible for non-STDIO transport types */}
-
-
- Connection Type
-
-
- setConnectionType(value)
- }
- >
-
-
-
-
- Via Proxy
- Direct
-
-
-
+
+
+
+
+ Connection Type
+
+
+ setConnectionType(value)
+ }
+ >
+
+
+
+
+ Via Proxy
+ Direct
+
+
+
+
+ {connectionTypeTip}
+
>
)}