diff --git a/client/src/App.tsx b/client/src/App.tsx index 7880b2a5..6e76f95d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -128,6 +128,10 @@ const App = () => { return localStorage.getItem("lastHeaderName") || ""; }); + const [oauthClientId, setOauthClientId] = useState(() => { + return localStorage.getItem("lastOauthClientId") || ""; + }); + const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -181,6 +185,7 @@ const App = () => { env, bearerToken, headerName, + oauthClientId, config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); @@ -224,6 +229,10 @@ const App = () => { localStorage.setItem("lastHeaderName", headerName); }, [headerName]); + useEffect(() => { + localStorage.setItem("lastOauthClientId", oauthClientId); + }, [oauthClientId]); + useEffect(() => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); @@ -502,6 +511,8 @@ const App = () => { setBearerToken={setBearerToken} headerName={headerName} setHeaderName={setHeaderName} + oauthClientId={oauthClientId} + setOauthClientId={setOauthClientId} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx index 6bfa8a3b..32ff1277 100644 --- a/client/src/components/OAuthCallback.tsx +++ b/client/src/components/OAuthCallback.tsx @@ -1,5 +1,8 @@ import { useEffect, useRef } from "react"; -import { InspectorOAuthClientProvider } from "../lib/auth"; +import { + InspectorOAuthClientProvider, + getClientInformationFromSessionStorage, +} from "../lib/auth"; import { SESSION_KEYS } from "../lib/constants"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { useToast } from "@/hooks/use-toast.ts"; @@ -41,10 +44,16 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => { return notifyError("Missing Server URL"); } + const clientInformation = + await getClientInformationFromSessionStorage(serverUrl); + let result; try { // Create an auth provider with the current server URL - const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl); + const serverAuthProvider = new InspectorOAuthClientProvider( + serverUrl, + clientInformation, + ); result = await auth(serverAuthProvider, { serverUrl, diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index bc6af52f..a01cc771 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -53,6 +53,8 @@ interface SidebarProps { setBearerToken: (token: string) => void; headerName?: string; setHeaderName?: (name: string) => void; + oauthClientId: string; + setOauthClientId: (id: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; @@ -80,6 +82,8 @@ const Sidebar = ({ setBearerToken, headerName, setHeaderName, + oauthClientId, + setOauthClientId, onConnect, onDisconnect, stdErrNotifications, @@ -95,6 +99,7 @@ const Sidebar = ({ const [showBearerToken, setShowBearerToken] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); + const [showOauthConfig, setShowOauthConfig] = useState(false); return (
@@ -221,6 +226,44 @@ const Sidebar = ({
)} + {/* OAuth Configuration */} +
+ + {showOauthConfig && ( +
+ + setOauthClientId(e.target.value)} + value={oauthClientId} + data-testid="oauth-client-id-input" + className="font-mono" + /> + + +
+ )} +
)} {transportType === "stdio" && ( diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 7ef31822..1b94a299 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -7,10 +7,30 @@ import { } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; +export const getClientInformationFromSessionStorage = async ( + serverUrl: string, +) => { + const key = getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, serverUrl); + const value = sessionStorage.getItem(key); + if (!value) { + return undefined; + } + + return await OAuthClientInformationSchema.parseAsync(JSON.parse(value)); +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor(private serverUrl: string) { + constructor( + private serverUrl: string, + clientInformation?: OAuthClientInformation, + ) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); + + // Save the client information to session storage if provided + if (clientInformation) { + this.saveClientInformation(clientInformation); + } } get redirectUrl() { @@ -29,16 +49,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } async clientInformation() { - const key = getServerSpecificKey( - SESSION_KEYS.CLIENT_INFORMATION, - this.serverUrl, - ); - const value = sessionStorage.getItem(key); - if (!value) { - return undefined; - } - - return await OAuthClientInformationSchema.parseAsync(JSON.parse(value)); + return await getClientInformationFromSessionStorage(this.serverUrl); } saveClientInformation(clientInformation: OAuthClientInformation) { diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index abbeb7c3..3be4d8f4 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -25,7 +25,7 @@ import { Progress, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useToast } from "@/hooks/use-toast"; import { z } from "zod"; import { ConnectionStatus } from "../constants"; @@ -40,6 +40,7 @@ import { } from "@/utils/configUtils"; import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; +import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; interface UseConnectionOptions { transportType: "stdio" | "sse" | "streamable-http"; @@ -49,6 +50,7 @@ interface UseConnectionOptions { env: Record; bearerToken?: string; headerName?: string; + oauthClientId?: string; config: InspectorConfig; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -66,6 +68,7 @@ export function useConnection({ env, bearerToken, headerName, + oauthClientId, config, onNotification, onStdErrNotification, @@ -83,6 +86,15 @@ export function useConnection({ >([]); const [completionsSupported, setCompletionsSupported] = useState(true); + const oauthClientInformation: OAuthClientInformation | undefined = + useMemo(() => { + if (!oauthClientId) { + return undefined; + } + + return { client_id: oauthClientId }; + }, [oauthClientId]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -247,7 +259,10 @@ export function useConnection({ const handleAuthError = async (error: unknown) => { if (error instanceof SseError && error.code === 401) { // Create a new auth provider with the current server URL - const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); + const serverAuthProvider = new InspectorOAuthClientProvider( + sseUrl, + oauthClientInformation, + ); const result = await auth(serverAuthProvider, { serverUrl: sseUrl }); return result === "AUTHORIZED"; @@ -294,7 +309,10 @@ export function useConnection({ const headers: HeadersInit = {}; // Create an auth provider with the current server URL - const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); + const serverAuthProvider = new InspectorOAuthClientProvider( + sseUrl, + oauthClientInformation, + ); // Use manually provided bearer token if available, otherwise use OAuth tokens const token = @@ -396,7 +414,10 @@ export function useConnection({ const disconnect = async () => { await mcpClient?.close(); - const authProvider = new InspectorOAuthClientProvider(sseUrl); + const authProvider = new InspectorOAuthClientProvider( + sseUrl, + oauthClientInformation, + ); authProvider.clear(); setMcpClient(null); setConnectionStatus("disconnected");