Merge branch 'main' into fix/optional-arrays-issue-771

This commit is contained in:
Jean-Marc Le Roux 2025-09-12 23:34:35 +02:00 • committed by GitHub
commit d5a877a444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 343 additions and 178 deletions

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,36 @@
---
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.

View file

@ -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,23 @@ 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.

View file

@ -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)",
@ -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"
}

View file

@ -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)",
@ -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",

View file

@ -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;
}
}

View file

@ -240,7 +240,7 @@ export const OAuthFlowProgress = ({
<p className="font-medium mb-2 text-sm">Authorization URL:</p>
<div className="flex items-center gap-2">
<p className="text-xs break-all">
{authState.authorizationUrl}
{String(authState.authorizationUrl)}
</p>
<button
onClick={() => {

View file

@ -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(() => {

View file

@ -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",
}),
}),
);
});

View file

@ -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 () => {

View file

@ -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;

View file

@ -132,7 +132,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
context.provider.saveCodeVerifier(codeVerifier);
context.updateState({
authorizationUrl: authorizationUrl.toString(),
authorizationUrl: authorizationUrl,
oauthStep: "authorization_code",
});
},

View file

@ -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:") {

30
package-lock.json generated
View file

@ -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,10 +14,10 @@
"cli"
],
"dependencies": {
"@modelcontextprotocol/inspector-cli": "^0.16.6",
"@modelcontextprotocol/inspector-client": "^0.16.6",
"@modelcontextprotocol/inspector-server": "^0.16.6",
"@modelcontextprotocol/sdk": "^1.17.5",
"@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",
"shell-quote": "^1.8.3",
@ -46,10 +46,10 @@
},
"cli": {
"name": "@modelcontextprotocol/inspector-cli",
"version": "0.16.6",
"version": "0.16.7",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
"@modelcontextprotocol/sdk": "^1.18.0",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
},
@ -69,10 +69,10 @@
},
"client": {
"name": "@modelcontextprotocol/inspector-client",
"version": "0.16.6",
"version": "0.16.7",
"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",
@ -11746,10 +11746,10 @@
},
"server": {
"name": "@modelcontextprotocol/inspector-server",
"version": "0.16.6",
"version": "0.16.7",
"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",

View file

@ -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,10 +47,10 @@
"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/sdk": "^1.17.5",
"@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",
"shell-quote": "^1.8.3",

View file

@ -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)",
@ -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",

View file

@ -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,43 +40,84 @@ 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<string, string> => {
const headers: Record<string, string> = {};
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;
};
/**
* 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<string, string>,
newHeaders: Record<string, string>,
) => {
// 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) => {
@ -92,6 +127,7 @@ app.use((req, res, next) => {
const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId
const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId
const sessionHeaderHolders: Map<string, { headers: HeadersInit }> = new Map(); // For dynamic header updates
// Use provided token from environment or generate a new one
const sessionToken =
@ -174,7 +210,38 @@ const authMiddleware = (
next();
};
const createTransport = async (req: express.Request): Promise<Transport> => {
/**
* 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<Response> => {
// 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 +265,13 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
});
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 +279,28 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
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 +314,15 @@ 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) {
updateHeadersInPlace(
headerHolder.headers as Record<string, string>,
getHttpHeaders(req),
);
}
try {
const transport = webAppTransports.get(
sessionId,
@ -267,34 +346,54 @@ 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) {
updateHeadersInPlace(
headerHolder.headers as Record<string, string>,
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 +410,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 +432,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 +462,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 +496,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 +550,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 +572,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 +624,18 @@ 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) {
updateHeadersInPlace(
headerHolder.headers as Record<string, string>,
getHttpHeaders(req),
);
}
const transport = webAppTransports.get(sessionId) as SSEServerTransport;
if (!transport) {
res.status(404).end("Session not found");
return;