@modelcontextprotocol/inspector 0.19.0 → 0.20.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/bin/start.js +14 -4
- package/client/dist/assets/{OAuthCallback-C7u-W3fG.js → OAuthCallback-CcWP_GMC.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-CJZOHlWo.js → OAuthDebugCallback-D0p8RB9e.js} +1 -1
- package/client/dist/assets/{index-3HVc23AL.js → index-B8iPFz0O.js} +19282 -6657
- package/client/dist/assets/{index-6PUMPh30.css → index-DdtP67NK.css} +20 -0
- package/client/dist/index.html +2 -2
- package/package.json +4 -4
- package/server/build/index.js +22 -0
- package/server/build/static/sandbox_proxy.html +197 -0
|
@@ -990,6 +990,9 @@ video {
|
|
|
990
990
|
.h-96 {
|
|
991
991
|
height: 24rem;
|
|
992
992
|
}
|
|
993
|
+
.h-\[600px\] {
|
|
994
|
+
height: 600px;
|
|
995
|
+
}
|
|
993
996
|
.h-\[var\(--radix-select-trigger-height\)\] {
|
|
994
997
|
height: var(--radix-select-trigger-height);
|
|
995
998
|
}
|
|
@@ -1077,6 +1080,9 @@ video {
|
|
|
1077
1080
|
.min-w-\[var\(--radix-select-trigger-width\)\] {
|
|
1078
1081
|
min-width: var(--radix-select-trigger-width);
|
|
1079
1082
|
}
|
|
1083
|
+
.max-w-\[200px\] {
|
|
1084
|
+
max-width: 200px;
|
|
1085
|
+
}
|
|
1080
1086
|
.max-w-full {
|
|
1081
1087
|
max-width: 100%;
|
|
1082
1088
|
}
|
|
@@ -1151,6 +1157,9 @@ video {
|
|
|
1151
1157
|
.resize-none {
|
|
1152
1158
|
resize: none;
|
|
1153
1159
|
}
|
|
1160
|
+
.grid-cols-1 {
|
|
1161
|
+
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
1162
|
+
}
|
|
1154
1163
|
.grid-cols-2 {
|
|
1155
1164
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1156
1165
|
}
|
|
@@ -1400,6 +1409,9 @@ video {
|
|
|
1400
1409
|
.bg-muted {
|
|
1401
1410
|
background-color: hsl(var(--muted));
|
|
1402
1411
|
}
|
|
1412
|
+
.bg-muted\/30 {
|
|
1413
|
+
background-color: hsl(var(--muted) / 0.3);
|
|
1414
|
+
}
|
|
1403
1415
|
.bg-popover {
|
|
1404
1416
|
background-color: hsl(var(--popover));
|
|
1405
1417
|
}
|
|
@@ -1490,6 +1502,10 @@ video {
|
|
|
1490
1502
|
padding-top: 0.375rem;
|
|
1491
1503
|
padding-bottom: 0.375rem;
|
|
1492
1504
|
}
|
|
1505
|
+
.py-12 {
|
|
1506
|
+
padding-top: 3rem;
|
|
1507
|
+
padding-bottom: 3rem;
|
|
1508
|
+
}
|
|
1493
1509
|
.py-2 {
|
|
1494
1510
|
padding-top: 0.5rem;
|
|
1495
1511
|
padding-bottom: 0.5rem;
|
|
@@ -2558,6 +2574,10 @@ h1 {
|
|
|
2558
2574
|
.md\:max-w-\[420px\] {
|
|
2559
2575
|
max-width: 420px;
|
|
2560
2576
|
}
|
|
2577
|
+
|
|
2578
|
+
.md\:grid-cols-2 {
|
|
2579
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2580
|
+
}
|
|
2561
2581
|
}
|
|
2562
2582
|
|
|
2563
2583
|
.\[\&\+div\]\:text-xs+div {
|
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-B8iPFz0O.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DdtP67NK.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": "0.20.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.",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"check-version": "node scripts/check-version-consistency.js"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/inspector-cli": "^0.
|
|
51
|
-
"@modelcontextprotocol/inspector-client": "^0.
|
|
52
|
-
"@modelcontextprotocol/inspector-server": "^0.
|
|
50
|
+
"@modelcontextprotocol/inspector-cli": "^0.20.0",
|
|
51
|
+
"@modelcontextprotocol/inspector-client": "^0.20.0",
|
|
52
|
+
"@modelcontextprotocol/inspector-server": "^0.20.0",
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
54
54
|
"concurrently": "^9.2.0",
|
|
55
55
|
"node-fetch": "^3.3.2",
|
package/server/build/index.js
CHANGED
|
@@ -12,10 +12,18 @@ import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelconte
|
|
|
12
12
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
13
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
14
14
|
import express from "express";
|
|
15
|
+
import rateLimit from "express-rate-limit";
|
|
15
16
|
import { findActualExecutable } from "spawn-rx";
|
|
16
17
|
import mcpProxy from "./mcpProxy.js";
|
|
17
18
|
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { dirname, join } from "path";
|
|
21
|
+
import { readFileSync } from "fs";
|
|
18
22
|
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
|
23
|
+
const sandboxRateLimiter = rateLimit({
|
|
24
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
25
|
+
max: 100, // limit each IP to 100 /sandbox requests per windowMs
|
|
26
|
+
});
|
|
19
27
|
const defaultEnvironment = {
|
|
20
28
|
...getDefaultEnvironment(),
|
|
21
29
|
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
|
|
@@ -625,6 +633,20 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
|
|
|
625
633
|
res.status(500).json(error);
|
|
626
634
|
}
|
|
627
635
|
});
|
|
636
|
+
app.get("/sandbox", sandboxRateLimiter, (req, res) => {
|
|
637
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
638
|
+
const __dirname = dirname(__filename);
|
|
639
|
+
const filePath = join(__dirname, "..", "static", "sandbox_proxy.html");
|
|
640
|
+
let sandboxHtml;
|
|
641
|
+
try {
|
|
642
|
+
sandboxHtml = readFileSync(filePath, "utf-8");
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
sandboxHtml = "MCP Apps sandbox not loaded: " + e;
|
|
646
|
+
}
|
|
647
|
+
res.set("Cache-Control", "no-cache, no-store, max-age=0");
|
|
648
|
+
res.send(sandboxHtml);
|
|
649
|
+
});
|
|
628
650
|
const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT, 10);
|
|
629
651
|
const HOST = process.env.HOST || "localhost";
|
|
630
652
|
const server = app.listen(PORT, HOST);
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
|
+
<!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).
|
|
7
|
+
The inner iframe inherits this CSP since we use document.write(). -->
|
|
8
|
+
<title>MCP-UI Proxy</title>
|
|
9
|
+
<style>
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
height: 100vh;
|
|
14
|
+
width: 100vw;
|
|
15
|
+
/* Transparent background allows parent page to show through */
|
|
16
|
+
background-color: transparent;
|
|
17
|
+
}
|
|
18
|
+
body {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
iframe {
|
|
26
|
+
background-color: transparent;
|
|
27
|
+
border: 0px none transparent;
|
|
28
|
+
padding: 0px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
flex-grow: 1;
|
|
31
|
+
/* Inherit color scheme from parent for consistent transparency */
|
|
32
|
+
color-scheme: inherit;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<script>
|
|
38
|
+
function buildAllowAttribute(permissions) {
|
|
39
|
+
if (!permissions) return "";
|
|
40
|
+
|
|
41
|
+
const allowList = [];
|
|
42
|
+
if (permissions["camera"]) allowList.push("camera");
|
|
43
|
+
if (permissions["microphone"]) allowList.push("microphone");
|
|
44
|
+
if (permissions["geolocation"]) allowList.push("geolocation");
|
|
45
|
+
if (permissions["clipboardWrite"]) allowList.push("clipboard-write");
|
|
46
|
+
|
|
47
|
+
return allowList.join("; ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ALLOWED_REFERRER_PATTERN =
|
|
51
|
+
/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;
|
|
52
|
+
|
|
53
|
+
if (window.self === window.top) {
|
|
54
|
+
throw new Error("This file is only to be used in an iframe sandbox.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!document.referrer) {
|
|
58
|
+
throw new Error("No referrer, cannot validate embedding site.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract the expected host origin from the referrer for origin validation.
|
|
68
|
+
// This is the origin we expect all parent messages to come from.
|
|
69
|
+
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
|
|
70
|
+
|
|
71
|
+
const OWN_ORIGIN = new URL(window.location.href).origin;
|
|
72
|
+
|
|
73
|
+
// Security self-test: verify iframe isolation is working correctly.
|
|
74
|
+
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
|
|
75
|
+
// configuration is dangerously broken and untrusted content could escape.
|
|
76
|
+
try {
|
|
77
|
+
window.top?.alert(
|
|
78
|
+
"If you see this, the sandbox is not setup securely.",
|
|
79
|
+
);
|
|
80
|
+
throw "FAIL";
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e === "FAIL") {
|
|
83
|
+
throw new Error("The sandbox is not setup securely.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Expected: SecurityError confirms proper sandboxing.
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Double-iframe sandbox architecture: THIS file is the outer sandbox proxy
|
|
90
|
+
// iframe on a separate origin. It creates an inner iframe for untrusted HTML
|
|
91
|
+
// content. Per the specification, the Host and the Sandbox MUST have different
|
|
92
|
+
// origins.
|
|
93
|
+
const inner = document.createElement("iframe");
|
|
94
|
+
inner.style = "width:100%; height:100%; border:none;";
|
|
95
|
+
inner.setAttribute(
|
|
96
|
+
"sandbox",
|
|
97
|
+
"allow-scripts allow-same-origin allow-forms",
|
|
98
|
+
);
|
|
99
|
+
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
|
|
100
|
+
// based on the permissions requested by the app
|
|
101
|
+
document.body.appendChild(inner);
|
|
102
|
+
|
|
103
|
+
const RESOURCE_READY_NOTIFICATION =
|
|
104
|
+
"ui/notifications/sandbox-resource-ready";
|
|
105
|
+
const PROXY_READY_NOTIFICATION = "ui/notifications/sandbox-proxy-ready";
|
|
106
|
+
|
|
107
|
+
// Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,
|
|
108
|
+
// forwarding messages between:
|
|
109
|
+
//
|
|
110
|
+
// Host (parent window) ↔ Sandbox (outer frame) ↔ View (inner iframe)
|
|
111
|
+
//
|
|
112
|
+
// Reason: the parent window and inner iframe have different origins and can't
|
|
113
|
+
// communicate directly, so the outer iframe forwards messages in both
|
|
114
|
+
// directions to connect them.
|
|
115
|
+
//
|
|
116
|
+
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
|
117
|
+
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
|
118
|
+
// load the inner iframe with the view HTML content.
|
|
119
|
+
//
|
|
120
|
+
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
|
121
|
+
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
|
122
|
+
|
|
123
|
+
window.addEventListener("message", async (event) => {
|
|
124
|
+
if (event.source === window.parent) {
|
|
125
|
+
// Validate that messages from parent come from the expected host origin.
|
|
126
|
+
// This prevents malicious pages from sending messages to this sandbox.
|
|
127
|
+
if (event.origin !== EXPECTED_HOST_ORIGIN) {
|
|
128
|
+
console.error(
|
|
129
|
+
"[Sandbox] Rejecting message from unexpected origin:",
|
|
130
|
+
event.origin,
|
|
131
|
+
"expected:",
|
|
132
|
+
EXPECTED_HOST_ORIGIN,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
|
138
|
+
const { html, sandbox, permissions } = event.data.params;
|
|
139
|
+
if (typeof sandbox === "string") {
|
|
140
|
+
inner.setAttribute("sandbox", sandbox);
|
|
141
|
+
}
|
|
142
|
+
// Set Permission Policy allow attribute if permissions are requested
|
|
143
|
+
const allowAttribute = buildAllowAttribute(permissions);
|
|
144
|
+
if (allowAttribute) {
|
|
145
|
+
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
|
146
|
+
inner.setAttribute("allow", allowAttribute);
|
|
147
|
+
}
|
|
148
|
+
if (typeof html === "string") {
|
|
149
|
+
// Use document.write instead of srcdoc (which the CesiumJS Map won't work with)
|
|
150
|
+
const doc =
|
|
151
|
+
inner.contentDocument || inner.contentWindow?.document;
|
|
152
|
+
if (doc) {
|
|
153
|
+
doc.open();
|
|
154
|
+
doc.write(html);
|
|
155
|
+
doc.close();
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback to srcdoc if document is not accessible
|
|
158
|
+
console.warn(
|
|
159
|
+
"[Sandbox] document.write not available, falling back to srcdoc",
|
|
160
|
+
);
|
|
161
|
+
inner.srcdoc = html;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (inner && inner.contentWindow) {
|
|
166
|
+
inner.contentWindow.postMessage(event.data, "*");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else if (event.source === inner.contentWindow) {
|
|
170
|
+
if (event.origin !== OWN_ORIGIN) {
|
|
171
|
+
console.error(
|
|
172
|
+
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
|
|
173
|
+
event.origin,
|
|
174
|
+
"expected:",
|
|
175
|
+
OWN_ORIGIN,
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Relay messages from inner frame to parent window.
|
|
180
|
+
// Use specific origin instead of "*" to prevent message interception.
|
|
181
|
+
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Notify the Host that the Sandbox is ready to receive view HTML.
|
|
186
|
+
// Use specific origin instead of "*" to ensure only the expected host receives this.
|
|
187
|
+
window.parent.postMessage(
|
|
188
|
+
{
|
|
189
|
+
jsonrpc: "2.0",
|
|
190
|
+
method: PROXY_READY_NOTIFICATION,
|
|
191
|
+
params: {},
|
|
192
|
+
},
|
|
193
|
+
EXPECTED_HOST_ORIGIN,
|
|
194
|
+
);
|
|
195
|
+
</script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|