Skip to content

Commit 737bc50

Browse files
committed
feat: support manual entry of OAuth client information
1 parent 05e41d2 commit 737bc50

File tree

5 files changed

+112
-17
lines changed

5 files changed

+112
-17
lines changed

client/src/App.tsx

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

112+
const [oauthClientId, setOauthClientId] = useState<string>(() => {
113+
return localStorage.getItem("lastOauthClientId") || "";
114+
});
115+
112116
const [pendingSampleRequests, setPendingSampleRequests] = useState<
113117
Array<
114118
PendingRequest & {
@@ -183,6 +187,7 @@ const App = () => {
183187
env,
184188
bearerToken,
185189
headerName,
190+
oauthClientId,
186191
config,
187192
onNotification: (notification) => {
188193
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -226,6 +231,10 @@ const App = () => {
226231
localStorage.setItem("lastHeaderName", headerName);
227232
}, [headerName]);
228233

234+
useEffect(() => {
235+
localStorage.setItem("lastOauthClientId", oauthClientId);
236+
}, [oauthClientId]);
237+
229238
useEffect(() => {
230239
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
231240
}, [config]);
@@ -581,6 +590,8 @@ const App = () => {
581590
setBearerToken={setBearerToken}
582591
headerName={headerName}
583592
setHeaderName={setHeaderName}
593+
oauthClientId={oauthClientId}
594+
setOauthClientId={setOauthClientId}
584595
onConnect={connectMcpServer}
585596
onDisconnect={disconnectMcpServer}
586597
stdErrNotifications={stdErrNotifications}

client/src/components/OAuthCallback.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { useEffect, useRef } from "react";
2-
import { InspectorOAuthClientProvider } from "../lib/auth";
2+
import {
3+
InspectorOAuthClientProvider,
4+
getClientInformationFromSessionStorage,
5+
} from "../lib/auth";
36
import { SESSION_KEYS } from "../lib/constants";
47
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
58
import { useToast } from "@/lib/hooks/useToast";
@@ -41,10 +44,16 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
4144
return notifyError("Missing Server URL");
4245
}
4346

47+
const clientInformation =
48+
await getClientInformationFromSessionStorage(serverUrl);
49+
4450
let result;
4551
try {
4652
// Create an auth provider with the current server URL
47-
const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
53+
const serverAuthProvider = new InspectorOAuthClientProvider(
54+
serverUrl,
55+
clientInformation,
56+
);
4857

4958
result = await auth(serverAuthProvider, {
5059
serverUrl,

client/src/components/Sidebar.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ interface SidebarProps {
5656
setBearerToken: (token: string) => void;
5757
headerName?: string;
5858
setHeaderName?: (name: string) => void;
59+
oauthClientId: string;
60+
setOauthClientId: (id: string) => void;
5961
onConnect: () => void;
6062
onDisconnect: () => void;
6163
stdErrNotifications: StdErrNotification[];
@@ -83,6 +85,8 @@ const Sidebar = ({
8385
setBearerToken,
8486
headerName,
8587
setHeaderName,
88+
oauthClientId,
89+
setOauthClientId,
8690
onConnect,
8791
onDisconnect,
8892
stdErrNotifications,
@@ -98,6 +102,7 @@ const Sidebar = ({
98102
const [showBearerToken, setShowBearerToken] = useState(false);
99103
const [showConfig, setShowConfig] = useState(false);
100104
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
105+
const [showOauthConfig, setShowOauthConfig] = useState(false);
101106
const [copiedServerEntry, setCopiedServerEntry] = useState(false);
102107
const [copiedServerFile, setCopiedServerFile] = useState(false);
103108
const { toast } = useToast();
@@ -338,6 +343,44 @@ const Sidebar = ({
338343
</div>
339344
)}
340345
</div>
346+
{/* OAuth Configuration */}
347+
<div className="space-y-2">
348+
<Button
349+
variant="outline"
350+
onClick={() => setShowOauthConfig(!showOauthConfig)}
351+
className="flex items-center w-full"
352+
data-testid="oauth-config-button"
353+
aria-expanded={showOauthConfig}
354+
>
355+
{showOauthConfig ? (
356+
<ChevronDown className="w-4 h-4 mr-2" />
357+
) : (
358+
<ChevronRight className="w-4 h-4 mr-2" />
359+
)}
360+
OAuth Configuration
361+
</Button>
362+
{showOauthConfig && (
363+
<div className="space-y-2">
364+
<label className="text-sm font-medium">Client ID</label>
365+
<Input
366+
placeholder="Client ID"
367+
onChange={(e) => setOauthClientId(e.target.value)}
368+
value={oauthClientId}
369+
data-testid="oauth-client-id-input"
370+
className="font-mono"
371+
/>
372+
<label className="text-sm font-medium">
373+
Redirect URL (auto-populated)
374+
</label>
375+
<Input
376+
readOnly
377+
placeholder="Redirect URL"
378+
value={window.location.origin + "/oauth/callback"}
379+
className="font-mono"
380+
/>
381+
</div>
382+
)}
383+
</div>
341384
</>
342385
)}
343386

client/src/lib/auth.ts

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

12+
export const getClientInformationFromSessionStorage = async (
13+
serverUrl: string,
14+
) => {
15+
const key = getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, serverUrl);
16+
const value = sessionStorage.getItem(key);
17+
if (!value) {
18+
return undefined;
19+
}
20+
21+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
22+
};
23+
1224
export class InspectorOAuthClientProvider implements OAuthClientProvider {
13-
constructor(public serverUrl: string) {
25+
constructor(
26+
protected serverUrl: string,
27+
clientInformation?: OAuthClientInformation,
28+
) {
1429
// Save the server URL to session storage
1530
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
31+
32+
// Save the client information to session storage if provided
33+
if (clientInformation) {
34+
this.saveClientInformation(clientInformation);
35+
}
1636
}
1737

1838
get redirectUrl() {
@@ -31,16 +51,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
3151
}
3252

3353
async clientInformation() {
34-
const key = getServerSpecificKey(
35-
SESSION_KEYS.CLIENT_INFORMATION,
36-
this.serverUrl,
37-
);
38-
const value = sessionStorage.getItem(key);
39-
if (!value) {
40-
return undefined;
41-
}
42-
43-
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
54+
return await getClientInformationFromSessionStorage(this.serverUrl);
4455
}
4556

4657
saveClientInformation(clientInformation: OAuthClientInformation) {

client/src/lib/hooks/useConnection.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
Progress,
3131
} from "@modelcontextprotocol/sdk/types.js";
3232
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
33-
import { useState } from "react";
33+
import { useMemo, useState } from "react";
3434
import { useToast } from "@/lib/hooks/useToast";
3535
import { z } from "zod";
3636
import { ConnectionStatus } from "../constants";
@@ -45,6 +45,7 @@ import {
4545
} from "@/utils/configUtils";
4646
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
4747
import { InspectorConfig } from "../configurationTypes";
48+
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
4849

4950
interface UseConnectionOptions {
5051
transportType: "stdio" | "sse" | "streamable-http";
@@ -54,6 +55,7 @@ interface UseConnectionOptions {
5455
env: Record<string, string>;
5556
bearerToken?: string;
5657
headerName?: string;
58+
oauthClientId?: string;
5759
config: InspectorConfig;
5860
onNotification?: (notification: Notification) => void;
5961
onStdErrNotification?: (notification: Notification) => void;
@@ -71,6 +73,7 @@ export function useConnection({
7173
env,
7274
bearerToken,
7375
headerName,
76+
oauthClientId,
7477
config,
7578
onNotification,
7679
onStdErrNotification,
@@ -88,6 +91,15 @@ export function useConnection({
8891
>([]);
8992
const [completionsSupported, setCompletionsSupported] = useState(true);
9093

94+
const oauthClientInformation: OAuthClientInformation | undefined =
95+
useMemo(() => {
96+
if (!oauthClientId) {
97+
return undefined;
98+
}
99+
100+
return { client_id: oauthClientId };
101+
}, [oauthClientId]);
102+
91103
const pushHistory = (request: object, response?: object) => {
92104
setRequestHistory((prev) => [
93105
...prev,
@@ -252,7 +264,10 @@ export function useConnection({
252264
const handleAuthError = async (error: unknown) => {
253265
if (error instanceof SseError && error.code === 401) {
254266
// Create a new auth provider with the current server URL
255-
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
267+
const serverAuthProvider = new InspectorOAuthClientProvider(
268+
sseUrl,
269+
oauthClientInformation,
270+
);
256271

257272
const result = await auth(serverAuthProvider, { serverUrl: sseUrl });
258273
return result === "AUTHORIZED";
@@ -290,7 +305,10 @@ export function useConnection({
290305
const headers: HeadersInit = {};
291306

292307
// Create an auth provider with the current server URL
293-
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
308+
const serverAuthProvider = new InspectorOAuthClientProvider(
309+
sseUrl,
310+
oauthClientInformation,
311+
);
294312

295313
// Use manually provided bearer token if available, otherwise use OAuth tokens
296314
const token =
@@ -463,7 +481,10 @@ export function useConnection({
463481

464482
const disconnect = async () => {
465483
await mcpClient?.close();
466-
const authProvider = new InspectorOAuthClientProvider(sseUrl);
484+
const authProvider = new InspectorOAuthClientProvider(
485+
sseUrl,
486+
oauthClientInformation,
487+
);
467488
authProvider.clear();
468489
setMcpClient(null);
469490
setConnectionStatus("disconnected");

0 commit comments

Comments
 (0)