Skip to content

feat: support custom header #549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
107 changes: 107 additions & 0 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Set<string>>(new Set());
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
const [copiedServerFile, setCopiedServerFile] = useState(false);
Expand Down Expand Up @@ -620,6 +624,109 @@ const Sidebar = ({
)}
</div>

{/* Headers */}
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowHeaders(!showHeaders)}
className="flex items-center w-full"
data-testid="headers-button"
aria-expanded={showHeaders}
>
{showHeaders ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
<Hash className="w-4 h-4 mr-2" />
Headers
</Button>
{showHeaders && (
<div className="space-y-2">
{(() => {
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) => (
<div key={index} className="space-y-2 p-2 border rounded">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-green-600">
Header {index + 1}
</label>
<Button
variant="outline"
size="sm"
onClick={() => removeHeader(index)}
data-testid={`remove-header-${index}`}
>
<Minus className="h-4 w-4" />
</Button>
</div>
<Input
placeholder="Header name (e.g., X-API-Key)"
value={header.name}
onChange={(e) => updateHeader(index, "name", e.target.value)}
data-testid={`header-name-${index}`}
className="font-mono"
/>
<Input
placeholder="Header value"
value={header.value}
onChange={(e) => updateHeader(index, "value", e.target.value)}
data-testid={`header-value-${index}`}
className="font-mono"
type="password"
/>
</div>
))}
<Button
variant="outline"
onClick={addHeader}
className="w-full"
data-testid="add-header-button"
>
<Plus className="h-4 w-4 mr-2" />
Add Header
</Button>
</>
);
})()}
</div>
)}
</div>

<div className="space-y-2">
{connectionStatus === "connected" && (
<div className="grid grid-cols-2 gap-4">
Expand Down
138 changes: 138 additions & 0 deletions client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions client/src/lib/configurationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
6 changes: 6 additions & 0 deletions client/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
26 changes: 26 additions & 0 deletions client/src/lib/hooks/useConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -346,6 +371,7 @@ export function useConnection({
| SSEClientTransportOptions;

let mcpProxyServerUrl;

switch (transportType) {
case "stdio":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
Expand Down
14 changes: 14 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Loading