Skip to content

Commit b8120d9

Browse files
committed
feat: support manual entry of OAuth client information
1 parent b39528d commit b8120d9

File tree

6 files changed

+242
-24
lines changed

6 files changed

+242
-24
lines changed

client/src/App.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ const App = () => {
116116
return localStorage.getItem("lastHeaderName") || "";
117117
});
118118

119+
const [oauthClientId, setOauthClientId] = useState<string>(() => {
120+
return localStorage.getItem("lastOauthClientId") || "";
121+
});
122+
123+
const [oauthScope, setOauthScope] = useState<string>(() => {
124+
return localStorage.getItem("lastOauthScope") || "";
125+
});
126+
127+
const [oauthResource, setOauthResource] = useState<string>(() => {
128+
return localStorage.getItem("lastOauthResource") || "";
129+
});
130+
119131
const [pendingSampleRequests, setPendingSampleRequests] = useState<
120132
Array<
121133
PendingRequest & {
@@ -184,6 +196,9 @@ const App = () => {
184196
env,
185197
bearerToken,
186198
headerName,
199+
oauthClientId,
200+
oauthScope,
201+
oauthResource,
187202
config,
188203
onNotification: (notification) => {
189204
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -227,6 +242,18 @@ const App = () => {
227242
localStorage.setItem("lastHeaderName", headerName);
228243
}, [headerName]);
229244

245+
useEffect(() => {
246+
localStorage.setItem("lastOauthClientId", oauthClientId);
247+
}, [oauthClientId]);
248+
249+
useEffect(() => {
250+
localStorage.setItem("lastOauthScope", oauthScope);
251+
}, [oauthScope]);
252+
253+
useEffect(() => {
254+
localStorage.setItem("lastOauthResource", oauthResource);
255+
}, [oauthResource]);
256+
230257
useEffect(() => {
231258
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
232259
}, [config]);
@@ -650,6 +677,12 @@ const App = () => {
650677
setBearerToken={setBearerToken}
651678
headerName={headerName}
652679
setHeaderName={setHeaderName}
680+
oauthClientId={oauthClientId}
681+
setOauthClientId={setOauthClientId}
682+
oauthScope={oauthScope}
683+
setOauthScope={setOauthScope}
684+
oauthResource={oauthResource}
685+
setOauthResource={setOauthResource}
653686
onConnect={connectMcpServer}
654687
onDisconnect={disconnectMcpServer}
655688
stdErrNotifications={stdErrNotifications}

client/src/components/Sidebar.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ 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;
63+
oauthResource: string;
64+
setOauthResource: (resource: string) => void;
5965
onConnect: () => void;
6066
onDisconnect: () => void;
6167
stdErrNotifications: StdErrNotification[];
@@ -83,6 +89,12 @@ const Sidebar = ({
8389
setBearerToken,
8490
headerName,
8591
setHeaderName,
92+
oauthClientId,
93+
setOauthClientId,
94+
oauthScope,
95+
setOauthScope,
96+
oauthResource,
97+
setOauthResource,
8698
onConnect,
8799
onDisconnect,
88100
stdErrNotifications,
@@ -98,6 +110,7 @@ const Sidebar = ({
98110
const [showBearerToken, setShowBearerToken] = useState(false);
99111
const [showConfig, setShowConfig] = useState(false);
100112
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
113+
const [showOauthConfig, setShowOauthConfig] = useState(false);
101114
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
102115
const [copiedServerFile, setCopiedServerFile] = useState(false);
103116
const { toast } = useToast();
@@ -353,6 +366,60 @@ const Sidebar = ({
353366
</div>
354367
)}
355368
</div>
369+
{/* OAuth Configuration */}
370+
<div className="space-y-2">
371+
<Button
372+
variant="outline"
373+
onClick={() => setShowOauthConfig(!showOauthConfig)}
374+
className="flex items-center w-full"
375+
data-testid="oauth-config-button"
376+
aria-expanded={showOauthConfig}
377+
>
378+
{showOauthConfig ? (
379+
<ChevronDown className="w-4 h-4 mr-2" />
380+
) : (
381+
<ChevronRight className="w-4 h-4 mr-2" />
382+
)}
383+
OAuth Configuration
384+
</Button>
385+
{showOauthConfig && (
386+
<div className="space-y-2">
387+
<label className="text-sm font-medium">Client ID</label>
388+
<Input
389+
placeholder="Client ID"
390+
onChange={(e) => setOauthClientId(e.target.value)}
391+
value={oauthClientId}
392+
data-testid="oauth-client-id-input"
393+
className="font-mono"
394+
/>
395+
<label className="text-sm font-medium">
396+
Redirect URL (auto-populated)
397+
</label>
398+
<Input
399+
readOnly
400+
placeholder="Redirect URL"
401+
value={window.location.origin + "/oauth/callback"}
402+
className="font-mono"
403+
/>
404+
<label className="text-sm font-medium">Scope</label>
405+
<Input
406+
placeholder="Scope (space-separated)"
407+
onChange={(e) => setOauthScope(e.target.value)}
408+
value={oauthScope}
409+
data-testid="oauth-scope-input"
410+
className="font-mono"
411+
/>
412+
<label className="text-sm font-medium">Resource</label>
413+
<Input
414+
placeholder="Resource"
415+
onChange={(e) => setOauthResource(e.target.value)}
416+
value={oauthResource}
417+
data-testid="oauth-resource-input"
418+
className="font-mono"
419+
/>
420+
</div>
421+
)}
422+
</div>
356423
</>
357424
)}
358425

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ 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(),
49+
oauthResource: "",
50+
setOauthResource: jest.fn(),
4551
env: {},
4652
setEnv: jest.fn(),
4753
bearerToken: "",

client/src/lib/auth.ts

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,67 @@ 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(
70+
protected serverUrl: string,
71+
protected resource?: string,
72+
) {
1473
// Save the server URL to session storage
1574
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
1675
}
@@ -31,24 +90,29 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
3190
}
3291

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

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

54118
async tokens() {
@@ -67,6 +131,18 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
67131
}
68132

69133
redirectToAuthorization(authorizationUrl: URL) {
134+
/**
135+
* Note: This resource parameter is for testing purposes in Inspector.
136+
* Once MCP Client SDK supports resource indicators, this parameter
137+
* will be passed to the SDK's auth method similar to how scope is passed.
138+
*
139+
* See: https://github.com/modelcontextprotocol/typescript-sdk/pull/498
140+
*
141+
* TODO: @xiaoyijun Remove this once MCP Client SDK supports resource indicators.
142+
*/
143+
if (this.resource) {
144+
authorizationUrl.searchParams.set("resource", this.resource);
145+
}
70146
window.location.href = authorizationUrl.href;
71147
}
72148

@@ -92,9 +168,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
92168
}
93169

94170
clear() {
95-
sessionStorage.removeItem(
96-
getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, this.serverUrl),
97-
);
171+
clearClientInformationFromSessionStorage({
172+
serverUrl: this.serverUrl,
173+
isPreregistered: false,
174+
});
98175
sessionStorage.removeItem(
99176
getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),
100177
);

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;

0 commit comments

Comments
 (0)