Merge branch 'main' into accurate-cli-version

This commit is contained in:
Ola Hungerford 2025-09-22 20:46:14 -07:00 • committed by GitHub
commit 32ea112051
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,10 +13,14 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { discoverScopes } from "../../auth";
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
@ -573,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: {
@ -625,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,
@ -648,9 +703,17 @@ describe("useConnection", () => {
});
test("preserves server Authorization header when proxy auth is configured", async () => {
const customHeaders: CustomHeaders = [
{
name: "Authorization",
value: "Bearer server-auth-token",
enabled: true,
},
];
const propsWithBothAuth = {
...defaultProps,
bearerToken: "server-auth-token",
customHeaders,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
@ -740,6 +803,190 @@ describe("useConnection", () => {
});
});
describe("Custom Headers", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the mock transport objects
mockSSETransport.url = undefined;
mockSSETransport.options = undefined;
mockStreamableHTTPTransport.url = undefined;
mockStreamableHTTPTransport.options = undefined;
});
test("sends multiple custom headers correctly", async () => {
const customHeaders: CustomHeaders = [
{ name: "Authorization", value: "Bearer token123", enabled: true },
{ name: "X-Tenant-ID", value: "acme-inc", enabled: true },
{ name: "X-Environment", value: "staging", enabled: true },
];
const propsWithCustomHeaders = {
...defaultProps,
customHeaders,
};
const { result } = renderHook(() =>
useConnection(propsWithCustomHeaders),
);
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?.headers).toBeDefined();
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("Authorization", "Bearer token123");
expect(headers).toHaveProperty("X-Tenant-ID", "acme-inc");
expect(headers).toHaveProperty("X-Environment", "staging");
expect(headers).toHaveProperty(
"x-custom-auth-headers",
JSON.stringify(["X-Tenant-ID", "X-Environment"]),
);
});
test("ignores disabled custom headers", async () => {
const customHeaders: CustomHeaders = [
{ name: "Authorization", value: "Bearer token123", enabled: true },
{ name: "X-Disabled", value: "should-not-appear", enabled: false },
{ name: "X-Enabled", value: "should-appear", enabled: true },
];
const propsWithCustomHeaders = {
...defaultProps,
customHeaders,
};
const { result } = renderHook(() =>
useConnection(propsWithCustomHeaders),
);
await act(async () => {
await result.current.connect();
});
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("Authorization", "Bearer token123");
expect(headers).toHaveProperty("X-Enabled", "should-appear");
expect(headers).not.toHaveProperty("X-Disabled");
});
test("handles migrated legacy auth via custom headers", async () => {
// Simulate what App.tsx would do - migrate legacy auth to custom headers
const customHeaders: CustomHeaders = [
{ name: "X-Custom-Auth", value: "legacy-token", enabled: true },
];
const propsWithMigratedAuth = {
...defaultProps,
customHeaders,
};
const { result } = renderHook(() => useConnection(propsWithMigratedAuth));
await act(async () => {
await result.current.connect();
});
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("X-Custom-Auth", "legacy-token");
expect(headers).toHaveProperty(
"x-custom-auth-headers",
JSON.stringify(["X-Custom-Auth"]),
);
});
test("uses OAuth token when no custom headers or legacy auth provided", async () => {
const propsWithoutAuth = {
...defaultProps,
};
const { result } = renderHook(() => useConnection(propsWithoutAuth));
await act(async () => {
await result.current.connect();
});
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("Authorization", "Bearer mock-token");
});
test("prioritizes custom headers over legacy auth", async () => {
const customHeaders: CustomHeaders = [
{ name: "Authorization", value: "Bearer custom-token", enabled: true },
];
const propsWithBothAuth = {
...defaultProps,
customHeaders,
bearerToken: "legacy-token",
headerName: "Authorization",
};
const { result } = renderHook(() => useConnection(propsWithBothAuth));
await act(async () => {
await result.current.connect();
});
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty("Authorization", "Bearer custom-token");
});
});
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();
@ -877,4 +1124,129 @@ describe("useConnection", () => {
});
});
});
describe("MCP_PROXY_FULL_ADDRESS Configuration", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the mock transport objects
mockSSETransport.url = undefined;
mockSSETransport.options = undefined;
mockStreamableHTTPTransport.url = undefined;
mockStreamableHTTPTransport.options = undefined;
});
test("sends proxyFullAddress query parameter for stdio transport when configured", async () => {
const propsWithProxyFullAddress = {
...defaultProps,
transportType: "stdio" as const,
command: "test-command",
args: "test-args",
env: {},
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_FULL_ADDRESS: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
value: "https://example.com/inspector/mcp_proxy",
},
},
};
const { result } = renderHook(() =>
useConnection(propsWithProxyFullAddress),
);
await act(async () => {
await result.current.connect();
});
// Check that the URL contains the proxyFullAddress parameter
expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
"https://example.com/inspector/mcp_proxy",
);
});
test("sends proxyFullAddress query parameter for sse transport when configured", async () => {
const propsWithProxyFullAddress = {
...defaultProps,
transportType: "sse" as const,
sseUrl: "http://localhost:8080",
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_FULL_ADDRESS: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
value: "https://example.com/inspector/mcp_proxy",
},
},
};
const { result } = renderHook(() =>
useConnection(propsWithProxyFullAddress),
);
await act(async () => {
await result.current.connect();
});
// Check that the URL contains the proxyFullAddress parameter
expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
"https://example.com/inspector/mcp_proxy",
);
});
test("does not send proxyFullAddress parameter when MCP_PROXY_FULL_ADDRESS is empty", async () => {
const propsWithEmptyProxy = {
...defaultProps,
transportType: "stdio" as const,
command: "test-command",
args: "test-args",
env: {},
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_FULL_ADDRESS: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
value: "",
},
},
};
const { result } = renderHook(() => useConnection(propsWithEmptyProxy));
await act(async () => {
await result.current.connect();
});
// Check that the URL does not contain the proxyFullAddress parameter
expect(
mockSSETransport.url?.searchParams.get("proxyFullAddress"),
).toBeNull();
});
test("does not send proxyFullAddress parameter for streamable-http transport", async () => {
const propsWithStreamableHttp = {
...defaultProps,
transportType: "streamable-http" as const,
sseUrl: "http://localhost:8080",
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_FULL_ADDRESS: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
value: "https://example.com/inspector/mcp_proxy",
},
},
};
const { result } = renderHook(() =>
useConnection(propsWithStreamableHttp),
);
await act(async () => {
await result.current.connect();
});
// Check that streamable-http transport doesn't get proxyFullAddress parameter
expect(
mockStreamableHTTPTransport.url?.searchParams.get("proxyFullAddress"),
).toBeNull();
});
});
});