@modelcontextprotocol/inspector 0.14.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.
package/README.md CHANGED
@@ -137,6 +137,53 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
137
137
 
138
138
  The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.
139
139
 
140
+ #### Authentication
141
+
142
+ The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console:
143
+
144
+ ```
145
+ 🔑 Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
146
+
147
+ 🔗 Open inspector with token pre-filled:
148
+ http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
149
+ ```
150
+
151
+ This token must be included as a Bearer token in the Authorization header for all requests to the server. When authentication is enabled, auto-open is disabled by default to ensure you use the secure URL.
152
+
153
+ **Recommended: Use the pre-filled URL** - Click or copy the link shown in the console to open the inspector with the token already configured.
154
+
155
+ **Alternative: Manual configuration** - If you already have the inspector open:
156
+
157
+ 1. Click the "Configuration" button in the sidebar
158
+ 2. Find "Proxy Session Token" and enter the token displayed in the proxy console
159
+ 3. Click "Save" to apply the configuration
160
+
161
+ The token will be saved in your browser's local storage for future use.
162
+
163
+ If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable:
164
+
165
+ ```bash
166
+ DANGEROUSLY_OMIT_AUTH=true npm start
167
+ ```
168
+
169
+ #### Local-only Binding
170
+
171
+ By default, the MCP Inspector proxy server binds only to `127.0.0.1` (localhost) to prevent network access. This ensures the server is not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable:
172
+
173
+ ```bash
174
+ HOST=0.0.0.0 npm start
175
+ ```
176
+
177
+ **Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes.
178
+
179
+ #### DNS Rebinding Protection
180
+
181
+ To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list):
182
+
183
+ ```bash
184
+ ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:8000 npm start
185
+ ```
186
+
140
187
  ### Configuration
141
188
 
142
189
  The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
