@modelcontextprotocol/inspector 0.21.1 → 0.22.0

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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>MCP Inspector</title>
8
- <script type="module" crossorigin src="/assets/index-uKaJPCZp.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-_5aZhNPC.css">
8
+ <script type="module" crossorigin src="/assets/index-CngsKvMW.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CfzB1GD8.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root" class="w-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelcontextprotocol/inspector",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Model Context Protocol inspector",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Model Context Protocol a Series of LF Projects, LLC.",
@@ -10,6 +10,10 @@
10
10
  "bin": {
11
11
  "mcp-inspector": "cli/build/cli.js"
12
12
  },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/modelcontextprotocol/inspector"
16
+ },
13
17
  "files": [
14
18
  "client/bin",
15
19
  "client/dist",
@@ -48,9 +52,9 @@
48
52
  "check-version": "node scripts/check-version-consistency.js"
49
53
  },
50
54
  "dependencies": {
51
- "@modelcontextprotocol/inspector-cli": "^0.21.1",
52
- "@modelcontextprotocol/inspector-client": "^0.21.1",
53
- "@modelcontextprotocol/inspector-server": "^0.21.1",
55
+ "@modelcontextprotocol/inspector-cli": "^0.22.0",
56
+ "@modelcontextprotocol/inspector-client": "^0.22.0",
57
+ "@modelcontextprotocol/inspector-server": "^0.22.0",
54
58
  "@modelcontextprotocol/sdk": "^1.25.2",
55
59
  "concurrently": "^9.2.0",
56
60
  "node-fetch": "^3.3.2",
@@ -53,6 +53,33 @@ const is401Error = (error) => {
53
53
  return true;
54
54
  return false;
55
55
  };
56
+ /**
57
+ * Sends a sanitized JSON error response to the client without exposing
58
+ * stack traces or internal error details. The full error is expected to
59
+ * have already been logged server-side via console.error.
60
+ */
61
+ const sendErrorResponse = (res, status, message) => {
62
+ res.status(status).json({ error: message });
63
+ };
64
+ /**
65
+ * Prefer forwarding the upstream MCP 401 (WWW-Authenticate + body) so the browser
66
+ * matches direct-mode OAuth behavior. Falls back to a generic 401 JSON response
67
+ * when no upstream details were captured, to avoid leaking stack traces.
68
+ */
69
+ const sendProxiedUnauthorized = (res, headerHolder) => {
70
+ const captured = headerHolder?.lastUpstream401;
71
+ if (captured && headerHolder) {
72
+ if (captured.wwwAuthenticate) {
73
+ res.setHeader("WWW-Authenticate", captured.wwwAuthenticate);
74
+ }
75
+ res.status(401);
76
+ res.setHeader("Content-Type", captured.contentType);
77
+ res.send(captured.body);
78
+ delete headerHolder.lastUpstream401;
79
+ return;
80
+ }
81
+ sendErrorResponse(res, 401, "Unauthorized");
82
+ };
56
83
  // Function to get HTTP headers.
57
84
  const getHttpHeaders = (req) => {
58
85
  const headers = {};
@@ -142,7 +169,7 @@ const updateHeadersInPlace = (currentHeaders, newHeaders) => {
142
169
  const app = express();
143
170
  app.use(cors());
144
171
  app.use((req, res, next) => {
145
- res.header("Access-Control-Expose-Headers", "mcp-session-id");
172
+ res.header("Access-Control-Expose-Headers", "mcp-session-id, WWW-Authenticate");
146
173
  next();
147
174
  });
148
175
  const webAppTransports = new Map(); // Web app transports by web app sessionId
@@ -262,6 +289,25 @@ const createCustomFetch = (headerHolder) => {
262
289
  });
263
290
  // Get the response from node-fetch (cast input and init to handle type differences)
264
291
  const response = await fetch(input, { ...init, headers: headersObject });
292
+ if (response.status === 401) {
293
+ const wwwAuthenticate = response.headers.get("www-authenticate") ?? undefined;
294
+ const contentType = response.headers.get("content-type") ?? "application/json";
295
+ const body = await response.text();
296
+ headerHolder.lastUpstream401 = {
297
+ wwwAuthenticate,
298
+ body,
299
+ contentType,
300
+ };
301
+ const responseHeaders = {};
302
+ response.headers.forEach((value, key) => {
303
+ responseHeaders[key] = value;
304
+ });
305
+ return new Response(body, {
306
+ status: 401,
307
+ statusText: response.statusText,
308
+ headers: responseHeaders,
309
+ });
310
+ }
265
311
  // Check if this is an SSE request by looking at the Accept header
266
312
  const acceptHeader = finalHeaders.get("Accept");
267
313
  const isSSE = acceptHeader?.includes("text/event-stream");
@@ -357,7 +403,7 @@ app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
357
403
  }
358
404
  catch (error) {
359
405
  console.error("Error in /mcp route:", error);
360
- res.status(500).json(error);
406
+ sendErrorResponse(res, 500, "Internal server error");
361
407
  }
362
408
  });
