Skip to content

Commit ce026d7

Browse files
committed
feat: support custom header
1 parent ac99874 commit ce026d7

File tree

6 files changed

+296
-0
lines changed

6 files changed

+296
-0
lines changed

client/src/components/Sidebar.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
RefreshCwOff,
1515
Copy,
1616
CheckCheck,
17+
Hash,
18+
Plus,
19+
Minus,
1720
} from "lucide-react";
1821
import { Button } from "@/components/ui/button";
1922
import { Input } from "@/components/ui/input";
@@ -97,6 +100,7 @@ const Sidebar = ({
97100
const [showEnvVars, setShowEnvVars] = useState(false);
98101
const [showBearerToken, setShowBearerToken] = useState(false);
99102
const [showConfig, setShowConfig] = useState(false);
103+
const [showHeaders, setShowHeaders] = useState(false);
100104
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
101105
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
102106
const [copiedServerFile, setCopiedServerFile] = useState(false);
@@ -620,6 +624,109 @@ const Sidebar = ({
620624
)}
621625
</div>
622626

627+
{/* Headers */}
628+
<div className="space-y-2">
629+
<Button
630+
variant="outline"
631+
onClick={() => setShowHeaders(!showHeaders)}
632+
className="flex items-center w-full"
633+
data-testid="headers-button"
634+
aria-expanded={showHeaders}
635+
>
636+
{showHeaders ? (
637+
<ChevronDown className="w-4 h-4 mr-2" />
638+
) : (
639+
<ChevronRight className="w-4 h-4 mr-2" />
640+
)}
641+
<Hash className="w-4 h-4 mr-2" />
642+
Headers
643+
</Button>
644+
{showHeaders && (
645+
<div className="space-y-2">
646+
{(() => {
647+
const headersJson = config.MCP_CUSTOM_HEADERS?.value as string || "[]";
648+
let headers: Array<{ name: string; value: string }> = [];
649+
650+
try {
651+
headers = JSON.parse(headersJson);
652+
} catch {
653+
headers = [];
654+
}
655+
656+
const updateHeaders = (newHeaders: Array<{ name: string; value: string }>) => {
657+
const newConfig = { ...config };
658+
newConfig.MCP_CUSTOM_HEADERS = {
659+
...config.MCP_CUSTOM_HEADERS,
660+
value: JSON.stringify(newHeaders),
661+
};
662+
setConfig(newConfig);
663+
};
664+
665+
const addHeader = () => {
666+
updateHeaders([...headers, { name: "", value: "" }]);
667+
};
668+
669+
const removeHeader = (index: number) => {
670+
const newHeaders = headers.filter((_, i) => i !== index);
671+
updateHeaders(newHeaders);
672+
};
673+
674+
const updateHeader = (index: number, field: "name" | "value", value: string) => {
675+
const newHeaders = [...headers];
676+
newHeaders[index] = { ...newHeaders[index], [field]: value };
677+
updateHeaders(newHeaders);
678+
};
679+
680+
return (
681+
<>
682+
{headers.map((header, index) => (
683+
<div key={index} className="space-y-2 p-2 border rounded">
684+
<div className="flex items-center justify-between">
685+
<label className="text-sm font-medium text-green-600">
686+
Header {index + 1}
687+
</label>
688+
<Button
689+
variant="outline"
690+
size="sm"
691+
onClick={() => removeHeader(index)}
692+
data-testid={`remove-header-${index}`}
693+
>
694+
<Minus className="h-4 w-4" />
695+
</Button>
696+
</div>
697+
<Input
698+
placeholder="Header name (e.g., X-API-Key)"
699+
value={header.name}
700+
onChange={(e) => updateHeader(index, "name", e.target.value)}
701+
data-testid={`header-name-${index}`}
702+
className="font-mono"
703+
/>
704+
<Input
705+
placeholder="Header value"
706+
value={header.value}
707+
onChange={(e) => updateHeader(index, "value", e.target.value)}
708+
data-testid={`header-value-${index}`}
709+
className="font-mono"
710+
type="password"
711+
/>
712+
</div>
713+
))}
714+
<Button
715+
variant="outline"
716+
onClick={addHeader}
717+
className="w-full"
718+
data-testid="add-header-button"
719+
>
720+
<Plus className="h-4 w-4 mr-2" />
721+
Add Header
722+
</Button>
723+
</>
724+
);
725+
})()}
726+
</div>
727+
)}
728+
</div>
729+
623730
<div className="space-y-2">
624731
{connectionStatus === "connected" && (
625732
<div className="grid grid-cols-2 gap-4">

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,144 @@ describe("Sidebar Environment Variables", () => {
648648
});
649649
});
650650

651+
describe("Headers Operations", () => {
652+
const openHeadersSection = () => {
653+
const button = screen.getByTestId("headers-button");
654+
fireEvent.click(button);
655+
};
656+
657+
it("should add a new header", () => {
658+
const setConfig = jest.fn();
659+
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
660+
661+
openHeadersSection();
662+
663+
const addButton = screen.getByTestId("add-header-button");
664+
fireEvent.click(addButton);
665+
666+
expect(setConfig).toHaveBeenCalledWith(
667+
expect.objectContaining({
668+
MCP_CUSTOM_HEADERS: {
669+
label: "Custom Headers",
670+
description: "Custom headers for authentication with the MCP server (stored as JSON array)",
671+
value: '[{"name":"","value":""}]',
672+
is_session_item: true,
673+
},
674+
}),
675+
);
676+
});
677+
678+
it("should update header name", () => {
679+
const setConfig = jest.fn();
680+
const config = {
681+
...DEFAULT_INSPECTOR_CONFIG,
682+
MCP_CUSTOM_HEADERS: {
683+
...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS,
684+
value: '[{"name":"","value":""}]',
685+
},
686+
};
687+
688+
renderSidebar({ config, setConfig });
689+
690+
openHeadersSection();
691+
692+
const headerNameInput = screen.getByTestId("header-name-0");
693+
694+
fireEvent.change(headerNameInput, { target: { value: "X-API-Key" } });
695+
696+
expect(setConfig).toHaveBeenCalledWith(
697+
expect.objectContaining({
698+
MCP_CUSTOM_HEADERS: {
699+
label: "Custom Headers",
700+
description: "Custom headers for authentication with the MCP server (stored as JSON array)",
701+
value: '[{"name":"X-API-Key","value":""}]',
702+
is_session_item: true,
703+
},
704+
}),
705+
);
706+
});
707+
708+
it("should update header value", () => {
709+
const setConfig = jest.fn();
710+
const config = {
711+
...DEFAULT_INSPECTOR_CONFIG,
712+
MCP_CUSTOM_HEADERS: {
713+
...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS,
714+
value: '[{"name":"","value":""}]',
715+
},
716+
};
717+
718+
renderSidebar({ config, setConfig });
719+
720+
openHeadersSection();
721+
722+
const headerValueInput = screen.getByTestId("header-value-0");
723+
724+
fireEvent.change(headerValueInput, { target: { value: "secret-key-123" } });
725+
726+
expect(setConfig).toHaveBeenCalledWith(
727+
expect.objectContaining({
728+
MCP_CUSTOM_HEADERS: {
729+
label: "Custom Headers",
730+
description: "Custom headers for authentication with the MCP server (stored as JSON array)",
731+
value: '[{"name":"","value":"secret-key-123"}]',
732+
is_session_item: true,
733+
},
734+
}),
735+
);
736+
});
737+
738+
it("should remove a header", () => {
739+
const setConfig = jest.fn();
740+
const config = {
741+
...DEFAULT_INSPECTOR_CONFIG,
742+
MCP_CUSTOM_HEADERS: {
743+
...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS,
744+
value: '[{"name":"X-API-Key","value":"secret-key-123"}]',
745+
},
746+
};
747+
748+
renderSidebar({ config, setConfig });
749+
750+
openHeadersSection();
751+
752+
const removeButton = screen.getByTestId("remove-header-0");
753+
fireEvent.click(removeButton);
754+
755+
expect(setConfig).toHaveBeenCalledWith(
756+
expect.objectContaining({
757+
MCP_CUSTOM_HEADERS: {
758+
label: "Custom Headers",
759+
description: "Custom headers for authentication with the MCP server (stored as JSON array)",
760+
value: "[]",
761+
is_session_item: true,
762+
},
763+
}),
764+
);
765+
});
766+
767+
it("should handle multiple headers", () => {
768+
const setConfig = jest.fn();
769+
const config = {
770+
...DEFAULT_INSPECTOR_CONFIG,
771+
MCP_CUSTOM_HEADERS: {
772+
...DEFAULT_INSPECTOR_CONFIG.MCP_CUSTOM_HEADERS,
773+
value: '[{"name":"X-API-Key","value":"key1"},{"name":"Authorization","value":"Bearer token"}]',
774+
},
775+
};
776+
777+
renderSidebar({ config, setConfig });
778+
779+
openHeadersSection();
780+
781+
// Verify both headers are displayed
782+
expect(screen.getByTestId("header-name-0")).toHaveValue("X-API-Key");
783+
expect(screen.getByTestId("header-value-0")).toHaveValue("key1");
784+
expect(screen.getByTestId("header-name-1")).toHaveValue("Authorization");
785+
expect(screen.getByTestId("header-value-1")).toHaveValue("Bearer token");
786+
});
787+
});
788+
651789
describe("Copy Configuration Features", () => {
652790
beforeEach(() => {
653791
jest.clearAllMocks();

client/src/lib/configurationTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ export type InspectorConfig = {
3939
* Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
4040
*/
4141
MCP_PROXY_AUTH_TOKEN: ConfigItem;
42+
43+
/**
44+
* Custom headers for authentication with the MCP server. JSON string containing array of {name, value} objects.
45+
*/
46+
MCP_CUSTOM_HEADERS: ConfigItem;
4247
};

client/src/lib/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,10 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
6565
value: "",
6666
is_session_item: true,
6767
},
68+
MCP_CUSTOM_HEADERS: {
69+
label: "Custom Headers",
70+
description: "Custom headers for authentication with the MCP server (stored as JSON array)",
71+
value: "[]",
72+
is_session_item: true,
73+
},
6874
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,32 @@ export function useConnection({
320320
// Use manually provided bearer token if available, otherwise use OAuth tokens
321321
const token =
322322
bearerToken || (await serverAuthProvider.tokens())?.access_token;
323+
324+
// Check for custom headers from configuration
325+
const customHeadersJson = config.MCP_CUSTOM_HEADERS?.value as string;
326+
let customHeaders: Array<{ name: string; value: string }> = [];
327+
328+
try {
329+
if (customHeadersJson) {
330+
customHeaders = JSON.parse(customHeadersJson);
331+
}
332+
} catch (error) {
333+
console.warn("Failed to parse custom headers:", error);
334+
}
335+
336+
if (customHeaders.length > 0) {
337+
// Use custom headers from configuration
338+
// Send headers with x-mcp-custom- prefix so server can identify them
339+
customHeaders.forEach(({ name, value }) => {
340+
if (name && value) {
341+
const headerKey = `x-mcp-custom-${name.toLowerCase()}`;
342+
headers[headerKey] = value;
343+
}
344+
});
345+
}
346+
323347
if (token) {
348+
// Fallback to bearer token with header name
324349
const authHeaderName = headerName || "Authorization";
325350

326351
// 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({
346371
| SSEClientTransportOptions;
347372

348373
let mcpProxyServerUrl;
374+
349375
switch (transportType) {
350376
case "stdio":
351377
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);

server/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ const getHttpHeaders = (
7676
headers[customHeaderName] = value as string;
7777
}
7878
}
79+
80+
// Handle multiple custom headers sent by the new client implementation
81+
// Look for headers that start with 'x-mcp-custom-' prefix
82+
Object.keys(req.headers).forEach((headerName) => {
83+
if (headerName.startsWith('x-mcp-custom-')) {
84+
// Extract the actual header name from x-mcp-custom-[actual-header-name]
85+
const actualHeaderName = headerName.substring('x-mcp-custom-'.length);
86+
const headerValue = req.headers[headerName];
87+
const value = Array.isArray(headerValue) ? headerValue[headerValue.length - 1] : headerValue;
88+
if (value) {
89+
headers[actualHeaderName] = value;
90+
}
91+
}
92+
});
7993
return headers;
8094
};
8195

0 commit comments

Comments
 (0)