@modelcontextprotocol/inspector 2.3.0 → 2.4.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/README.md +185 -27
- package/clients/cli/build/index.js +11489 -10818
- package/clients/launcher/build/index.js +6 -4
- package/clients/tui/build/index.js +2580 -374
- package/clients/web/build/index.js +2083 -259
- package/clients/web/dist/assets/{index-CiUpWldH.css → index-DQVN73FG.css} +1 -1
- package/clients/web/dist/assets/index-JCsLrWVa.js +1484 -0
- package/clients/web/dist/assets/{prism-light-DBxbqjnM.js → prism-light-HLcnHJHw.js} +1 -1
- package/clients/web/dist/assets/worker-json-8CDXFfik.js +1631 -0
- package/clients/web/dist/index.html +2 -2
- package/clients/web/static/sandbox_proxy.html +122 -10
- package/package.json +10 -6
- package/clients/tui/build/chunk-MCKGQKYU.js +0 -15
- package/clients/tui/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/build/chunk-MCKGQKYU.js +0 -15
- package/clients/web/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/dist/assets/index-CwhDusR3.js +0 -237
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@300..700&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet" />
|
|
10
10
|
<title>MCP Inspector</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-JCsLrWVa.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DQVN73FG.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="color-scheme" content="light dark" />
|
|
6
|
-
<!-- Isolation model: the inner iframe is sandboxed WITHOUT
|
|
6
|
+
<!-- Isolation model: by default the inner iframe is sandboxed WITHOUT
|
|
7
7
|
`allow-same-origin`, so the untrusted app runs under an opaque
|
|
8
8
|
("null") origin. That is the structural boundary — the app cannot
|
|
9
9
|
touch this proxy's DOM, window, or cookies, so it cannot bypass its
|
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
resource-ready message's `sandbox` field; `allow-same-origin` is
|
|
16
16
|
always stripped from a server-supplied value.
|
|
17
17
|
|
|
18
|
+
Dedicated-origin exception (#2056): an app whose UI resource declares
|
|
19
|
+
`_meta.ui.domain` is instead served from the host's own app-origin
|
|
20
|
+
listener, handed to us as the resource-ready message's `src`. That
|
|
21
|
+
frame IS granted `allow-same-origin` — an opaque origin sends
|
|
22
|
+
`Origin: null`, which defeats the CORS / OAuth-callback / API-key
|
|
23
|
+
allowlists the field exists to support. The boundary is preserved by
|
|
24
|
+
the origin itself rather than by opacity: the listener is on its own
|
|
25
|
+
port, so the app is cross-origin to this proxy and to the host, and
|
|
26
|
+
the grant is refused outright if the URL's origin equals this page's.
|
|
27
|
+
The grant is host-driven — it is never reachable from the server's
|
|
28
|
+
own `sandbox` string, which is still stripped.
|
|
29
|
+
|
|
18
30
|
Network/resource egress from the inner document is constrained by a
|
|
19
31
|
Content-Security-Policy that the HOST builds and bakes into the
|
|
20
32
|
delivered HTML before it reaches this proxy (see
|
|
@@ -85,13 +97,21 @@
|
|
|
85
97
|
// This is the origin we expect all parent messages to come from.
|
|
86
98
|
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
|
|
87
99
|
|
|
88
|
-
// The inner iframe is sandboxed without allow-same-origin, so
|
|
89
|
-
// document has an opaque origin and every postMessage from it arrives
|
|
100
|
+
// The inner iframe is sandboxed without allow-same-origin by default, so
|
|
101
|
+
// its document has an opaque origin and every postMessage from it arrives
|
|
90
102
|
// with event.origin === "null" (the literal string). That value is
|
|
91
103
|
// checked below as a defense-in-depth assertion; the actual identity
|
|
92
104
|
// proof is `event.source === inner.contentWindow`, which a sibling
|
|
93
105
|
// frame cannot forge.
|
|
94
|
-
|
|
106
|
+
//
|
|
107
|
+
// The dedicated-origin path (#2056) is the one exception: when the host
|
|
108
|
+
// supplies a `src` on its own app-origin listener, the frame is granted
|
|
109
|
+
// allow-same-origin so the app has a REAL origin — which is the entire
|
|
110
|
+
// point, since an opaque origin sends `Origin: null` and defeats any
|
|
111
|
+
// CORS / OAuth-callback / API-key allowlist the app's backend applies.
|
|
112
|
+
// `innerOrigin` is then that URL's origin, and the assertion above still
|
|
113
|
+
// holds against it.
|
|
114
|
+
let innerOrigin = "null";
|
|
95
115
|
|
|
96
116
|
// Security self-test: verify iframe isolation is working correctly.
|
|
97
117
|
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
|
|
@@ -158,7 +178,47 @@
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
|
161
|
-
const { html, sandbox, permissions } = event.data.params;
|
|
181
|
+
const { html, sandbox, permissions, src } = event.data.params;
|
|
182
|
+
// The host asked for the dedicated-origin path: it published the
|
|
183
|
+
// wrapped document to its own app-origin listener and is handing
|
|
184
|
+
// us the URL. Only an absolute http(s) URL is honored — anything
|
|
185
|
+
// else (a relative path, a `javascript:` or `data:` URL) falls
|
|
186
|
+
// through to the srcdoc path below rather than being navigated to.
|
|
187
|
+
let srcUrl = null;
|
|
188
|
+
if (typeof src === "string") {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = new URL(src);
|
|
191
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
192
|
+
srcUrl = parsed;
|
|
193
|
+
} else {
|
|
194
|
+
console.warn("[Sandbox] ignoring non-http(s) src:", src);
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.warn("[Sandbox] ignoring unparseable src:", src);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// The app origin MUST differ from BOTH trusted origins — this
|
|
201
|
+
// proxy's own and the Inspector page above it. Granting
|
|
202
|
+
// allow-same-origin below is what makes the app's origin real, and
|
|
203
|
+
// an app sharing the proxy's origin would land in this realm
|
|
204
|
+
// outright; one sharing the HOST's origin is no better, since
|
|
205
|
+
// same-origin with the top document means direct access to
|
|
206
|
+
// `window.top`, its DOM and its storage — the cross-origin frame in
|
|
207
|
+
// between buys nothing once the inner document is same-origin with
|
|
208
|
+
// the thing it is trying to reach. Either is a host
|
|
209
|
+
// misconfiguration, not app input, so refuse rather than degrade
|
|
210
|
+
// quietly.
|
|
211
|
+
if (
|
|
212
|
+
srcUrl &&
|
|
213
|
+
(srcUrl.origin === window.location.origin ||
|
|
214
|
+
srcUrl.origin === EXPECTED_HOST_ORIGIN)
|
|
215
|
+
) {
|
|
216
|
+
console.error(
|
|
217
|
+
"[Sandbox] refusing a src on a trusted origin:",
|
|
218
|
+
srcUrl.origin,
|
|
219
|
+
);
|
|
220
|
+
srcUrl = null;
|
|
221
|
+
}
|
|
162
222
|
if (typeof sandbox === "string") {
|
|
163
223
|
// Never grant allow-same-origin even if the server asks for it —
|
|
164
224
|
// that would let the untrusted app reach this proxy's DOM and
|
|
@@ -179,13 +239,42 @@
|
|
|
179
239
|
}
|
|
180
240
|
inner.setAttribute("sandbox", safeTokens.join(" "));
|
|
181
241
|
}
|
|
242
|
+
if (srcUrl) {
|
|
243
|
+
// Re-grant what the strip above removes unconditionally. Safe
|
|
244
|
+
// here and only here: the document is served from a DIFFERENT
|
|
245
|
+
// origin than this proxy (asserted above), so same-origin policy
|
|
246
|
+
// still denies it this realm's DOM — it gains its own origin,
|
|
247
|
+
// not ours.
|
|
248
|
+
const tokens = (inner.getAttribute("sandbox") || "")
|
|
249
|
+
.split(/\s+/)
|
|
250
|
+
.filter(Boolean);
|
|
251
|
+
if (!tokens.some((t) => t.toLowerCase() === "allow-same-origin")) {
|
|
252
|
+
tokens.push("allow-same-origin");
|
|
253
|
+
}
|
|
254
|
+
inner.setAttribute("sandbox", tokens.join(" "));
|
|
255
|
+
}
|
|
182
256
|
// Set Permission Policy allow attribute if permissions are requested
|
|
183
257
|
const allowAttribute = buildAllowAttribute(permissions);
|
|
184
258
|
if (allowAttribute) {
|
|
185
259
|
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
|
186
260
|
inner.setAttribute("allow", allowAttribute);
|
|
187
261
|
}
|
|
188
|
-
if (
|
|
262
|
+
if (srcUrl) {
|
|
263
|
+
// Navigate to the host's app-origin URL. The per-app CSP is
|
|
264
|
+
// delivered as a real response header there (stronger than the
|
|
265
|
+
// <meta> the srcdoc path relies on), so no wrapping applies here
|
|
266
|
+
// — this proxy still never parses the untrusted bytes.
|
|
267
|
+
//
|
|
268
|
+
// Drop any `srcdoc` a previous render left behind first: it takes
|
|
269
|
+
// precedence over `src`, so a frame that had rendered an opaque
|
|
270
|
+
// document would keep showing it and never load this URL. Not
|
|
271
|
+
// reachable today (each render remounts the iframe, which reloads
|
|
272
|
+
// this document), but the cost of being wrong about that later is
|
|
273
|
+
// an app silently rendering the wrong bytes.
|
|
274
|
+
inner.removeAttribute("srcdoc");
|
|
275
|
+
innerOrigin = srcUrl.origin;
|
|
276
|
+
inner.src = srcUrl.href;
|
|
277
|
+
} else if (typeof html === "string") {
|
|
189
278
|
// The host has already wrapped the app's HTML in a fixed shell
|
|
190
279
|
// whose first <head> child is the CSP <meta>; assign it verbatim.
|
|
191
280
|
// srcdoc is the only delivery path that works without
|
|
@@ -194,11 +283,34 @@
|
|
|
194
283
|
// boundary). Known trade-off: a small set of libraries (e.g.
|
|
195
284
|
// CesiumJS Map) don't initialise correctly under srcdoc — that
|
|
196
285
|
// is accepted in exchange for the isolation guarantee.
|
|
286
|
+
//
|
|
287
|
+
// Restore the opaque-origin posture first. A frame that
|
|
288
|
+
// previously took the dedicated-origin path still carries
|
|
289
|
+
// `allow-same-origin`, and `innerOrigin` still names that origin;
|
|
290
|
+
// assigning `srcdoc` under those would put the untrusted document
|
|
291
|
+
// in THIS proxy's realm — the exact escape the double-iframe
|
|
292
|
+
// model exists to prevent. The sandbox attribute is read at
|
|
293
|
+
// navigation time and assigning `srcdoc` is the navigation, so
|
|
294
|
+
// stripping it here takes effect for the document being loaded.
|
|
295
|
+
const fallbackTokens = (inner.getAttribute("sandbox") || "")
|
|
296
|
+
.split(/\s+/)
|
|
297
|
+
.filter((t) => t && t.toLowerCase() !== "allow-same-origin");
|
|
298
|
+
inner.setAttribute("sandbox", fallbackTokens.join(" "));
|
|
299
|
+
innerOrigin = "null";
|
|
197
300
|
inner.srcdoc = html;
|
|
198
301
|
}
|
|
199
302
|
} else {
|
|
200
303
|
if (inner && inner.contentWindow) {
|
|
201
|
-
inner
|
|
304
|
+
// Target the inner frame's real origin on the dedicated-origin
|
|
305
|
+
// path, so a frame that navigated itself elsewhere stops
|
|
306
|
+
// receiving host messages. The opaque default has no targetable
|
|
307
|
+
// origin ("null" is not a valid targetOrigin), so it keeps "*" —
|
|
308
|
+
// safe there because the frame is `srcdoc` and cannot navigate
|
|
309
|
+
// to another origin's document without allow-top-navigation.
|
|
310
|
+
inner.contentWindow.postMessage(
|
|
311
|
+
event.data,
|
|
312
|
+
innerOrigin === "null" ? "*" : innerOrigin,
|
|
313
|
+
);
|
|
202
314
|
}
|
|
203
315
|
}
|
|
204
316
|
} else if (event.source === inner.contentWindow) {
|
|
@@ -207,12 +319,12 @@
|
|
|
207
319
|
// sandbox means its origin serialises as the literal string "null";
|
|
208
320
|
// assert that as defense-in-depth so a future change that
|
|
209
321
|
// accidentally re-grants allow-same-origin is caught here.
|
|
210
|
-
if (event.origin !==
|
|
322
|
+
if (event.origin !== innerOrigin) {
|
|
211
323
|
console.error(
|
|
212
324
|
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
|
|
213
325
|
event.origin,
|
|
214
|
-
"expected
|
|
215
|
-
|
|
326
|
+
"expected:",
|
|
327
|
+
innerOrigin,
|
|
216
328
|
);
|
|
217
329
|
return;
|
|
218
330
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelcontextprotocol/inspector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "The Model Context Protocol Inspector",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MCP",
|
|
@@ -38,17 +38,18 @@
|
|
|
38
38
|
"build:tui": "cd clients/tui && npm run build",
|
|
39
39
|
"build:web": "cd clients/web && npm run build",
|
|
40
40
|
"build:launcher": "cd clients/launcher && npm run build",
|
|
41
|
-
"ci": "npm run validate && npm run coverage && npm run verify:build-gate && npm run smoke && npm run ci:storybook",
|
|
41
|
+
"ci": "npm run validate && npm run coverage && npm run verify:build-gate && npm run verify:bundle-externals && npm run smoke && npm run ci:storybook",
|
|
42
42
|
"ci:storybook": "cd clients/web && npx playwright install chromium && npm run test:storybook",
|
|
43
43
|
"verify:build-gate": "node scripts/verify-build-gate.mjs",
|
|
44
|
+
"verify:bundle-externals": "node scripts/verify-bundle-externals.mjs",
|
|
44
45
|
"verify:typecheck-coverage": "node scripts/verify-typecheck-coverage.mjs",
|
|
45
46
|
"test:scripts": "node --test \"scripts/**/*.test.mjs\"",
|
|
46
47
|
"validate": "npm run verify:format-coverage && npm run verify:typecheck-coverage && npm run verify:dep-lockstep && npm run test:scripts && npm run validate:core && npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher",
|
|
47
48
|
"verify:format-coverage": "node scripts/verify-format-coverage.mjs",
|
|
48
49
|
"verify:dep-lockstep": "node scripts/verify-dep-lockstep.mjs",
|
|
49
50
|
"validate:core": "npm run format:check:core && npm run format:check:scripts && npm run format:check:shared && npm run lint:core && npm run lint:shared",
|
|
50
|
-
"lint:core": "eslint \"core/**/*.{ts,tsx}\"",
|
|
51
|
-
"lint:shared": "eslint \"test-servers/src/**/*.{ts,tsx,mts,cts}\" vitest.shared.mts eslint.config.js",
|
|
51
|
+
"lint:core": "eslint \"core/**/*.{ts,tsx}\" --max-warnings 0",
|
|
52
|
+
"lint:shared": "eslint \"test-servers/src/**/*.{ts,tsx,mts,cts}\" vitest.shared.mts eslint.config.js --max-warnings 0",
|
|
52
53
|
"format:core": "prettier --write \"core/**/*.{ts,tsx}\"",
|
|
53
54
|
"format:check:core": "prettier --check \"core/**/*.{ts,tsx}\"",
|
|
54
55
|
"format:scripts": "prettier --write \"scripts/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}\"",
|
|
@@ -65,14 +66,15 @@
|
|
|
65
66
|
"coverage:tui": "cd clients/tui && npm run test:coverage",
|
|
66
67
|
"coverage:web": "cd clients/web && npm run test:coverage",
|
|
67
68
|
"coverage:launcher": "cd clients/launcher && npm run test:coverage",
|
|
68
|
-
"smoke": "npm run smoke:launcher && npm run smoke:cli && npm run smoke:tui && npm run smoke:web && npm run smoke:web:browser && npm run smoke:web:app",
|
|
69
|
+
"smoke": "npm run smoke:launcher && npm run smoke:cli && npm run smoke:tui && npm run smoke:web && npm run smoke:web:browser && npm run smoke:web:app && npm run smoke:web:elicit",
|
|
69
70
|
"smoke:cli": "node scripts/smoke-cli.mjs",
|
|
70
71
|
"smoke:tui": "node scripts/smoke-tui.mjs",
|
|
71
72
|
"smoke:web": "node scripts/smoke-web.mjs",
|
|
72
73
|
"smoke:web:browser": "cd clients/web && npx playwright install chromium && node ../../scripts/smoke-web-browser.mjs",
|
|
73
74
|
"smoke:web:app": "cd clients/web && npx playwright install chromium && node ../../scripts/smoke-web-app.mjs",
|
|
75
|
+
"smoke:web:elicit": "cd clients/web && npx playwright install chromium && node ../../scripts/smoke-web-elicitation.mjs",
|
|
74
76
|
"smoke:launcher": "node scripts/smoke-launcher.mjs",
|
|
75
|
-
"pack:verify": "node scripts/pack-and-verify.mjs",
|
|
77
|
+
"pack:verify": "cd clients/web && npx playwright install chromium && cd ../.. && node scripts/pack-and-verify.mjs",
|
|
76
78
|
"prepack": "npm run build",
|
|
77
79
|
"postinstall": "node scripts/install-clients.mjs"
|
|
78
80
|
},
|
|
@@ -93,6 +95,7 @@
|
|
|
93
95
|
"ink": "^6.0.0",
|
|
94
96
|
"open": "^10.2.0",
|
|
95
97
|
"pino": "^9.14.0",
|
|
98
|
+
"proper-lockfile": "^4.1.2",
|
|
96
99
|
"react": "^19.0.0",
|
|
97
100
|
"undici": "^8.5.0",
|
|
98
101
|
"vite": "^8.1.5",
|
|
@@ -104,6 +107,7 @@
|
|
|
104
107
|
},
|
|
105
108
|
"devDependencies": {
|
|
106
109
|
"@eslint/js": "^10.0.1",
|
|
110
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
107
111
|
"eslint": "^10.8.0",
|
|
108
112
|
"express": "^5.2.1",
|
|
109
113
|
"globals": "^17.7.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
var __commonJS = (cb, mod) => function __require2() {
|
|
9
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
__require,
|
|
14
|
-
__commonJS
|
|
15
|
-
};
|