@modelcontextprotocol/inspector 0.21.2 → 1.0.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-zNfT6w38.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-D3cjomHV.css">
8
+ <script type="module" crossorigin src="/assets/index-DjSLdS7f.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BlUSNvoM.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.2",
3
+ "version": "1.0.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.",
@@ -52,14 +52,14 @@
52
52
  "check-version": "node scripts/check-version-consistency.js"
53
53
  },
54
54
  "dependencies": {
55
- "@modelcontextprotocol/inspector-cli": "^0.21.2",
56
- "@modelcontextprotocol/inspector-client": "^0.21.2",
57
- "@modelcontextprotocol/inspector-server": "^0.21.2",
55
+ "@modelcontextprotocol/inspector-cli": "^1.0.0",
56
+ "@modelcontextprotocol/inspector-client": "^1.0.0",
57
+ "@modelcontextprotocol/inspector-server": "^1.0.0",
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) => {