From b8120d9f8588d2f1f2a8af435c8326215ca293b2 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 16 May 2025 16:03:01 +0800 Subject: [PATCH 1/6] feat: support manual entry of OAuth client information --- client/src/App.tsx | 33 +++++ client/src/components/Sidebar.tsx | 67 +++++++++++ .../src/components/__tests__/Sidebar.test.tsx | 6 + client/src/lib/auth.ts | 113 +++++++++++++++--- client/src/lib/constants.ts | 1 + client/src/lib/hooks/useConnection.ts | 46 ++++++- 6 files changed, 242 insertions(+), 24 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 3ceafcae4..edcace8d9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -116,6 +116,18 @@ const App = () => { return localStorage.getItem("lastHeaderName") || ""; }); + const [oauthClientId, setOauthClientId] = useState(() => { + return localStorage.getItem("lastOauthClientId") || ""; + }); + + const [oauthScope, setOauthScope] = useState(() => { + return localStorage.getItem("lastOauthScope") || ""; + }); + + const [oauthResource, setOauthResource] = useState(() => { + return localStorage.getItem("lastOauthResource") || ""; + }); + const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -184,6 +196,9 @@ const App = () => { env, bearerToken, headerName, + oauthClientId, + oauthScope, + oauthResource, config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); @@ -227,6 +242,18 @@ const App = () => { localStorage.setItem("lastHeaderName", headerName); }, [headerName]); + useEffect(() => { + localStorage.setItem("lastOauthClientId", oauthClientId); + }, [oauthClientId]); + + useEffect(() => { + localStorage.setItem("lastOauthScope", oauthScope); + }, [oauthScope]); + + useEffect(() => { + localStorage.setItem("lastOauthResource", oauthResource); + }, [oauthResource]); + useEffect(() => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); @@ -650,6 +677,12 @@ const App = () => { setBearerToken={setBearerToken} headerName={headerName} setHeaderName={setHeaderName} + oauthClientId={oauthClientId} + setOauthClientId={setOauthClientId} + oauthScope={oauthScope} + setOauthScope={setOauthScope} + oauthResource={oauthResource} + setOauthResource={setOauthResource} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 938e5b5a3..badbd679e 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -56,6 +56,12 @@ interface SidebarProps { setBearerToken: (token: string) => void; headerName?: string; setHeaderName?: (name: string) => void; + oauthClientId: string; + setOauthClientId: (id: string) => void; + oauthScope: string; + setOauthScope: (scope: string) => void; + oauthResource: string; + setOauthResource: (resource: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; @@ -83,6 +89,12 @@ const Sidebar = ({ setBearerToken, headerName, setHeaderName, + oauthClientId, + setOauthClientId, + oauthScope, + setOauthScope, + oauthResource, + setOauthResource, onConnect, onDisconnect, stdErrNotifications, @@ -98,6 +110,7 @@ const Sidebar = ({ const [showBearerToken, setShowBearerToken] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); + const [showOauthConfig, setShowOauthConfig] = useState(false); const [copiedServerEntry, setCopiedServerEntry] = useState(false); const [copiedServerFile, setCopiedServerFile] = useState(false); const { toast } = useToast(); @@ -353,6 +366,60 @@ const Sidebar = ({ )} + {/* OAuth Configuration */} +
+ + {showOauthConfig && ( +
+ + setOauthClientId(e.target.value)} + value={oauthClientId} + data-testid="oauth-client-id-input" + className="font-mono" + /> + + + + setOauthScope(e.target.value)} + value={oauthScope} + data-testid="oauth-scope-input" + className="font-mono" + /> + + setOauthResource(e.target.value)} + value={oauthResource} + data-testid="oauth-resource-input" + className="font-mono" + /> +
+ )} +
)} diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index d818bdbb6..58c6eedfd 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -42,6 +42,12 @@ describe("Sidebar Environment Variables", () => { setArgs: jest.fn(), sseUrl: "", setSseUrl: jest.fn(), + oauthClientId: "", + setOauthClientId: jest.fn(), + oauthScope: "", + setOauthScope: jest.fn(), + oauthResource: "", + setOauthResource: jest.fn(), env: {}, setEnv: jest.fn(), bearerToken: "", diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 3e3516e0b..aa391a0eb 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -9,8 +9,67 @@ import { } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; +export const getClientInformationFromSessionStorage = async ({ + serverUrl, + isPreregistered, +}: { + serverUrl: string; + isPreregistered?: boolean; +}) => { + const key = getServerSpecificKey( + isPreregistered + ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION + : SESSION_KEYS.CLIENT_INFORMATION, + serverUrl, + ); + + const value = sessionStorage.getItem(key); + if (!value) { + return undefined; + } + + return await OAuthClientInformationSchema.parseAsync(JSON.parse(value)); +}; + +export const saveClientInformationToSessionStorage = ({ + serverUrl, + clientInformation, + isPreregistered, +}: { + serverUrl: string; + clientInformation: OAuthClientInformation; + isPreregistered?: boolean; +}) => { + const key = getServerSpecificKey( + isPreregistered + ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION + : SESSION_KEYS.CLIENT_INFORMATION, + serverUrl, + ); + sessionStorage.setItem(key, JSON.stringify(clientInformation)); +}; + +export const clearClientInformationFromSessionStorage = ({ + serverUrl, + isPreregistered, +}: { + serverUrl: string; + isPreregistered?: boolean; +}) => { + const key = getServerSpecificKey( + isPreregistered + ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION + : SESSION_KEYS.CLIENT_INFORMATION, + serverUrl, + ); + sessionStorage.removeItem(key); +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor(public serverUrl: string) { + constructor( + protected serverUrl: string, + protected resource?: string, + ) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } @@ -31,24 +90,29 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } async clientInformation() { - const key = getServerSpecificKey( - SESSION_KEYS.CLIENT_INFORMATION, - this.serverUrl, + // Try to get the preregistered client information from session storage first + const preregisteredClientInformation = await getClientInformationFromSessionStorage({ + serverUrl: this.serverUrl, + isPreregistered: true, + }); + + // If no preregistered client information is found, get the dynamically registered client information + return ( + preregisteredClientInformation ?? + (await getClientInformationFromSessionStorage({ + serverUrl: this.serverUrl, + isPreregistered: false, + })) ); - const value = sessionStorage.getItem(key); - if (!value) { - return undefined; - } - - return await OAuthClientInformationSchema.parseAsync(JSON.parse(value)); } saveClientInformation(clientInformation: OAuthClientInformation) { - const key = getServerSpecificKey( - SESSION_KEYS.CLIENT_INFORMATION, - this.serverUrl, - ); - sessionStorage.setItem(key, JSON.stringify(clientInformation)); + // Save the dynamically registered client information to session storage + saveClientInformationToSessionStorage({ + serverUrl: this.serverUrl, + clientInformation, + isPreregistered: false, + }); } async tokens() { @@ -67,6 +131,18 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } redirectToAuthorization(authorizationUrl: URL) { + /** + * Note: This resource parameter is for testing purposes in Inspector. + * Once MCP Client SDK supports resource indicators, this parameter + * will be passed to the SDK's auth method similar to how scope is passed. + * + * See: https://github.com/modelcontextprotocol/typescript-sdk/pull/498 + * + * TODO: @xiaoyijun Remove this once MCP Client SDK supports resource indicators. + */ + if (this.resource) { + authorizationUrl.searchParams.set("resource", this.resource); + } window.location.href = authorizationUrl.href; } @@ -92,9 +168,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } clear() { - sessionStorage.removeItem( - getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, this.serverUrl), - ); + clearClientInformationFromSessionStorage({ + serverUrl: this.serverUrl, + isPreregistered: false, + }); sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl), ); diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 922f1943f..2e983302e 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -6,6 +6,7 @@ export const SESSION_KEYS = { SERVER_URL: "mcp_server_url", TOKENS: "mcp_tokens", CLIENT_INFORMATION: "mcp_client_information", + PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information", SERVER_METADATA: "mcp_server_metadata", AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state", } as const; diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9009e698f..47e593ac6 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -30,13 +30,13 @@ import { Progress, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useToast } from "@/lib/hooks/useToast"; import { z } from "zod"; import { ConnectionStatus } from "../constants"; import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; -import { InspectorOAuthClientProvider } from "../auth"; +import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage } from "../auth"; import packageJson from "../../../package.json"; import { getMCPProxyAddress, @@ -56,6 +56,9 @@ interface UseConnectionOptions { env: Record; bearerToken?: string; headerName?: string; + oauthClientId?: string; + oauthScope?: string; + oauthResource?: string; config: InspectorConfig; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -73,6 +76,9 @@ export function useConnection({ env, bearerToken, headerName, + oauthClientId, + oauthScope, + oauthResource, config, onNotification, onStdErrNotification, @@ -93,6 +99,22 @@ export function useConnection({ >([]); const [completionsSupported, setCompletionsSupported] = useState(true); + useEffect(() => { + if (!oauthClientId) { + clearClientInformationFromSessionStorage({ + serverUrl: sseUrl, + isPreregistered: true, + }); + return; + } + + saveClientInformationToSessionStorage({ + serverUrl: sseUrl, + clientInformation: { client_id: oauthClientId }, + isPreregistered: true, + }); + }, [oauthClientId, sseUrl]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -277,9 +299,15 @@ export function useConnection({ const handleAuthError = async (error: unknown) => { if (is401Error(error)) { - const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); + const serverAuthProvider = new InspectorOAuthClientProvider( + sseUrl, + oauthResource, + ); - const result = await auth(serverAuthProvider, { serverUrl: sseUrl }); + const result = await auth(serverAuthProvider, { + serverUrl: sseUrl, + scope: oauthScope, + }); return result === "AUTHORIZED"; } @@ -315,7 +343,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, + oauthResource, + ); // Use manually provided bearer token if available, otherwise use OAuth tokens const token = @@ -535,7 +566,10 @@ export function useConnection({ clientTransport as StreamableHTTPClientTransport ).terminateSession(); await mcpClient?.close(); - const authProvider = new InspectorOAuthClientProvider(sseUrl); + const authProvider = new InspectorOAuthClientProvider( + sseUrl, + oauthResource, + ); authProvider.clear(); setMcpClient(null); setClientTransport(null); From 3b3205228ff105163ba89486723e142817af0162 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 8 Jul 2025 10:21:40 +0800 Subject: [PATCH 2/6] refactor: remove resource config --- client/src/App.tsx | 11 ----------- client/src/components/Sidebar.tsx | 12 ------------ .../src/components/__tests__/Sidebar.test.tsx | 2 -- client/src/lib/auth.ts | 18 ++---------------- client/src/lib/hooks/useConnection.ts | 13 ++----------- 5 files changed, 4 insertions(+), 52 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index edcace8d9..06b208b1d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -124,10 +124,6 @@ const App = () => { return localStorage.getItem("lastOauthScope") || ""; }); - const [oauthResource, setOauthResource] = useState(() => { - return localStorage.getItem("lastOauthResource") || ""; - }); - const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -198,7 +194,6 @@ const App = () => { headerName, oauthClientId, oauthScope, - oauthResource, config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); @@ -250,10 +245,6 @@ const App = () => { localStorage.setItem("lastOauthScope", oauthScope); }, [oauthScope]); - useEffect(() => { - localStorage.setItem("lastOauthResource", oauthResource); - }, [oauthResource]); - useEffect(() => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); @@ -681,8 +672,6 @@ const App = () => { setOauthClientId={setOauthClientId} oauthScope={oauthScope} setOauthScope={setOauthScope} - oauthResource={oauthResource} - setOauthResource={setOauthResource} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index badbd679e..972aef720 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -60,8 +60,6 @@ interface SidebarProps { setOauthClientId: (id: string) => void; oauthScope: string; setOauthScope: (scope: string) => void; - oauthResource: string; - setOauthResource: (resource: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; @@ -93,8 +91,6 @@ const Sidebar = ({ setOauthClientId, oauthScope, setOauthScope, - oauthResource, - setOauthResource, onConnect, onDisconnect, stdErrNotifications, @@ -409,14 +405,6 @@ const Sidebar = ({ data-testid="oauth-scope-input" className="font-mono" /> - - setOauthResource(e.target.value)} - value={oauthResource} - data-testid="oauth-resource-input" - className="font-mono" - /> )} diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index 58c6eedfd..e892a7f8b 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -46,8 +46,6 @@ describe("Sidebar Environment Variables", () => { setOauthClientId: jest.fn(), oauthScope: "", setOauthScope: jest.fn(), - oauthResource: "", - setOauthResource: jest.fn(), env: {}, setEnv: jest.fn(), bearerToken: "", diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index aa391a0eb..9450a3a52 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -66,10 +66,8 @@ export const clearClientInformationFromSessionStorage = ({ }; export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor( - protected serverUrl: string, - protected resource?: string, - ) { + + constructor(protected serverUrl: string) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } @@ -131,18 +129,6 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } redirectToAuthorization(authorizationUrl: URL) { - /** - * Note: This resource parameter is for testing purposes in Inspector. - * Once MCP Client SDK supports resource indicators, this parameter - * will be passed to the SDK's auth method similar to how scope is passed. - * - * See: https://github.com/modelcontextprotocol/typescript-sdk/pull/498 - * - * TODO: @xiaoyijun Remove this once MCP Client SDK supports resource indicators. - */ - if (this.resource) { - authorizationUrl.searchParams.set("resource", this.resource); - } window.location.href = authorizationUrl.href; } diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 47e593ac6..245c21fd2 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -58,7 +58,6 @@ interface UseConnectionOptions { headerName?: string; oauthClientId?: string; oauthScope?: string; - oauthResource?: string; config: InspectorConfig; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; @@ -78,7 +77,6 @@ export function useConnection({ headerName, oauthClientId, oauthScope, - oauthResource, config, onNotification, onStdErrNotification, @@ -301,7 +299,6 @@ export function useConnection({ if (is401Error(error)) { const serverAuthProvider = new InspectorOAuthClientProvider( sseUrl, - oauthResource, ); const result = await auth(serverAuthProvider, { @@ -343,10 +340,7 @@ export function useConnection({ const headers: HeadersInit = {}; // Create an auth provider with the current server URL - const serverAuthProvider = new InspectorOAuthClientProvider( - sseUrl, - oauthResource, - ); + const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); // Use manually provided bearer token if available, otherwise use OAuth tokens const token = @@ -566,10 +560,7 @@ export function useConnection({ clientTransport as StreamableHTTPClientTransport ).terminateSession(); await mcpClient?.close(); - const authProvider = new InspectorOAuthClientProvider( - sseUrl, - oauthResource, - ); + const authProvider = new InspectorOAuthClientProvider(sseUrl); authProvider.clear(); setMcpClient(null); setClientTransport(null); From 9e8042949d9f003080c60db457ee387471c899bb Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 14 Jul 2025 17:22:38 +0800 Subject: [PATCH 3/6] refactor: update label for redirect URL --- client/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 972aef720..f4797098b 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -389,7 +389,7 @@ const Sidebar = ({ className="font-mono" /> Date: Mon, 21 Jul 2025 11:24:23 +0800 Subject: [PATCH 4/6] refactor: ui improvement --- client/src/components/Sidebar.tsx | 182 +++++++++++++++--------------- 1 file changed, 89 insertions(+), 93 deletions(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index f4797098b..a41e4bfc7 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -103,10 +103,9 @@ const Sidebar = ({ }: SidebarProps) => { const [theme, setTheme] = useTheme(); const [showEnvVars, setShowEnvVars] = useState(false); - const [showBearerToken, setShowBearerToken] = useState(false); + const [showAuthConfig, setShowAuthConfig] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); - const [showOauthConfig, setShowOauthConfig] = useState(false); const [copiedServerEntry, setCopiedServerEntry] = useState(false); const [copiedServerFile, setCopiedServerFile] = useState(false); const { toast } = useToast(); @@ -317,97 +316,6 @@ const Sidebar = ({ /> )} -
- - {showBearerToken && ( -
- - - setHeaderName && setHeaderName(e.target.value) - } - data-testid="header-input" - className="font-mono" - value={headerName} - /> - - setBearerToken(e.target.value)} - data-testid="bearer-token-input" - className="font-mono" - type="password" - /> -
- )} -
- {/* OAuth Configuration */} -
- - {showOauthConfig && ( -
- - setOauthClientId(e.target.value)} - value={oauthClientId} - data-testid="oauth-client-id-input" - className="font-mono" - /> - - - - setOauthScope(e.target.value)} - value={oauthScope} - data-testid="oauth-scope-input" - className="font-mono" - /> -
- )} -
)} @@ -576,6 +484,94 @@ const Sidebar = ({ +
+ + {showAuthConfig && ( + <> + {/* Bearer Token Section */} +
+

+ API Token Authentication +

+
+ + + setHeaderName && setHeaderName(e.target.value) + } + data-testid="header-input" + className="font-mono" + value={headerName} + /> + + setBearerToken(e.target.value)} + data-testid="bearer-token-input" + className="font-mono" + type="password" + /> +
+
+ {transportType !== "stdio" && ( + // OAuth Configuration +
+

+ OAuth 2.0 Flow +

+
+ + setOauthClientId(e.target.value)} + value={oauthClientId} + data-testid="oauth-client-id-input" + className="font-mono" + /> + + + + setOauthScope(e.target.value)} + value={oauthScope} + data-testid="oauth-scope-input" + className="font-mono" + /> +
+
+ )} + + )} +
{/* Configuration */}