Skip to content

Commit 5710318

Browse files
Merge branch 'main' into sebastian/patch-remove-addition-slash
2 parents 03e21bd + 1859e56 commit 5710318

20 files changed

+1100
-291
lines changed

.git-blame-ignore-revs

Whitespace-only changes.

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:

cli/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-cli",
3-
"version": "0.14.0",
3+
"version": "0.14.2",
44
"description": "CLI for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

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/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-client",
3-
"version": "0.14.0",
3+
"version": "0.14.2",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

client/src/App.tsx

Lines changed: 12 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,14 @@ const App = () => {
344346
}, [sseUrl]);
345347

346348
useEffect(() => {
347-
fetch(`${getMCPProxyAddress(config)}/config`)
349+
const headers: HeadersInit = {};
350+
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
351+
getMCPProxyAuthToken(config);
352+
if (proxyAuthToken) {
353+
headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
354+
}
355+
356+
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
348357
.then((response) => response.json())
349358
.then((data) => {
350359
setEnv(data.defaultEnvironment);
@@ -358,8 +367,7 @@ const App = () => {
358367
.catch((error) =>
359368
console.error("Error fetching default environment:", error),
360369
);
361-
// eslint-disable-next-line react-hooks/exhaustive-deps
362-
}, []);
370+
}, [config]);
363371

364372
useEffect(() => {
365373
rootsRef.current = roots;
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import App from "../App";
3+
import { DEFAULT_INSPECTOR_CONFIG } from "../lib/constants";
4+
import { InspectorConfig } from "../lib/configurationTypes";
5+
import * as configUtils from "../utils/configUtils";
6+
7+
// Mock auth dependencies first
8+
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
9+
auth: jest.fn(),
10+
}));
11+
12+
jest.mock("../lib/oauth-state-machine", () => ({
13+
OAuthStateMachine: jest.fn(),
14+
}));
15+
16+
jest.mock("../lib/auth", () => ({
17+
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
18+
tokens: jest.fn().mockResolvedValue(null),
19+
clear: jest.fn(),
20+
})),
21+
DebugInspectorOAuthClientProvider: jest.fn(),
22+
}));
23+
24+
// Mock the config utils
25+
jest.mock("../utils/configUtils", () => ({
26+
...jest.requireActual("../utils/configUtils"),
27+
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
28+
getMCPProxyAuthToken: jest.fn((config: InspectorConfig) => ({
29+
token: config.MCP_PROXY_AUTH_TOKEN.value,
30+
header: "X-MCP-Proxy-Auth",
31+
})),
32+
getInitialTransportType: jest.fn(() => "stdio"),
33+
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
34+
getInitialCommand: jest.fn(() => "mcp-server-everything"),
35+
getInitialArgs: jest.fn(() => ""),
36+
initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG),
37+
saveInspectorConfig: jest.fn(),
38+
}));
39+
40+
// Get references to the mocked functions
41+
const mockGetMCPProxyAuthToken = configUtils.getMCPProxyAuthToken as jest.Mock;
42+
const mockInitializeInspectorConfig =
43+
configUtils.initializeInspectorConfig as jest.Mock;
44+
45+
// Mock other dependencies
46+
jest.mock("../lib/hooks/useConnection", () => ({
47+
useConnection: () => ({
48+
connectionStatus: "disconnected",
49+
serverCapabilities: null,
50+
mcpClient: null,
51+
requestHistory: [],
52+
makeRequest: jest.fn(),
53+
sendNotification: jest.fn(),
54+
handleCompletion: jest.fn(),
55+
completionsSupported: false,
56+
connect: jest.fn(),
57+
disconnect: jest.fn(),
58+
}),
59+
}));
60+
61+
jest.mock("../lib/hooks/useDraggablePane", () => ({
62+
useDraggablePane: () => ({
63+
height: 300,
64+
handleDragStart: jest.fn(),
65+
}),
66+
useDraggableSidebar: () => ({
67+
width: 320,
68+
isDragging: false,
69+
handleDragStart: jest.fn(),
70+
}),
71+
}));
72+
73+
jest.mock("../components/Sidebar", () => ({
74+
__esModule: true,
75+
default: () => <div>Sidebar</div>,
76+
}));
77+
78+
// Mock fetch
79+
global.fetch = jest.fn();
80+
81+
describe("App - Config Endpoint", () => {
82+
beforeEach(() => {
83+
jest.clearAllMocks();
84+
(global.fetch as jest.Mock).mockResolvedValue({
85+
json: () =>
86+
Promise.resolve({
87+
defaultEnvironment: { TEST_ENV: "test" },
88+
defaultCommand: "test-command",
89+
defaultArgs: "test-args",
90+
}),
91+
});
92+
});
93+
94+
afterEach(() => {
95+
jest.clearAllMocks();
96+
97+
// Reset getMCPProxyAuthToken to default behavior
98+
mockGetMCPProxyAuthToken.mockImplementation((config: InspectorConfig) => ({
99+
token: config.MCP_PROXY_AUTH_TOKEN.value,
100+
header: "X-MCP-Proxy-Auth",
101+
}));
102+
});
103+
104+
test("sends X-MCP-Proxy-Auth header when fetching config with proxy auth token", async () => {
105+
const mockConfig = {
106+
...DEFAULT_INSPECTOR_CONFIG,
107+
MCP_PROXY_AUTH_TOKEN: {
108+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
109+
value: "test-proxy-token",
110+
},
111+
};
112+
113+
// Mock initializeInspectorConfig to return our test config
114+
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
115+
116+
render(<App />);
117+
118+
await waitFor(() => {
119+
expect(global.fetch).toHaveBeenCalledWith(
120+
"http://localhost:6277/config",
121+
{
122+
headers: {
123+
"X-MCP-Proxy-Auth": "Bearer test-proxy-token",
124+
},
125+
},
126+
);
127+
});
128+
});
129+
130+
test("does not send auth header when proxy auth token is empty", async () => {
131+
const mockConfig = {
132+
...DEFAULT_INSPECTOR_CONFIG,
133+
MCP_PROXY_AUTH_TOKEN: {
134+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
135+
value: "",
136+
},
137+
};
138+
139+
// Mock initializeInspectorConfig to return our test config
140+
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
141+
142+
render(<App />);
143+
144+
await waitFor(() => {
145+
expect(global.fetch).toHaveBeenCalledWith(
146+
"http://localhost:6277/config",
147+
{
148+
headers: {},
149+
},
150+
);
151+
});
152+
});
153+
154+
test("uses custom header name if getMCPProxyAuthToken returns different header", async () => {
155+
const mockConfig = {
156+
...DEFAULT_INSPECTOR_CONFIG,
157+
MCP_PROXY_AUTH_TOKEN: {
158+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
159+
value: "test-proxy-token",
160+
},
161+
};
162+
163+
// Mock to return a custom header name
164+
mockGetMCPProxyAuthToken.mockReturnValue({
165+
token: "test-proxy-token",
166+
header: "X-Custom-Auth",
167+
});
168+
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
169+
170+
render(<App />);
171+
172+
await waitFor(() => {
173+
expect(global.fetch).toHaveBeenCalledWith(
174+
"http://localhost:6277/config",
175+
{
176+
headers: {
177+
"X-Custom-Auth": "Bearer test-proxy-token",
178+
},
179+
},
180+
);
181+
});
182+
});
183+
184+
test("config endpoint response updates app state", async () => {
185+
const mockConfig = {
186+
...DEFAULT_INSPECTOR_CONFIG,
187+
MCP_PROXY_AUTH_TOKEN: {
188+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
189+
value: "test-proxy-token",
190+
},
191+
};
192+
193+
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
194+
195+
render(<App />);
196+
197+
await waitFor(() => {
198+
expect(global.fetch).toHaveBeenCalledTimes(1);
199+
});
200+
201+
// Verify the fetch was called with correct parameters
202+
expect(global.fetch).toHaveBeenCalledWith(
203+
"http://localhost:6277/config",
204+
expect.objectContaining({
205+
headers: expect.objectContaining({
206+
"X-MCP-Proxy-Auth": "Bearer test-proxy-token",
207+
}),
208+
}),
209+
);
210+
});
211+
212+
test("handles config endpoint errors gracefully", async () => {
213+
const mockConfig = {
214+
...DEFAULT_INSPECTOR_CONFIG,
215+
MCP_PROXY_AUTH_TOKEN: {
216+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
217+
value: "test-proxy-token",
218+
},
219+
};
220+
221+
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
222+
223+
// Mock fetch to reject
224+
(global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));
225+
226+
// Spy on console.error
227+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
228+
229+
render(<App />);
230+
231+
await waitFor(() => {
232+
expect(consoleErrorSpy).toHaveBeenCalledWith(
233+
"Error fetching default environment:",
234+
expect.any(Error),
235+
);
236+
});
237+
238+
consoleErrorSpy.mockRestore();
239+
});
240+
});

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:

0 commit comments

Comments
 (0)