diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 938e5b5a..703bf666 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -14,6 +14,9 @@ import { RefreshCwOff, Copy, CheckCheck, + Hash, + Plus, + Minus, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -97,6 +100,7 @@ const Sidebar = ({ const [showEnvVars, setShowEnvVars] = useState(false); const [showBearerToken, setShowBearerToken] = useState(false); const [showConfig, setShowConfig] = useState(false); + const [showHeaders, setShowHeaders] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); const [copiedServerEntry, setCopiedServerEntry] = useState(false); const [copiedServerFile, setCopiedServerFile] = useState(false); @@ -620,6 +624,109 @@ const Sidebar = ({ )} + {/* Headers */} +
+ + {showHeaders && ( +
+ {(() => { + const headersJson = config.MCP_CUSTOM_HEADERS?.value as string || "[]"; + let headers: Array<{ name: string; value: string }> = []; + + try { + headers = JSON.parse(headersJson); + } catch { + headers = []; + } + + const updateHeaders = (newHeaders: Array<{ name: string; value: string }>) => { + const newConfig = { ...config }; + newConfig.MCP_CUSTOM_HEADERS = { + ...config.MCP_CUSTOM_HEADERS, + value: JSON.stringify(newHeaders), + }; + setConfig(newConfig); + }; + + const addHeader = () => { + updateHeaders([...headers, { name: "", value: "" }]); + }; + + const removeHeader = (index: number) => { + const newHeaders = headers.filter((_, i) => i !== index); + updateHeaders(newHeaders); + }; + + const updateHeader = (index: number, field: "name" | "value", value: string) => { + const newHeaders = [...headers]; + newHeaders[index] = { ...newHeaders[index], [field]: value }; + updateHeaders(newHeaders); + }; + + return ( + <> + {headers.map((header, index) => ( +
+
+ + +
+ updateHeader(index, "name", e.target.value)} + data-testid={`header-name-${index}`} + className="font-mono" + /> + updateHeader(index, "value", e.target.value)} + data-testid={`header-value-${index}`} + className="font-mono" + type="password" + /> +
+ ))} + + + ); + })()} +
+ )} +
+
{connectionStatus === "connected" && (
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index d818bdbb..ea481f93 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -648,6 +648,144 @@ describe("Sidebar Environment Variables", () => { }); }); + describe("Headers Operations", () => { + const openHeadersSection = () => { + const button = screen.getByTestId("headers-button"); + fireEvent.click(button); + }; + + it("should add a new header", () => { + const setConfig = jest.fn(); + renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + + openHeadersSection(); + + const addButton = screen.getByTestId("add-header-button"); + fireEvent.click(addButton); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_CUSTOM_HEADERS: { + label: "Custom Headers", + description: "Custom headers for authentication with the MCP server (stored as JSON array)", + value: '[{"name":"","value":""}]', + is_session_item: true, + }, + }), + ); + }); + + it("should update header name", () => { + const setConfig = jest.fn(); + const config = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_CUSTOM_HEADERS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS, + value: '[{"name":"","value":""}]', + }, + }; + + renderSidebar({ config, setConfig }); + + openHeadersSection(); + + const headerNameInput = screen.getByTestId("header-name-0"); + + fireEvent.change(headerNameInput, { target: { value: "X-API-Key" } }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_CUSTOM_HEADERS: { + label: "Custom Headers", + description: "Custom headers for authentication with the MCP server (stored as JSON array)", + value: '[{"name":"X-API-Key","value":""}]', + is_session_item: true, + }, + }), + ); + }); + + it("should update header value", () => { + const setConfig = jest.fn(); + const config = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_CUSTOM_HEADERS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS, + value: '[{"name":"","value":""}]', + }, + }; + + renderSidebar({ config, setConfig }); + + openHeadersSection(); + + const headerValueInput = screen.getByTestId("header-value-0"); + + fireEvent.change(headerValueInput, { target: { value: "secret-key-123" } }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_CUSTOM_HEADERS: { + label: "Custom Headers", + description: "Custom headers for authentication with the MCP server (stored as JSON array)", + value: '[{"name":"","value":"secret-key-123"}]', + is_session_item: true, + }, + }), + ); + }); + + it("should remove a header", () => { + const setConfig = jest.fn(); + const config = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_CUSTOM_HEADERS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS, + value: '[{"name":"X-API-Key","value":"secret-key-123"}]', + }, + }; + + renderSidebar({ config, setConfig }); + + openHeadersSection(); + + const removeButton = screen.getByTestId("remove-header-0"); + fireEvent.click(removeButton); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_CUSTOM_HEADERS: { + label: "Custom Headers", + description: "Custom headers for authentication with the MCP server (stored as JSON array)", + value: "[]", + is_session_item: true, + }, + }), + ); + }); + + it("should handle multiple headers", () => { + const setConfig = jest.fn(); + const config = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_CUSTOM_HEADERS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS, + value: '[{"name":"X-API-Key","value":"key1"},{"name":"Authorization","value":"Bearer token"}]', + }, + }; + + renderSidebar({ config, setConfig }); + + openHeadersSection(); + + // Verify both headers are displayed + expect(screen.getByTestId("header-name-0")).toHaveValue("X-API-Key"); + expect(screen.getByTestId("header-value-0")).toHaveValue("key1"); + expect(screen.getByTestId("header-name-1")).toHaveValue("Authorization"); + expect(screen.getByTestId("header-value-1")).toHaveValue("Bearer token"); + }); + }); + describe("Copy Configuration Features", () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/client/src/lib/configurationTypes.ts b/client/src/lib/configurationTypes.ts index 263b2cf2..2fac4591 100644 --- a/client/src/lib/configurationTypes.ts +++ b/client/src/lib/configurationTypes.ts @@ -39,4 +39,9 @@ export type InspectorConfig = { * Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup. */ MCP_PROXY_AUTH_TOKEN: ConfigItem; + + /** + * Custom headers for authentication with the MCP server. JSON string containing array of {name, value} objects. + */ + MCP_CUSTOM_HEADERS: ConfigItem; }; diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 922f1943..e562fcbe 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -65,4 +65,10 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = { value: "", is_session_item: true, }, + MCP_CUSTOM_HEADERS: { + label: "Custom Headers", + description: "Custom headers for authentication with the MCP server (stored as JSON array)", + value: "[]", + is_session_item: true, + }, } as const; diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9009e698..6d508a9f 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -320,7 +320,32 @@ export function useConnection({ // Use manually provided bearer token if available, otherwise use OAuth tokens const token = bearerToken || (await serverAuthProvider.tokens())?.access_token; + + // Check for custom headers from configuration + const customHeadersJson = config.MCP_CUSTOM_HEADERS?.value as string; + let customHeaders: Array<{ name: string; value: string }> = []; + + try { + if (customHeadersJson) { + customHeaders = JSON.parse(customHeadersJson); + } + } catch (error) { + console.warn("Failed to parse custom headers:", error); + } + + if (customHeaders.length > 0) { + // Use custom headers from configuration + // Send headers with x-mcp-custom- prefix so server can identify them + customHeaders.forEach(({ name, value }) => { + if (name && value) { + const headerKey = `x-mcp-custom-${name.toLowerCase()}`; + headers[headerKey] = value; + } + }); + } + if (token) { + // Fallback to bearer token with header name const authHeaderName = headerName || "Authorization"; // Add custom header name as a special request header to let the server know which header to pass through @@ -346,6 +371,7 @@ export function useConnection({ | SSEClientTransportOptions; let mcpProxyServerUrl; + switch (transportType) { case "stdio": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`); diff --git a/server/src/index.ts b/server/src/index.ts index 971cf158..3d9d3b16 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -77,6 +77,20 @@ const getHttpHeaders = ( headers[customHeaderName] = value as string; } } + + // Handle multiple custom headers sent by the new client implementation + // Look for headers that start with 'x-mcp-custom-' prefix + Object.keys(req.headers).forEach((headerName) => { + if (headerName.startsWith('x-mcp-custom-')) { + // Extract the actual header name from x-mcp-custom-[actual-header-name] + const actualHeaderName = headerName.substring('x-mcp-custom-'.length); + const headerValue = req.headers[headerName]; + const value = Array.isArray(headerValue) ? headerValue[headerValue.length - 1] : headerValue; + if (value) { + headers[actualHeaderName] = value; + } + } + }); return headers; };