@modelcontextprotocol/inspector 0.13.0 → 0.14.1

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-Dhd4mhZ2.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CIxCnj5w.css">
8
+ <script type="module" crossorigin src="/assets/index-TghXC4TH.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-C1MLjTGf.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelcontextprotocol/inspector",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Model Context Protocol inspector",
5
5
  "license": "MIT",
6
6
  "author": "Anthropic, PBC (https://anthropic.com)",
@@ -36,14 +36,15 @@
36
36
  "test-cli": "cd cli && npm run test",
37
37
  "prettier-fix": "prettier --write .",
38
38
  "prettier-check": "prettier --check .",
39
+ "lint": "prettier --check . && cd client && npm run lint",
39
40
  "prepare": "npm run build",
40
41
  "publish-all": "npm publish --workspaces --access public && npm publish --access public"
41
42
  },
42
43
  "dependencies": {
43
- "@modelcontextprotocol/inspector-cli": "^0.13.0",
44
- "@modelcontextprotocol/inspector-client": "^0.13.0",
45
- "@modelcontextprotocol/inspector-server": "^0.13.0",
46
- "@modelcontextprotocol/sdk": "^1.11.5",
44
+ "@modelcontextprotocol/inspector-cli": "^0.14.1",
45
+ "@modelcontextprotocol/inspector-client": "^0.14.1",
46
+ "@modelcontextprotocol/inspector-server": "^0.14.1",
47
+ "@modelcontextprotocol/sdk": "^1.12.1",
47
48
  "concurrently": "^9.0.1",
48
49
  "open": "^10.1.0",
49
50
  "shell-quote": "^1.8.2",
@@ -10,7 +10,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10
10
  import express from "express";
11
11
  import { findActualExecutable } from "spawn-rx";
12
12
  import mcpProxy from "./mcpProxy.js";
13
- import { randomUUID } from "node:crypto";
13
+ import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
14
14
  const SSE_HEADERS_PASSTHROUGH = ["authorization"];
15
15
  const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
16
16
  "authorization",
@@ -28,16 +28,100 @@ const { values } = parseArgs({
28
28
  args: { type: "string", default: "" },
29
29
  },
30
30
  });
31
+ // Function to get HTTP headers.
32
+ // Supports only "sse" and "streamable-http" transport types.
33
+ const getHttpHeaders = (req, transportType) => {
34
+ const headers = {
35
+ Accept: transportType === "sse"
36
+ ? "text/event-stream"
37
+ : "text/event-stream, application/json",
38
+ };
39
+ const defaultHeaders = transportType === "sse"
40
+ ? SSE_HEADERS_PASSTHROUGH
41
+ : STREAMABLE_HTTP_HEADERS_PASSTHROUGH;
42
+ for (const key of defaultHeaders) {
43
+ if (req.headers[key] === undefined) {
44
+ continue;
45
+ }
46
+ const value = req.headers[key];
47
+ headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
48
+ }
49
+ // If the header "x-custom-auth-header" is present, use its value as the custom header name.
50
+ if (req.headers["x-custom-auth-header"] !== undefined) {
51
+ const customHeaderName = req.headers["x-custom-auth-header"];
52
+ const lowerCaseHeaderName = customHeaderName.toLowerCase();
53
+ if (req.headers[lowerCaseHeaderName] !== undefined) {
54
+ const value = req.headers[lowerCaseHeaderName];
55
+ headers[customHeaderName] = value;
56
+ }
57
+ }
58
+ return headers;
59
+ };
31
60
  const app = express();
32
61
  app.use(cors());
33
62
  app.use((req, res, next) => {
34
63
  res.header("Access-Control-Expose-Headers", "mcp-session-id");
35
64
  next();
36
65
  });
