Skip to content

Commit cf9acf6

Browse files
authored
Merge pull request modelcontextprotocol#810 from cliffhall/no-proxy-for-http-transports
Allow bypassing proxy with Connection Type dropdown
2 parents 01caa53 + 6b7c752 commit cf9acf6

File tree

5 files changed

+353
-107
lines changed

5 files changed

+353
-107
lines changed

client/src/App.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ const App = () => {
107107
const [transportType, setTransportType] = useState<
108108
"stdio" | "sse" | "streamable-http"
109109
>(getInitialTransportType);
110+
const [connectionType, setConnectionType] = useState<"direct" | "proxy">(
111+
() => {
112+
return (
113+
(localStorage.getItem("lastConnectionType") as "direct" | "proxy") ||
114+
"proxy"
115+
);
116+
},
117+
);
110118
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
111119
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
112120
const [roots, setRoots] = useState<Root[]>([]);
@@ -254,6 +262,7 @@ const App = () => {
254262
oauthClientId,
255263
oauthScope,
256264
config,
265+
connectionType,
257266
onNotification: (notification) => {
258267
setNotifications((prev) => [...prev, notification as ServerNotification]);
259268
},
@@ -338,6 +347,10 @@ const App = () => {
338347
localStorage.setItem("lastTransportType", transportType);
339348
}, [transportType]);
340349

350+
useEffect(() => {
351+
localStorage.setItem("lastConnectionType", connectionType);
352+
}, [connectionType]);
353+
341354
useEffect(() => {
342355
if (bearerToken) {
343356
localStorage.setItem("lastBearerToken", bearerToken);
@@ -882,6 +895,8 @@ const App = () => {
882895
logLevel={logLevel}
883896
sendLogLevelRequest={sendLogLevelRequest}
884897
loggingSupported={!!serverCapabilities?.logging || false}
898+
connectionType={connectionType}
899+
setConnectionType={setConnectionType}
885900
/>
886901
<div
887902
onMouseDown={handleSidebarDragStart}

client/src/components/Sidebar.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface SidebarProps {
6767
loggingSupported: boolean;
6868
config: InspectorConfig;
6969
setConfig: (config: InspectorConfig) => void;
70+
connectionType: "direct" | "proxy";
71+
setConnectionType: (type: "direct" | "proxy") => void;
7072
}
7173

7274
const Sidebar = ({
@@ -94,6 +96,8 @@ const Sidebar = ({
9496
loggingSupported,
9597
config,
9698
setConfig,
99+
connectionType,
100+
setConnectionType,
97101
}: SidebarProps) => {
98102
const [theme, setTheme] = useTheme();
99103
const [showEnvVars, setShowEnvVars] = useState(false);
@@ -104,6 +108,8 @@ const Sidebar = ({
104108
const [copiedServerFile, setCopiedServerFile] = useState(false);
105109
const { toast } = useToast();
106110

111+
const connectionTypeTip =
112+
"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
107113
// Reusable error reporter for copy actions
108114
const reportError = useCallback(
109115
(error: unknown) => {
@@ -313,6 +319,35 @@ const Sidebar = ({
313319
/>
314320
)}
315321
</div>
322+
323+
{/* Connection Type switch - only visible for non-STDIO transport types */}
324+
<Tooltip>
325+
<TooltipTrigger asChild>
326+
<div className="space-y-2">
327+
<label
328+
className="text-sm font-medium"
329+
htmlFor="connection-type-select"
330+
>
331+
Connection Type
332+
</label>
333+
<Select
334+
value={connectionType}
335+
onValueChange={(value: "direct" | "proxy") =>
336+
setConnectionType(value)
337+
}
338+
>
339+
<SelectTrigger id="connection-type-select">
340+
<SelectValue placeholder="Select connection type" />
341+
</SelectTrigger>
342+
<SelectContent>
343+
<SelectItem value="proxy">Via Proxy</SelectItem>
344+
<SelectItem value="direct">Direct</SelectItem>
345+
</SelectContent>
346+
</Select>
347+
</div>
348+
</TooltipTrigger>
349+
<TooltipContent>{connectionTypeTip}</TooltipContent>
350+
</Tooltip>
316351
</>
317352
)}
318353

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ describe("Sidebar", () => {
5959
loggingSupported: true,
6060
config: DEFAULT_INSPECTOR_CONFIG,
6161
setConfig: jest.fn(),
62+
connectionType: "proxy" as const,
63+
setConnectionType: jest.fn(),
6264
};
6365

6466
const renderSidebar = (props = {}) => {

client/src/lib/hooks/__tests__/useConnection.test.tsx

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { CustomHeaders } from "../../types/customHeaders";
1818
// Mock fetch
1919
global.fetch = jest.fn().mockResolvedValue({
2020
json: () => Promise.resolve({ status: "ok" }),
21+
headers: {
22+
get: jest.fn().mockReturnValue(null),
23+
},
2124
});
2225

2326
// Mock the SDK dependencies
@@ -574,9 +577,10 @@ describe("useConnection", () => {
574577
mockStreamableHTTPTransport.options = undefined;
575578
});
576579

577-
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
580+
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured for proxy connectionType", async () => {
578581
const propsWithProxyAuth = {
579582
...defaultProps,
583+
connectionType: "proxy" as const,
580584
config: {
581585
...DEFAULT_INSPECTOR_CONFIG,
582586
MCP_PROXY_AUTH_TOKEN: {
@@ -626,6 +630,56 @@ describe("useConnection", () => {
626630
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
627631
});
628632

633+
test("does NOT send X-MCP-Proxy-Auth header when proxy auth token is configured for direct connectionType", async () => {
634+
const propsWithProxyAuth = {
635+
...defaultProps,
636+
connectionType: "direct" as const,
637+
config: {
638+
...DEFAULT_INSPECTOR_CONFIG,
639+
MCP_PROXY_AUTH_TOKEN: {
640+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
641+
value: "test-proxy-token",
642+
},
643+
},
644+
};
645+
646+
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
647+
648+
await act(async () => {
649+
await result.current.connect();
650+
});
651+
652+
// Check that the transport was created with the correct headers
653+
expect(mockSSETransport.options).toBeDefined();
654+
expect(mockSSETransport.options?.requestInit).toBeDefined();
655+
656+
// Verify that X-MCP-Proxy-Auth header is NOT present for direct connections
657+
expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
658+
"X-MCP-Proxy-Auth",
659+
);
660+
expect(mockSSETransport?.options?.fetch).toBeDefined();
661+
662+
// Verify the fetch function does NOT include the proxy auth header
663+
const mockFetch = mockSSETransport.options?.fetch;
664+
const testUrl = "http://test.com";
665+
await mockFetch?.(testUrl, {
666+
headers: {
667+
Accept: "text/event-stream",
668+
},
669+
cache: "no-store",
670+
mode: "cors",
671+
signal: new AbortController().signal,
672+
redirect: "follow",
673+
credentials: "include",
674+
});
675+
676+
expect(global.fetch).toHaveBeenCalledTimes(1);
677+
expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(testUrl);
678+
expect(
679+
(global.fetch as jest.Mock).mock.calls[0][1].headers,
680+
).not.toHaveProperty("X-MCP-Proxy-Auth");
681+
});
682+
629683
test("does NOT send Authorization header for proxy auth", async () => {
630684
const propsWithProxyAuth = {
631685
...defaultProps,
@@ -882,6 +936,57 @@ describe("useConnection", () => {
882936
});
883937
});
884938

939+
describe("Connection URL Verification", () => {
940+
beforeEach(() => {
941+
jest.clearAllMocks();
942+
// Reset the mock transport objects
943+
mockSSETransport.url = undefined;
944+
mockSSETransport.options = undefined;
945+
mockStreamableHTTPTransport.url = undefined;
946+
mockStreamableHTTPTransport.options = undefined;
947+
});
948+
949+
test("uses server URL directly when connectionType is 'direct'", async () => {
950+
const directProps = {
951+
...defaultProps,
952+
connectionType: "direct" as const,
953+
};
954+
955+
const { result } = renderHook(() => useConnection(directProps));
956+
957+
await act(async () => {
958+
await result.current.connect();
959+
});
960+
961+
// Verify the transport was created with the direct server URL
962+
expect(mockSSETransport.url).toBeDefined();
963+
expect(mockSSETransport.url?.toString()).toBe("http://localhost:8080/");
964+
});
965+
966+
test("uses proxy server URL when connectionType is 'proxy'", async () => {
967+
const proxyProps = {
968+
...defaultProps,
969+
connectionType: "proxy" as const,
970+
};
971+
972+
const { result } = renderHook(() => useConnection(proxyProps));
973+
974+
await act(async () => {
975+
await result.current.connect();
976+
});
977+
978+
// Verify the transport was created with a proxy server URL
979+
expect(mockSSETransport.url).toBeDefined();
980+
expect(mockSSETransport.url?.pathname).toBe("/sse");
981+
expect(mockSSETransport.url?.searchParams.get("url")).toBe(
982+
"http://localhost:8080",
983+
);
984+
expect(mockSSETransport.url?.searchParams.get("transportType")).toBe(
985+
"sse",
986+
);
987+
});
988+
});
989+
885990
describe("OAuth Error Handling with Scope Discovery", () => {
886991
beforeEach(() => {
887992
jest.clearAllMocks();

0 commit comments

Comments
 (0)