Skip to content

Commit 50df0e1

Browse files
authored
Merge commit from fork
Address inspector security vulnerabilities
2 parents 03edbb0 + 4d336af commit 50df0e1

File tree

10 files changed

+276
-45
lines changed

10 files changed

+276
-45
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,53 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
137137

138138
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.
139139

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+
140187
### Configuration
141188

142189
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:

client/bin/start.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ async function main() {
100100

101101
if (serverOk) {
102102
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) {
104106
open(`http://127.0.0.1:${CLIENT_PORT}`);
105107
}
106108
await spawnPromise("node", [inspectorClientPath], {

client/src/App.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ import ToolsTab from "./components/ToolsTab";
6363
import { InspectorConfig } from "./lib/configurationTypes";
6464
import {
6565
getMCPProxyAddress,
66+
getMCPProxyAuthToken,
6667
getInitialSseUrl,
6768
getInitialTransportType,
6869
getInitialCommand,
6970
getInitialArgs,
7071
initializeInspectorConfig,
72+
saveInspectorConfig,
7173
} from "./utils/configUtils";
7274

7375
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
@@ -226,7 +228,7 @@ const App = () => {
226228
}, [headerName]);
227229

228230
useEffect(() => {
229-
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
231+
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
230232
}, [config]);
231233

232234
// Auto-connect to previously saved serverURL after OAuth callback
@@ -344,7 +346,13 @@ const App = () => {
344346
}, [sseUrl]);
345347

346348
useEffect(() => {
347-
fetch(`${getMCPProxyAddress(config)}/config`)
349+
const headers: HeadersInit = {};
350+
const proxyAuthToken = getMCPProxyAuthToken(config);
351+
if (proxyAuthToken) {
352+
headers['Authorization'] = `Bearer ${proxyAuthToken}`;
353+
}
354+
355+
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
348356
.then((response) => response.json())
349357
.then((data) => {
350358
setEnv(data.defaultEnvironment);
@@ -358,8 +366,7 @@ const App = () => {
358366
.catch((error) =>
359367
console.error("Error fetching default environment:", error),
360368
);
361-
// eslint-disable-next-line react-hooks/exhaustive-deps
362-
}, []);
369+
}, [config]);
363370

364371
useEffect(() => {
365372
rootsRef.current = roots;

client/src/components/Sidebar.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,13 @@ const Sidebar = ({
666666
switch (connectionStatus) {
667667
case "connected":
668668
return "Connected";
669-
case "error":
670-
return "Connection Error, is your MCP server running?";
669+
case "error": {
670+
const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value;
671+
if (!hasProxyToken) {
672+
return "Connection Error - Did you add the proxy session token in Configuration?";
673+
}
674+
return "Connection Error - Check if your MCP server is running and proxy token is correct";
675+
}
671676
case "error-connecting-to-proxy":
672677
return "Error Connecting to MCP Inspector Proxy - Check Console logs";
673678
default:

client/src/lib/configurationTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type ConfigItem = {
22
label: string;
33
description: string;
44
value: string | number | boolean;
5+
is_session_item: boolean;
56
};
67

78
/**
@@ -33,4 +34,9 @@ export type InspectorConfig = {
3334
* The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
3435
*/
3536
MCP_PROXY_FULL_ADDRESS: ConfigItem;
37+
38+
/**
39+
* Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
40+
*/
41+
MCP_PROXY_AUTH_TOKEN: ConfigItem;
3642
};

client/src/lib/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,33 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
3636
label: "Request Timeout",
3737
description: "Timeout for requests to the MCP server (ms)",
3838
value: 10000,
39+
is_session_item: false,
3940
},
4041
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
4142
label: "Reset Timeout on Progress",
4243
description: "Reset timeout on progress notifications",
4344
value: true,
45+
is_session_item: false,
4446
},
4547
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
4648
label: "Maximum Total Timeout",
4749
description:
4850
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
4951
value: 60000,
52+
is_session_item: false,
5053
},
5154
MCP_PROXY_FULL_ADDRESS: {
5255
label: "Inspector Proxy Address",
5356
description:
5457
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
5558
value: "",
59+
is_session_item: false,
60+
},
61+
MCP_PROXY_AUTH_TOKEN: {
62+
label: "Proxy Session Token",
63+
description:
64+
"Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)",
65+
value: "",
66+
is_session_item: true,
5667
},
5768
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
getMCPProxyAddress,
4343
getMCPServerRequestMaxTotalTimeout,
4444
resetRequestTimeoutOnProgress,
45+
getMCPProxyAuthToken,
4546
} from "@/utils/configUtils";
4647
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
4748
import { InspectorConfig } from "../configurationTypes";
@@ -242,7 +243,12 @@ export function useConnection({
242243
const checkProxyHealth = async () => {
243244
try {
244245
const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);
245-
const proxyHealthResponse = await fetch(proxyHealthUrl);
246+
const proxyAuthToken = getMCPProxyAuthToken(config);
247+
const headers: HeadersInit = {};
248+
if (proxyAuthToken) {
249+
headers['Authorization'] = `Bearer ${proxyAuthToken}`;
250+
}
251+
const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });
246252
const proxyHealth = await proxyHealthResponse.json();
247253
if (proxyHealth?.status !== "ok") {
248254
throw new Error("MCP Proxy Server is not healthy");
@@ -261,6 +267,13 @@ export function useConnection({
261267
);
262268
};
263269

270+
const isProxyAuthError = (error: unknown): boolean => {
271+
return (
272+
error instanceof Error &&
273+
error.message.includes("Authentication required. Use the session token")
274+
);
275+
};
276+
264277
const handleAuthError = async (error: unknown) => {
265278
if (is401Error(error)) {
266279
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
@@ -318,6 +331,13 @@ export function useConnection({
318331
}
319332
}
320333

334+
// Add proxy authentication
335+
const proxyAuthToken = getMCPProxyAuthToken(config);
336+
const proxyHeaders: HeadersInit = {};
337+
if (proxyAuthToken) {
338+
proxyHeaders['Authorization'] = `Bearer ${proxyAuthToken}`;
339+
}
340+
321341
// Create appropriate transport
322342
let transportOptions:
323343
| StreamableHTTPClientTransportOptions
@@ -336,10 +356,10 @@ export function useConnection({
336356
fetch: (
337357
url: string | URL | globalThis.Request,
338358
init: RequestInit | undefined,
339-
) => fetch(url, { ...init, headers }),
359+
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }),
340360
},
341361
requestInit: {
342-
headers,
362+
headers: { ...headers, ...proxyHeaders },
343363
},
344364
};
345365
break;
@@ -352,10 +372,10 @@ export function useConnection({
352372
fetch: (
353373
url: string | URL | globalThis.Request,
354374
init: RequestInit | undefined,
355-
) => fetch(url, { ...init, headers }),
375+
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }),
356376
},
357377
requestInit: {
358-
headers,
378+
headers: { ...headers, ...proxyHeaders },
359379
},
360380
};
361381
break;
@@ -368,10 +388,10 @@ export function useConnection({
368388
fetch: (
369389
url: string | URL | globalThis.Request,
370390
init: RequestInit | undefined,
371-
) => fetch(url, { ...init, headers }),
391+
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }),
372392
},
373393
requestInit: {
374-
headers,
394+
headers: { ...headers, ...proxyHeaders },
375395
},
376396
// TODO these should be configurable...
377397
reconnectionOptions: {
@@ -447,6 +467,17 @@ export function useConnection({
447467
error,
448468
);
449469

470+
// Check if it's a proxy auth error
471+
if (isProxyAuthError(error)) {
472+
toast({
473+
title: "Proxy Authentication Required",
474+
description: "Please enter the session token from the proxy server console in the Configuration settings.",
475+
variant: "destructive",
476+
});
477+
setConnectionStatus("error");
478+
return;
479+
}
480+
450481
const shouldRetry = await handleAuthError(error);
451482
if (shouldRetry) {
452483
return connect(undefined, retryCount + 1);

client/src/utils/configUtils.ts

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export const getMCPServerRequestMaxTotalTimeout = (
2828
return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number;
2929
};
3030

31+
export const getMCPProxyAuthToken = (config: InspectorConfig): string => {
32+
return config.MCP_PROXY_AUTH_TOKEN.value as string;
33+
};
34+
3135
const getSearchParam = (key: string): string | null => {
3236
try {
3337
const url = new URL(window.location.href);
@@ -100,27 +104,67 @@ export const getConfigOverridesFromQueryParams = (
100104
export const initializeInspectorConfig = (
101105
localStorageKey: string,
102106
): InspectorConfig => {
103-
const savedConfig = localStorage.getItem(localStorageKey);
104-
let baseConfig: InspectorConfig;
105-
if (savedConfig) {
106-
// merge default config with saved config
107-
const mergedConfig = {
108-
...DEFAULT_INSPECTOR_CONFIG,
109-
...JSON.parse(savedConfig),
110-
} as InspectorConfig;
111-
112-
// update description of keys to match the new description (in case of any updates to the default config description)
113-
for (const [key, value] of Object.entries(mergedConfig)) {
114-
mergedConfig[key as keyof InspectorConfig] = {
115-
...value,
116-
label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
117-
};
118-
}
119-
baseConfig = mergedConfig;
120-
} else {
121-
baseConfig = DEFAULT_INSPECTOR_CONFIG;
107+
// Read persistent config from localStorage
108+
const savedPersistentConfig = localStorage.getItem(localStorageKey);
109+
// Read ephemeral config from sessionStorage
110+
const savedEphemeralConfig = sessionStorage.getItem(
111+
`${localStorageKey}_ephemeral`,
112+
);
113+
114+
// Start with default config
115+
let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG };
116+
117+
// Apply saved persistent config
118+
if (savedPersistentConfig) {
119+
const parsedPersistentConfig = JSON.parse(savedPersistentConfig);
120+
baseConfig = { ...baseConfig, ...parsedPersistentConfig };
121+
}
122+
123+
// Apply saved ephemeral config
124+
if (savedEphemeralConfig) {
125+
const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig);
126+
baseConfig = { ...baseConfig, ...parsedEphemeralConfig };
127+
}
128+
129+
// Ensure all config items have the latest labels/descriptions from defaults
130+
for (const [key, value] of Object.entries(baseConfig)) {
131+
baseConfig[key as keyof InspectorConfig] = {
132+
...value,
133+
label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
134+
description:
135+
DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].description,
136+
is_session_item:
137+
DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].is_session_item,
138+
};
122139
}
140+
123141
// Apply query param overrides
124142
const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG);
125143
return { ...baseConfig, ...overrides };
126144
};
145+
146+
export const saveInspectorConfig = (
147+
localStorageKey: string,
148+
config: InspectorConfig,
149+
): void => {
150+
const persistentConfig: Partial<InspectorConfig> = {};
151+
const ephemeralConfig: Partial<InspectorConfig> = {};
152+
153+
// Split config based on is_session_item flag
154+
for (const [key, value] of Object.entries(config)) {
155+
if (value.is_session_item) {
156+
ephemeralConfig[key as keyof InspectorConfig] = value;
157+
} else {
158+
persistentConfig[key as keyof InspectorConfig] = value;
159+
}
160+
}
161+
162+
// Save persistent config to localStorage
163+
localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig));
164+
165+
// Save ephemeral config to sessionStorage
166+
sessionStorage.setItem(
167+
`${localStorageKey}_ephemeral`,
168+
JSON.stringify(ephemeralConfig),
169+
);
170+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"description": "Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

0 commit comments

Comments
 (0)