@modelcontextprotocol/inspector 0.16.6 → 0.16.7

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 CHANGED
@@ -98,6 +98,16 @@ The MCP Inspector provides convenient buttons to export server launch configurat
98
98
  }
99
99
  ```
100
100
 
101
+ **Streamable HTTP transport example:**
102
+
103
+ ```json
104
+ {
105
+ "type": "streamable-http",
106
+ "url": "http://localhost:3000/mcp",
107
+ "note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
108
+ }
109
+ ```
110
+
101
111
  - **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`.
102
112
 
103
113
  **STDIO transport example:**
@@ -131,9 +141,23 @@ The MCP Inspector provides convenient buttons to export server launch configurat
131
141
  }
132
142
  ```
133
143
 
144
+ **Streamable HTTP transport example:**
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "default-server": {
150
+ "type": "streamable-http",
151
+ "url": "http://localhost:3000/mcp",
152
+ "note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
134
158
  These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.
135
159
 
136
- For SSE transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the SSE URL configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the SSE URL for direct use in clients.
160
+ For SSE and Streamable HTTP transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the URL for direct use in clients.
137
161
 
138
162
  You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file.
139
163
 
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-B_qh-Btq.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-Dg7mXHRE.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-B_qh-Btq.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-Dg7mXHRE.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;
@@ -735,6 +735,39 @@ video {
735
735
  background-color: hsl(var(--background));
736
736
  color: hsl(var(--foreground));
737
737
  }
738
+ .container {
739
+ width: 100%;
740
+ }
741
+ @media (min-width: 640px) {
742
+
743
+ .container {
744
+ max-width: 640px;
745
+ }
746
+ }
747
+ @media (min-width: 768px) {
748
+
749
+ .container {
750
+ max-width: 768px;
751
+ }
752
+ }
753
+ @media (min-width: 1024px) {
754
+
755
+ .container {
756
+ max-width: 1024px;
757
+ }
758
+ }
759
+ @media (min-width: 1280px) {
760
+
761
+ .container {
762
+ max-width: 1280px;
763
+ }
764
+ }
765
+ @media (min-width: 1536px) {
766
+
767
+ .container {
768
+ max-width: 1536px;
769
+ }
770
+ }
738
771
  .sr-only {
739
772
  position: absolute;
740
773
  width: 1px;
@@ -15230,6 +15230,20 @@ const CancelledNotificationSchema = NotificationSchema.extend({
15230
15230
  reason: stringType().optional()
15231
15231
  })
15232
15232
  });
15233
+ const IconSchema = objectType({
15234
+ /**
15235
+ * URL or data URI for the icon.
15236
+ */
15237
+ src: stringType(),
15238
+ /**
15239
+ * Optional MIME type for the icon.
15240
+ */
15241
+ mimeType: optionalType(stringType()),
15242
+ /**
15243
+ * Optional string specifying icon dimensions (e.g., "48x48 96x96").
15244
+ */
15245
+ sizes: optionalType(stringType())
15246
+ }).passthrough();
15233
15247
  const BaseMetadataSchema = objectType({
15234
15248
  /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */
15235
15249
  name: stringType(),
@@ -15244,7 +15258,20 @@ const BaseMetadataSchema = objectType({
15244
15258
  title: optionalType(stringType())
15245
15259
  }).passthrough();
15246
15260
  const ImplementationSchema = BaseMetadataSchema.extend({
15247
- version: stringType()
15261
+ version: stringType(),
15262
+ /**
15263
+ * An optional URL of the website for this implementation.
15264
+ */
15265
+ websiteUrl: optionalType(stringType()),
15266
+ /**
15267
+ * An optional list of icons for this implementation.
15268
+ * This can be used by clients to display the implementation in a user interface.
15269
+ * Each icon should have a `kind` property that specifies whether it is a data representation or a URL source, a `src` property that points to the icon file or data representation, and may also include a `mimeType` and `sizes` property.
15270
+ * The `mimeType` property should be a valid MIME type for the icon file, such as "image/png" or "image/svg+xml".
15271
+ * The `sizes` property should be a string that specifies one or more sizes at which the icon file can be used, such as "48x48" or "any" for scalable formats like SVG.
15272
+ * The `sizes` property is optional, and if not provided, the client should assume that the icon can be used at any size.
15273
+ */
15274
+ icons: optionalType(arrayType(IconSchema))
15248
15275
  });
15249
15276
  const ClientCapabilitiesSchema = objectType({
15250
15277
  /**
@@ -15435,6 +15462,10 @@ const ResourceSchema = BaseMetadataSchema.extend({
15435
15462
  * The MIME type of this resource, if known.
15436
15463
  */
15437
15464
  mimeType: optionalType(stringType()),
15465
+ /**
15466
+ * An optional list of icons for this resource.
15467
+ */
15468
+ icons: optionalType(arrayType(IconSchema)),
15438
15469
  /**
15439
15470
  * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
15440
15471
  * for notes on _meta usage.
@@ -15539,6 +15570,10 @@ const PromptSchema = BaseMetadataSchema.extend({
15539
15570
  * A list of arguments to use for templating the prompt.
15540
15571
  */
15541
15572
  arguments: optionalType(arrayType(PromptArgumentSchema)),
15573
+ /**
15574
+ * An optional list of icons for this prompt.
15575
+ */
15576
+ icons: optionalType(arrayType(IconSchema)),
15542
15577
  /**
15543
15578
  * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
15544
15579
  * for notes on _meta usage.
@@ -15706,6 +15741,10 @@ const ToolSchema = BaseMetadataSchema.extend({
15706
15741
  * Optional additional tool information.
15707
15742
  */
15708
15743
  annotations: optionalType(ToolAnnotationsSchema),
15744
+ /**
15745
+ * An optional list of icons for this tool.
15746
+ */
15747
+ icons: optionalType(arrayType(IconSchema)),
15709
15748
  /**
15710
15749
  * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
15711
15750
  * for notes on _meta usage.
@@ -17176,7 +17215,7 @@ const oauthTransitions = {
17176
17215
  );
17177
17216
  context.provider.saveCodeVerifier(codeVerifier);
17178
17217
  context.updateState({
17179
- authorizationUrl: authorizationUrl.toString(),
17218
+ authorizationUrl,
17180
17219
  oauthStep: "authorization_code"
17181
17220
  });
17182
17221
  }
@@ -24866,7 +24905,7 @@ class StreamableHTTPClientTransport {
24866
24905
  return this._protocolVersion;
24867
24906
  }
24868
24907
  }
24869
- const version$1 = "0.16.6";
24908
+ const version$1 = "0.16.7";
24870
24909
  const packageJson = {
24871
24910
  version: version$1
24872
24911
  };
@@ -25432,11 +25471,15 @@ function useConnection({
25432
25471
  setCompletionsSupported(false);
25433
25472
  setServerCapabilities(null);
25434
25473
  };
25474
+ const clearRequestHistory = () => {
25475
+ setRequestHistory([]);
25476
+ };
25435
25477
  return {
25436
25478
  connectionStatus,
25437
25479
  serverCapabilities,
25438
25480
  mcpClient,
25439
25481
  requestHistory,
25482
+ clearRequestHistory,
25440
25483
  makeRequest,
25441
25484
  sendNotification,
25442
25485
  handleCompletion,
@@ -26730,7 +26773,7 @@ const OAuthFlowProgress = ({
26730
26773
  children: authState.authorizationUrl && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 p-3 border rounded-md bg-muted", children: [
26731
26774
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-medium mb-2 text-sm", children: "Authorization URL:" }),
26732
26775
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
26733
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs break-all", children: authState.authorizationUrl }),
26776
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs break-all", children: String(authState.authorizationUrl) }),
26734
26777
  /* @__PURE__ */ jsxRuntimeExports.jsx(
26735
26778
  "button",
26736
26779
  {
@@ -26992,7 +27035,7 @@ const AuthDebugger = ({
26992
27035
  SESSION_KEYS.AUTH_DEBUGGER_STATE,
26993
27036
  JSON.stringify(currentState)
26994
27037
  );
26995
- window.location.href = currentState.authorizationUrl;
27038
+ window.location.href = currentState.authorizationUrl.toString();
26996
27039
  break;
26997
27040
  }
26998
27041
  }
@@ -27383,7 +27426,9 @@ const JsonNode = reactExports.memo(
27383
27426
  JsonNode.displayName = "JsonNode";
27384
27427
  const HistoryAndNotifications = ({
27385
27428
  requestHistory,
27386
- serverNotifications
27429
+ serverNotifications,
27430
+ onClearHistory,
27431
+ onClearNotifications
27387
27432
  }) => {
27388
27433
  const [expandedRequests, setExpandedRequests] = reactExports.useState({});
27389
27434
  const [expandedNotifications, setExpandedNotifications] = reactExports.useState({});
@@ -27395,7 +27440,19 @@ const HistoryAndNotifications = ({
27395
27440
  };
27396
27441
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-card overflow-hidden flex h-full", children: [
27397
27442
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4 border-r", children: [
27398
- /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold mb-4", children: "History" }),
27443
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
27444
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold", children: "History" }),
27445
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
27446
+ Button,
27447
+ {
27448
+ variant: "outline",
27449
+ size: "sm",
27450
+ onClick: onClearHistory,
27451
+ disabled: requestHistory.length === 0,
27452
+ children: "Clear"
27453
+ }
27454
+ )
27455
+ ] }),
27399
27456
  requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 italic", children: "No history yet" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "space-y-3", children: requestHistory.slice().reverse().map((request, index2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
27400
27457
  "li",
27401
27458
  {
@@ -27445,7 +27502,19 @@ const HistoryAndNotifications = ({
27445
27502
  )) })
27446
27503
  ] }),
27447
27504
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4", children: [
27448
- /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold mb-4", children: "Server Notifications" }),
27505
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
27506
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold", children: "Server Notifications" }),
27507
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
27508
+ Button,
27509
+ {
27510
+ variant: "outline",
27511
+ size: "sm",
27512
+ onClick: onClearNotifications,
27513
+ disabled: serverNotifications.length === 0,
27514
+ children: "Clear"
27515
+ }
27516
+ )
27517
+ ] }),
27449
27518
  serverNotifications.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 italic", children: "No notifications yet" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "space-y-3", children: serverNotifications.slice().reverse().map((notification, index2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
27450
27519
  "li",
27451
27520
  {
@@ -36472,7 +36541,7 @@ const useTheme = () => {
36472
36541
  [theme, setThemeWithSideEffect]
36473
36542
  );
36474
36543
  };
36475
- const version = "0.16.6";
36544
+ const version = "0.16.7";
36476
36545
  var [createTooltipContext, createTooltipScope] = createContextScope("Tooltip", [
36477
36546
  createPopperScope
36478
36547
  ]);
@@ -37046,7 +37115,7 @@ const Sidebar = ({
37046
37115
  setCopiedServerEntry(true);
37047
37116
  toast2({
37048
37117
  title: "Config entry copied",
37049
- description: transportType === "stdio" ? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name." : "SSE URL has been copied. Use this URL directly in your MCP Client."
37118
+ description: transportType === "stdio" ? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name." : transportType === "streamable-http" ? "Streamable HTTP URL has been copied. Use this URL directly in your MCP Client." : "SSE URL has been copied. Use this URL directly in your MCP Client."
37050
37119
  });
37051
37120
  setTimeout(() => {
37052
37121
  setCopiedServerEntry(false);
@@ -38636,6 +38705,7 @@ const App = () => {
38636
38705
  serverCapabilities,
38637
38706
  mcpClient,
38638
38707
  requestHistory,
38708
+ clearRequestHistory,
38639
38709
  makeRequest,
38640
38710
  sendNotification,
38641
38711
  handleCompletion,
@@ -39083,6 +39153,9 @@ const App = () => {
39083
39153
  const handleRootsChange = async () => {
39084
39154
  await sendNotification({ method: "notifications/roots/list_changed" });
39085
39155
  };
39156
+ const handleClearNotifications = () => {
39157
+ setNotifications([]);
39158
+ };
39086
39159
  const sendLogLevelRequest = async (level) => {
39087
39160
  await sendMCPRequest(
39088
39161
  {
@@ -39104,13 +39177,13 @@ const App = () => {
39104
39177
  ) });
39105
39178
  if (window.location.pathname === "/oauth/callback") {
39106
39179
  const OAuthCallback = React.lazy(
39107
- () => __vitePreload(() => import("./OAuthCallback-Cmffn8om.js"), true ? [] : void 0)
39180
+ () => __vitePreload(() => import("./OAuthCallback-aXDAN43J.js"), true ? [] : void 0)
39108
39181
  );
39109
39182
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthCallback, { onConnect: onOAuthConnect }) });
39110
39183
  }
39111
39184
  if (window.location.pathname === "/oauth/callback/debug") {
39112
39185
  const OAuthDebugCallback = React.lazy(
39113
- () => __vitePreload(() => import("./OAuthDebugCallback-BhzcPtuF.js"), true ? [] : void 0)
39186
+ () => __vitePreload(() => import("./OAuthDebugCallback-BanjyQ_V.js"), true ? [] : void 0)
39114
39187
  );
39115
39188
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthDebugCallback, { onConnect: onOAuthDebugConnect }) });
39116
39189
  }
@@ -39457,7 +39530,9 @@ const App = () => {
39457
39530
  HistoryAndNotifications,
39458
39531
  {
39459
39532
  requestHistory,
39460
- serverNotifications: notifications
39533
+ serverNotifications: notifications,
39534
+ onClearHistory: clearRequestHistory,
39535
+ onClearNotifications: handleClearNotifications
39461
39536
  }
39462
39537
  ) })
39463
39538
  ]
@@ -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-B_qh-Btq.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C217Ot8c.css">
8
+ <script type="module" crossorigin src="/assets/index-Dg7mXHRE.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DZ1A0VH0.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.16.6",
3
+ "version": "0.16.7",
4
4
  "description": "Model Context Protocol inspector",
5
5
  "license": "MIT",
6
6
  "author": "Anthropic, PBC (https://anthropic.com)",
@@ -47,10 +47,10 @@
47
47
  "check-version": "node scripts/check-version-consistency.js"
48
48
  },
49
49
  "dependencies": {
50
- "@modelcontextprotocol/inspector-cli": "^0.16.6",
51
- "@modelcontextprotocol/inspector-client": "^0.16.6",
52
- "@modelcontextprotocol/inspector-server": "^0.16.6",
53
- "@modelcontextprotocol/sdk": "^1.17.5",
50
+ "@modelcontextprotocol/inspector-cli": "^0.16.7",
51
+ "@modelcontextprotocol/inspector-client": "^0.16.7",
52
+ "@modelcontextprotocol/inspector-server": "^0.16.7",
53
+ "@modelcontextprotocol/sdk": "^1.18.0",
54
54
  "concurrently": "^9.2.0",
55
55
  "open": "^10.2.0",
56
56
  "shell-quote": "^1.8.3",
@@ -12,12 +12,6 @@ import { findActualExecutable } from "spawn-rx";
12
12
  import mcpProxy from "./mcpProxy.js";
13
13
  import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
14
14
  const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
15
- const SSE_HEADERS_PASSTHROUGH = ["authorization"];
16
- const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
17
- "authorization",
18
- "mcp-session-id",
19
- "last-event-id",
20
- ];
21
15
  const defaultEnvironment = {
22
16
  ...getDefaultEnvironment(),
23
17
  ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
@@ -33,34 +27,71 @@ const { values } = parseArgs({
33
27
  },
34
28
  });
35
29
  // Function to get HTTP headers.
36
- // Supports only "sse" and "streamable-http" transport types.
37
- const getHttpHeaders = (req, transportType) => {
38
- const headers = {
39
- Accept: transportType === "sse"
40
- ? "text/event-stream"
41
- : "text/event-stream, application/json",
42
- };
43
- const defaultHeaders = transportType === "sse"
44
- ? SSE_HEADERS_PASSTHROUGH
45
- : STREAMABLE_HTTP_HEADERS_PASSTHROUGH;
46
- for (const key of defaultHeaders) {
47
- if (req.headers[key] === undefined) {
48
- continue;
30
+ const getHttpHeaders = (req) => {
31
+ const headers = {};
32
+ // Iterate over all headers in the request
33
+ for (const key in req.headers) {
34
+ const lowerKey = key.toLowerCase();
35
+ // Check if the header is one we want to forward
36
+ if (lowerKey.startsWith("mcp-") ||
37
+ lowerKey === "authorization" ||
38
+ lowerKey === "last-event-id") {
39
+ // Exclude the proxy's own authentication header
40
+ if (lowerKey !== "x-mcp-proxy-auth") {
41
+ const value = req.headers[key];
42
+ if (typeof value === "string") {
43
+ // If the value is a string, use it directly
44
+ headers[key] = value;
45
+ }
46
+ else if (Array.isArray(value)) {
47
+ // If the value is an array, use the last element
48
+ const lastValue = value.at(-1);
49
+ if (lastValue !== undefined) {
50
+ headers[key] = lastValue;
51
+ }
52
+ }
53
+ // If value is undefined, it's skipped, which is correct.
54
+ }
49
55
  }
50
- const value = req.headers[key];
51
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
52
56
  }
53
- // If the header "x-custom-auth-header" is present, use its value as the custom header name.
54
- if (req.headers["x-custom-auth-header"] !== undefined) {
55
- const customHeaderName = req.headers["x-custom-auth-header"];
56
- const lowerCaseHeaderName = customHeaderName.toLowerCase();
57
- if (req.headers[lowerCaseHeaderName] !== undefined) {
58
- const value = req.headers[lowerCaseHeaderName];
59
- headers[customHeaderName] = value;
57
+ // Handle the custom auth header separately. We expect `x-custom-auth-header`
58
+ // to be a string containing the name of the actual authentication header.
59
+ const customAuthHeaderName = req.headers["x-custom-auth-header"];
60
+ if (typeof customAuthHeaderName === "string") {
61
+ const lowerCaseHeaderName = customAuthHeaderName.toLowerCase();
62
+ const value = req.headers[lowerCaseHeaderName];
63
+ if (typeof value === "string") {
64
+ headers[customAuthHeaderName] = value;
65
+ }
66
+ else if (Array.isArray(value)) {
67
+ // If the actual auth header was sent multiple times, use the last value.
68
+ const lastValue = value.at(-1);
69
+ if (lastValue !== undefined) {
70
+ headers[customAuthHeaderName] = lastValue;
71
+ }
60
72
  }
61
73
  }
62
74
  return headers;
63
75
  };
76
+ /**
77
+ * Updates a headers object in-place, preserving the original Accept header.
78
+ * This is necessary to ensure that transports holding a reference to the headers
79
+ * object see the updates.
80
+ * @param currentHeaders The headers object to update.
81
+ * @param newHeaders The new headers to apply.
82
+ */
83
+ const updateHeadersInPlace = (currentHeaders, newHeaders) => {
84
+ // Preserve the Accept header, which is set at transport creation and
85
+ // is not present in subsequent client requests.
86
+ const accept = currentHeaders["Accept"];
87
+ // Clear the old headers and apply the new ones.
88
+ Object.keys(currentHeaders).forEach((key) => delete currentHeaders[key]);
89
+ Object.assign(currentHeaders, newHeaders);
90
+ // Restore the Accept header.
91
+ if (accept) {
92
+ currentHeaders["Accept"] = accept;
93
+ }
94
+ };
64
95
  const app = express();
65
96
  app.use(cors());
66
97
  app.use((req, res, next) => {
@@ -69,6 +100,7 @@ app.use((req, res, next) => {
69
100
  });
70
101
  const webAppTransports = new Map(); // Web app transports by web app sessionId
71
102
  const serverTransports = new Map(); // Server Transports by web app sessionId
103
+ const sessionHeaderHolders = new Map(); // For dynamic header updates
72
104
  // Use provided token from environment or generate a new one
73
105
  const sessionToken = process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
74
106
  const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
@@ -126,6 +158,27 @@ const authMiddleware = (req, res, next) => {
126
158
  }
127
159
  next();
128
160
  };
161
+ /**
162
+ * Creates a `fetch` function that merges dynamic session headers with the
163
+ * headers from the actual request, ensuring that request-specific headers like
164
+ * `Content-Type` are preserved.
165
+ */
166
+ const createCustomFetch = (headerHolder) => {
167
+ return (input, init) => {
168
+ // Determine the headers from the original request/init.
169
+ // The SDK may pass a Request object or a URL and an init object.
170
+ const originalHeaders = input instanceof Request ? input.headers : init?.headers;
171
+ // Start with our dynamic session headers.
172
+ const finalHeaders = new Headers(headerHolder.headers);
173
+ // Merge the SDK's request-specific headers, letting them overwrite.
174
+ // This is crucial for preserving Content-Type on POST requests.
175
+ new Headers(originalHeaders).forEach((value, key) => {
176
+ finalHeaders.set(key, value);
177
+ });
178
+ // This works for both `fetch(url, init)` and `fetch(request)` style calls.
179
+ return fetch(input, { ...init, headers: finalHeaders });
180
+ };
181
+ };
129
182
  const createTransport = async (req) => {
130
183
  const query = req.query;
131
184
  console.log("Query parameters:", JSON.stringify(query));
@@ -144,32 +197,35 @@ const createTransport = async (req) => {
144
197
  stderr: "pipe",
145
198
  });
146
199
  await transport.start();
147
- return transport;
200
+ return { transport };
148
201
  }
149
202
  else if (transportType === "sse") {
150
203
  const url = query.url;
151
- const headers = getHttpHeaders(req, transportType);
204
+ const headers = getHttpHeaders(req);
205
+ headers["Accept"] = "text/event-stream";
206
+ const headerHolder = { headers };
152
207
  console.log(`SSE transport: url=${url}, headers=${JSON.stringify(headers)}`);
153
208
  const transport = new SSEClientTransport(new URL(url), {
154
209
  eventSourceInit: {
155
- fetch: (url, init) => fetch(url, { ...init, headers }),
210
+ fetch: createCustomFetch(headerHolder),
156
211
  },
157
212
  requestInit: {
158
- headers,
213
+ headers: headerHolder.headers,
159
214
  },
160
215
  });
161
216
  await transport.start();
162
- return transport;
217
+ return { transport, headerHolder };
163
218
  }
164
219
  else if (transportType === "streamable-http") {
165
- const headers = getHttpHeaders(req, transportType);
220
+ const headers = getHttpHeaders(req);
221
+ headers["Accept"] = "text/event-stream, application/json";
222
+ const headerHolder = { headers };
166
223
  const transport = new StreamableHTTPClientTransport(new URL(query.url), {
167
- requestInit: {
168
- headers,
169
- },
224
+ // Pass a custom fetch to inject the latest headers on each request
225
+ fetch: createCustomFetch(headerHolder),
170
226
  });
171
227
  await transport.start();
172
- return transport;
228
+ return { transport, headerHolder };
173
229
  }
174
230
  else {
175
231
  console.error(`Invalid transport type: ${transportType}`);
@@ -179,6 +235,10 @@ const createTransport = async (req) => {
179
235
  app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
180
236
  const sessionId = req.headers["mcp-session-id"];
181
237
  console.log(`Received GET message for sessionId ${sessionId}`);
238
+ const headerHolder = sessionHeaderHolders.get(sessionId);
239
+ if (headerHolder) {
240
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
241
+ }
182
242
  try {
183
243
  const transport = webAppTransports.get(sessionId);
184
244
  if (!transport) {
@@ -196,29 +256,45 @@ app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
196
256
  });
197
257
  app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
198
258
  const sessionId = req.headers["mcp-session-id"];
199
- let serverTransport;
200
- if (!sessionId) {
259
+ if (sessionId) {
260
+ console.log(`Received POST message for sessionId ${sessionId}`);
261
+ const headerHolder = sessionHeaderHolders.get(sessionId);
262
+ if (headerHolder) {
263
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
264
+ }
201
265
  try {
202
- console.log("New StreamableHttp connection request");
203
- try {
204
- serverTransport = await createTransport(req);
266
+ const transport = webAppTransports.get(sessionId);
267
+ if (!transport) {
268
+ res.status(404).end("Transport not found for sessionId " + sessionId);
205
269
  }
206
- catch (error) {
207
- if (error instanceof SseError && error.code === 401) {
208
- console.error("Received 401 Unauthorized from MCP server:", error.message);
209
- res.status(401).json(error);
210
- return;
211
- }
212
- throw error;
270
+ else {
271
+ await transport.handleRequest(req, res);
213
272
  }
214
- console.log("Created StreamableHttp server transport");
273
+ }
274
+ catch (error) {
275
+ console.error("Error in /mcp route:", error);
276
+ res.status(500).json(error);
277
+ }
278
+ }
279
+ else {
280
+ console.log("New StreamableHttp connection request");
281
+ try {
282
+ const { transport: serverTransport, headerHolder } = await createTransport(req);
215
283
  const webAppTransport = new StreamableHTTPServerTransport({
216
284
  sessionIdGenerator: randomUUID,
217
285
  onsessioninitialized: (sessionId) => {
218
286
  webAppTransports.set(sessionId, webAppTransport);
219
- serverTransports.set(sessionId, serverTransport);
287
+ serverTransports.set(sessionId, serverTransport); // eslint-disable-line @typescript-eslint/no-non-null-assertion
288
+ if (headerHolder) {
289
+ sessionHeaderHolders.set(sessionId, headerHolder);
290
+ }
220
291
  console.log("Client <-> Proxy sessionId: " + sessionId);
221
292
  },
293
+ onsessionclosed: (sessionId) => {
294
+ webAppTransports.delete(sessionId);
295
+ serverTransports.delete(sessionId);
296
+ sessionHeaderHolders.delete(sessionId);
297
+ },
222
298
  });
223
299
  console.log("Created StreamableHttp client transport");
224
300
  await webAppTransport.start();
@@ -229,23 +305,12 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
229
305
  await webAppTransport.handleRequest(req, res, req.body);
230
306
  }
231
307
  catch (error) {
232
- console.error("Error in /mcp POST route:", error);
233
- res.status(500).json(error);
234
- }
235
- }
236
- else {
237
- console.log(`Received POST message for sessionId ${sessionId}`);
238
- try {
239
- const transport = webAppTransports.get(sessionId);
240
- if (!transport) {
241
- res.status(404).end("Transport not found for sessionId " + sessionId);
242
- }
243
- else {
244
- await transport.handleRequest(req, res);
308
+ if (error instanceof SseError && error.code === 401) {
309
+ console.error("Received 401 Unauthorized from MCP server:", error.message);
310
+ res.status(401).json(error);
311
+ return;
245
312
  }
246
- }
247
- catch (error) {
248
- console.error("Error in /mcp route:", error);
313
+ console.error("Error in /mcp POST route:", error);
249
314
  res.status(500).json(error);
250
315
  }
251
316
  }
@@ -253,10 +318,9 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
253
318
  app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
254
319
  const sessionId = req.headers["mcp-session-id"];
255
320
  console.log(`Received DELETE message for sessionId ${sessionId}`);
256
- let serverTransport;
257
321
  if (sessionId) {
258
322
  try {
259
- serverTransport = serverTransports.get(sessionId);
323
+ const serverTransport = serverTransports.get(sessionId);
260
324
  if (!serverTransport) {
261
325
  res.status(404).end("Transport not found for sessionId " + sessionId);
262
326
  }
@@ -264,6 +328,7 @@ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res)
264
328
  await serverTransport.terminateSession();
265
329
  webAppTransports.delete(sessionId);
266
330
  serverTransports.delete(sessionId);
331
+ sessionHeaderHolders.delete(sessionId);
267
332
  console.log(`Transports removed for sessionId ${sessionId}`);
268
333
  }
269
334
  res.status(200).end();
@@ -277,18 +342,7 @@ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res)
277
342
  app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => {
278
343
  try {
279
344
  console.log("New STDIO connection request");
280
- let serverTransport;
281
- try {
282
- serverTransport = await createTransport(req);
283
- }
284
- catch (error) {
285
- if (error instanceof SseError && error.code === 401) {
286
- console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
287
- res.status(401).json(error);
288
- return;
289
- }
290
- throw error;
291
- }
345
+ const { transport: serverTransport } = await createTransport(req);
292
346
  const proxyFullAddress = req.query.proxyFullAddress || "";
293
347
  const prefix = proxyFullAddress || "";
294
348
  const endpoint = `${prefix}/message`;
@@ -317,6 +371,7 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
317
371
  serverTransport.close();
318
372
  webAppTransports.delete(webAppTransport.sessionId);
319
373
  serverTransports.delete(webAppTransport.sessionId);
374
+ sessionHeaderHolders.delete(webAppTransport.sessionId);
320
375
  console.error(message);
321
376
  }
322
377
  else {
@@ -382,6 +437,11 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
382
437
  });
383
438
  }
384
439
  catch (error) {
440
+ if (error instanceof SseError && error.code === 401) {
441
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
442
+ res.status(401).json(error);
443
+ return;
444
+ }
385
445
  console.error("Error in /stdio route:", error);
386
446
  res.status(500).json(error);
387
447
  }
@@ -389,46 +449,39 @@ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) =
389
449
  app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
390
450
  try {
391
451
  console.log("New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp");
392
- let serverTransport;
393
- try {
394
- serverTransport = await createTransport(req);
395
- }
396
- catch (error) {
397
- if (error instanceof SseError && error.code === 401) {
398
- console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
399
- res.status(401).json(error);
400
- return;
401
- }
402
- else if (error instanceof SseError && error.code === 404) {
403
- console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
404
- res.status(404).json(error);
405
- return;
406
- }
407
- else if (JSON.stringify(error).includes("ECONNREFUSED")) {
408
- console.error("Connection refused. Is the MCP server running?");
409
- res.status(500).json(error);
410
- }
411
- else {
412
- throw error;
413
- }
414
- }
415
- if (serverTransport) {
416
- const proxyFullAddress = req.query.proxyFullAddress || "";
417
- const prefix = proxyFullAddress || "";
418
- const endpoint = `${prefix}/message`;
419
- const webAppTransport = new SSEServerTransport(endpoint, res);
420
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
421
- console.log("Created client transport");
422
- serverTransports.set(webAppTransport.sessionId, serverTransport);
423
- console.log("Created server transport");
424
- await webAppTransport.start();
425
- mcpProxy({
426
- transportToClient: webAppTransport,
427
- transportToServer: serverTransport,
428
- });
452
+ const { transport: serverTransport, headerHolder } = await createTransport(req);
453
+ const proxyFullAddress = req.query.proxyFullAddress || "";
454
+ const prefix = proxyFullAddress || "";
455
+ const endpoint = `${prefix}/message`;
456
+ const webAppTransport = new SSEServerTransport(endpoint, res);
457
+ webAppTransports.set(webAppTransport.sessionId, webAppTransport);
458
+ console.log("Created client transport");
459
+ serverTransports.set(webAppTransport.sessionId, serverTransport); // eslint-disable-line @typescript-eslint/no-non-null-assertion
460
+ if (headerHolder) {
461
+ sessionHeaderHolders.set(webAppTransport.sessionId, headerHolder);
429
462
  }
463
+ console.log("Created server transport");
464
+ await webAppTransport.start();
465
+ mcpProxy({
466
+ transportToClient: webAppTransport,
467
+ transportToServer: serverTransport,
468
+ });
430
469
  }
431
470
  catch (error) {
471
+ if (error instanceof SseError && error.code === 401) {
472
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
473
+ res.status(401).json(error);
474
+ return;
475
+ }
476
+ else if (error instanceof SseError && error.code === 404) {
477
+ console.error("Received 404 not found from MCP server. Does the MCP server support SSE?");
478
+ res.status(404).json(error);
479
+ return;
480
+ }
481
+ else if (JSON.stringify(error).includes("ECONNREFUSED")) {
482
+ console.error("Connection refused. Is the MCP server running?");
483
+ res.status(500).json(error);
484
+ }
432
485
  console.error("Error in /sse route:", error);
433
486
  res.status(500).json(error);
434
487
  }
@@ -437,6 +490,10 @@ app.post("/message", originValidationMiddleware, authMiddleware, async (req, res
437
490
  try {
438
491
  const sessionId = req.query.sessionId;
439
492
  console.log(`Received POST message for sessionId ${sessionId}`);
493
+ const headerHolder = sessionHeaderHolders.get(sessionId);
494
+ if (headerHolder) {
495
+ updateHeadersInPlace(headerHolder.headers, getHttpHeaders(req));
496
+ }
440
497
  const transport = webAppTransports.get(sessionId);
441
498
  if (!transport) {
442
499
  res.status(404).end("Session not found");