@modelcontextprotocol/inspector 0.22.0 → 1.0.1
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/README.md +11 -1
- package/cli/build/cli.js +13 -0
- package/client/bin/start.js +5 -0
- package/client/dist/assets/{OAuthCallback-Cd9z3Q8P.js → OAuthCallback-DKaW5Tk8.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-h_lUPqCA.js → OAuthDebugCallback-Bx3X7Zkh.js} +1 -1
- package/client/dist/assets/{index-CngsKvMW.js → index-CH-qD91V.js} +3586 -2830
- package/client/dist/assets/{index-CfzB1GD8.css → index-DXy0twoH.css} +52 -5
- package/client/dist/index.html +2 -2
- package/package.json +15 -7
- package/server/build/index.js +126 -5
|
@@ -65,11 +65,6 @@ code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-select
|
|
|
65
65
|
background: #b3d4fc;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
|
69
|
-
text-shadow: none;
|
|
70
|
-
background: #b3d4fc;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
68
|
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
|
74
69
|
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
|
75
70
|
text-shadow: none;
|
|
@@ -1023,6 +1018,9 @@ video {
|
|
|
1023
1018
|
.max-h-screen {
|
|
1024
1019
|
max-height: 100vh;
|
|
1025
1020
|
}
|
|
1021
|
+
.min-h-0 {
|
|
1022
|
+
min-height: 0px;
|
|
1023
|
+
}
|
|
1026
1024
|
.min-h-\[100px\] {
|
|
1027
1025
|
min-height: 100px;
|
|
1028
1026
|
}
|
|
@@ -1371,6 +1369,10 @@ video {
|
|
|
1371
1369
|
.border-transparent {
|
|
1372
1370
|
border-color: transparent;
|
|
1373
1371
|
}
|
|
1372
|
+
.border-yellow-300 {
|
|
1373
|
+
--tw-border-opacity: 1;
|
|
1374
|
+
border-color: rgb(253 224 71 / var(--tw-border-opacity, 1));
|
|
1375
|
+
}
|
|
1374
1376
|
.border-t-transparent {
|
|
1375
1377
|
border-top-color: transparent;
|
|
1376
1378
|
}
|
|
@@ -1462,6 +1464,14 @@ video {
|
|
|
1462
1464
|
.bg-transparent {
|
|
1463
1465
|
background-color: transparent;
|
|
1464
1466
|
}
|
|
1467
|
+
.bg-yellow-100 {
|
|
1468
|
+
--tw-bg-opacity: 1;
|
|
1469
|
+
background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1));
|
|
1470
|
+
}
|
|
1471
|
+
.bg-yellow-200 {
|
|
1472
|
+
--tw-bg-opacity: 1;
|
|
1473
|
+
background-color: rgb(254 240 138 / var(--tw-bg-opacity, 1));
|
|
1474
|
+
}
|
|
1465
1475
|
.object-contain {
|
|
1466
1476
|
-o-object-fit: contain;
|
|
1467
1477
|
object-fit: contain;
|
|
@@ -1748,6 +1758,10 @@ video {
|
|
|
1748
1758
|
--tw-text-opacity: 1;
|
|
1749
1759
|
color: rgb(202 138 4 / var(--tw-text-opacity, 1));
|
|
1750
1760
|
}
|
|
1761
|
+
.text-yellow-900 {
|
|
1762
|
+
--tw-text-opacity: 1;
|
|
1763
|
+
color: rgb(113 63 18 / var(--tw-text-opacity, 1));
|
|
1764
|
+
}
|
|
1751
1765
|
.underline-offset-4 {
|
|
1752
1766
|
text-underline-offset: 4px;
|
|
1753
1767
|
}
|
|
@@ -2017,6 +2031,11 @@ h1 {
|
|
|
2017
2031
|
background-color: hsl(var(--secondary) / 0.8);
|
|
2018
2032
|
}
|
|
2019
2033
|
|
|
2034
|
+
.hover\:bg-yellow-200:hover {
|
|
2035
|
+
--tw-bg-opacity: 1;
|
|
2036
|
+
background-color: rgb(254 240 138 / var(--tw-bg-opacity, 1));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2020
2039
|
.hover\:text-accent-foreground:hover {
|
|
2021
2040
|
color: hsl(var(--accent-foreground));
|
|
2022
2041
|
}
|
|
@@ -2436,6 +2455,11 @@ h1 {
|
|
|
2436
2455
|
border-color: rgb(71 85 105 / var(--tw-border-opacity, 1));
|
|
2437
2456
|
}
|
|
2438
2457
|
|
|
2458
|
+
.dark\:border-yellow-700:is(.dark *) {
|
|
2459
|
+
--tw-border-opacity: 1;
|
|
2460
|
+
border-color: rgb(161 98 7 / var(--tw-border-opacity, 1));
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2439
2463
|
.dark\:bg-blue-900:is(.dark *) {
|
|
2440
2464
|
--tw-bg-opacity: 1;
|
|
2441
2465
|
background-color: rgb(30 58 138 / var(--tw-bg-opacity, 1));
|
|
@@ -2465,11 +2489,24 @@ h1 {
|
|
|
2465
2489
|
background-color: rgb(127 29 29 / 0.2);
|
|
2466
2490
|
}
|
|
2467
2491
|
|
|
2492
|
+
.dark\:bg-red-950\/30:is(.dark *) {
|
|
2493
|
+
background-color: rgb(69 10 10 / 0.3);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2468
2496
|
.dark\:bg-slate-800:is(.dark *) {
|
|
2469
2497
|
--tw-bg-opacity: 1;
|
|
2470
2498
|
background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1));
|
|
2471
2499
|
}
|
|
2472
2500
|
|
|
2501
|
+
.dark\:bg-yellow-800:is(.dark *) {
|
|
2502
|
+
--tw-bg-opacity: 1;
|
|
2503
|
+
background-color: rgb(133 77 14 / var(--tw-bg-opacity, 1));
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
.dark\:bg-yellow-900\/40:is(.dark *) {
|
|
2507
|
+
background-color: rgb(113 63 18 / 0.4);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2473
2510
|
.dark\:text-blue-200:is(.dark *) {
|
|
2474
2511
|
--tw-text-opacity: 1;
|
|
2475
2512
|
color: rgb(191 219 254 / var(--tw-text-opacity, 1));
|
|
@@ -2530,6 +2567,11 @@ h1 {
|
|
|
2530
2567
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
|
2531
2568
|
}
|
|
2532
2569
|
|
|
2570
|
+
.dark\:text-yellow-100:is(.dark *) {
|
|
2571
|
+
--tw-text-opacity: 1;
|
|
2572
|
+
color: rgb(254 249 195 / var(--tw-text-opacity, 1));
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2533
2575
|
.dark\:text-yellow-500:is(.dark *) {
|
|
2534
2576
|
--tw-text-opacity: 1;
|
|
2535
2577
|
color: rgb(234 179 8 / var(--tw-text-opacity, 1));
|
|
@@ -2552,6 +2594,11 @@ h1 {
|
|
|
2552
2594
|
background-color: hsl(var(--secondary));
|
|
2553
2595
|
}
|
|
2554
2596
|
|
|
2597
|
+
.dark\:hover\:bg-yellow-800:hover:is(.dark *) {
|
|
2598
|
+
--tw-bg-opacity: 1;
|
|
2599
|
+
background-color: rgb(133 77 14 / var(--tw-bg-opacity, 1));
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2555
2602
|
.dark\:hover\:text-blue-300:hover:is(.dark *) {
|
|
2556
2603
|
--tw-text-opacity: 1;
|
|
2557
2604
|
color: rgb(147 197 253 / var(--tw-text-opacity, 1));
|
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-CH-qD91V.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DXy0twoH.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.
|
|
3
|
+
"version": "1.0.1",
|
|
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.",
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
"check-version": "node scripts/check-version-consistency.js"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/inspector-cli": "^0.
|
|
56
|
-
"@modelcontextprotocol/inspector-client": "^0.
|
|
57
|
-
"@modelcontextprotocol/inspector-server": "^0.
|
|
55
|
+
"@modelcontextprotocol/inspector-cli": "^1.0.1",
|
|
56
|
+
"@modelcontextprotocol/inspector-client": "^1.0.1",
|
|
57
|
+
"@modelcontextprotocol/inspector-server": "^1.0.1",
|
|
58
58
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
59
59
|
"concurrently": "^9.2.0",
|
|
60
60
|
"node-fetch": "^3.3.2",
|
|
61
61
|
"open": "^10.2.0",
|
|
62
|
-
"shell-quote": "^1.8.
|
|
62
|
+
"shell-quote": "^1.8.4",
|
|
63
63
|
"spawn-rx": "^5.1.2",
|
|
64
64
|
"ts-node": "^10.9.2",
|
|
65
65
|
"zod": "^3.25.76"
|
|
@@ -73,12 +73,20 @@
|
|
|
73
73
|
"jest-fixed-jsdom": "^0.0.9",
|
|
74
74
|
"lint-staged": "^16.1.5",
|
|
75
75
|
"playwright": "^1.56.1",
|
|
76
|
-
"prettier": "
|
|
76
|
+
"prettier": "3.9.5",
|
|
77
77
|
"rimraf": "^6.0.1",
|
|
78
78
|
"typescript": "^5.4.2"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
|
-
"get-intrinsic": "1.3.0"
|
|
81
|
+
"get-intrinsic": "1.3.0",
|
|
82
|
+
"shell-quote": "^1.8.4",
|
|
83
|
+
"hono": "^4.12.25",
|
|
84
|
+
"form-data": "^4.0.6",
|
|
85
|
+
"@babel/core@<=7.29.0": "^7.29.6",
|
|
86
|
+
"esbuild@<0.28.1": "^0.28.1",
|
|
87
|
+
"minimatch@<3.1.4": "3.1.5",
|
|
88
|
+
"js-yaml@<4": "3.15.0",
|
|
89
|
+
"js-yaml@>=4": "4.2.0"
|
|
82
90
|
},
|
|
83
91
|
"engines": {
|
|
84
92
|
"node": ">=22.7.5"
|
package/server/build/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import rateLimit from "express-rate-limit";
|
|
|
16
16
|
import { findActualExecutable } from "spawn-rx";
|
|
17
17
|
import mcpProxy from "./mcpProxy.js";
|
|
18
18
|
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
|
|
19
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
20
|
+
import { isIP } from "node:net";
|
|
19
21
|
import { fileURLToPath } from "url";
|
|
20
22
|
import { dirname, join } from "path";
|
|
21
23
|
import { readFileSync } from "fs";
|
|
@@ -673,6 +675,116 @@ app.get("/health", (req, res) => {
|
|
|
673
675
|
status: "ok",
|
|
674
676
|
});
|
|
675
677
|
});
|
|
678
|
+
// Raised when the /fetch proxy is asked to reach a disallowed target, so the
|
|
679
|
+
// route can distinguish an SSRF rejection (403) from an upstream failure (500).
|
|
680
|
+
class ProxyTargetError extends Error {
|
|
681
|
+
}
|
|
682
|
+
const MAX_PROXY_REDIRECTS = 5;
|
|
683
|
+
// The /fetch proxy is used by the client to reach OAuth discovery/token
|
|
684
|
+
// endpoints on the connected MCP server (bypassing browser CORS). Connecting
|
|
685
|
+
// to loopback and RFC1918/private LAN hosts is a core use case (testing local
|
|
686
|
+
// or self-hosted MCP servers), so those stay reachable. We only block
|
|
687
|
+
// link-local addresses — 169.254.0.0/16 (the AWS/GCP/Azure/etc. cloud metadata
|
|
688
|
+
// endpoint 169.254.169.254 lives here) and IPv6 link-local / the AWS IPv6 IMDS
|
|
689
|
+
// address — which are never legitimate proxy targets and are the real SSRF
|
|
690
|
+
// exfiltration vector.
|
|
691
|
+
function isBlockedProxyAddress(ip) {
|
|
692
|
+
const kind = isIP(ip);
|
|
693
|
+
if (kind === 4) {
|
|
694
|
+
const parts = ip.split(".").map(Number);
|
|
695
|
+
// 169.254.0.0/16 — IPv4 link-local (incl. cloud metadata 169.254.169.254)
|
|
696
|
+
return parts[0] === 169 && parts[1] === 254;
|
|
697
|
+
}
|
|
698
|
+
if (kind === 6) {
|
|
699
|
+
const addr = ip.toLowerCase();
|
|
700
|
+
// IPv4-mapped IPv6 (::ffff:x). The WHATWG URL parser serializes the embedded
|
|
701
|
+
// IPv4 in hex (::ffff:a9fe:a9fe for 169.254.169.254), while DNS and other
|
|
702
|
+
// sources may use the dotted form (::ffff:169.254.169.254) — handle both by
|
|
703
|
+
// extracting the IPv4 and re-checking it.
|
|
704
|
+
const mapped = addr.match(/^::ffff:(.+)$/);
|
|
705
|
+
if (mapped) {
|
|
706
|
+
const tail = mapped[1];
|
|
707
|
+
if (isIP(tail) === 4) {
|
|
708
|
+
return isBlockedProxyAddress(tail);
|
|
709
|
+
}
|
|
710
|
+
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
711
|
+
if (hex) {
|
|
712
|
+
const high = parseInt(hex[1], 16);
|
|
713
|
+
const low = parseInt(hex[2], 16);
|
|
714
|
+
return isBlockedProxyAddress(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// fe80::/10 — IPv6 link-local
|
|
718
|
+
if (/^fe[89ab]/.test(addr)) {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
// fd00:ec2::254 — AWS IPv6 instance metadata endpoint
|
|
722
|
+
return addr === "fd00:ec2::254";
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
// Resolves the target host and rejects it if any resolved address is a blocked
|
|
727
|
+
// (link-local/metadata) address, mitigating SSRF including the DNS case where a
|
|
728
|
+
// hostname resolves to the cloud metadata IP.
|
|
729
|
+
async function assertSafeProxyTarget(parsedUrl) {
|
|
730
|
+
const host = parsedUrl.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
731
|
+
let addresses;
|
|
732
|
+
if (isIP(host)) {
|
|
733
|
+
addresses = [host];
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
try {
|
|
737
|
+
addresses = (await dnsLookup(host, { all: true })).map((r) => r.address);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (addresses.length === 0) {
|
|
744
|
+
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
745
|
+
}
|
|
746
|
+
for (const address of addresses) {
|
|
747
|
+
if (isBlockedProxyAddress(address)) {
|
|
748
|
+
throw new ProxyTargetError(`Refusing to proxy request to blocked address (${address})`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Fetch that follows redirects manually so every hop's target is re-validated
|
|
753
|
+
// against assertSafeProxyTarget — otherwise an allowed host could redirect the
|
|
754
|
+
// proxy into a blocked internal/metadata address.
|
|
755
|
+
async function safeProxyFetch(initialUrl, init) {
|
|
756
|
+
let currentUrl = new URL(initialUrl);
|
|
757
|
+
let method = init?.method ?? "GET";
|
|
758
|
+
let body = init?.body;
|
|
759
|
+
const headers = init?.headers ?? {};
|
|
760
|
+
for (let hop = 0; hop <= MAX_PROXY_REDIRECTS; hop++) {
|
|
761
|
+
if (!["http:", "https:"].includes(currentUrl.protocol)) {
|
|
762
|
+
throw new ProxyTargetError("Only http/https URLs are allowed");
|
|
763
|
+
}
|
|
764
|
+
await assertSafeProxyTarget(currentUrl);
|
|
765
|
+
const response = await fetch(currentUrl.toString(), {
|
|
766
|
+
method,
|
|
767
|
+
headers,
|
|
768
|
+
body,
|
|
769
|
+
redirect: "manual",
|
|
770
|
+
});
|
|
771
|
+
if (response.status >= 300 && response.status < 400) {
|
|
772
|
+
const location = response.headers.get("location");
|
|
773
|
+
if (!location) {
|
|
774
|
+
return response;
|
|
775
|
+
}
|
|
776
|
+
currentUrl = new URL(location, currentUrl);
|
|
777
|
+
// Mirror browser/fetch semantics: 301/302/303 downgrade to a bodyless GET.
|
|
778
|
+
if ([301, 302, 303].includes(response.status)) {
|
|
779
|
+
method = "GET";
|
|
780
|
+
body = undefined;
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
return response;
|
|
785
|
+
}
|
|
786
|
+
throw new ProxyTargetError("Too many redirects");
|
|
787
|
+
}
|
|
676
788
|
app.post("/fetch", express.json(), originValidationMiddleware, authMiddleware, async (req, res) => {
|
|
677
789
|
try {
|
|
678
790
|
const { url, init } = req.body;
|
|
@@ -692,11 +804,17 @@ app.post("/fetch", express.json(), originValidationMiddleware, authMiddleware, a
|
|
|
692
804
|
res.status(400).json({ error: "Only http/https URLs are allowed" });
|
|
693
805
|
return;
|
|
694
806
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
807
|
+
let response;
|
|
808
|
+
try {
|
|
809
|
+
response = await safeProxyFetch(url, init);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (error instanceof ProxyTargetError) {
|
|
813
|
+
res.status(403).json({ error: error.message });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
700
818
|
const responseBody = await response.text();
|
|
701
819
|
const headers = {};
|
|
702
820
|
response.headers.forEach((value, key) => {
|
|
@@ -748,6 +866,9 @@ const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT,
|
|
|
748
866
|
const HOST = process.env.HOST || "localhost";
|
|
749
867
|
const server = app.listen(PORT, HOST);
|
|
750
868
|
server.on("listening", () => {
|
|
869
|
+
console.log(`\n⚠️ @modelcontextprotocol/inspector v1 is deprecated.\n` +
|
|
870
|
+
` Upgrade: npx @modelcontextprotocol/inspector@latest\n` +
|
|
871
|
+
` v1 receives security fixes only.\n`);
|
|
751
872
|
console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);
|
|
752
873
|
if (!authDisabled) {
|
|
753
874
|
console.log(`🔑 Session token: ${sessionToken}\n ` +
|