Skip to content

Commit 1e9dfc5

Browse files
committed
feat: support manual entry of OAuth client information
1 parent 7df0ac4 commit 1e9dfc5

File tree

8 files changed

+250
-81
lines changed

8 files changed

+250
-81
lines changed

client/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.13.0",
3+
"version": "0.12.0",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -23,9 +23,8 @@
2323
"test:watch": "jest --config jest.config.cjs --watch"
2424
},
2525
"dependencies": {
26-
"@modelcontextprotocol/sdk": "^1.11.5",
26+
"@modelcontextprotocol/sdk": "^1.11.3",
2727
"@radix-ui/react-checkbox": "^1.1.4",
28-
"ajv": "^6.12.6",
2928
"@radix-ui/react-dialog": "^1.1.3",
3029
"@radix-ui/react-icons": "^1.3.0",
3130
"@radix-ui/react-label": "^2.1.0",

client/src/App.tsx

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

113+
const [oauthClientId, setOauthClientId] = useState<string>(() => {
114+
return localStorage.getItem("lastOauthClientId") || "";
115+
});
116+
117+
const [oauthScope, setOauthScope] = useState<string>(() => {
118+
return localStorage.getItem("lastOauthScope") || "";
119+
});
120+
121+
const [oauthResource, setOauthResource] = useState<string>(() => {
122+
return localStorage.getItem("lastOauthResource") || "";
123+
});
124+
113125
const [pendingSampleRequests, setPendingSampleRequests] = useState<
114126
Array<
115127
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
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
232259
}, [config]);
@@ -583,6 +610,12 @@ const App = () => {
583610
setBearerToken={setBearerToken}
584611
headerName={headerName}
585612
setHeaderName={setHeaderName}
613+
oauthClientId={oauthClientId}
614+
setOauthClientId={setOauthClientId}
615+
oauthScope={oauthScope}
616+
setOauthScope={setOauthScope}
617+
oauthResource={oauthResource}
618+
setOauthResource={setOauthResource}
586619
onConnect={connectMcpServer}
587620
onDisconnect={disconnectMcpServer}
588621
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();
@@ -338,6 +351,60 @@ const Sidebar = ({
338351
</div>
339352
)}
340353
</div>
354+
{/* OAuth Configuration */}
355+
<div className="space-y-2">
356+
<Button
357+
variant="outline"
358+
onClick={() => setShowOauthConfig(!showOauthConfig)}
359+
className="flex items-center w-full"
360+
data-testid="oauth-config-button"
361+
aria-expanded={showOauthConfig}
362+
>
363+
{showOauthConfig ? (
364+
<ChevronDown className="w-4 h-4 mr-2" />
365+
) : (
366+
<ChevronRight className="w-4 h-4 mr-2" />
367+
)}
368+
OAuth Configuration
369+
</Button>
370+
{showOauthConfig && (
371+
<div className="space-y-2">
372+
<label className="text-sm font-medium">Client ID</label>
373+
<Input
374+
placeholder="Client ID"
375+
onChange={(e) => setOauthClientId(e.target.value)}
376+
value={oauthClientId}
377+
data-testid="oauth-client-id-input"
378+
className="font-mono"
379+
/>
380+
<label className="text-sm font-medium">
381+
Redirect URL (auto-populated)
382+
</label>
383+
<Input
384+
readOnly
385+
placeholder="Redirect URL"
386+
value={window.location.origin + "/oauth/callback"}
387+
className="font-mono"
388+
/>
389+
<label className="text-sm font-medium">Scope</label>
390+
<Input
391+
placeholder="Scope (space-separated)"
392+
onChange={(e) => setOauthScope(e.target.value)}
393+
value={oauthScope}
394+
data-testid="oauth-scope-input"
395+
className="font-mono"
396+
/>
397+
<label className="text-sm font-medium">Resource</label>
398+
<Input
399+
placeholder="Resource"
400+
onChange={(e) => setOauthResource(e.target.value)}
401+
value={oauthResource}
402+
data-testid="oauth-resource-input"
403+
className="font-mono"
404+
/>
405+
</div>
406+
)}
407+
</div>
341408
</>
342409
)}
343410

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
} as const;
1112

0 commit comments

Comments
 (0)