Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ const App = () => {
const [transportType, setTransportType] = useState<
"stdio" | "sse" | "streamable-http"
>(getInitialTransportType);
const [connectionType, setConnectionType] = useState<"direct" | "proxy">(
() => {
return (
(localStorage.getItem("lastConnectionType") as "direct" | "proxy") ||
"proxy"
);
},
);
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [roots, setRoots] = useState<Root[]>([]);
Expand Down Expand Up @@ -254,6 +262,7 @@ const App = () => {
oauthClientId,
oauthScope,
config,
connectionType,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
Expand Down Expand Up @@ -338,6 +347,10 @@ const App = () => {
localStorage.setItem("lastTransportType", transportType);
}, [transportType]);

useEffect(() => {
localStorage.setItem("lastConnectionType", connectionType);
}, [connectionType]);

useEffect(() => {
if (bearerToken) {
localStorage.setItem("lastBearerToken", bearerToken);
Expand Down Expand Up @@ -882,6 +895,8 @@ const App = () => {
logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false}
connectionType={connectionType}
setConnectionType={setConnectionType}
/>
<div
onMouseDown={handleSidebarDragStart}
Expand Down
35 changes: 35 additions & 0 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ interface SidebarProps {
loggingSupported: boolean;
config: InspectorConfig;
setConfig: (config: InspectorConfig) => void;
connectionType: "direct" | "proxy";
setConnectionType: (type: "direct" | "proxy") => void;
}

const Sidebar = ({
Expand Down Expand Up @@ -94,6 +96,8 @@ const Sidebar = ({
loggingSupported,
config,
setConfig,
connectionType,
setConnectionType,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
Expand All @@ -104,6 +108,8 @@ const Sidebar = ({
const [copiedServerFile, setCopiedServerFile] = useState(false);
const { toast } = useToast();

const connectionTypeTip =
"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
// Reusable error reporter for copy actions
const reportError = useCallback(
(error: unknown) => {
Expand Down Expand Up @@ -313,6 +319,35 @@ const Sidebar = ({
/>
)}
</div>

{/* Connection Type switch - only visible for non-STDIO transport types */}
<Tooltip>
<TooltipTrigger asChild>
<div className="space-y-2">
<label
className="text-sm font-medium"
htmlFor="connection-type-select"
>
Connection Type
</label>
<Select
value={connectionType}
onValueChange={(value: "direct" | "proxy") =>
setConnectionType(value)
}
>
<SelectTrigger id="connection-type-select">
<SelectValue placeholder="Select connection type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="proxy">Via Proxy</SelectItem>
<SelectItem value="direct">Direct</SelectItem>
</SelectContent>
</Select>
</div>
</TooltipTrigger>
<TooltipContent>{connectionTypeTip}</TooltipContent>
</Tooltip>
</>
)}

Expand Down
2 changes: 2 additions & 0 deletions client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ describe("Sidebar", () => {
loggingSupported: true,
config: DEFAULT_INSPECTOR_CONFIG,
setConfig: jest.fn(),
connectionType: "proxy" as const,
setConnectionType: jest.fn(),
};

const renderSidebar = (props = {}) => {
Expand Down
107 changes: 106 additions & 1 deletion client/src/lib/hooks/__tests__/useConnection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { CustomHeaders } from "../../types/customHeaders";
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ status: "ok" }),
headers: {
get: jest.fn().mockReturnValue(null),
},
});

// Mock the SDK dependencies
Expand Down Expand Up @@ -574,9 +577,10 @@ describe("useConnection", () => {
mockStreamableHTTPTransport.options = undefined;
});

test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured for proxy connectionType", async () => {
const propsWithProxyAuth = {
...defaultProps,
connectionType: "proxy" as const,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
Expand Down Expand Up @@ -626,6 +630,56 @@ describe("useConnection", () => {
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
});

test("does NOT send X-MCP-Proxy-Auth header when proxy auth token is configured for direct connectionType", async () => {
const propsWithProxyAuth = {
...defaultProps,
connectionType: "direct" as const,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
},
};

const { result } = renderHook(() => useConnection(propsWithProxyAuth));

await act(async () => {
await result.current.connect();
});

// Check that the transport was created with the correct headers
expect(mockSSETransport.options).toBeDefined();
expect(mockSSETransport.options?.requestInit).toBeDefined();

// Verify that X-MCP-Proxy-Auth header is NOT present for direct connections
expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
"X-MCP-Proxy-Auth",
);
expect(mockSSETransport?.options?.fetch).toBeDefined();

// Verify the fetch function does NOT include the proxy auth header
const mockFetch = mockSSETransport.options?.fetch;
const testUrl = "http://test.com";
await mockFetch?.(testUrl, {
headers: {
Accept: "text/event-stream",
},
cache: "no-store",
mode: "cors",
signal: new AbortController().signal,
redirect: "follow",
credentials: "include",
});

expect(global.fetch).toHaveBeenCalledTimes(1);
expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(testUrl);
expect(
(global.fetch as jest.Mock).mock.calls[0][1].headers,
).not.toHaveProperty("X-MCP-Proxy-Auth");
});

test("does NOT send Authorization header for proxy auth", async () => {
const propsWithProxyAuth = {
...defaultProps,
Expand Down Expand Up @@ -882,6 +936,57 @@ describe("useConnection", () => {
});
});

describe("Connection URL Verification", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the mock transport objects
mockSSETransport.url = undefined;
mockSSETransport.options = undefined;
mockStreamableHTTPTransport.url = undefined;
mockStreamableHTTPTransport.options = undefined;
});

test("uses server URL directly when connectionType is 'direct'", async () => {
const directProps = {
...defaultProps,
connectionType: "direct" as const,
};

const { result } = renderHook(() => useConnection(directProps));

await act(async () => {
await result.current.connect();
});

// Verify the transport was created with the direct server URL
expect(mockSSETransport.url).toBeDefined();
expect(mockSSETransport.url?.toString()).toBe("http://localhost:8080/");
});

test("uses proxy server URL when connectionType is 'proxy'", async () => {
const proxyProps = {
...defaultProps,
connectionType: "proxy" as const,
};

const { result } = renderHook(() => useConnection(proxyProps));

await act(async () => {
await result.current.connect();
});

// Verify the transport was created with a proxy server URL
expect(mockSSETransport.url).toBeDefined();
expect(mockSSETransport.url?.pathname).toBe("/sse");
expect(mockSSETransport.url?.searchParams.get("url")).toBe(
"http://localhost:8080",
);
expect(mockSSETransport.url?.searchParams.get("transportType")).toBe(
"sse",
);
});
});

describe("OAuth Error Handling with Scope Discovery", () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
Loading
Loading