@modelcontextprotocol/inspector 0.16.6 → 0.16.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,10 @@
2
2
  import cors from "cors";
3
3
  import { parseArgs } from "node:util";
4
4
  import { parse as shellParseArgs } from "shell-quote";
5
+ import nodeFetch, { Headers as NodeHeaders } from "node-fetch";
6
+ // Type-compatible wrappers for node-fetch to work with browser-style types
7
+ const fetch = nodeFetch;
8
+ const Headers = NodeHeaders;
5
9
  import { SSEClientTransport, SseError, } from "@modelcontextprotocol/sdk/client/sse.js";
6
10
  import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js";
7
11
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -12,12 +16,6 @@ import { findActualExecutable } from "spawn-rx";
12
16
  import mcpProxy from "./mcpProxy.js";
13
17
  import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
14
18
  const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
15
- const SSE_HEADERS_PASSTHROUGH = ["authorization"];
16
- const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
17
- "authorization",
18
- "mcp-session-id",
19
- "last-event-id",
20
- ];
21
19
  const defaultEnvironment = {
22
20
  ...getDefaultEnvironment(),
23
21
  ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
@@ -33,34 +31,91 @@ const { values } = parseArgs({
33
31
  },
34
32
  });
35
33
  // Function to get HTTP headers.
36
- // Supports only "sse" and "streamable-http" transport types.
37
- const getHttpHeaders = (req, transportType) => {
38
- const headers = {
39
- Accept: transportType === "sse"
40
- ? "text/event-stream"
41
- : "text/event-stream, application/json",
42
- };
43
- const defaultHeaders = transportType === "sse"
44
- ? SSE_HEADERS_PASSTHROUGH
45
- : STREAMABLE_HTTP_HEADERS_PASSTHROUGH;
46
- for (const key of defaultHeaders) {
47
- if (req.headers[key] === undefined) {
48
- continue;
34
+ const getHttpHeaders = (req) => {
35
+ const headers = {};
36
+ // Iterate over all headers in the request
37
+ for (const key in req.headers) {
38
+ const lowerKey = key.toLowerCase();
39
+ // Check if the header is one we want to forward
40
+ if (lowerKey.startsWith("mcp-") ||
41
+ lowerKey === "authorization" ||
42
+ lowerKey === "last-event-id") {
43
+ // Exclude the proxy's own authentication header and the Client <-> Proxy session ID header
44
+ if (lowerKey !== "x-mcp-proxy-auth" && lowerKey !== "mcp-session-id") {
45
+ const value = req.headers[key];
46
+ if (typeof value === "string") {
47
+ // If the value is a string, use it directly
48
+ headers[key] = value;
49
+ }
50
+ else if (Array.isArray(value)) {
51
+ // If the value is an array, use the last element
52
+ const lastValue = value.at(-1);
53
+ if (lastValue !== undefined) {
54
+ headers[key] = lastValue;
55
+ }
56
+ }
57
+ // If value is undefined, it's skipped, which is correct.
58
+ }
49
59
  }
50
- const value = req.headers[key];
51
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
52
60
  }
53
- // If the header "x-custom-auth-header" is present, use its value as the custom header name.
54
- if (req.headers["x-custom-auth-header"] !== undefined) {
55
- const customHeaderName = req.headers["x-custom-auth-header"];
56
- const lowerCaseHeaderName = customHeaderName.toLowerCase();
57
- if (req.headers[lowerCaseHeaderName] !== undefined) {
58
- const value = req.headers[lowerCaseHeaderName];
59
- headers[customHeaderName] = value;
61
+ // Handle the custom auth header separately. We expect `x-custom-auth-header`
62
+ // to be a string containing the name of the actual authentication header.
63
+ const customAuthHeaderName = req.headers["x-custom-auth-header"];
64
+ if (typeof customAuthHeaderName === "string") {
65
+ const lowerCaseHeaderName = customAuthHeaderName.toLowerCase();
66
+ const value = req.headers[lowerCaseHeaderName];
67
+ if (typeof value === "string") {
68
+ headers[customAuthHeaderName] = value;
69
+ }
70
+ else if (Array.isArray(value)) {
71
+ // If the actual auth header was sent multiple times, use the last value.
72
+ const lastValue = value.at(-1);
73
+ if (lastValue !== undefined) {
74
+ headers[customAuthHeaderName] = lastValue;
75
+ }
76
+ }
77
+ }
78
+ // Handle multiple custom headers (new approach)
79
+ if (req.headers["x-custom-auth-headers"] !== undefined) {
80
+ try {
81
+ const customHeaderNames = JSON.parse(req.headers["x-custom-auth-headers"]);
82
+ if (Array.isArray(customHeaderNames)) {
83
+ customHeaderNames.forEach((headerName) => {
84
+ const lowerCaseHeaderName = headerName.toLowerCase();
85
+ if (req.headers[lowerCaseHeaderName] !== undefined) {
86
+ const value = req.headers[lowerCaseHeaderName];
87
+ headers[headerName] = Array.isArray(value)
88
+ ? value[value.length - 1]
89
+ : value;
90
+ }
91
+ });
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.warn("Failed to parse x-custom-auth-headers:", error);
60
96
  }
61
97
  }
62
98
  return headers;
63
99
  };
100
+ /**
101
+ * Updates a headers object in-place, preserving the original Accept header.
102
+ * This is necessary to ensure that transports holding a reference to the headers
103
+ * object see the updates.
104
+ * @param currentHeaders The headers object to update.
105
+ * @param newHeaders The new headers to apply.
106
+ */
107
+ const updateHeadersInPlace = (currentHeaders, newHeaders) => {
108
+ // Preserve the Accept header, which is set at transport creation and
109
+ // is not present in subsequent client requests.
110
+ const accept = currentHeaders["Accept"];
111
+ // Clear the old headers and apply the new ones.
112
+ Object.keys(currentHeaders).forEach((key) => delete currentHeaders[key]);
113
+ Object.assign(currentHeaders, newHeaders);
114
+ // Restore the Accept header.
115
+ if (accept) {
116
+ currentHeaders["Accept"] = accept;
117
+ }
118
+ };
64
119
  const app = express();
65
120
  app.use(cors());
66
121
  app.use((req, res, next) => {
@@ -69,6 +124,7 @@ app.use((req, res, next) => {
69
124
  });
70
125
  const webAppTransports = new Map(); // Web app transports by web app sessionId
71
126
  const serverTransports = new Map(); // Server Transports by web app sessionId
127
+ const sessionHeaderHolders = new Map(); // For dynamic header updates
72
128
  // Use provided token from environment or generate a new one
73
129
  const sessionToken = process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
74
130
  const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
@@ -126,6 +182,73 @@ const authMiddleware = (req, res, next) => {
126
182
  }
127
183
  next();
128
184
  };
185
+ /**
186
+ * Converts a Node.js ReadableStream to a web-compatible ReadableStream
187
+ * This is necessary for the EventSource polyfill which expects web streams
188
+ */
189
+ const createWebReadableStream = (nodeStream) => {
190
+ return new ReadableStream({
191
+ start(controller) {
192
+ nodeStream.on("data", (chunk) => {
193
+ controller.enqueue(chunk);
194
+ });
195
+ nodeStream.on("end", () => {
196
+ controller.close();
197
+ });
198
+ nodeStream.on("error", (err) => {
199
+ controller.error(err);
200
+ });
201
+ },
202
+ });
203
+ };
204
+ /**
205
+ * Creates a `fetch` function that merges dynamic session headers with the
206
+ * headers from the actual request, ensuring that request-specific headers like
207
+ * `Content-Type` are preserved. For SSE requests, it also converts Node.js
208
+ * streams to web-compatible streams.
209
+ */
210
+ const createCustomFetch = (headerHolder) => {
211
+ return async (input, init) => {
212
+ // Determine the headers from the original request/init.
213
+ // The SDK may pass a Request object or a URL and an init object.
214
+ const originalHeaders = input instanceof Request ? input.headers : init?.headers;
215
+ // Start with our dynamic session headers.
216
+ const finalHeaders = new Headers(headerHolder.headers);
217
+ // Merge the SDK's request-specific headers, letting them overwrite.
218
+ // This is crucial for preserving Content-Type on POST requests.
219
+ new Headers(originalHeaders).forEach((value, key) => {
220
+ finalHeaders.set(key, value);
221
+ });
222
+ // Convert Headers to a plain object for node-fetch compatibility
223
+ const headersObject = {};
224
+ finalHeaders.forEach((value, key) => {
225
+ headersObject[key] = value;
226
+ });
227
+ // Get the response from node-fetch (cast input and init to handle type differences)
228
+ const response = await fetch(input, { ...init, headers: headersObject });
229
+ // Check if this is an SSE request by looking at the Accept header
230
+ const acceptHeader = finalHeaders.get("Accept");
231
+ const isSSE = acceptHeader?.includes("text/event-stream");
232
+ if (isSSE && response.body) {
233
+ // For SSE requests, we need to convert the Node.js stream to a web ReadableStream
234
+ // because the EventSource polyfill expects web-compatible streams
235
+ const webStream = createWebReadableStream(response.body);
236
+ // Create a new response with the web-compatible stream
237
+ // Convert node-fetch headers to plain object for web Response compatibility
238
+ const responseHeaders = {};
239
+ response.headers.forEach((value, key) => {
240
+ responseHeaders[key] = value;
241
+ });
242
+ return new Response(webStream, {
243
+ status: response.status,
244
+ statusText: response.statusText,
245
+ headers: responseHeaders,
246
+ });
247
+ }
248
+ // For non-SSE requests, return the response as-is (cast to handle type differences)
249
+ return response;
250
+ };
251
+ };
129
252
  const createTransport = async (req) => {
130
253
  const query = req.query;
131
254
  console.log("Query parameters:", JSON.stringify(query));
@@ -144,32 +267,35 @@ const createTransport = async (req) => {
144
267
  stderr: "pipe",
145
268
  });
146
269
  await transport.start();
147
- return transport;
270
+ return { transport };
148
271
  }
149
272
  else if (transportType === "sse") {
150
273
  const url = query.url;
151
- const headers = getHttpHeaders(req, transportType);
274
+ const headers = getHttpHeaders(req);
275
+ headers["Accept"] = "text/event-stream";
276
+ const headerHolder = { headers };
152
277
  console.log(`SSE transport: url=${url}, headers=${JSON.stringify(headers)}`);
153
278
  const transport = new SSEClientTransport(new URL(url), {
154
279
  eventSourceInit: {
155
- fetch: (url, init) => fetch(url, { ...init, headers }),
280
+ fetch: createCustomFetch(headerHolder),
156
281
  },
157
282
  requestInit: {
158
- headers,
283
+ headers: headerHolder.headers,
159
284
  },
160
285
  });
161
286
  await transport.start();
162
- return transport;
287
+ return { transport, headerHolder };
163
288
  }
164
289
  else if (transportType === "streamable-http") {
165
- const headers = getHttpHeaders(req, transportType);
290
+ const headers = getHttpHeaders(req);
291
+ headers["Accept"] = "text/event-stream, application/json";
292
+ const headerHolder = { headers };
166
293
  const transport = new StreamableHTTPClientTransport(new URL(query.url), {
167
- requestInit: {
168
- headers,
169
- },
294
+ // Pass a custom fetch to inject the latest headers on each request
295
+ fetch: createCustomFetch(headerHolder),
170
296
  });
171
297
  await transport.start();
172
- return transport;
298
+ return { transport, headerHolder };
173
299
  }
174
300
  else {
175
301
  console.error(`Invalid transport type: ${transportType}`);
@@ -179,6 +305,10 @@ const createTransport = async (req) => {
179
305
  app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
180
306
  const sessionId = req.headers["mcp-session-id"];
181
307
  console.log(`Received GET message for sessionId ${sessionId}`);
308
+ const headerHolder = sessionHeaderHolders.get(sessionId);
309
+ if (headerHolder) {
310
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
311
+ }
182
312
  try {
183
313
  const transport = webAppTransports.get(sessionId);
184
314
  if (!transport) {
@@ -196,29 +326,45 @@ app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
196
326
  });
197
327
  app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
198
328
  const sessionId = req.headers["mcp-session-id"];
199
- let serverTransport;
200
- if (!sessionId) {
329
+ if (sessionId) {
330
+ console.log(`Received POST message for sessionId ${sessionId}`);
331
+ const headerHolder = sessionHeaderHolders.get(sessionId);
332
+ if (headerHolder) {
333
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
334
+ }
201
335
  try {
202
- console.log("New StreamableHttp connection request");
203
- try {
204
- serverTransport = await createTransport(req);
336
+ const transport = webAppTransports.get(sessionId);
337
+ if (!transport) {
338
+ res.status(404).end("Transport not found for sessionId " + sessionId);
205
339
  }
206
- catch (error) {
207
- if (error instanceof SseError && error.code === 401) {
208
- console.error("Received 401 Unauthorized from MCP server:", error.message);
209
- res.status(401).json(error);
210
- return;
211
- }
212
- throw error;
340
+ else {
341
+ await transport.handleRequest(req, res);
213
342
  }
214
- console.log("Created StreamableHttp server transport");
343
+ }
344
+ catch (error) {
345
+ console.error("Error in /mcp route:", error);
346
+ res.status(500).json(error);
347
+ }
348
+ }
349
+ else {
350
+ console.log("New StreamableHttp connection request");
351
+ try {
352
+ const { transport: serverTransport, headerHolder } = await createTransport(req);
215
353
  const webAppTransport = new StreamableHTTPServerTransport({
216
354
  sessionIdGenerator: randomUUID,
217
355
  onsessioninitialized: (sessionId) => {
218
356
  webAppTransports.set(sessionId, webAppTransport);
219
- serverTransports.set(sessionId, serverTransport);
357
+ serverTransports.set(sessionId, serverTransport); // eslint-disable-line @typescript-eslint/no-non-null-assertion
358
+ if (headerHolder) {
359
+ sessionHeaderHolders.set(sessionId, headerHolder);
360
+ }
220
361
  console.log("Client <-> Proxy sessionId: " + sessionId);
221
362
  },
363
+ onsessionclosed: (sessionId) => {
364
+ webAppTransports.delete(sessionId);
365
+ serverTransports.delete(sessionId);
366
+ sessionHeaderHolders.delete(sessionId);
367
+ },
222
368
  });
223
369
  console.log("Created StreamableHttp client transport");
224
370
  await webAppTransport.start();
@@ -229,23 +375,12 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
229
375
  await webAppTransport.handleRequest(req, res, req.body);
230
376
  }
231
377
  catch (error) {
232
- console.error("Error in /mcp POST route:", error);
233
- res.status(500).json(error);
234
- }
235
- }
236
- else {
237
- console.log(`Received POST message for sessionId ${sessionId}`);
238
- try {
239
- const transport = webAppTransports.get(sessionId);
240
- if (!transport) {
241
- res.status(404).end("Transport not found for sessionId " + sessionId);
242
- }
243
- else {
244
- await transport.handleRequest(req, res);
378
+ if (error instanceof SseError && error.code === 401) {
379
+ console.error("Received 401 Unauthorized from MCP server:", error.message);
380
+ res.status(401).json(error);
381
+ return;
245
382
  }
246
- }
247
- catch (error) {
248
- console.error("Error in /mcp route:", error);
383
+ console.error("Error in /mcp POST route:", error);
249
384
  res.status(500).json(error);
250
385
  }
251
386
  }
@@ -253,10 +388,9 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
253
388
  app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
254
389
  const sessionId = req.headers["mcp-session-id"];
255
390
  console.log(`Received DELETE message for sessionId ${sessionId}`);
256
- let serverTransport;
257
391
  if (sessionId) {
258
392
  try {
259
- serverTransport = serverTransports.get(sessionId);
393
+ const serverTransport = serverTransports.get(sessionId);
260
394
  if (!serverTransport) {
261
395
  res.status(404).end("Transport not found for sessionId " + sessionId);
262
396
  }
@@ -264,6 +398,7 @@ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res)
264
398
  await serverTransport.terminateSession();
265
399
  webAppTransports.delete(sessionId);
266
400
  serverTransports.delete(sessionId);
401
+ sessionHeaderHolders.delete(sessionId);
267
402
  console.log(`Transports removed for sessionId ${sessionId}`);
268
403
  }
269
404
  res.status(200).end();
@@ -277,18 +412,7 @@ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res)
277
412
  app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => {
278
413
  try {
279
414
  console.log("New STDIO connection request");
280
- let serverTransport;
281
- try {
282
- serverTransport = await createTransport(req);
283
- }
284
- catch (error) {
285
- if (error instanceof SseError && error.code === 401) {
286
- console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
287
- res.status(401).json(error);
288
- return;
289
- }
290
- throw error;
291
- }
415
+ const { transport: serverTransport } = await createTransport(req);
292
416
  const proxyFullAddress = req.query.proxyFullAddress || "";
293
417
  const prefix = proxyFullAddress || "";
294
418
  const endpoint = `${prefix}/message`;
@@ -317,6 +441,7 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
317
441
  serverTransport.close();
318
442
  webAppTransports.delete(webAppTransport.sessionId);
319
443
  serverTransports.delete(webAppTransport.sessionId);
444
+ sessionHeaderHolders.delete(webAppTransport.sessionId);
320
445
  console.error(message);
321
446
  }
322
447
  else {
@@ -382,6 +507,11 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
382
507
  });
383
508
  }
384
509
  catch (error) {
510
+ if (error instanceof SseError && error.code === 401) {
511
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
512
+ res.status(401).json(error);
513
+ return;
514
+ }
385
515
  console.error("Error in /stdio route:", error);
386
516
  res.status(500).json(error);
387
517
  }
@@ -389,46 +519,39 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
389
519
  app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
390
520
  try {
391
521
  console.log("New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp");
392
- let serverTransport;
393
- try {
394
- serverTransport = await createTransport(req);
395
- }
396
- catch (error) {
397
- if (error instanceof SseError && error.code === 401) {
398
- console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
399
- res.status(401).json(error);
400
- return;
401
- }
402
- else if (error instanceof SseError && error.code === 404) {
403
- console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
404
- res.status(404).json(error);
405
- return;
406
- }
407
- else if (JSON.stringify(error).includes("ECONNREFUSED")) {
408
- console.error("Connection refused. Is the MCP server running?");
409
- res.status(500).json(error);
410
- }
411
- else {
412
- throw error;
413
- }
414
- }
415
- if (serverTransport) {
416
- const proxyFullAddress = req.query.proxyFullAddress || "";
417
- const prefix = proxyFullAddress || "";
418
- const endpoint = `${prefix}/message`;
419
- const webAppTransport = new SSEServerTransport(endpoint, res);
420
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
421
- console.log("Created client transport");
422
- serverTransports.set(webAppTransport.sessionId, serverTransport);
423
- console.log("Created server transport");
424
- await webAppTransport.start();
425
- mcpProxy({
426
- transportToClient: webAppTransport,
427
- transportToServer: serverTransport,
428
- });
522
+ const { transport: serverTransport, headerHolder } = await createTransport(req);
523
+ const proxyFullAddress = req.query.proxyFullAddress || "";
524
+ const prefix = proxyFullAddress || "";
525
+ const endpoint = `${prefix}/message`;
526
+ const webAppTransport = new SSEServerTransport(endpoint, res);
527
+ webAppTransports.set(webAppTransport.sessionId, webAppTransport);
528
+ console.log("Created client transport");
529
+ serverTransports.set(webAppTransport.sessionId, serverTransport); // eslint-disable-line @typescript-eslint/no-non-null-assertion
530
+ if (headerHolder) {
531
+ sessionHeaderHolders.set(webAppTransport.sessionId, headerHolder);
429
532
  }
533
+ console.log("Created server transport");
534
+ await webAppTransport.start();
535
+ mcpProxy({
536
+ transportToClient: webAppTransport,
537
+ transportToServer: serverTransport,
538
+ });
430
539
  }
431
540
  catch (error) {
541
+ if (error instanceof SseError && error.code === 401) {
542
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
543
+ res.status(401).json(error);
544
+ return;
545
+ }
546
+ else if (error instanceof SseError && error.code === 404) {
547
+ console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
548
+ res.status(404).json(error);
549
+ return;
550
+ }
551
+ else if (JSON.stringify(error).includes("ECONNREFUSED")) {
552
+ console.error("Connection refused. Is the MCP server running?");
553
+ res.status(500).json(error);
554
+ }
432
555
  console.error("Error in /sse route:", error);
433
556
  res.status(500).json(error);
434
557
  }
@@ -437,6 +560,10 @@ app.post("/message", originValidationMiddleware, authMiddleware, async (req, res
437
560
  try {
438
561
  const sessionId = req.query.sessionId;
439
562
  console.log(`Received POST message for sessionId ${sessionId}`);
563
+ const headerHolder = sessionHeaderHolders.get(sessionId);
564
+ if (headerHolder) {
565
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
566
+ }
440
567
  const transport = webAppTransports.get(sessionId);
441
568
  if (!transport) {
442
569
  res.status(404).end("Session not found");