Merge pull request #272 from idosal/set-header

Enable Authentication header name configuration
This commit is contained in:
Cliff Hall
2025-04-14 18:52:34 -04:00
committed by GitHub
5 changed files with 184 additions and 2 deletions

View File

@@ -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 using the input field in the sidebar.
### Security Considerations

View File

@@ -117,6 +117,10 @@ const App = () => {
return localStorage.getItem("lastBearerToken") || "";
});
const [headerName, setHeaderName] = useState<string>(() => {
return localStorage.getItem("lastHeaderName") || "";
});
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
PendingRequest & {
@@ -169,6 +173,7 @@ const App = () => {
sseUrl,
env,
bearerToken,
headerName,
config,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -208,6 +213,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]);
@@ -500,6 +509,8 @@ const App = () => {
setConfig={setConfig}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}

View File

@@ -51,6 +51,8 @@ interface SidebarProps {
setEnv: (env: Record<string, string>) => void;
bearerToken: string;
setBearerToken: (token: string) => void;
headerName?: string;
setHeaderName?: (name: string) => void;
onConnect: () => void;
onDisconnect: () => void;
stdErrNotifications: StdErrNotification[];
@@ -76,6 +78,8 @@ const Sidebar = ({
setEnv,
bearerToken,
setBearerToken,
headerName,
setHeaderName,
onConnect,
onDisconnect,
stdErrNotifications,
@@ -176,6 +180,7 @@ const Sidebar = ({
variant="outline"
onClick={() => setShowBearerToken(!showBearerToken)}
className="flex items-center w-full"
data-testid="auth-button"
aria-expanded={showBearerToken}
>
{showBearerToken ? (
@@ -187,6 +192,16 @@ const Sidebar = ({
</Button>
{showBearerToken && (
<div className="space-y-2">
<label className="text-sm font-medium">Header Name</label>
<Input
placeholder="Authorization"
onChange={(e) =>
setHeaderName && setHeaderName(e.target.value)
}
data-testid="header-input"
className="font-mono"
value={headerName}
/>
<label
className="text-sm font-medium"
htmlFor="bearer-token-input"
@@ -198,6 +213,7 @@ const Sidebar = ({
placeholder="Bearer Token"
value={bearerToken}
onChange={(e) => setBearerToken(e.target.value)}
data-testid="bearer-token-input"
className="font-mono"
type="password"
/>

View File

@@ -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";
@@ -109,6 +110,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(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// 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(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// 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();

View File

@@ -48,6 +48,7 @@ interface UseConnectionOptions {
sseUrl: string;
env: Record<string, string>;
bearerToken?: string;
headerName?: string;
config: InspectorConfig;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
@@ -64,6 +65,7 @@ export function useConnection({
sseUrl,
env,
bearerToken,
headerName,
config,
onNotification,
onStdErrNotification,
@@ -293,7 +295,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] = `Bearer ${token}`;
}
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {