@modelcontextprotocol/inspector 0.22.0 → 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.
- package/client/dist/assets/{OAuthCallback-Cd9z3Q8P.js → OAuthCallback-D56s1eS_.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-h_lUPqCA.js → OAuthDebugCallback-CdGJkhXe.js} +1 -1
- package/client/dist/assets/{index-CfzB1GD8.css → index-BlUSNvoM.css} +4 -5
- package/client/dist/assets/{index-CngsKvMW.js → index-DjSLdS7f.js} +1521 -1554
- package/client/dist/index.html +2 -2
- package/package.json +15 -7
- package/server/build/index.js +123 -5
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-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.
|
|
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.
|
|
56
|
-
"@modelcontextprotocol/inspector-client": "^0.
|
|
57
|
-
"@modelcontextprotocol/inspector-server": "^0.
|
|
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.
|
|
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) => {
|