@modelcontextprotocol/inspector 0.18.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.
@@ -863,10 +863,17 @@ video {
863
863
  .row-auto {
864
864
  grid-row: auto;
865
865
  }
866
+ .m-0 {
867
+ margin: 0px;
868
+ }
866
869
  .-mx-1 {
867
870
  margin-left: -0.25rem;
868
871
  margin-right: -0.25rem;
869
872
  }
873
+ .mx-auto {
874
+ margin-left: auto;
875
+ margin-right: auto;
876
+ }
870
877
  .my-1 {
871
878
  margin-top: 0.25rem;
872
879
  margin-bottom: 0.25rem;
@@ -953,6 +960,9 @@ video {
953
960
  .h-10 {
954
961
  height: 2.5rem;
955
962
  }
963
+ .h-12 {
964
+ height: 3rem;
965
+ }
956
966
  .h-2 {
957
967
  height: 0.5rem;
958
968
  }
@@ -980,6 +990,9 @@ video {
980
990
  .h-96 {
981
991
  height: 24rem;
982
992
  }
993
+ .h-\[600px\] {
994
+ height: 600px;
995
+ }
983
996
  .h-\[var\(--radix-select-trigger-height\)\] {
984
997
  height: var(--radix-select-trigger-height);
985
998
  }
@@ -1016,6 +1029,12 @@ video {
1016
1029
  .min-h-\[60px\] {
1017
1030
  min-height: 60px;
1018
1031
  }
1032
+ .w-1\/3 {
1033
+ width: 33.333333%;
1034
+ }
1035
+ .w-12 {
1036
+ width: 3rem;
1037
+ }
1019
1038
  .w-2 {
1020
1039
  width: 0.5rem;
1021
1040
  }
@@ -1061,6 +1080,9 @@ video {
1061
1080
  .min-w-\[var\(--radix-select-trigger-width\)\] {
1062
1081
  min-width: var(--radix-select-trigger-width);
1063
1082
  }
1083
+ .max-w-\[200px\] {
1084
+ max-width: 200px;
1085
+ }
1064
1086
  .max-w-full {
1065
1087
  max-width: 100%;
1066
1088
  }
@@ -1100,6 +1122,15 @@ video {
1100
1122
  .transform {
1101
1123
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1102
1124
  }
1125
+ @keyframes pulse {
1126
+
1127
+ 50% {
1128
+ opacity: .5;
1129
+ }
1130
+ }
1131
+ .animate-pulse {
1132
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1133
+ }
1103
1134
  @keyframes spin {
1104
1135
 
1105
1136
  to {
@@ -1126,6 +1157,9 @@ video {
1126
1157
  .resize-none {
1127
1158
  resize: none;
1128
1159
  }
1160
+ .grid-cols-1 {
1161
+ grid-template-columns: repeat(1, minmax(0, 1fr));
1162
+ }
1129
1163
  .grid-cols-2 {
1130
1164
  grid-template-columns: repeat(2, minmax(0, 1fr));
1131
1165
  }
@@ -1375,6 +1409,9 @@ video {
1375
1409
  .bg-muted {
1376
1410
  background-color: hsl(var(--muted));
1377
1411
  }
1412
+ .bg-muted\/30 {
1413
+ background-color: hsl(var(--muted) / 0.3);
1414
+ }
1378
1415
  .bg-popover {
1379
1416
  background-color: hsl(var(--popover));
1380
1417
  }
@@ -1465,6 +1502,10 @@ video {
1465
1502
  padding-top: 0.375rem;
1466
1503
  padding-bottom: 0.375rem;
1467
1504
  }
1505
+ .py-12 {
1506
+ padding-top: 3rem;
1507
+ padding-bottom: 3rem;
1508
+ }
1468
1509
  .py-2 {
1469
1510
  padding-top: 0.5rem;
1470
1511
  padding-bottom: 0.5rem;
@@ -1534,6 +1575,9 @@ video {
1534
1575
  .font-semibold {
1535
1576
  font-weight: 600;
1536
1577
  }
1578
+ .capitalize {
1579
+ text-transform: capitalize;
1580
+ }
1537
1581
  .italic {
1538
1582
  font-style: italic;
1539
1583
  }
@@ -1667,6 +1711,14 @@ video {
1667
1711
  --tw-text-opacity: 1;
1668
1712
  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1669
1713
  }
1714
+ .text-yellow-500 {
1715
+ --tw-text-opacity: 1;
1716
+ color: rgb(234 179 8 / var(--tw-text-opacity, 1));
1717
+ }
1718
+ .text-yellow-600 {
1719
+ --tw-text-opacity: 1;
1720
+ color: rgb(202 138 4 / var(--tw-text-opacity, 1));
1721
+ }
1670
1722
  .text-yellow-800 {
1671
1723
  --tw-text-opacity: 1;
1672
1724
  color: rgb(133 77 14 / var(--tw-text-opacity, 1));
@@ -1680,6 +1732,9 @@ video {
1680
1732
  .opacity-100 {
1681
1733
  opacity: 1;
1682
1734
  }
1735
+ .opacity-20 {
1736
+ opacity: 0.2;
1737
+ }
1683
1738
  .opacity-50 {
1684
1739
  opacity: 0.5;
1685
1740
  }
@@ -2519,6 +2574,10 @@ h1 {
2519
2574
  .md\:max-w-\[420px\] {
2520
2575
  max-width: 420px;
2521
2576
  }
2577
+
2578
+ .md\:grid-cols-2 {
2579
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2580
+ }
2522
2581
  }
2523
2582
 
2524
2583
  .\[\&\+div\]\:text-xs+div {
@@ -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-Dw52pmVD.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DoQUGvvr.css">
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,9 +1,9 @@
1
1
  {
2
2
  "name": "@modelcontextprotocol/inspector",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Model Context Protocol inspector",
5
- "license": "MIT",
6
- "author": "Anthropic, PBC (https://anthropic.com)",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "Model Context Protocol a Series of LF Projects, LLC.",
7
7
  "homepage": "https://modelcontextprotocol.io",
8
8
  "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
9
9
  "type": "module",
@@ -47,10 +47,10 @@
47
47
  "check-version": "node scripts/check-version-consistency.js"
48
48
  },
49
49
  "dependencies": {
50
- "@modelcontextprotocol/inspector-cli": "^0.18.0",
51
- "@modelcontextprotocol/inspector-client": "^0.18.0",
52
- "@modelcontextprotocol/inspector-server": "^0.18.0",
53
- "@modelcontextprotocol/sdk": "^1.24.3",
50
+ "@modelcontextprotocol/inspector-cli": "^0.20.0",
51
+ "@modelcontextprotocol/inspector-client": "^0.20.0",
52
+ "@modelcontextprotocol/inspector-server": "^0.20.0",
53
+ "@modelcontextprotocol/sdk": "^1.25.2",
54
54
  "concurrently": "^9.2.0",
55
55
  "node-fetch": "^3.3.2",
56
56
  "open": "^10.2.0",
@@ -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);
@@ -26,7 +26,9 @@ export default function mcpProxy({ transportToClient, transportToServer, }) {
26
26
  id: message.id,
27
27
  error: {
28
28
  code: -32001,
29
- message: error.message,
29
+ message: error.cause
30
+ ? `${error.message} (cause: ${error.cause})`
31
+ : error.message,
30
32
  data: error,
31
33
  },
32
34
  };
@@ -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>