Skip to content

Commit 24418bc

Browse files
authored
Merge branch 'main' into fix_readme_missing_character
2 parents e4bfc05 + 0b37722 commit 24418bc

File tree

10 files changed

+217
-17
lines changed

10 files changed

+217
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ For more details on ways to use the inspector, see the [Inspector section of the
4040

4141
### Authentication
4242

43-
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.
43+
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.
4444

4545
### Security Considerations
4646

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.8.2",
3+
"version": "0.9.0",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

client/src/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ const App = () => {
117117
return localStorage.getItem("lastBearerToken") || "";
118118
});
119119

120+
const [headerName, setHeaderName] = useState<string>(() => {
121+
return localStorage.getItem("lastHeaderName") || "";
122+
});
123+
120124
const [pendingSampleRequests, setPendingSampleRequests] = useState<
121125
Array<
122126
PendingRequest & {
@@ -169,6 +173,7 @@ const App = () => {
169173
sseUrl,
170174
env,
171175
bearerToken,
176+
headerName,
172177
config,
173178
onNotification: (notification) => {
174179
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -208,6 +213,10 @@ const App = () => {
208213
localStorage.setItem("lastBearerToken", bearerToken);
209214
}, [bearerToken]);
210215

216+
useEffect(() => {
217+
localStorage.setItem("lastHeaderName", headerName);
218+
}, [headerName]);
219+
211220
useEffect(() => {
212221
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
213222
}, [config]);
@@ -467,6 +476,10 @@ const App = () => {
467476
setLogLevel(level);
468477
};
469478

479+
const clearStdErrNotifications = () => {
480+
setStdErrNotifications([]);
481+
};
482+
470483
if (window.location.pathname === "/oauth/callback") {
471484
const OAuthCallback = React.lazy(
472485
() => import("./components/OAuthCallback"),
@@ -496,12 +509,15 @@ const App = () => {
496509
setConfig={setConfig}
497510
bearerToken={bearerToken}
498511
setBearerToken={setBearerToken}
512+
headerName={headerName}
513+
setHeaderName={setHeaderName}
499514
onConnect={connectMcpServer}
500515
onDisconnect={disconnectMcpServer}
501516
stdErrNotifications={stdErrNotifications}
502517
logLevel={logLevel}
503518
sendLogLevelRequest={sendLogLevelRequest}
504519
loggingSupported={!!serverCapabilities?.logging || false}
520+
clearStdErrNotifications={clearStdErrNotifications}
505521
/>
506522
<div className="flex-1 flex flex-col overflow-hidden">
507523
<div className="flex-1 overflow-auto">

client/src/components/JsonView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ const JsonNode = memo(
227227
)}
228228
<pre
229229
className={clsx(
230-
typeStyleMap.string,
230+
isError ? typeStyleMap.error : typeStyleMap.string,
231231
"break-all whitespace-pre-wrap",
232232
)}
233233
>

client/src/components/Sidebar.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ interface SidebarProps {
5151
setEnv: (env: Record<string, string>) => void;
5252
bearerToken: string;
5353
setBearerToken: (token: string) => void;
54+
headerName?: string;
55+
setHeaderName?: (name: string) => void;
5456
onConnect: () => void;
5557
onDisconnect: () => void;
5658
stdErrNotifications: StdErrNotification[];
59+
clearStdErrNotifications: () => void;
5760
logLevel: LoggingLevel;
5861
sendLogLevelRequest: (level: LoggingLevel) => void;
5962
loggingSupported: boolean;
@@ -75,9 +78,12 @@ const Sidebar = ({
7578
setEnv,
7679
bearerToken,
7780
setBearerToken,
81+
headerName,
82+
setHeaderName,
7883
onConnect,
7984
onDisconnect,
8085
stdErrNotifications,
86+
clearStdErrNotifications,
8187
logLevel,
8288
sendLogLevelRequest,
8389
loggingSupported,
@@ -174,6 +180,7 @@ const Sidebar = ({
174180
variant="outline"
175181
onClick={() => setShowBearerToken(!showBearerToken)}
176182
className="flex items-center w-full"
183+
data-testid="auth-button"
177184
aria-expanded={showBearerToken}
178185
>
179186
{showBearerToken ? (
@@ -185,6 +192,16 @@ const Sidebar = ({
185192
</Button>
186193
{showBearerToken && (
187194
<div className="space-y-2">
195+
<label className="text-sm font-medium">Header Name</label>
196+
<Input
197+
placeholder="Authorization"
198+
onChange={(e) =>
199+
setHeaderName && setHeaderName(e.target.value)
200+
}
201+
data-testid="header-input"
202+
className="font-mono"
203+
value={headerName}
204+
/>
188205
<label
189206
className="text-sm font-medium"
190207
htmlFor="bearer-token-input"
@@ -196,6 +213,7 @@ const Sidebar = ({
196213
placeholder="Bearer Token"
197214
value={bearerToken}
198215
onChange={(e) => setBearerToken(e.target.value)}
216+
data-testid="bearer-token-input"
199217
className="font-mono"
200218
type="password"
201219
/>
@@ -514,9 +532,19 @@ const Sidebar = ({
514532
{stdErrNotifications.length > 0 && (
515533
<>
516534
<div className="mt-4 border-t border-gray-200 pt-4">
517-
<h3 className="text-sm font-medium">
518-
Error output from MCP server
519-
</h3>
535+
<div className="flex justify-between items-center">
536+
<h3 className="text-sm font-medium">
537+
Error output from MCP server
538+
</h3>
539+
<Button
540+
variant="outline"
541+
size="sm"
542+
onClick={clearStdErrNotifications}
543+
className="h-8 px-2"
544+
>
545+
Clear
546+
</Button>
547+
</div>
520548
<div className="mt-2 max-h-80 overflow-y-auto">
521549
{stdErrNotifications.map((notification, index) => (
522550
<div

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

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen, fireEvent } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
23
import { describe, it, beforeEach, jest } from "@jest/globals";
34
import Sidebar from "../Sidebar";
45
import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
@@ -29,6 +30,7 @@ describe("Sidebar Environment Variables", () => {
2930
onConnect: jest.fn(),
3031
onDisconnect: jest.fn(),
3132
stdErrNotifications: [],
33+
clearStdErrNotifications: jest.fn(),
3234
logLevel: "info" as const,
3335
sendLogLevelRequest: jest.fn(),
3436
loggingSupported: true,
@@ -108,6 +110,157 @@ describe("Sidebar Environment Variables", () => {
108110
});
109111
});
110112

113+
describe("Authentication", () => {
114+
const openAuthSection = () => {
115+
const button = screen.getByTestId("auth-button");
116+
fireEvent.click(button);
117+
};
118+
119+
it("should update bearer token", () => {
120+
const setBearerToken = jest.fn();
121+
renderSidebar({
122+
bearerToken: "",
123+
setBearerToken,
124+
transportType: "sse", // Set transport type to SSE
125+
});
126+
127+
openAuthSection();
128+
129+
const tokenInput = screen.getByTestId("bearer-token-input");
130+
fireEvent.change(tokenInput, { target: { value: "new_token" } });
131+
132+
expect(setBearerToken).toHaveBeenCalledWith("new_token");
133+
});
134+
135+
it("should update header name", () => {
136+
const setHeaderName = jest.fn();
137+
renderSidebar({
138+
headerName: "Authorization",
139+
setHeaderName,
140+
transportType: "sse",
141+
});
142+
143+
openAuthSection();
144+
145+
const headerInput = screen.getByTestId("header-input");
146+
fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
147+
148+
expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
149+
});
150+
151+
it("should clear bearer token", () => {
152+
const setBearerToken = jest.fn();
153+
renderSidebar({
154+
bearerToken: "existing_token",
155+
setBearerToken,
156+
transportType: "sse", // Set transport type to SSE
157+
});
158+
159+
openAuthSection();
160+
161+
const tokenInput = screen.getByTestId("bearer-token-input");
162+
fireEvent.change(tokenInput, { target: { value: "" } });
163+
164+
expect(setBearerToken).toHaveBeenCalledWith("");
165+
});
166+
167+
it("should properly render bearer token input", () => {
168+
const { rerender } = renderSidebar({
169+
bearerToken: "existing_token",
170+
transportType: "sse", // Set transport type to SSE
171+
});
172+
173+
openAuthSection();
174+
175+
// Token input should be a password field
176+
const tokenInput = screen.getByTestId("bearer-token-input");
177+
expect(tokenInput).toHaveProperty("type", "password");
178+
179+
// Update the token
180+
fireEvent.change(tokenInput, { target: { value: "new_token" } });
181+
182+
// Rerender with updated token
183+
rerender(
184+
<TooltipProvider>
185+
<Sidebar
186+
{...defaultProps}
187+
bearerToken="new_token"
188+
transportType="sse"
189+
/>
190+
</TooltipProvider>,
191+
);
192+
193+
// Token input should still exist after update
194+
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
195+
});
196+
197+
it("should maintain token visibility state after update", () => {
198+
const { rerender } = renderSidebar({
199+
bearerToken: "existing_token",
200+
transportType: "sse", // Set transport type to SSE
201+
});
202+
203+
openAuthSection();
204+
205+
// Token input should be a password field
206+
const tokenInput = screen.getByTestId("bearer-token-input");
207+
expect(tokenInput).toHaveProperty("type", "password");
208+
209+
// Update the token
210+
fireEvent.change(tokenInput, { target: { value: "new_token" } });
211+
212+
// Rerender with updated token
213+
rerender(
214+
<TooltipProvider>
215+
<Sidebar
216+
{...defaultProps}
217+
bearerToken="new_token"
218+
transportType="sse"
219+
/>
220+
</TooltipProvider>,
221+
);
222+
223+
// Token input should still exist after update
224+
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
225+
});
226+
227+
it("should maintain header name when toggling auth section", () => {
228+
renderSidebar({
229+
headerName: "X-API-Key",
230+
transportType: "sse",
231+
});
232+
233+
// Open auth section
234+
openAuthSection();
235+
236+
// Verify header name is displayed
237+
const headerInput = screen.getByTestId("header-input");
238+
expect(headerInput).toHaveValue("X-API-Key");
239+
240+
// Close auth section
241+
const authButton = screen.getByTestId("auth-button");
242+
fireEvent.click(authButton);
243+
244+
// Reopen auth section
245+
fireEvent.click(authButton);
246+
247+
// Verify header name is still preserved
248+
expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
249+
});
250+
251+
it("should display default header name when not specified", () => {
252+
renderSidebar({
253+
headerName: undefined,
254+
transportType: "sse",
255+
});
256+
257+
openAuthSection();
258+
259+
const headerInput = screen.getByTestId("header-input");
260+
expect(headerInput).toHaveAttribute("placeholder", "Authorization");
261+
});
262+
});
263+
111264
describe("Key Editing", () => {
112265
it("should maintain order when editing first key", () => {
113266
const setEnv = jest.fn();

client/src/lib/hooks/useConnection.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface UseConnectionOptions {
4848
sseUrl: string;
4949
env: Record<string, string>;
5050
bearerToken?: string;
51+
headerName?: string;
5152
config: InspectorConfig;
5253
onNotification?: (notification: Notification) => void;
5354
onStdErrNotification?: (notification: Notification) => void;
@@ -64,6 +65,7 @@ export function useConnection({
6465
sseUrl,
6566
env,
6667
bearerToken,
68+
headerName,
6769
config,
6870
onNotification,
6971
onStdErrNotification,
@@ -293,7 +295,8 @@ export function useConnection({
293295
// Use manually provided bearer token if available, otherwise use OAuth tokens
294296
const token = bearerToken || (await authProvider.tokens())?.access_token;
295297
if (token) {
296-
headers["Authorization"] = `Bearer ${token}`;
298+
const authHeaderName = headerName || "Authorization";
299+
headers[authHeaderName] = `Bearer ${token}`;
297300
}
298301

299302
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {

0 commit comments

Comments
 (0)