Skip to content

Commit 752a415

Browse files
Merge pull request #509 from modelcontextprotocol/jerome/fix/proxy-token-mixup
fix: use X-MCP-Proxy-Auth header for proxy authentication
2 parents 65910eb + ef90c58 commit 752a415

File tree

7 files changed

+553
-21
lines changed

7 files changed

+553
-21
lines changed

client/src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,10 @@ const App = () => {
347347

348348
useEffect(() => {
349349
const headers: HeadersInit = {};
350-
const proxyAuthToken = getMCPProxyAuthToken(config);
350+
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
351+
getMCPProxyAuthToken(config);
351352
if (proxyAuthToken) {
352-
headers["Authorization"] = `Bearer ${proxyAuthToken}`;
353+
headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
353354
}
354355

355356
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
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+
});

0 commit comments

Comments
 (0)