363
409
  app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
@@ -379,13 +425,15 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
379
425
  }
380
426
  catch (error) {
381
427
  console.error("Error in /mcp route:", error);
382
- res.status(500).json(error);
428
+ sendErrorResponse(res, 500, "Internal server error");
383
429
  }
384
430
  }
385
431
  else {
386
432
  console.log("New StreamableHttp connection request");
433
+ let streamableHeaderHolder;
387
434
  try {
388
435
  const { transport: serverTransport, headerHolder } = await createTransport(req);
436
+ streamableHeaderHolder = headerHolder;
389
437
  const webAppTransport = new StreamableHTTPServerTransport({
390
438
  sessionIdGenerator: randomUUID,
391
439
  onsessioninitialized: (sessionId) => {
@@ -407,17 +455,18 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
407
455
  mcpProxy({
408
456
  transportToClient: webAppTransport,
409
457
  transportToServer: serverTransport,
458
+ headerHolder,
410
459
  });
411
460
  await webAppTransport.handleRequest(req, res, req.body);
412
461
  }
413
462
  catch (error) {
414
463
  if (is401Error(error)) {
415
464
  console.error("Received 401 Unauthorized from MCP server:", error instanceof Error ? error.message : error);
416
- res.status(401).json(error);
465
+ sendProxiedUnauthorized(res, streamableHeaderHolder);
417
466
  return;
418
467
  }
419
468
  console.error("Error in /mcp POST route:", error);
420
- res.status(500).json(error);
469
+ sendErrorResponse(res, 500, "Internal server error");
421
470
  }
422
471
  }
423
472
  });
@@ -442,7 +491,7 @@ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res)
442
491
  }
443
492
  catch (error) {
444
493
  console.error("Error in /mcp route:", error);
445
- res.status(500).json(error);
494
+ sendErrorResponse(res, 500, "Internal server error");
446
495
  }
447
496
  }
448
497
  });
@@ -546,17 +595,19 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
546
595
  catch (error) {
547
596
  if (is401Error(error)) {
548
597
  console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
549
- res.status(401).json(error);
598
+ sendProxiedUnauthorized(res, undefined);
550
599
  return;
551
600
  }
552
601
  console.error("Error in /stdio route:", error);
553
- res.status(500).json(error);
602
+ sendErrorResponse(res, 500, "Internal server error");
554
603
  }
555
604
  });
556
605
  app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
606
+ let sseHeaderHolder;
557
607
  try {
558
608
  console.log("New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp");
559
609
  const { transport: serverTransport, headerHolder } = await createTransport(req);
610
+ sseHeaderHolder = headerHolder;
560
611
  const proxyFullAddress = req.query.proxyFullAddress || "";
561
612
  const prefix = proxyFullAddress || "";
562
613
  const endpoint = `${prefix}/message`;
@@ -572,25 +623,29 @@ app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) =>
572
623
  mcpProxy({
573
624
  transportToClient: webAppTransport,
574
625
  transportToServer: serverTransport,
626
+ headerHolder,
575
627
  });