37
- const webAppTransports = new Map(); // Transports by sessionId
66
+ const webAppTransports = new Map(); // Web app transports by web app sessionId
67
+ const serverTransports = new Map(); // Server Transports by web app sessionId
68
+ const sessionToken = randomBytes(32).toString("hex");
69
+ const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
70
+ // Origin validation middleware to prevent DNS rebinding attacks
71
+ const originValidationMiddleware = (req, res, next) => {
72
+ const origin = req.headers.origin;
73
+ // Default origins based on CLIENT_PORT or use environment variable
74
+ const clientPort = process.env.CLIENT_PORT || "6274";
75
+ const defaultOrigins = [
76
+ `http://localhost:${clientPort}`,
77
+ `http://127.0.0.1:${clientPort}`,
78
+ ];
79
+ const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || defaultOrigins;
80
+ if (origin && !allowedOrigins.includes(origin)) {
81
+ console.error(`Invalid origin: ${origin}`);
82
+ res.status(403).json({
83
+ error: "Forbidden - invalid origin",
84
+ message: "Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.",
85
+ });
86
+ return;
87
+ }
88
+ next();
89
+ };
90
+ const authMiddleware = (req, res, next) => {
91
+ if (authDisabled) {
92
+ return next();
93
+ }
94
+ const sendUnauthorized = () => {
95
+ res.status(401).json({
96
+ error: "Unauthorized",
97
+ message: "Authentication required. Use the session token shown in the console when starting the server.",
98
+ });
99
+ };
100
+ const authHeader = req.headers.authorization;
101
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
102
+ sendUnauthorized();
103
+ return;
104
+ }
105
+ const providedToken = authHeader.substring(7); // Remove 'Bearer ' prefix
106
+ const expectedToken = sessionToken;
107
+ // Convert to buffers for timing-safe comparison
108
+ const providedBuffer = Buffer.from(providedToken);
109
+ const expectedBuffer = Buffer.from(expectedToken);
110
+ // Check length first to prevent timing attacks
111
+ if (providedBuffer.length !== expectedBuffer.length) {
112
+ sendUnauthorized();
113
+ return;
114
+ }
115
+ // Perform timing-safe comparison
116
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
117
+ sendUnauthorized();
118
+ return;
119
+ }
120
+ next();
121
+ };
38
122
  const createTransport = async (req) => {
39
123
  const query = req.query;
40
- console.log("Query parameters:", query);
124
+ console.log("Query parameters:", JSON.stringify(query));
41
125
  const transportType = query.transportType;
42
126
  if (transportType === "stdio") {
43
127
  const command = query.command;
@@ -45,7 +129,7 @@ const createTransport = async (req) => {
45
129
  const queryEnv = query.env ? JSON.parse(query.env) : {};
46
130
  const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
47
131
  const { cmd, args } = findActualExecutable(command, origArgs);
48
- console.log(`Stdio transport: command=${cmd}, args=${args}`);
132
+ console.log(`STDIO transport: command=${cmd}, args=${args}`);
49
133
  const transport = new StdioClientTransport({
50
134
  command: cmd,
51
135
  args,
@@ -53,21 +137,11 @@ const createTransport = async (req) => {
53
137
  stderr: "pipe",
54
138
  });
55
139
  await transport.start();
56
- console.log("Spawned stdio transport");
57
140
  return transport;
58
141
  }
59
142
  else if (transportType === "sse") {
60
143
  const url = query.url;
61
- const headers = {
62
- Accept: "text/event-stream",
63
- };
64
- for (const key of SSE_HEADERS_PASSTHROUGH) {
65
- if (req.headers[key] === undefined) {
66
- continue;
67
- }
68
- const value = req.headers[key];
69
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
70
- }
144
+ const headers = getHttpHeaders(req, transportType);
71
145
  console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`);
72
146
  const transport = new SSEClientTransport(new URL(url), {
73
147
  eventSourceInit: {
@@ -78,27 +152,16 @@ const createTransport = async (req) => {
78
152
  },
79
153
  });
80
154
  await transport.start();
81
- console.log("Connected to SSE transport");
82
155
  return transport;
83
156
  }
84
157
  else if (transportType === "streamable-http") {
85
- const headers = {
86
- Accept: "text/event-stream, application/json",
87
- };
88
- for (const key of STREAMABLE_HTTP_HEADERS_PASSTHROUGH) {
89
- if (req.headers[key] === undefined) {
90
- continue;
91
- }
92
- const value = req.headers[key];
93
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
94
- }
158
+ const headers = getHttpHeaders(req, transportType);
95
159
  const transport = new StreamableHTTPClientTransport(new URL(query.url), {
96
160
  requestInit: {
97
161
  headers,
98
162
  },
99
163
  });
100
164
  await transport.start();
101
- console.log("Connected to Streamable HTTP transport");
102
165
  return transport;
103
166
  }
104
167
  else {
@@ -106,8 +169,7 @@ const createTransport = async (req) => {
106
169
  throw new Error("Invalid transport type specified");
107
170
  }
108
171
  };
109
- let backingServerTransport;
110
- app.get("/mcp", async (req, res) => {
172
+ app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
111
173
  const sessionId = req.headers["mcp-session-id"];
112
174
  console.log(`Received GET message for sessionId ${sessionId}`);
113
175
  try {
@@ -125,15 +187,14 @@ app.get("/mcp", async (req, res) => {
125
187
  res.status(500).json(error);
126
188
  }
127
189
  });
128
- app.post("/mcp", async (req, res) => {
190
+ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
129
191
  const sessionId = req.headers["mcp-session-id"];
130
- console.log(`Received POST message for sessionId ${sessionId}`);
192
+ let serverTransport;
131
193
  if (!sessionId) {
132
194
  try {
133
- console.log("New streamable-http connection");
195
+ console.log("New StreamableHttp connection request");
134
196
  try {
135
- await backingServerTransport?.close();
136
- backingServerTransport = await createTransport(req);
197
+ serverTransport = await createTransport(req);
137
198
  }
138
199
  catch (error) {
139
200
  if (error instanceof SseError && error.code === 401) {
@@ -143,18 +204,20 @@ app.post("/mcp", async (req, res) => {
143
204
  }
144
205
  throw error;
145
206
  }
146
- console.log("Connected MCP client to backing server transport");
207
+ console.log("Created StreamableHttp server transport");
147
208
  const webAppTransport = new StreamableHTTPServerTransport({
148
209
  sessionIdGenerator: randomUUID,
149
210
  onsessioninitialized: (sessionId) => {
150
211
  webAppTransports.set(sessionId, webAppTransport);
151
- console.log("Created streamable web app transport " + sessionId);
212
+ serverTransports.set(sessionId, serverTransport);
213
+ console.log("Client <-> Proxy sessionId: " + sessionId);
152
214
  },
153
215
  });
216
+ console.log("Created StreamableHttp client transport");
154
217
  await webAppTransport.start();
155
218
  mcpProxy({
156
219
  transportToClient: webAppTransport,
157
- transportToServer: backingServerTransport,
220
+ transportToServer: serverTransport,
158
221
  });
159
222
  await webAppTransport.handleRequest(req, res, req.body);
160
223
  }
@@ -164,6 +227,7 @@ app.post("/mcp", async (req, res) => {
164
227
  }
165
228
  }
166
229
  else {
230
+ console.log(`Received POST message for sessionId ${sessionId}`);
167
231
  try {
168
232
  const transport = webAppTransports.get(sessionId);
169
233
  if (!transport) {
@@ -179,81 +243,134 @@ app.post("/mcp", async (req, res) => {
179
243
  }
180
244
  }
181
245
  });
182
- app.get("/stdio", async (req, res) => {
246
+ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
247
+ const sessionId = req.headers["mcp-session-id"];
248
+ console.log(`Received DELETE message for sessionId ${sessionId}`);
249
+ let serverTransport;
250
+ if (sessionId) {
251
+ try {
252
+ serverTransport = serverTransports.get(sessionId);
253
+ if (!serverTransport) {
254
+ res.status(404).end("Transport not found for sessionId " + sessionId);
255
+ }
256
+ else {
257
+ await serverTransport.terminateSession();
258
+ webAppTransports.delete(sessionId);
259
+ serverTransports.delete(sessionId);
260
+ console.log(`Transports removed for sessionId ${sessionId}`);
261
+ }
262
+ res.status(200).end();
263
+ }
264
+ catch (error) {
265
+ console.error("Error in /mcp route:", error);
266
+ res.status(500).json(error);
267
+ }
268
+ }
269
+ });
270
+ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => {
183
271
  try {
184
- console.log("New connection");
272
+ console.log("New STDIO connection request");
273
+ let serverTransport;
185
274
  try {
186
- await backingServerTransport?.close();
187
- backingServerTransport = await createTransport(req);
275
+ serverTransport = await createTransport(req);
276
+ console.log("Created server transport");
188
277
  }
189
278
  catch (error) {
190
279
  if (error instanceof SseError && error.code === 401) {
191
- console.error("Received 401 Unauthorized from MCP server:", error.message);
280
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
192
281
  res.status(401).json(error);
193
282
  return;
194
283
  }
195
284
  throw error;
196
285
  }
197
- console.log("Connected MCP client to backing server transport");
198
286
  const webAppTransport = new SSEServerTransport("/message", res);
287
+ console.log("Created client transport");
199
288
  webAppTransports.set(webAppTransport.sessionId, webAppTransport);
200
- console.log("Created web app transport");
289
+ serverTransports.set(webAppTransport.sessionId, serverTransport);
201
290
  await webAppTransport.start();
202
- backingServerTransport.stderr.on("data", (chunk) => {
203
- webAppTransport.send({
204
- jsonrpc: "2.0",
205
- method: "notifications/stderr",
206
- params: {
207
- content: chunk.toString(),
208
- },
209
- });
291
+ serverTransport.stderr.on("data", (chunk) => {
292
+ if (chunk.toString().includes("MODULE_NOT_FOUND")) {
293
+ webAppTransport.send({
294
+ jsonrpc: "2.0",
295
+ method: "notifications/stderr",
296
+ params: {
297
+ content: "Command not found, transports removed",
298
+ },
299
+ });
300
+ webAppTransport.close();
301
+ serverTransport.close();
302
+ webAppTransports.delete(webAppTransport.sessionId);
303
+ serverTransports.delete(webAppTransport.sessionId);
304
+ console.error("Command not found, transports removed");
305
+ }
306
+ else {
307
+ webAppTransport.send({
308
+ jsonrpc: "2.0",
309
+ method: "notifications/stderr",
310
+ params: {
311
+ content: chunk.toString(),
312
+ },
313
+ });
314
+ }
210
315
  });
211
316
  mcpProxy({
212
317
  transportToClient: webAppTransport,
213
- transportToServer: backingServerTransport,
318
+ transportToServer: serverTransport,
214
319
  });
215
- console.log("Set up MCP proxy");
216
320
  }
217
321
  catch (error) {
218
322
  console.error("Error in /stdio route:", error);
219
323
  res.status(500).json(error);
220
324
  }
221
325
  });
222
- app.get("/sse", async (req, res) => {
326
+ app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
223
327
  try {
224
- console.log("New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http");
328
+ console.log("New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp");
329
+ let serverTransport;
225
330
  try {
226
- await backingServerTransport?.close();
227
- backingServerTransport = await createTransport(req);
331
+ serverTransport = await createTransport(req);
228
332
  }
229
333
  catch (error) {
230
334
  if (error instanceof SseError && error.code === 401) {
231
- console.error("Received 401 Unauthorized from MCP server:", error.message);
335
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
232
336
  res.status(401).json(error);
233
337
  return;
234
338
  }
235
- throw error;
339
+ else if (error instanceof SseError && error.code === 404) {
340
+ console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
341
+ res.status(404).json(error);
342
+ return;
343
+ }
344
+ else if (JSON.stringify(error).includes("ECONNREFUSED")) {
345
+ console.error("Connection refused. Is the MCP server running?");
346
+ res.status(500).json(error);
347
+ }
348
+ else {
349
+ throw error;
350
+ }
351
+ }
352
+ if (serverTransport) {
353
+ const webAppTransport = new SSEServerTransport("/message", res);
354
+ webAppTransports.set(webAppTransport.sessionId, webAppTransport);
355
+ console.log("Created client transport");
356
+ serverTransports.set(webAppTransport.sessionId, serverTransport);
357
+ console.log("Created server transport");
358
+ await webAppTransport.start();
359
+ mcpProxy({
360
+ transportToClient: webAppTransport,
361
+ transportToServer: serverTransport,
362
+ });
236
363
  }
237
- console.log("Connected MCP client to backing server transport");
238
- const webAppTransport = new SSEServerTransport("/message", res);
239
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
240
- console.log("Created web app transport");
241
- await webAppTransport.start();
242
- mcpProxy({
243
- transportToClient: webAppTransport,
244
- transportToServer: backingServerTransport,
245
- });
246
- console.log("Set up MCP proxy");
247
364
  }
248
365
  catch (error) {
249
366
  console.error("Error in /sse route:", error);
250
367
  res.status(500).json(error);
251
368
  }
252
369
  });
253
- app.post("/message", async (req, res) => {
370
+ app.post("/message", originValidationMiddleware, authMiddleware, async (req, res) => {
254
371
  try {
255
372
  const sessionId = req.query.sessionId;
256
- console.log(`Received message for sessionId ${sessionId}`);
373
+ console.log(`Received POST message for sessionId ${sessionId}`);
257
374
  const transport = webAppTransports.get(sessionId);
258
375
  if (!transport) {
259
376
  res.status(404).end("Session not found");
@@ -271,7 +388,7 @@ app.get("/health", (req, res) => {
271
388
  status: "ok",
272
389
  });
273
390
  });
274
- app.get("/config", (req, res) => {
391
+ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
275
392
  try {
276
393
  res.json({
277
394
  defaultEnvironment,
@@ -284,10 +401,22 @@ app.get("/config", (req, res) => {
284
401
  res.status(500).json(error);
285
402
  }
286
403
  });
287
- const PORT = process.env.PORT || 6277;
288
- const server = app.listen(PORT);
404
+ const PORT = parseInt(process.env.PORT || "6277", 10);
405
+ const HOST = process.env.HOST || "127.0.0.1";
406
+ const server = app.listen(PORT, HOST);
289
407
  server.on("listening", () => {
290
- console.log(`⚙️ Proxy server listening on port ${PORT}`);
408
+ console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);
409
+ if (!authDisabled) {
410
+ console.log(`🔑 Session token: ${sessionToken}`);
411
+ console.log(`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`);
412
+ // Display clickable URL with pre-filled token
413
+ const clientPort = process.env.CLIENT_PORT || "6274";
414
+ const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
415
+ console.log(`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`);
416
+ }
417
+ else {
418
+ console.log(`⚠️ WARNING: Authentication is disabled. This is not recommended.`);
419
+ }
291
420
  });
292
421
  server.on("error", (err) => {
293
422
  if (err.message.includes(`EADDRINUSE`)) {
@@ -3,11 +3,19 @@ function onClientError(error) {
3
3
  console.error("Error from inspector client:", error);
4
4
  }
5
5
  function onServerError(error) {
6
- console.error("Error from MCP server:", error);
6
+ if ((error?.message &&
7
+ error.message.includes("Error POSTing to endpoint (HTTP 404)")) ||
8
+ (error?.cause && JSON.stringify(error.cause).includes("ECONNREFUSED"))) {
9
+ console.error("Connection refused. Is the MCP server running?");
10
+ }
11
+ else {
12
+ console.error("Error from MCP server:", error);
13
+ }
7
14
  }
8
15
  export default function mcpProxy({ transportToClient, transportToServer, }) {
9
16
  let transportToClientClosed = false;
10
17
  let transportToServerClosed = false;
18
+ let reportedServerSession = false;
11
19
  transportToClient.onmessage = (message) => {
12
20
  transportToServer.send(message).catch((error) => {
13
21
  // Send error response back to client if it was a request (has id) and connection is still open
@@ -26,6 +34,13 @@ export default function mcpProxy({ transportToClient, transportToServer, }) {
26
34
  });
27
35
  };
28
36
  transportToServer.onmessage = (message) => {
37
+ if (!reportedServerSession) {
38
+ if (transportToServer.sessionId) {
39
+ // Can only report for StreamableHttp
40
+ console.error("Proxy <-> Server sessionId: " + transportToServer.sessionId);
41
+ }
42
+ reportedServerSession = true;
43
+ }
29
44
  transportToClient.send(message).catch(onClientError);
30
45
  };
31
46
  transportToClient.onclose = () => {