Skip to content

Commit eda6c05

Browse files
authored
Merge branch 'main' into feature/elicitations
2 parents 144755b + 0db0bbb commit eda6c05

File tree

7 files changed

+237
-67
lines changed

7 files changed

+237
-67
lines changed

client/src/App.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ const App = () => {
123123
return localStorage.getItem("lastHeaderName") || "";
124124
});
125125

126+
const [oauthClientId, setOauthClientId] = useState<string>(() => {
127+
return localStorage.getItem("lastOauthClientId") || "";
128+
});
129+
130+
const [oauthScope, setOauthScope] = useState<string>(() => {
131+
return localStorage.getItem("lastOauthScope") || "";
132+
});
133+
126134
const [pendingSampleRequests, setPendingSampleRequests] = useState<
127135
Array<
128136
PendingRequest & {
@@ -210,6 +218,8 @@ const App = () => {
210218
env,
211219
bearerToken,
212220
headerName,
221+
oauthClientId,
222+
oauthScope,
213223
config,
214224
onNotification: (notification) => {
215225
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -308,6 +318,14 @@ const App = () => {
308318
localStorage.setItem("lastHeaderName", headerName);
309319
}, [headerName]);
310320

321+
useEffect(() => {
322+
localStorage.setItem("lastOauthClientId", oauthClientId);
323+
}, [oauthClientId]);
324+
325+
useEffect(() => {
326+
localStorage.setItem("lastOauthScope", oauthScope);
327+
}, [oauthScope]);
328+
311329
useEffect(() => {
312330
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
313331
}, [config]);
@@ -782,6 +800,10 @@ const App = () => {
782800
setBearerToken={setBearerToken}
783801
headerName={headerName}
784802
setHeaderName={setHeaderName}
803+
oauthClientId={oauthClientId}
804+
setOauthClientId={setOauthClientId}
805+
oauthScope={oauthScope}
806+
setOauthScope={setOauthScope}
785807
onConnect={connectMcpServer}
786808
onDisconnect={disconnectMcpServer}
787809
stdErrNotifications={stdErrNotifications}

client/src/components/Sidebar.tsx

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ interface SidebarProps {
5656
setBearerToken: (token: string) => void;
5757
headerName?: string;
5858
setHeaderName?: (name: string) => void;
59+
oauthClientId: string;
60+
setOauthClientId: (id: string) => void;
61+
oauthScope: string;
62+
setOauthScope: (scope: string) => void;
5963
onConnect: () => void;
6064
onDisconnect: () => void;
6165
stdErrNotifications: StdErrNotification[];
@@ -83,6 +87,10 @@ const Sidebar = ({
8387
setBearerToken,
8488
headerName,
8589
setHeaderName,
90+
oauthClientId,
91+
setOauthClientId,
92+
oauthScope,
93+
setOauthScope,
8694
onConnect,
8795
onDisconnect,
8896
stdErrNotifications,
@@ -95,7 +103,7 @@ const Sidebar = ({
95103
}: SidebarProps) => {
96104
const [theme, setTheme] = useTheme();
97105
const [showEnvVars, setShowEnvVars] = useState(false);
98-
const [showBearerToken, setShowBearerToken] = useState(false);
106+
const [showAuthConfig, setShowAuthConfig] = useState(false);
99107
const [showConfig, setShowConfig] = useState(false);
100108
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
101109
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
@@ -308,51 +316,6 @@ const Sidebar = ({
308316
/>
309317
)}
310318
</div>
311-
<div className="space-y-2">
312-
<Button
313-
variant="outline"
314-
onClick={() => setShowBearerToken(!showBearerToken)}
315-
className="flex items-center w-full"
316-
data-testid="auth-button"
317-
aria-expanded={showBearerToken}
318-
>
319-
{showBearerToken ? (
320-
<ChevronDown className="w-4 h-4 mr-2" />
321-
) : (
322-
<ChevronRight className="w-4 h-4 mr-2" />
323-
)}
324-
Authentication
325-
</Button>
326-
{showBearerToken && (
327-
<div className="space-y-2">
328-
<label className="text-sm font-medium">Header Name</label>
329-
<Input
330-
placeholder="Authorization"
331-
onChange={(e) =>
332-
setHeaderName && setHeaderName(e.target.value)
333-
}
334-
data-testid="header-input"
335-
className="font-mono"
336-
value={headerName}
337-
/>
338-
<label
339-
className="text-sm font-medium"
340-
htmlFor="bearer-token-input"
341-
>
342-
Bearer Token
343-
</label>
344-
<Input
345-
id="bearer-token-input"
346-
placeholder="Bearer Token"
347-
value={bearerToken}
348-
onChange={(e) => setBearerToken(e.target.value)}
349-
data-testid="bearer-token-input"
350-
className="font-mono"
351-
type="password"
352-
/>
353-
</div>
354-
)}
355-
</div>
356319
</>
357320
)}
358321

@@ -521,6 +484,94 @@ const Sidebar = ({
521484
</Tooltip>
522485
</div>
523486

487+
<div className="space-y-2">
488+
<Button
489+
variant="outline"
490+
onClick={() => setShowAuthConfig(!showAuthConfig)}
491+
className="flex items-center w-full"
492+
data-testid="auth-button"
493+
aria-expanded={showAuthConfig}
494+
>
495+
{showAuthConfig ? (
496+
<ChevronDown className="w-4 h-4 mr-2" />
497+
) : (
498+
<ChevronRight className="w-4 h-4 mr-2" />
499+
)}
500+
Authentication
501+
</Button>
502+
{showAuthConfig && (
503+
<>
504+
{/* Bearer Token Section */}
505+
<div className="space-y-2 p-3 rounded border">
506+
<h4 className="text-sm font-semibold flex items-center">
507+
API Token Authentication
508+
</h4>
509+
<div className="space-y-2">
510+
<label className="text-sm font-medium">Header Name</label>
511+
<Input
512+
placeholder="Authorization"
513+
onChange={(e) =>
514+
setHeaderName && setHeaderName(e.target.value)
515+
}
516+
data-testid="header-input"
517+
className="font-mono"
518+
value={headerName}
519+
/>
520+
<label
521+
className="text-sm font-medium"
522+
htmlFor="bearer-token-input"
523+
>
524+
Bearer Token
525+
</label>
526+
<Input
527+
id="bearer-token-input"
528+
placeholder="Bearer Token"
529+
value={bearerToken}
530+
onChange={(e) => setBearerToken(e.target.value)}
531+
data-testid="bearer-token-input"
532+
className="font-mono"
533+
type="password"
534+
/>
535+
</div>
536+
</div>
537+
{transportType !== "stdio" && (
538+
// OAuth Configuration
539+
<div className="space-y-2 p-3 rounded border">
540+
<h4 className="text-sm font-semibold flex items-center">
541+
OAuth 2.0 Flow
542+
</h4>
543+
<div className="space-y-2">
544+
<label className="text-sm font-medium">Client ID</label>
545+
<Input
546+
placeholder="Client ID"
547+
onChange={(e) => setOauthClientId(e.target.value)}
548+
value={oauthClientId}
549+
data-testid="oauth-client-id-input"
550+
className="font-mono"
551+
/>
552+
<label className="text-sm font-medium">
553+
Redirect URL
554+
</label>
555+
<Input
556+
readOnly
557+
placeholder="Redirect URL"
558+
value={window.location.origin + "/oauth/callback"}
559+
className="font-mono"
560+
/>
561+
<label className="text-sm font-medium">Scope</label>
562+
<Input
563+
placeholder="Scope (space-separated)"
564+
onChange={(e) => setOauthScope(e.target.value)}
565+
value={oauthScope}
566+
data-testid="oauth-scope-input"
567+
className="font-mono"
568+
/>
569+
</div>
570+
</div>
571+
)}
572+
</>
573+
)}
574+
</div>
524575
{/* Configuration */}
525576
<div className="space-y-2">
526577
<Button

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ describe("Sidebar Environment Variables", () => {
4242
setArgs: jest.fn(),
4343
sseUrl: "",
4444
setSseUrl: jest.fn(),
45+
oauthClientId: "",
46+
setOauthClientId: jest.fn(),
47+
oauthScope: "",
48+
setOauthScope: jest.fn(),
4549
env: {},
4650
setEnv: jest.fn(),
4751
bearerToken: "",

client/src/lib/auth.ts

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,64 @@ import {
99
} from "@modelcontextprotocol/sdk/shared/auth.js";
1010
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
1111

12+
export const getClientInformationFromSessionStorage = async ({
13+
serverUrl,
14+
isPreregistered,
15+
}: {
16+
serverUrl: string;
17+
isPreregistered?: boolean;
18+
}) => {
19+
const key = getServerSpecificKey(
20+
isPreregistered
21+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
22+
: SESSION_KEYS.CLIENT_INFORMATION,
23+
serverUrl,
24+
);
25+
26+
const value = sessionStorage.getItem(key);
27+
if (!value) {
28+
return undefined;
29+
}
30+
31+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
32+
};
33+
34+
export const saveClientInformationToSessionStorage = ({
35+
serverUrl,
36+
clientInformation,
37+
isPreregistered,
38+
}: {
39+
serverUrl: string;
40+
clientInformation: OAuthClientInformation;
41+
isPreregistered?: boolean;
42+
}) => {
43+
const key = getServerSpecificKey(
44+
isPreregistered
45+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
46+
: SESSION_KEYS.CLIENT_INFORMATION,
47+
serverUrl,
48+
);
49+
sessionStorage.setItem(key, JSON.stringify(clientInformation));
50+
};
51+
52+
export const clearClientInformationFromSessionStorage = ({
53+
serverUrl,
54+
isPreregistered,
55+
}: {
56+
serverUrl: string;
57+
isPreregistered?: boolean;
58+
}) => {
59+
const key = getServerSpecificKey(
60+
isPreregistered
61+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
62+
: SESSION_KEYS.CLIENT_INFORMATION,
63+
serverUrl,
64+
);
65+
sessionStorage.removeItem(key);
66+
};
67+
1268
export class InspectorOAuthClientProvider implements OAuthClientProvider {
13-
constructor(public serverUrl: string) {
69+
constructor(protected serverUrl: string) {
1470
// Save the server URL to session storage
1571
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
1672
}
@@ -31,24 +87,30 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
3187
}
3288

3389
async clientInformation() {
34-
const key = getServerSpecificKey(
35-
SESSION_KEYS.CLIENT_INFORMATION,
36-
this.serverUrl,
90+
// Try to get the preregistered client information from session storage first
91+
const preregisteredClientInformation =
92+
await getClientInformationFromSessionStorage({
93+
serverUrl: this.serverUrl,
94+
isPreregistered: true,
95+
});
96+
97+
// If no preregistered client information is found, get the dynamically registered client information
98+
return (
99+
preregisteredClientInformation ??
100+
(await getClientInformationFromSessionStorage({
101+
serverUrl: this.serverUrl,
102+
isPreregistered: false,
103+
}))
37104
);
38-
const value = sessionStorage.getItem(key);
39-
if (!value) {
40-
return undefined;
41-
}
42-
43-
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
44105
}
45106

46107
saveClientInformation(clientInformation: OAuthClientInformation) {
47-
const key = getServerSpecificKey(
48-
SESSION_KEYS.CLIENT_INFORMATION,
49-
this.serverUrl,
50-
);
51-
sessionStorage.setItem(key, JSON.stringify(clientInformation));
108+
// Save the dynamically registered client information to session storage
109+
saveClientInformationToSessionStorage({
110+
serverUrl: this.serverUrl,
111+
clientInformation,
112+
isPreregistered: false,
113+
});
52114
}
53115

54116
async tokens() {
@@ -92,9 +154,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
92154
}
93155

94156
clear() {
95-
sessionStorage.removeItem(
96-
getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, this.serverUrl),
97-
);
157+
clearClientInformationFromSessionStorage({
158+
serverUrl: this.serverUrl,
159+
isPreregistered: false,
160+
});
98161
sessionStorage.removeItem(
99162
getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),
100163
);

client/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const SESSION_KEYS = {
66
SERVER_URL: "mcp_server_url",
77
TOKENS: "mcp_tokens",
88
CLIENT_INFORMATION: "mcp_client_information",
9+
PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information",
910
SERVER_METADATA: "mcp_server_metadata",
1011
AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state",
1112
} as const;

client/src/lib/hooks/__tests__/useConnection.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ jest.mock("../../auth", () => ({
8686
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
8787
tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }),
8888
})),
89+
clearClientInformationFromSessionStorage: jest.fn(),
90+
saveClientInformationToSessionStorage: jest.fn(),
8991
}));
9092

9193
describe("useConnection", () => {

0 commit comments

Comments
 (0)