576
628
  }
577
629
  catch (error) {
578
630
  if (is401Error(error)) {
579
631
  console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
580
- res.status(401).json(error);
632
+ sendProxiedUnauthorized(res, sseHeaderHolder);
581
633
  return;
582
634
  }
583
635
  else if (error instanceof SseError && error.code === 404) {
584
636
  console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
585
- res.status(404).json(error);
637
+ sendErrorResponse(res, 404, "MCP server returned 404. Does it support SSE?");
586
638
  return;
587
639
  }
588
- else if (JSON.stringify(error).includes("ECONNREFUSED")) {
640
+ else if (error instanceof Error &&
641
+ (error.message.includes("ECONNREFUSED") ||
642
+ (error.cause && String(error.cause).includes("ECONNREFUSED")))) {
589
643
  console.error("Connection refused. Is the MCP server running?");
590
- res.status(500).json(error);
644
+ sendErrorResponse(res, 500, "Connection refused. Is the MCP server running?");
645
+ return;
591
646
  }
592
647
  console.error("Error in /sse route:", error);
593
- res.status(500).json(error);
648
+ sendErrorResponse(res, 500, "Internal server error");
594
649
  }
595
650
  });
596
651
  app.post("/message", originValidationMiddleware, authMiddleware, async (req, res) => {
@@ -610,7 +665,7 @@ app.post("/message", originValidationMiddleware, authMiddleware, async (req, res
610
665
  }
611
666
  catch (error) {
612
667
  console.error("Error in /message route:", error);
613
- res.status(500).json(error);
668
+ sendErrorResponse(res, 500, "Internal server error");
614
669
  }
615
670
  });
616
671
  app.get("/health", (req, res) => {
@@ -618,6 +673,48 @@ app.get("/health", (req, res) => {
618
673
  status: "ok",
619
674
  });
620
675
  });
676
+ app.post("/fetch", express.json(), originValidationMiddleware, authMiddleware, async (req, res) => {
677
+ try {
678
+ const { url, init } = req.body;
679
+ if (typeof url !== "string" || url.length === 0) {
680
+ res.status(400).json({ error: "Missing or invalid url" });
681
+ return;
682
+ }
683
+ let parsedUrl;
684
+ try {
685
+ parsedUrl = new URL(url);
686
+ }
687
+ catch {
688
+ res.status(400).json({ error: "Invalid URL" });
689
+ return;
690
+ }
691
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
692
+ res.status(400).json({ error: "Only http/https URLs are allowed" });
693
+ return;
694
+ }
695
+ const response = await fetch(url, {
696
+ method: init?.method ?? "GET",
697
+ headers: init?.headers ?? {},
698
+ body: init?.body,
699
+ });
700
+ const responseBody = await response.text();
701
+ const headers = {};
702
+ response.headers.forEach((value, key) => {
703
+ headers[key] = value;
704
+ });
705
+ res.status(response.status).json({
706
+ ok: response.ok,
707
+ status: response.status,
708
+ statusText: response.statusText,
709
+ headers,
710
+ body: responseBody,
711
+ });
712
+ }
713
+ catch (error) {
714
+ console.error("Error in /fetch route:", error);
715
+ sendErrorResponse(res, 500, "Internal server error");
716
+ }
717
+ });
621
718
  app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
622
719
  try {
623
720
  res.json({
@@ -630,7 +727,7 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
630
727
  }
631
728
  catch (error) {
632
729
  console.error("Error in /config route:", error);
633
- res.status(500).json(error);
730
+ sendErrorResponse(res, 500, "Internal server error");
634
731
  }
635
732
  });
