Skip to content

Commit ec230ff

Browse files
committed
feat: support manual entry of OAuth client information
1 parent 6ab7ac3 commit ec230ff

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
@@ -128,6 +128,10 @@ const App = () => {
128128
return localStorage.getItem("lastHeaderName") || "";
129129
});
130130

131+
const [oauthClientId, setOauthClientId] = useState<string>(() => {
132+
return localStorage.getItem("lastOauthClientId") || "";
133+
});
134+
131135
const [pendingSampleRequests, setPendingSampleRequests] = useState<
132136
Array<
133137
PendingRequest & {
@@ -181,6 +185,7 @@ const App = () => {
181185
env,
182186
bearerToken,
183187
headerName,
188+
oauthClientId,
184189
config,
185190
onNotification: (notification) => {
186191
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -224,6 +229,10 @@ const App = () => {
224229
localStorage.setItem("lastHeaderName", headerName);
225230
}, [headerName]);
226231

232+
useEffect(() => {
233+
localStorage.setItem("lastOauthClientId", oauthClientId);
234+
}, [oauthClientId]);
235+
227236
useEffect(() => {
228237
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
229238
}, [config]);
@@ -502,6 +511,8 @@ const App = () => {
502511
setBearerToken={setBearerToken}
503512
headerName={headerName}
504513
setHeaderName={setHeaderName}
514+
oauthClientId={oauthClientId}
515+
setOauthClientId={setOauthClientId}
505516
onConnect={connectMcpServer}
506517
onDisconnect={disconnectMcpServer}
507518
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 "@/hooks/use-toast.ts";
@@ -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
@@ -53,6 +53,8 @@ interface SidebarProps {
5353
setBearerToken: (token: string) => void;
5454
headerName?: string;
5555
setHeaderName?: (name: string) => void;
56+
oauthClientId: string;
57+
setOauthClientId: (id: string) => void;
5658
onConnect: () => void;
5759
onDisconnect: () => void;
5860
stdErrNotifications: StdErrNotification[];
@@ -80,6 +82,8 @@ const Sidebar = ({
8082
setBearerToken,
8183
headerName,
8284
setHeaderName,
85+
oauthClientId,
86+
setOauthClientId,
8387
onConnect,
8488
onDisconnect,
8589
stdErrNotifications,
@@ -95,6 +99,7 @@ const Sidebar = ({
9599
const [showBearerToken, setShowBearerToken] = useState(false);
96100
const [showConfig, setShowConfig] = useState(false);
97101
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
102+
const [showOauthConfig, setShowOauthConfig] = useState(false);
98103

99104
return (
100105
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
@@ -221,6 +226,44 @@ const Sidebar = ({
221226
</div>
222227
)}
223228
</div>
229+
{/* OAuth Configuration */}
230+
<div className="space-y-2">
231+
<Button
232+
variant="outline"
233+
onClick={() => setShowOauthConfig(!showOauthConfig)}
234+
className="flex items-center w-full"
235+
data-testid="oauth-config-button"
236+
aria-expanded={showOauthConfig}
237+
>
238+
{showOauthConfig ? (
239+
<ChevronDown className="w-4 h-4 mr-2" />
240+
) : (
241+
<ChevronRight className="w-4 h-4 mr-2" />
242+
)}
243+
OAuth Configuration
244+
</Button>
245+
{showOauthConfig && (
246+
<div className="space-y-2">
247+
<label className="text-sm font-medium">Client ID</label>
248+
<Input
249+
placeholder="Client ID"
250+
onChange={(e) => setOauthClientId(e.target.value)}
251+
value={oauthClientId}
252+
data-testid="oauth-client-id-input"
253+
className="font-mono"
254+
/>
255+
<label className="text-sm font-medium">
256+
Redirect URL (auto-populated)
257+
</label>
258+
<Input
259+
readOnly
260+
placeholder="Redirect URL"
261+
value={window.location.origin + "/oauth/callback"}
262+
className="font-mono"
263+
/>
264+
</div>
265+
)}
266+
</div>
224267
</>
225268
)}
226269
{transportType === "stdio" && (

client/src/lib/auth.ts

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

10+
export const getClientInformationFromSessionStorage = async (
11+
serverUrl: string,
12+
) => {
13+
const key = getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, serverUrl);
14+
const value = sessionStorage.getItem(key);
15+
if (!value) {
16+
return undefined;
17+
}
18+
19+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
20+
};
21+
1022
export class InspectorOAuthClientProvider implements OAuthClientProvider {
11-
constructor(private serverUrl: string) {
23+
constructor(
24+
private serverUrl: string,
25+
clientInformation?: OAuthClientInformation,
26+
) {
1227
// Save the server URL to session storage
1328
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
29+
30+
// Save the client information to session storage if provided
31+
if (clientInformation) {
32+
this.saveClientInformation(clientInformation);
33+
}
1434
}
1535

1636
get redirectUrl() {
@@ -29,16 +49,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
2949
}
3050

3151
async clientInformation() {
32-
const key = getServerSpecificKey(
33-
SESSION_KEYS.CLIENT_INFORMATION,
34-
this.serverUrl,
35-
);
36-
const value = sessionStorage.getItem(key);
37-
if (!value) {
38-
return undefined;
39-
}
40-
41-
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
52+
return await getClientInformationFromSessionStorage(this.serverUrl);
4253
}
4354

4455
saveClientInformation(clientInformation: OAuthClientInformation) {

client/src/lib/hooks/useConnection.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
Progress,
2626
} from "@modelcontextprotocol/sdk/types.js";
2727
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
28-
import { useState } from "react";
28+
import { useMemo, useState } from "react";
2929
import { useToast } from "@/hooks/use-toast";
3030
import { z } from "zod";
3131
import { ConnectionStatus } from "../constants";
@@ -40,6 +40,7 @@ import {
4040
} from "@/utils/configUtils";
4141
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
4242
import { InspectorConfig } from "../configurationTypes";
43+
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
4344

4445
interface UseConnectionOptions {
4546
transportType: "stdio" | "sse" | "streamable-http";
@@ -49,6 +50,7 @@ interface UseConnectionOptions {
4950
env: Record<string, string>;
5051
bearerToken?: string;
5152
headerName?: string;
53+
oauthClientId?: string;
5254
config: InspectorConfig;
5355
onNotification?: (notification: Notification) => void;
5456
onStdErrNotification?: (notification: Notification) => void;
@@ -66,6 +68,7 @@ export function useConnection({
6668
env,
6769
bearerToken,
6870
headerName,
71+
oauthClientId,
6972
config,
7073
onNotification,
7174
onStdErrNotification,
@@ -83,6 +86,15 @@ export function useConnection({
8386
>([]);
8487
const [completionsSupported, setCompletionsSupported] = useState(true);
8588

89+
const oauthClientInformation: OAuthClientInformation | undefined =
90+
useMemo(() => {
91+
if (!oauthClientId) {
92+
return undefined;
93+
}
94+
95+
return { client_id: oauthClientId };
96+
}, [oauthClientId]);
97+
8698
const pushHistory = (request: object, response?: object) => {
8799
setRequestHistory((prev) => [
88100
...prev,
@@ -247,7 +259,10 @@ export function useConnection({
247259
const handleAuthError = async (error: unknown) => {
248260
if (error instanceof SseError && error.code === 401) {
249261
// Create a new auth provider with the current server URL
250-
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
262+
const serverAuthProvider = new InspectorOAuthClientProvider(
263+
sseUrl,
264+
oauthClientInformation,
265+
);
251266

252267
const result = await auth(serverAuthProvider, { serverUrl: sseUrl });
253268
return result === "AUTHORIZED";
@@ -294,7 +309,10 @@ export function useConnection({
294309
const headers: HeadersInit = {};
295310

296311
// Create an auth provider with the current server URL
297-
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
312+
const serverAuthProvider = new InspectorOAuthClientProvider(
313+
sseUrl,
314+
oauthClientInformation,
315+
);
298316

299317
// Use manually provided bearer token if available, otherwise use OAuth tokens
300318
const token =
@@ -396,7 +414,10 @@ export function useConnection({
396414

397415
const disconnect = async () => {
398416
await mcpClient?.close();
399-
const authProvider = new InspectorOAuthClientProvider(sseUrl);
417+
const authProvider = new InspectorOAuthClientProvider(
418+
sseUrl,
419+
oauthClientInformation,
420+
);
400421
authProvider.clear();
401422
setMcpClient(null);
402423
setConnectionStatus("disconnected");

0 commit comments

Comments
 (0)