@@ -100,7 +100,9 @@ async function main() {
100
100
 
101
101
  if (serverOk) {
102
102
  try {
103
- if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
103
+ // Only auto-open when auth is disabled
104
+ const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
105
+ if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) {
104
106
  open(`http://127.0.0.1:${CLIENT_PORT}`);
105
107
  }
106
108
  await spawnPromise("node", [inspectorClientPath], {
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-CfNcvwpX.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-TghXC4TH.js";
2
2
  import { p as parseOAuthCallbackParams, g as generateOAuthErrorDescription } from "./oauthUtils-DTcoXpSP.js";
3
3
  const OAuthCallback = ({ onConnect }) => {
4
4
  const { toast } = useToast();
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, j as jsxRuntimeExports } from "./index-CfNcvwpX.js";
1
+ import { r as reactExports, S as SESSION_KEYS, j as jsxRuntimeExports } from "./index-TghXC4TH.js";
2
2
  import { p as parseOAuthCallbackParams, g as generateOAuthErrorDescription } from "./oauthUtils-DTcoXpSP.js";
3
3
  const OAuthDebugCallback = ({ onConnect }) => {
4
4
  reactExports.useEffect(() => {
@@ -989,9 +989,6 @@ video {
989
989
  .w-8 {
990
990
  width: 2rem;
991
991
  }
992
- .w-80 {
993
- width: 20rem;
994
- }
995
992
  .w-9 {
996
993
  width: 2.25rem;
997
994
  }
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/OAuthCallback-CJlw2xLt.js","assets/oauthUtils-DTcoXpSP.js","assets/OAuthDebugCallback-0U3Rpr19.js"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/OAuthCallback-D7GXYYW8.js","assets/oauthUtils-DTcoXpSP.js","assets/OAuthDebugCallback-25cy6Vcm.js"])))=>i.map(i=>d[i]);
2
2
  function _mergeNamespaces(n, m2) {
3
3
  for (var i = 0; i < m2.length; i++) {
4
4
  const e = m2[i];
@@ -16039,22 +16039,32 @@ const DEFAULT_INSPECTOR_CONFIG = {
16039
16039
  MCP_SERVER_REQUEST_TIMEOUT: {
16040
16040
  label: "Request Timeout",
16041
16041
  description: "Timeout for requests to the MCP server (ms)",
16042
- value: 1e4
16042
+ value: 1e4,
16043
+ is_session_item: false
16043
16044
  },
16044
16045
  MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
16045
16046
  label: "Reset Timeout on Progress",
16046
16047
  description: "Reset timeout on progress notifications",
16047
- value: true
16048
+ value: true,
16049
+ is_session_item: false
16048
16050
  },
16049
16051
  MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
16050
16052
  label: "Maximum Total Timeout",
16051
16053
  description: "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
16052
- value: 6e4
16054
+ value: 6e4,
16055
+ is_session_item: false
16053
16056
  },
16054
16057
  MCP_PROXY_FULL_ADDRESS: {
16055
16058
  label: "Inspector Proxy Address",
16056
16059
  description: "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
16057
- value: ""
16060
+ value: "",
16061
+ is_session_item: false
16062
+ },
16063
+ MCP_PROXY_AUTH_TOKEN: {
16064
+ label: "Proxy Session Token",
16065
+ description: "Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)",
16066
+ value: "",
16067
+ is_session_item: true
16058
16068
  }
16059
16069
  };
16060
16070
  const EMPTY_DEBUGGER_STATE = {
@@ -24146,7 +24156,7 @@ const StdErrNotificationSchema = NotificationSchema.extend({
24146
24156
  ClientNotificationSchema.or(
24147
24157
  StdErrNotificationSchema
24148
24158
  ).or(ServerNotificationSchema).or(NotificationSchema);
24149
- const version$1 = "0.14.0";
24159
+ const version$1 = "0.14.1";
24150
24160
  const packageJson = {
24151
24161
  version: version$1
24152
24162
  };
@@ -24166,6 +24176,9 @@ const resetRequestTimeoutOnProgress = (config) => {
24166
24176
  const getMCPServerRequestMaxTotalTimeout = (config) => {
24167
24177
  return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value;
24168
24178
  };
24179
+ const getMCPProxyAuthToken = (config) => {
24180
+ return config.MCP_PROXY_AUTH_TOKEN.value;
24181
+ };
24169
24182
  const getSearchParam = (key) => {
24170
24183
  try {
24171
24184
  const url = new URL(window.location.href);
@@ -24218,26 +24231,46 @@ const getConfigOverridesFromQueryParams = (defaultConfig) => {
24218
24231
  return overrides;
24219
24232
  };
24220
24233
  const initializeInspectorConfig = (localStorageKey) => {
24221
- const savedConfig = localStorage.getItem(localStorageKey);
24222
- let baseConfig;
24223
- if (savedConfig) {
24224
- const mergedConfig = {
24225
- ...DEFAULT_INSPECTOR_CONFIG,
24226
- ...JSON.parse(savedConfig)
24234
+ const savedPersistentConfig = localStorage.getItem(localStorageKey);
24235
+ const savedEphemeralConfig = sessionStorage.getItem(
24236
+ `${localStorageKey}_ephemeral`
24237
+ );
24238
+ let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG };
24239
+ if (savedPersistentConfig) {
24240
+ const parsedPersistentConfig = JSON.parse(savedPersistentConfig);
24241
+ baseConfig = { ...baseConfig, ...parsedPersistentConfig };
24242
+ }
24243
+ if (savedEphemeralConfig) {
24244
+ const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig);
24245
+ baseConfig = { ...baseConfig, ...parsedEphemeralConfig };
24246
+ }
24247
+ for (const [key, value] of Object.entries(baseConfig)) {
24248
+ baseConfig[key] = {
24249
+ ...value,
24250
+ label: DEFAULT_INSPECTOR_CONFIG[key].label,
24251
+ description: DEFAULT_INSPECTOR_CONFIG[key].description,
24252
+ is_session_item: DEFAULT_INSPECTOR_CONFIG[key].is_session_item
24227
24253
  };
24228
- for (const [key, value] of Object.entries(mergedConfig)) {
24229
- mergedConfig[key] = {
24230
- ...value,
24231
- label: DEFAULT_INSPECTOR_CONFIG[key].label
24232
- };
24233
- }
24234
- baseConfig = mergedConfig;
24235
- } else {
24236
- baseConfig = DEFAULT_INSPECTOR_CONFIG;
24237
24254
  }
24238
24255
  const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG);
24239
24256
  return { ...baseConfig, ...overrides };
24240
24257
  };
24258
+ const saveInspectorConfig = (localStorageKey, config) => {
24259
+ const persistentConfig = {};
24260
+ const ephemeralConfig = {};
24261
+ for (const [key, value] of Object.entries(config)) {
24262
+ if (value.is_session_item) {
24263
+ ephemeralConfig[key] = value;
24264
+ } else {
24265
+ persistentConfig[key] = value;
24266
+ }
24267
+ }
24268
+ localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig));
24269
+ sessionStorage.setItem(
24270
+ `${localStorageKey}_ephemeral`,
24271
+ JSON.stringify(ephemeralConfig)
24272
+ );
24273
+ };
24241
24274
  function useConnection({
24242
24275
  transportType,
24243
24276
  command,
@@ -24375,7 +24408,12 @@ function useConnection({
24375
24408
  const checkProxyHealth = async () => {
24376
24409
  try {
24377
24410
  const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);
24378
- const proxyHealthResponse = await fetch(proxyHealthUrl);
24411
+ const proxyAuthToken = getMCPProxyAuthToken(config);
24412
+ const headers = {};
24413
+ if (proxyAuthToken) {
24414
+ headers["Authorization"] = `Bearer ${proxyAuthToken}`;
24415
+ }
24416
+ const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });
24379
24417
  const proxyHealth = await proxyHealthResponse.json();
24380
24418
  if ((proxyHealth == null ? void 0 : proxyHealth.status) !== "ok") {
24381
24419
  throw new Error("MCP Proxy Server is not healthy");
@@ -24388,6 +24426,9 @@ function useConnection({
24388
24426
  const is401Error = (error) => {
24389
24427
  return error instanceof SseError && error.code === 401 || error instanceof Error && error.message.includes("401") || error instanceof Error && error.message.includes("Unauthorized");
24390
24428
  };
24429
+ const isProxyAuthError = (error) => {
24430
+ return error instanceof Error && error.message.includes("Authentication required. Use the session token");
24431
+ };
24391
24432
  const handleAuthError = async (error) => {
24392
24433
  if (is401Error(error)) {
24393
24434
  const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
@@ -24431,6 +24472,11 @@ function useConnection({
24431
24472
  headers[authHeaderName] = `Bearer ${token}`;
24432
24473
  }
24433
24474
  }
24475
+ const proxyAuthToken = getMCPProxyAuthToken(config);
24476
+ const proxyHeaders = {};
24477
+ if (proxyAuthToken) {
24478
+ proxyHeaders["Authorization"] = `Bearer ${proxyAuthToken}`;
24479
+ }
24434
24480
  let transportOptions;
24435
24481
  let mcpProxyServerUrl;
24436
24482
  switch (transportType) {
@@ -24442,10 +24488,13 @@ function useConnection({
24442
24488
  transportOptions = {
24443
24489
  authProvider: serverAuthProvider,
24444
24490
  eventSourceInit: {
24445
- fetch: (url, init) => fetch(url, { ...init, headers })
24491
+ fetch: (url, init) => fetch(url, {
24492
+ ...init,
24493
+ headers: { ...headers, ...proxyHeaders }
24494
+ })
24446
24495
  },
24447
24496
  requestInit: {
24448
- headers
24497
+ headers: { ...headers, ...proxyHeaders }
24449
24498
  }
24450
24499
  };
24451
24500
  break;
@@ -24454,10 +24503,13 @@ function useConnection({
24454
24503
  mcpProxyServerUrl.searchParams.append("url", sseUrl);
24455
24504
  transportOptions = {
24456
24505
  eventSourceInit: {
24457
- fetch: (url, init) => fetch(url, { ...init, headers })
24506
+ fetch: (url, init) => fetch(url, {
24507
+ ...init,
24508
+ headers: { ...headers, ...proxyHeaders }
24509
+ })
24458
24510
  },
24459
24511
  requestInit: {
24460
- headers
24512
+ headers: { ...headers, ...proxyHeaders }
24461
24513
  }
24462
24514
  };
24463
24515
  break;
@@ -24466,10 +24518,13 @@ function useConnection({
24466
24518
  mcpProxyServerUrl.searchParams.append("url", sseUrl);
24467
24519
  transportOptions = {
24468
24520
  eventSourceInit: {
24469
- fetch: (url, init) => fetch(url, { ...init, headers })
24521
+ fetch: (url, init) => fetch(url, {
24522
+ ...init,
24523
+ headers: { ...headers, ...proxyHeaders }
24524
+ })
24470
24525
  },
24471
24526
  requestInit: {
24472
- headers
24527
+ headers: { ...headers, ...proxyHeaders }
24473
24528
  },
24474
24529
  // TODO these should be configurable...
24475
24530
  reconnectionOptions: {
@@ -24532,6 +24587,15 @@ function useConnection({
24532
24587
  `Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
24533
24588
  error
24534
24589
  );
24590
+ if (isProxyAuthError(error)) {
24591
+ toast2({
24592
+ title: "Proxy Authentication Required",
24593
+ description: "Please enter the session token from the proxy server console in the Configuration settings.",
24594
+ variant: "destructive"
24595
+ });
24596
+ setConnectionStatus("error");
24597
+ return;
24598
+ }
24535
24599
  const shouldRetry = await handleAuthError(error);
24536
24600
  if (shouldRetry) {
24537
24601
  return connect(void 0, retryCount + 1);
@@ -24633,6 +24697,52 @@ function useDraggablePane(initialHeight) {
24633
24697
  handleDragStart
24634
24698
  };
24635
24699
  }
24700
+ function useDraggableSidebar(initialWidth) {
24701
+ const [width, setWidth] = reactExports.useState(initialWidth);
24702
+ const [isDragging, setIsDragging] = reactExports.useState(false);
24703
+ const dragStartX = reactExports.useRef(0);
24704
+ const dragStartWidth = reactExports.useRef(0);
24705
+ const handleDragStart = reactExports.useCallback(
24706
+ (e) => {
24707
+ setIsDragging(true);
24708
+ dragStartX.current = e.clientX;
24709
+ dragStartWidth.current = width;
24710
+ document.body.style.userSelect = "none";
24711
+ },
24712
+ [width]
24713
+ );
24714
+ const handleDragMove = reactExports.useCallback(
24715
+ (e) => {
24716
+ if (!isDragging) return;
24717
+ const deltaX = e.clientX - dragStartX.current;
24718
+ const newWidth = Math.max(
24719
+ 200,
24720
+ Math.min(600, dragStartWidth.current + deltaX)
24721
+ );
24722
+ setWidth(newWidth);
24723
+ },
24724
+ [isDragging]
24725
+ );
24726
+ const handleDragEnd = reactExports.useCallback(() => {
24727
+ setIsDragging(false);
24728
+ document.body.style.userSelect = "";
24729
+ }, []);
24730
+ reactExports.useEffect(() => {
24731
+ if (isDragging) {
24732
+ window.addEventListener("mousemove", handleDragMove);
24733
+ window.addEventListener("mouseup", handleDragEnd);
24734
+ return () => {
24735
+ window.removeEventListener("mousemove", handleDragMove);
24736
+ window.removeEventListener("mouseup", handleDragEnd);
24737
+ };
24738
+ }
24739
+ }, [isDragging, handleDragMove, handleDragEnd]);
24740
+ return {
24741
+ width,
24742
+ isDragging,
24743
+ handleDragStart
24744
+ };
24745
+ }
24636
24746
  var useReactId = React$1[" useId ".trim().toString()] || (() => void 0);
24637
24747
  var count$1 = 0;
24638
24748
  function useId(deterministicId) {
@@ -35185,7 +35295,7 @@ const useTheme = () => {
35185
35295
  [theme, setThemeWithSideEffect]
35186
35296
  );
35187
35297
  };
35188
- const version = "0.14.0";
35298
+ const version = "0.14.1";
35189
35299
  var [createTooltipContext, createTooltipScope] = createContextScope("Tooltip", [
35190
35300
  createPopperScope
35191
35301
  ]);
@@ -35788,7 +35898,7 @@ const Sidebar = ({
35788
35898
  reportError2(error);
35789
35899
  }
35790
35900
  }, [generateMCPServerFile, toast2, reportError2]);
35791
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-80 bg-card border-r border-border flex flex-col h-full", children: [
35901
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-card border-r border-border flex flex-col h-full", children: [
35792
35902
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-border", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "ml-2 text-lg font-semibold", children: [
35793
35903
  "MCP Inspector v",
35794
35904
  version
@@ -35856,7 +35966,19 @@ const Sidebar = ({
35856
35966
  ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
35857
35967
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
35858
35968
  /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm font-medium", htmlFor: "sse-url-input", children: "URL" }),
35859
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35969
+ sseUrl ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Tooltip, { children: [
35970
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
35971
+ Input,
35972
+ {
35973
+ id: "sse-url-input",
35974
+ placeholder: "URL",
35975
+ value: sseUrl,
35976
+ onChange: (e) => setSseUrl(e.target.value),
35977
+ className: "font-mono"
35978
+ }
35979
+ ) }),
35980
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TooltipContent, { children: sseUrl })
35981
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
35860
35982
  Input,
35861
35983
  {
35862
35984
  id: "sse-url-input",
@@ -36207,11 +36329,17 @@ const Sidebar = ({
36207
36329
  }
36208
36330
  ),
36209
36331
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-600 dark:text-gray-400", children: (() => {
36332
+ var _a;
36210
36333
  switch (connectionStatus) {
36211
36334
  case "connected":
36212
36335
  return "Connected";
36213
- case "error":
36214
- return "Connection Error, is your MCP server running?";
36336
+ case "error": {
36337
+ const hasProxyToken = (_a = config.MCP_PROXY_AUTH_TOKEN) == null ? void 0 : _a.value;
36338
+ if (!hasProxyToken) {
36339
+ return "Connection Error - Did you add the proxy session token in Configuration?";
36340
+ }
36341
+ return "Connection Error - Check if your MCP server is running and proxy token is correct";
36342
+ }
36215
36343
  case "error-connecting-to-proxy":
36216
36344
  return "Error Connecting to MCP Inspector Proxy - Check Console logs";
36217
36345
  default:
@@ -37015,6 +37143,11 @@ const App = () => {
37015
37143
  const [nextToolCursor, setNextToolCursor] = reactExports.useState();
37016
37144
  const progressTokenRef = reactExports.useRef(0);
37017
37145
  const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
37146
+ const {
37147
+ width: sidebarWidth,
37148
+ isDragging: isSidebarDragging,
37149
+ handleDragStart: handleSidebarDragStart
37150
+ } = useDraggableSidebar(320);
37018
37151
  const {
37019
37152
  connectionStatus,
37020
37153
  serverCapabilities,
@@ -37071,7 +37204,7 @@ const App = () => {
37071
37204
  localStorage.setItem("lastHeaderName", headerName);
37072
37205
  }, [headerName]);
37073
37206
  reactExports.useEffect(() => {
37074
- localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
37207
+ saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
37075
37208
  }, [config]);
37076
37209
  const onOAuthConnect = reactExports.useCallback(
37077
37210
  (serverUrl) => {
@@ -37163,7 +37296,12 @@ const App = () => {
37163
37296
  loadOAuthTokens();
37164
37297
  }, [sseUrl]);
37165
37298
  reactExports.useEffect(() => {
37166
- fetch(`${getMCPProxyAddress(config)}/config`).then((response) => response.json()).then((data2) => {
37299
+ const headers = {};
37300
+ const proxyAuthToken = getMCPProxyAuthToken(config);
37301
+ if (proxyAuthToken) {
37302
+ headers["Authorization"] = `Bearer ${proxyAuthToken}`;
37303
+ }
37304
+ fetch(`${getMCPProxyAddress(config)}/config`, { headers }).then((response) => response.json()).then((data2) => {
37167
37305
  setEnv(data2.defaultEnvironment);
37168
37306
  if (data2.defaultCommand) {
37169
37307
  setCommand(data2.defaultCommand);
@@ -37174,7 +37312,7 @@ const App = () => {
37174
37312
  }).catch(
37175
37313
  (error) => console.error("Error fetching default environment:", error)
37176
37314
  );
37177
- }, []);
37315
+ }, [config]);
37178
37316
  reactExports.useEffect(() => {
37179
37317
  rootsRef.current = roots;
37180
37318
  }, [roots]);
@@ -37378,44 +37516,76 @@ const App = () => {
37378
37516
  ) });
37379
37517
  if (window.location.pathname === "/oauth/callback") {
37380
37518
  const OAuthCallback = React.lazy(
37381
- () => __vitePreload(() => import("./OAuthCallback-CJlw2xLt.js"), true ? __vite__mapDeps([0,1]) : void 0)
37519
+ () => __vitePreload(() => import("./OAuthCallback-D7GXYYW8.js"), true ? __vite__mapDeps([0,1]) : void 0)
37382
37520
  );
37383
37521
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthCallback, { onConnect: onOAuthConnect }) });
37384
37522
  }
37385
37523
  if (window.location.pathname === "/oauth/callback/debug") {
37386
37524
  const OAuthDebugCallback = React.lazy(
37387
- () => __vitePreload(() => import("./OAuthDebugCallback-0U3Rpr19.js"), true ? __vite__mapDeps([2,1]) : void 0)
37525
+ () => __vitePreload(() => import("./OAuthDebugCallback-25cy6Vcm.js"), true ? __vite__mapDeps([2,1]) : void 0)
37388
37526
  );
37389
37527
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthDebugCallback, { onConnect: onOAuthDebugConnect }) });
37390
37528
  }
37391
37529
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex h-screen bg-background", children: [
37392
- /* @__PURE__ */ jsxRuntimeExports.jsx(
37393
- Sidebar,
37530
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
37531
+ "div",
37394
37532
  {
37395
- connectionStatus,
37396
- transportType,
37397
- setTransportType,
37398
- command,
37399
- setCommand,
37400
- args,
37401
- setArgs,
37402
- sseUrl,
37403
- setSseUrl,
37404
- env,
37405
- setEnv,
37406
- config,
37407
- setConfig,
37408
- bearerToken,
37409
- setBearerToken,
37410
- headerName,
37411
- setHeaderName,
37412
- onConnect: connectMcpServer,
37413
- onDisconnect: disconnectMcpServer,
37414
- stdErrNotifications,
37415
- logLevel,
37416
- sendLogLevelRequest,
37417
- loggingSupported: !!(serverCapabilities == null ? void 0 : serverCapabilities.logging) || false,
37418
- clearStdErrNotifications
37533
+ style: {
37534
+ width: sidebarWidth,
37535
+ minWidth: 200,
37536
+ maxWidth: 600,
37537
+ transition: isSidebarDragging ? "none" : "width 0.15s"
37538
+ },
37539
+ className: "bg-card border-r border-border flex flex-col h-full relative",
37540
+ children: [
37541
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
37542
+ Sidebar,
37543
+ {
37544
+ connectionStatus,
37545
+ transportType,
37546
+ setTransportType,
37547
+ command,
37548
+ setCommand,
37549
+ args,
37550
+ setArgs,
37551
+ sseUrl,
37552
+ setSseUrl,
37553
+ env,
37554
+ setEnv,
37555
+ config,
37556
+ setConfig,
37557
+ bearerToken,
37558
+ setBearerToken,
37559
+ headerName,
37560
+ setHeaderName,
37561
+ onConnect: connectMcpServer,
37562
+ onDisconnect: disconnectMcpServer,
37563
+ stdErrNotifications,
37564
+ logLevel,
37565
+ sendLogLevelRequest,
37566
+ loggingSupported: !!(serverCapabilities == null ? void 0 : serverCapabilities.logging) || false,
37567
+ clearStdErrNotifications
37568
+ }
37569
+ ),
37570
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
37571
+ "div",
37572
+ {
37573
+ onMouseDown: handleSidebarDragStart,
37574
+ style: {
37575
+ cursor: "col-resize",
37576
+ position: "absolute",
37577
+ top: 0,
37578
+ right: 0,
37579
+ width: 6,
37580
+ height: "100%",
37581
+ zIndex: 10,
37582
+ background: isSidebarDragging ? "rgba(0,0,0,0.08)" : "transparent"
37583
+ },
37584
+ "aria-label": "Resize sidebar",
37585
+ "data-testid": "sidebar-drag-handle"
37586
+ }
37587
+ )
37588
+ ]
37419
37589
  }
37420
37590
  ),
37421
37591
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
@@ -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-CfNcvwpX.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-1IEtev7A.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.14.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)",
@@ -41,9 +41,9 @@
41
41
  "publish-all": "npm publish --workspaces --access public && npm publish --access public"
42
42
  },
43
43
  "dependencies": {
44
- "@modelcontextprotocol/inspector-cli": "^0.14.0",
45
- "@modelcontextprotocol/inspector-client": "^0.14.0",
46
- "@modelcontextprotocol/inspector-server": "^0.14.0",
44
+ "@modelcontextprotocol/inspector-cli": "^0.14.1",
45
+ "@modelcontextprotocol/inspector-client": "^0.14.1",
46
+ "@modelcontextprotocol/inspector-server": "^0.14.1",
47
47
  "@modelcontextprotocol/sdk": "^1.12.1",
48
48
  "concurrently": "^9.0.1",
49
49
  "open": "^10.1.0",
@@ -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",
@@ -65,9 +65,63 @@ app.use((req, res, next) => {
65
65
  });
66
66
  const webAppTransports = new Map(); // Web app transports by web app sessionId
67
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
+ };
68
122
  const createTransport = async (req) => {
69
123
  const query = req.query;
70
- console.log("Query parameters:", query);
124
+ console.log("Query parameters:", JSON.stringify(query));
71
125
  const transportType = query.transportType;
72
126
  if (transportType === "stdio") {
73
127
  const command = query.command;
@@ -75,7 +129,7 @@ const createTransport = async (req) => {
75
129
  const queryEnv = query.env ? JSON.parse(query.env) : {};
76
130
  const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
77
131
  const { cmd, args } = findActualExecutable(command, origArgs);
78
- console.log(`Stdio transport: command=${cmd}, args=${args}`);
132
+ console.log(`STDIO transport: command=${cmd}, args=${args}`);
79
133
  const transport = new StdioClientTransport({
80
134
  command: cmd,
81
135
  args,
@@ -83,7 +137,6 @@ const createTransport = async (req) => {
83
137
  stderr: "pipe",
84
138
  });
85
139
  await transport.start();
86
- console.log("Spawned stdio transport");
87
140
  return transport;
88
141
  }
89
142
  else if (transportType === "sse") {
@@ -99,7 +152,6 @@ const createTransport = async (req) => {
99
152
  },
100
153
  });
101
154
  await transport.start();
102
- console.log("Connected to SSE transport");
103
155
  return transport;
104
156
  }
105
157
  else if (transportType === "streamable-http") {
@@ -110,7 +162,6 @@ const createTransport = async (req) => {
110
162
  },
111
163
  });
112
164
  await transport.start();
113
- console.log("Connected to Streamable HTTP transport");
114
165
  return transport;
115
166
  }
116
167
  else {
@@ -118,7 +169,7 @@ const createTransport = async (req) => {
118
169
  throw new Error("Invalid transport type specified");
119
170
  }
120
171
  };
121
- app.get("/mcp", async (req, res) => {
172
+ app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
122
173
  const sessionId = req.headers["mcp-session-id"];
123
174
  console.log(`Received GET message for sessionId ${sessionId}`);
124
175
  try {
@@ -136,13 +187,12 @@ app.get("/mcp", async (req, res) => {
136
187
  res.status(500).json(error);
137
188
  }
138
189
  });
139
- app.post("/mcp", async (req, res) => {
190
+ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
140
191
  const sessionId = req.headers["mcp-session-id"];
141
- console.log(`Received POST message for sessionId ${sessionId}`);
142
192
  let serverTransport;
143
193
  if (!sessionId) {
144
194
  try {
145
- console.log("New streamable-http connection");
195
+ console.log("New StreamableHttp connection request");
146
196
  try {
147
197
  serverTransport = await createTransport(req);
148
198
  }
@@ -154,15 +204,16 @@ app.post("/mcp", async (req, res) => {
154
204
  }
155
205
  throw error;
156
206
  }
157
- console.log("Connected MCP client to server transport");
207
+ console.log("Created StreamableHttp server transport");
158
208
  const webAppTransport = new StreamableHTTPServerTransport({
159
209
  sessionIdGenerator: randomUUID,
160
210
  onsessioninitialized: (sessionId) => {
161
211
  webAppTransports.set(sessionId, webAppTransport);
162
212
  serverTransports.set(sessionId, serverTransport);
163
- console.log("Created streamable web app transport " + sessionId);
213
+ console.log("Client <-> Proxy sessionId: " + sessionId);
164
214
  },
165
215
  });
216
+ console.log("Created StreamableHttp client transport");
166
217
  await webAppTransport.start();
167
218
  mcpProxy({
168
219
  transportToClient: webAppTransport,
@@ -176,6 +227,7 @@ app.post("/mcp", async (req, res) => {
176
227
  }
177
228
  }
178
229
  else {
230
+ console.log(`Received POST message for sessionId ${sessionId}`);
179
231
  try {
180
232
  const transport = webAppTransports.get(sessionId);
181
233
  if (!transport) {
@@ -191,7 +243,7 @@ app.post("/mcp", async (req, res) => {
191
243
  }
192
244
  }
193
245
  });
194
- app.delete("/mcp", async (req, res) => {
246
+ app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
195
247
  const sessionId = req.headers["mcp-session-id"];
196
248
  console.log(`Received DELETE message for sessionId ${sessionId}`);
197
249
  let serverTransport;
@@ -215,84 +267,110 @@ app.delete("/mcp", async (req, res) => {
215
267
  }
216
268
  }
217
269
  });
218
- app.get("/stdio", async (req, res) => {
270
+ app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => {
219
271
  try {
220
- console.log("New connection");
272
+ console.log("New STDIO connection request");
221
273
  let serverTransport;
222
274
  try {
223
275
  serverTransport = await createTransport(req);
276
+ console.log("Created server transport");
224
277
  }
225
278
  catch (error) {
226
279
  if (error instanceof SseError && error.code === 401) {
227
- console.error("Received 401 Unauthorized from MCP server:", error.message);
280
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
228
281
  res.status(401).json(error);
229
282
  return;
230
283
  }
231
284
  throw error;
232
285
  }
233
- console.log("Connected MCP client to backing server transport");
234
286
  const webAppTransport = new SSEServerTransport("/message", res);
287
+ console.log("Created client transport");
235
288
  webAppTransports.set(webAppTransport.sessionId, webAppTransport);
236
289
  serverTransports.set(webAppTransport.sessionId, serverTransport);
237
- console.log("Created client/server transports");
238
290
  await webAppTransport.start();
239
291
  serverTransport.stderr.on("data", (chunk) => {
240
- webAppTransport.send({
241
- jsonrpc: "2.0",
242
- method: "notifications/stderr",
243
- params: {
244
- content: chunk.toString(),
245
- },
246
- });
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
+ }
247
315
  });
248
316
  mcpProxy({
249
317
  transportToClient: webAppTransport,
250
318
  transportToServer: serverTransport,
251
319
  });
252
- console.log("Set up MCP proxy");
253
320
  }
254
321
  catch (error) {
255
322
  console.error("Error in /stdio route:", error);
256
323
  res.status(500).json(error);
257
324
  }
258
325
  });
259
- app.get("/sse", async (req, res) => {
326
+ app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
260
327
  try {
261
- 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");
262
329
  let serverTransport;
263
330
  try {
264
331
  serverTransport = await createTransport(req);
265
332
  }
266
333
  catch (error) {
267
334
  if (error instanceof SseError && error.code === 401) {
268
- console.error("Received 401 Unauthorized from MCP server:", error.message);
335
+ console.error("Received 401 Unauthorized from MCP server. Authentication failure.");
269
336
  res.status(401).json(error);
270
337
  return;
271
338
  }
272
- 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
+ });
273
363
  }
274
- console.log("Connected MCP client to backing server transport");
275
- const webAppTransport = new SSEServerTransport("/message", res);
276
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
277
- console.log("Created client transport");
278
- serverTransports.set(webAppTransport.sessionId, serverTransport);
279
- console.log("Created server transport");
280
- await webAppTransport.start();
281
- mcpProxy({
282
- transportToClient: webAppTransport,
283
- transportToServer: serverTransport,
284
- });
285
- console.log("Set up MCP proxy");
286
364
  }
287
365
  catch (error) {
288
366
  console.error("Error in /sse route:", error);
289
367
  res.status(500).json(error);
290
368
  }
291
369
  });
292
- app.post("/message", async (req, res) => {
370
+ app.post("/message", originValidationMiddleware, authMiddleware, async (req, res) => {
293
371
  try {
294
372
  const sessionId = req.query.sessionId;
295
- console.log(`Received message for sessionId ${sessionId}`);
373
+ console.log(`Received POST message for sessionId ${sessionId}`);
296
374
  const transport = webAppTransports.get(sessionId);
297
375
  if (!transport) {
298
376
  res.status(404).end("Session not found");
@@ -310,7 +388,7 @@ app.get("/health", (req, res) => {
310
388
  status: "ok",
311
389
  });
312
390
  });
313
- app.get("/config", (req, res) => {
391
+ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
314
392
  try {
315
393
  res.json({
316
394
  defaultEnvironment,
@@ -323,10 +401,22 @@ app.get("/config", (req, res) => {
323
401
  res.status(500).json(error);
324
402
  }
325
403
  });
326
- const PORT = process.env.PORT || 6277;
327
- 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);
328
407
  server.on("listening", () => {
329
- 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
+ }
330
420
  });
331
421
  server.on("error", (err) => {
332
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 = () => {