@modelcontextprotocol/inspector 0.21.1 → 0.21.2
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.
- package/client/dist/assets/{OAuthCallback-BmmrcnPJ.js → OAuthCallback-BbXbF8aU.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-DZk_RAqV.js → OAuthDebugCallback-dQjnRYOz.js} +1 -1
- package/client/dist/assets/{index-_5aZhNPC.css → index-D3cjomHV.css} +8 -0
- package/client/dist/assets/{index-uKaJPCZp.js → index-zNfT6w38.js} +9163 -14961
- package/client/dist/index.html +2 -2
- package/package.json +8 -4
- package/server/build/index.js +112 -15
- package/server/build/mcpProxy.js +62 -6
package/client/dist/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-zNfT6w38.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D3cjomHV.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.
|
|
3
|
+
"version": "0.21.2",
|
|
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.
|
|
52
|
-
"@modelcontextprotocol/inspector-client": "^0.21.
|
|
53
|
-
"@modelcontextprotocol/inspector-server": "^0.21.
|
|
55
|
+
"@modelcontextprotocol/inspector-cli": "^0.21.2",
|
|
56
|
+
"@modelcontextprotocol/inspector-client": "^0.21.2",
|
|
57
|
+
"@modelcontextprotocol/inspector-server": "^0.21.2",
|
|
54
58
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
55
59
|
"concurrently": "^9.2.0",
|
|
56
60
|
"node-fetch": "^3.3.2",
|
package/server/build/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
465
|
+
sendProxiedUnauthorized(res, streamableHeaderHolder);
|
|
417
466
|
return;
|
|
418
467
|
}
|
|
419
468
|
console.error("Error in /mcp POST route:", error);
|
|
420
|
-
res
|
|
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
|
|
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
|
|
598
|
+
sendProxiedUnauthorized(res, undefined);
|
|
550
599
|
return;
|
|
551
600
|
}
|
|
552
601
|
console.error("Error in /stdio route:", error);
|
|
553
|
-
res
|
|
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
|
|
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
|
|
637
|
+
sendErrorResponse(res, 404, "MCP server returned 404. Does it support SSE?");
|
|
586
638
|
return;
|
|
587
639
|
}
|
|
588
|
-
else if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
730
|
+
sendErrorResponse(res, 500, "Internal server error");
|
|
634
731
|
}
|
|
635
732
|
});
|
|
636
733
|
app.get("/sandbox", sandboxRateLimiter, (req, res) => {
|
package/server/build/mcpProxy.js
CHANGED
|
@@ -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
|
-
|
|
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((
|
|
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:
|
|
29
|
-
message:
|
|
30
|
-
? `${error.message} (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);
|