@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.
@@ -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));
@@ -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-CngsKvMW.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CfzB1GD8.css">
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.22.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.22.0",
56
- "@modelcontextprotocol/inspector-client": "^0.22.0",
57
- "@modelcontextprotocol/inspector-server": "^0.22.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.3",
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": "^3.7.1",
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"
@@ -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
- const response = await fetch(url, {
696
- method: init?.method ?? "GET",
697
- headers: init?.headers ?? {},
698
- body: init?.body,
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 ` +