636
733
  app.get("/sandbox", sandboxRateLimiter, (req, res) => {
@@ -1,4 +1,20 @@
1
+ import { SseError } from "@modelcontextprotocol/sdk/client/sse.js";
2
+ import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1
3
  import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
4
+ /**
5
+ * JSON-RPC error code for failed proxy → upstream transport sends.
6
+ *
7
+ * JSON-RPC 2.0 reserves **-32000 .. -32099** for implementation-defined *server* errors.
8
+ * MCP uses that band for protocol/SDK errors (see `@modelcontextprotocol/sdk` `ErrorCode`, e.g.
9
+ * `-32000` connection closed, `-32001` request timeout, `-32042` URL elicitation; other codes in
10
+ * the band are assigned over time — e.g. **-32002** is used for **ResourceNotFound** in parts of
11
+ * the MCP ecosystem). We use **-32099** so this inspector-only bridge error stays clear of those
12
+ * registered meanings; the client still keys off `error.data` shape, not the number alone.
13
+ *
14
+ * Keep in sync with `client/src/lib/constants.ts`; drift is caught by
15
+ * `client/src/lib/__tests__/mcpProxyTransportErrorCode.test.ts`.
16
+ */
17
+ export const MCP_PROXY_TRANSPORT_ERROR_CODE = -32099;
2
18
  function onClientError(error) {
3
19
  console.error("Error from inspector client:", error);
4
20
  }
@@ -13,23 +29,63 @@ function onServerError(error) {
13
29
  console.error("Error from MCP server:", error);
14
30
  }
15
31
  }
16
- export default function mcpProxy({ transportToClient, transportToServer, }) {
32
+ /** Exported for unit tests; used by the proxy when forwarding transport failures. */
33
+ export function serializeProxyTransportError(error, headerHolder) {
34
+ const data = {
35
+ message: error.message,
36
+ name: error.name,
37
+ };
38
+ if (error.cause !== undefined) {
39
+ data.cause =
40
+ error.cause instanceof Error ? error.cause.message : String(error.cause);
41
+ }
42
+ const attachHttpStatusIfValid = (code) => {
43
+ if (Number.isInteger(code) && code >= 100 && code <= 599) {
44
+ data.httpStatus = code;
45
+ }
46
+ };
47
+ if (error instanceof StreamableHTTPError) {
48
+ if (error.code !== undefined)
49
+ attachHttpStatusIfValid(error.code);
50
+ }
51
+ else if (error instanceof SseError) {
52
+ if (error.code !== undefined)
53
+ attachHttpStatusIfValid(error.code);
54
+ }
55
+ if (headerHolder?.lastUpstream401) {
56
+ const u = headerHolder.lastUpstream401;
57
+ data.upstream401 = {
58
+ wwwAuthenticate: u.wwwAuthenticate,
59
+ body: u.body,
60
+ contentType: u.contentType,
61
+ };
62
+ delete headerHolder.lastUpstream401;
63
+ }
64
+ return data;
65
+ }
66
+ export default function mcpProxy({ transportToClient, transportToServer, headerHolder, }) {
17
67
  let transportToClientClosed = false;
18
68
  let transportToServerClosed = false;
19
69
  let reportedServerSession = false;
20
70
  transportToClient.onmessage = (message) => {
21
- transportToServer.send(message).catch((error) => {
71
+ transportToServer.send(message).catch((err) => {
72
+ const error = err instanceof Error ? err : new Error(String(err));
22
73
  // Send error response back to client if it was a request (has id) and connection is still open
23
74
  if (isJSONRPCRequest(message) && !transportToClientClosed) {
75
+ const causeStr = error.cause !== undefined
76
+ ? error.cause instanceof Error
77
+ ? error.cause.message
78
+ : String(error.cause)
79
+ : null;
24
80
  const errorResponse = {
25
81
  jsonrpc: "2.0",
26
82
  id: message.id,
27
83
  error: {
28
- code: -32001,
29
- message: error.cause
30
- ? `${error.message} (cause: ${error.cause})`
84
+ code: MCP_PROXY_TRANSPORT_ERROR_CODE,
85
+ message: causeStr
86
+ ? `${error.message} (cause: ${causeStr})`
31
87
  : error.message,
32
- data: error,
88
+ data: serializeProxyTransportError(error, headerHolder),
33
89
  },
34
90
  };
35
91
  transportToClient.send(errorResponse).catch(onClientError);