From 91633de80f2b610179f9630b8cfdb9f1f40b4e7d Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 5 Apr 2025 12:46:25 +0300 Subject: [PATCH 1/8] set header name --- README.md | 2 +- client/src/App.tsx | 12 ++++++++++++ client/src/components/Sidebar.tsx | 12 ++++++++++++ client/src/lib/hooks/useConnection.ts | 5 ++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c43f943..7e27fbe 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For more details on ways to use the inspector, see the [Inspector section of the ### Authentication -The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. +The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name. ### Security Considerations diff --git a/client/src/App.tsx b/client/src/App.tsx index 7564544..7f29a31 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -108,8 +108,13 @@ const App = () => { const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; }); + + const [headerName, setHeaderName] = useState(() => { + return localStorage.getItem("lastHeaderName") || "Authorization"; + }); const [pendingSampleRequests, setPendingSampleRequests] = useState< + Array< PendingRequest & { resolve: (result: CreateMessageResult) => void; @@ -161,6 +166,7 @@ const App = () => { sseUrl, env, bearerToken, + headerName, proxyServerUrl: getMCPProxyAddress(config), requestTimeout: getMCPServerRequestTimeout(config), onNotification: (notification) => { @@ -201,6 +207,10 @@ const App = () => { localStorage.setItem("lastBearerToken", bearerToken); }, [bearerToken]); + useEffect(() => { + localStorage.setItem("lastHeaderName", headerName); + }, [headerName]); + useEffect(() => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); @@ -476,6 +486,8 @@ const App = () => { setConfig={setConfig} bearerToken={bearerToken} setBearerToken={setBearerToken} + headerName={headerName} + setHeaderName={setHeaderName} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 19f8020..64ffbda 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -51,6 +51,8 @@ interface SidebarProps { setEnv: (env: Record) => void; bearerToken: string; setBearerToken: (token: string) => void; + headerName?: string; + setHeaderName?: (name: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; @@ -75,6 +77,8 @@ const Sidebar = ({ setEnv, bearerToken, setBearerToken, + headerName, + setHeaderName, onConnect, onDisconnect, stdErrNotifications, @@ -167,6 +171,14 @@ const Sidebar = ({ {showBearerToken && (
+ + setHeaderName && setHeaderName(e.target.value)} + className="font-mono" + value={headerName} + /> ; proxyServerUrl: string; bearerToken?: string; + headerName?: string; requestTimeout?: number; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -64,6 +65,7 @@ export function useConnection({ env, proxyServerUrl, bearerToken, + headerName, requestTimeout, onNotification, onStdErrNotification, @@ -274,7 +276,8 @@ export function useConnection({ // Use manually provided bearer token if available, otherwise use OAuth tokens const token = bearerToken || (await authProvider.tokens())?.access_token; if (token) { - headers["Authorization"] = `Bearer ${token}`; + const authHeaderName = headerName || "Authorization"; + headers[authHeaderName] = `${token}`; } const clientTransport = new SSEClientTransport(mcpProxyServerUrl, { From eb9b2dd027c8ab62799fe3f7dc41a701af1da432 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 5 Apr 2025 12:57:49 +0300 Subject: [PATCH 2/8] remove default value --- client/src/components/Sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 64ffbda..ea0716b 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -174,7 +174,6 @@ const Sidebar = ({ setHeaderName && setHeaderName(e.target.value)} className="font-mono" value={headerName} From a98db777c555c7a7ba4b0a30ede1bb3b26b8dc7a Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Thu, 10 Apr 2025 20:34:40 +0300 Subject: [PATCH 3/8] add auth tests --- client/src/App.tsx | 3 +- client/src/components/Sidebar.tsx | 7 +- .../src/components/__tests__/Sidebar.test.tsx | 152 ++++++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 7f29a31..32931b8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -108,13 +108,12 @@ const App = () => { const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; }); - + const [headerName, setHeaderName] = useState(() => { return localStorage.getItem("lastHeaderName") || "Authorization"; }); const [pendingSampleRequests, setPendingSampleRequests] = useState< - Array< PendingRequest & { resolve: (result: CreateMessageResult) => void; diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index ea0716b..e555ac7 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -161,6 +161,7 @@ const Sidebar = ({ variant="outline" onClick={() => setShowBearerToken(!showBearerToken)} className="flex items-center w-full" + data-testid="auth-button" > {showBearerToken ? ( @@ -174,7 +175,10 @@ const Sidebar = ({ setHeaderName && setHeaderName(e.target.value)} + onChange={(e) => + setHeaderName && setHeaderName(e.target.value) + } + data-testid="header-input" className="font-mono" value={headerName} /> @@ -183,6 +187,7 @@ const Sidebar = ({ placeholder="Bearer Token" value={bearerToken} onChange={(e) => setBearerToken(e.target.value)} + data-testid="bearer-token-input" className="font-mono" type="password" /> diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index e527f6d..8ac8494 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -1,4 +1,5 @@ import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; import { describe, it, beforeEach, jest } from "@jest/globals"; import Sidebar from "../Sidebar"; import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants"; @@ -108,6 +109,157 @@ describe("Sidebar Environment Variables", () => { }); }); + describe("Authentication", () => { + const openAuthSection = () => { + const button = screen.getByTestId("auth-button"); + fireEvent.click(button); + }; + + it("should update bearer token", () => { + const setBearerToken = jest.fn(); + renderSidebar({ + bearerToken: "", + setBearerToken, + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + const tokenInput = screen.getByTestId("bearer-token-input"); + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + expect(setBearerToken).toHaveBeenCalledWith("new_token"); + }); + + it("should update header name", () => { + const setHeaderName = jest.fn(); + renderSidebar({ + headerName: "Authorization", + setHeaderName, + transportType: "sse", + }); + + openAuthSection(); + + const headerInput = screen.getByTestId("header-input"); + fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } }); + + expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth"); + }); + + it("should clear bearer token", () => { + const setBearerToken = jest.fn(); + renderSidebar({ + bearerToken: "existing_token", + setBearerToken, + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + const tokenInput = screen.getByTestId("bearer-token-input"); + fireEvent.change(tokenInput, { target: { value: "" } }); + + expect(setBearerToken).toHaveBeenCalledWith(""); + }); + + it("should properly render bearer token input", () => { + const { rerender } = renderSidebar({ + bearerToken: "existing_token", + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + // Token input should be a password field + const tokenInput = screen.getByTestId("bearer-token-input"); + expect(tokenInput).toHaveProperty("type", "password"); + + // Update the token + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + // Rerender with updated token + rerender( + + + , + ); + + // Token input should still exist after update + expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); + }); + + it("should maintain token visibility state after update", () => { + const { rerender } = renderSidebar({ + bearerToken: "existing_token", + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + // Token input should be a password field + const tokenInput = screen.getByTestId("bearer-token-input"); + expect(tokenInput).toHaveProperty("type", "password"); + + // Update the token + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + // Rerender with updated token + rerender( + + + , + ); + + // Token input should still exist after update + expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); + }); + + it("should maintain header name when toggling auth section", () => { + renderSidebar({ + headerName: "X-API-Key", + transportType: "sse", + }); + + // Open auth section + openAuthSection(); + + // Verify header name is displayed + const headerInput = screen.getByTestId("header-input"); + expect(headerInput).toHaveValue("X-API-Key"); + + // Close auth section + const authButton = screen.getByTestId("auth-button"); + fireEvent.click(authButton); + + // Reopen auth section + fireEvent.click(authButton); + + // Verify header name is still preserved + expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key"); + }); + + it("should display default header name when not specified", () => { + renderSidebar({ + headerName: undefined, + transportType: "sse", + }); + + openAuthSection(); + + const headerInput = screen.getByTestId("header-input"); + expect(headerInput).toHaveAttribute("placeholder", "Authorization"); + }); + }); + describe("Key Editing", () => { it("should maintain order when editing first key", () => { const setEnv = jest.fn(); From 53152e3fb11f78b107cb895305bad140022e31a1 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Fri, 11 Apr 2025 20:52:45 +0300 Subject: [PATCH 4/8] fix build --- client/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 6e66e6e..61a5b69 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -45,7 +45,7 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; -import { getMCPProxyAddress } from "./utils/configUtils"; +import { getMCPProxyAddress, getMCPServerRequestTimeout } from "./utils/configUtils"; import { useToast } from "@/hooks/use-toast"; const params = new URLSearchParams(window.location.search); @@ -118,7 +118,7 @@ const App = () => { }); const [headerName, setHeaderName] = useState(() => { - return localStorage.getItem("lastHeaderName") || "Authorization"; + return localStorage.getItem("lastHeaderName") || ""; }); const [pendingSampleRequests, setPendingSampleRequests] = useState< From d798d1a132a17b8af2a0c670882e86bb2744412c Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 12 Apr 2025 00:13:52 +0300 Subject: [PATCH 5/8] remove bad merge --- client/src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 61a5b69..8d17db4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -174,8 +174,6 @@ const App = () => { env, bearerToken, headerName, - proxyServerUrl: getMCPProxyAddress(config), - requestTimeout: getMCPServerRequestTimeout(config), config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); From a010f10c2635e482bc5a21d36d11221a7031dde1 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 12 Apr 2025 00:50:37 +0300 Subject: [PATCH 6/8] remove bad merges --- client/src/App.tsx | 2 +- client/src/lib/hooks/useConnection.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 8d17db4..fb78dc6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -45,7 +45,7 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; -import { getMCPProxyAddress, getMCPServerRequestTimeout } from "./utils/configUtils"; +import { getMCPProxyAddress } from "./utils/configUtils"; import { useToast } from "@/hooks/use-toast"; const params = new URLSearchParams(window.location.search); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 12ff2e1..cc91474 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -49,7 +49,6 @@ interface UseConnectionOptions { env: Record; bearerToken?: string; headerName?: string; - requestTimeout?: number; config: InspectorConfig; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -67,7 +66,6 @@ export function useConnection({ env, bearerToken, headerName, - requestTimeout, config, onNotification, onStdErrNotification, From cd1bcfb15fc96acd384cb2a732f1823ba932cbfe Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 12 Apr 2025 22:52:47 +0300 Subject: [PATCH 7/8] Update README.md Co-authored-by: Ola Hungerford --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e21508..2552a01 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For more details on ways to use the inspector, see the [Inspector section of the ### Authentication -The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name. +The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar. ### Security Considerations From 87fad79e7df959992b81cd4d1451c75a7bfb6bd2 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Sat, 12 Apr 2025 22:55:03 +0300 Subject: [PATCH 8/8] add bearer --- client/src/lib/hooks/useConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index cc91474..d5d7070 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -296,7 +296,7 @@ export function useConnection({ const token = bearerToken || (await authProvider.tokens())?.access_token; if (token) { const authHeaderName = headerName || "Authorization"; - headers[authHeaderName] = `${token}`; + headers[authHeaderName] = `Bearer ${token}`; } const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {