From 46d3d844f8884f2099a5e3fb078b5bf337ab2a9f Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 16 May 2025 19:20:40 +0100 Subject: [PATCH 01/20] reset authorization url --- client/src/components/AuthDebugger.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index d0b0432d0..98d58e236 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -183,6 +183,7 @@ const AuthDebugger = ({ latestError: null, oauthClientInfo: null, authorizationCode: "", + authorizationUrl: "", validationError: null, oauthMetadata: null, statusMessage: { From befadc0042ac31ad649dca02f31097cb387541cd Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 16 May 2025 20:53:21 +0100 Subject: [PATCH 02/20] very wip draft auth spec support --- client/src/App.tsx | 3 +- client/src/components/AuthDebugger.tsx | 2 +- client/src/components/OAuthFlowProgress.tsx | 25 +++++++++++++++++ client/src/lib/auth-types.ts | 3 ++ client/src/lib/oauth-state-machine.ts | 31 +++++++++++++++------ 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index cb411452b..a30dcc9c5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -125,8 +125,9 @@ const App = () => { isInitiatingAuth: false, oauthTokens: null, loading: true, - oauthStep: "metadata_discovery", + oauthStep: "resource_metadata_discovery", oauthMetadata: null, + resourceMetadata: null, oauthClientInfo: null, authorizationUrl: null, authorizationCode: "", diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 98d58e236..083fc39b2 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -179,7 +179,7 @@ const AuthDebugger = ({ serverAuthProvider.clear(); updateAuthState({ oauthTokens: null, - oauthStep: "metadata_discovery", + oauthStep: "resource_metadata_discovery", latestError: null, oauthClientInfo: null, authorizationCode: "", diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 396af142d..8167afa65 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -79,6 +79,16 @@ export const OAuthFlowProgress = ({ null, ); + const steps: Array = [ + "resource_metadata_discovery", + "metadata_discovery", + "client_registration", + "authorization_redirect", + "authorization_code", + "token_request", + "complete", + ]; + const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep); useEffect(() => { @@ -124,6 +134,21 @@ export const OAuthFlowProgress = ({

+ + {authState.resourceMetadata && ( +
+ + Retrieved OAuth Resource Metadata from {(new URL('/.well-known/oauth-protected-resource', serverUrl)).href} + +
+                {JSON.stringify(authState.resourceMetadata, null, 2)}
+              
+
+ )} +
Promise; execute: (context: StateMachineContext) => Promise; - nextStep: OAuthStep; } // State machine transitions export const oauthTransitions: Record = { - metadata_discovery: { + resource_metadata_discovery: { canTransition: async () => true, execute: async (context) => { - const metadata = await discoverOAuthMetadata(context.serverUrl); + // TODO: use sdk + const url = new URL("/.well-known/oauth-protected-resource", context.serverUrl); + const response = await fetch(url); + + const resourceMetadata = await response.json(); + context.updateState({ + resourceMetadata: resourceMetadata, + oauthStep: "metadata_discovery", + }); + }, + }, + + metadata_discovery: { + canTransition: async (context) => !!context.state.resourceMetadata, + execute: async (context) => { + // TODO: use sdk + let authServerUrl = context.serverUrl; + if (context.state.resourceMetadata?.authorization_servers?.[0]) { + authServerUrl = context.state.resourceMetadata.authorization_servers[0]; + } + const metadata = await discoverOAuthMetadata(authServerUrl); if (!metadata) { throw new Error("Failed to discover OAuth metadata"); } @@ -37,7 +56,6 @@ export const oauthTransitions: Record = { oauthStep: "client_registration", }); }, - nextStep: "client_registration", }, client_registration: { @@ -62,7 +80,6 @@ export const oauthTransitions: Record = { oauthStep: "authorization_redirect", }); }, - nextStep: "authorization_redirect", }, authorization_redirect: { @@ -93,7 +110,6 @@ export const oauthTransitions: Record = { oauthStep: "authorization_code", }); }, - nextStep: "authorization_code", }, authorization_code: { @@ -114,7 +130,6 @@ export const oauthTransitions: Record = { oauthStep: "token_request", }); }, - nextStep: "token_request", }, token_request: { @@ -144,7 +159,6 @@ export const oauthTransitions: Record = { oauthStep: "complete", }); }, - nextStep: "complete", }, complete: { @@ -152,7 +166,6 @@ export const oauthTransitions: Record = { execute: async () => { // No-op for complete state }, - nextStep: "complete", }, }; From 00893afc0be1577748cac65fc3f95cae2713051c Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 22 May 2025 18:36:48 +0100 Subject: [PATCH 03/20] wip new metadata --- client/src/App.tsx | 2 +- client/src/components/AuthDebugger.tsx | 2 +- client/src/components/OAuthFlowProgress.tsx | 20 ++----------- client/src/lib/auth-types.ts | 1 - client/src/lib/oauth-state-machine.ts | 32 +++++++++------------ 5 files changed, 19 insertions(+), 38 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index a30dcc9c5..4708f08e5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -125,7 +125,7 @@ const App = () => { isInitiatingAuth: false, oauthTokens: null, loading: true, - oauthStep: "resource_metadata_discovery", + oauthStep: "metadata_discovery", oauthMetadata: null, resourceMetadata: null, oauthClientInfo: null, diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 083fc39b2..98d58e236 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -179,7 +179,7 @@ const AuthDebugger = ({ serverAuthProvider.clear(); updateAuthState({ oauthTokens: null, - oauthStep: "resource_metadata_discovery", + oauthStep: "metadata_discovery", latestError: null, oauthClientInfo: null, authorizationCode: "", diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 8167afa65..9f079dde0 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -57,6 +57,7 @@ interface OAuthFlowProgressProps { } const steps: Array = [ + "resource_metadata_discovery", "metadata_discovery", "client_registration", "authorization_redirect", @@ -79,16 +80,6 @@ export const OAuthFlowProgress = ({ null, ); - const steps: Array = [ - "resource_metadata_discovery", - "metadata_discovery", - "client_registration", - "authorization_redirect", - "authorization_code", - "token_request", - "complete", - ]; - const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep); useEffect(() => { @@ -135,8 +126,8 @@ export const OAuthFlowProgress = ({
{authState.resourceMetadata && (
@@ -148,11 +139,6 @@ export const OAuthFlowProgress = ({
)} -
- {provider.getServerMetadata() && (
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index 87e8f8bcc..2a9171f02 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -7,7 +7,6 @@ import { // OAuth flow steps export type OAuthStep = - | "resource_metadata_discovery" | "metadata_discovery" | "client_registration" | "authorization_redirect" diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index bef728d72..a48b32e1a 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -20,31 +20,26 @@ export interface StateTransition { execute: (context: StateMachineContext) => Promise; } +const fetchProtectedResourceMetadata = async (serverUrl: string): Promise => { + // TODO: use sdk + const url = new URL("/.well-known/oauth-protected-resource", serverUrl); + const response = await fetch(url); + const resourceMetadata = await response.json(); + + return resourceMetadata; +} + // State machine transitions export const oauthTransitions: Record = { - resource_metadata_discovery: { + metadata_discovery: { canTransition: async () => true, execute: async (context) => { - // TODO: use sdk - const url = new URL("/.well-known/oauth-protected-resource", context.serverUrl); - const response = await fetch(url); - const resourceMetadata = await response.json(); - context.updateState({ - resourceMetadata: resourceMetadata, - oauthStep: "metadata_discovery", - }); - }, - }, + try { - metadata_discovery: { - canTransition: async (context) => !!context.state.resourceMetadata, - execute: async (context) => { - // TODO: use sdk - let authServerUrl = context.serverUrl; - if (context.state.resourceMetadata?.authorization_servers?.[0]) { - authServerUrl = context.state.resourceMetadata.authorization_servers[0]; } + const resourceMetadata = fetchProtectedResourceMetadata(serverUrl) + const metadata = await discoverOAuthMetadata(authServerUrl); if (!metadata) { throw new Error("Failed to discover OAuth metadata"); @@ -52,6 +47,7 @@ export const oauthTransitions: Record = { const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata); context.provider.saveServerMetadata(parsedMetadata); context.updateState({ + resourceMetadata, oauthMetadata: parsedMetadata, oauthStep: "client_registration", }); From c6ddcf4c90951319f9b6bce72c1d3b3ca72c9d9b Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 22 May 2025 18:40:27 +0100 Subject: [PATCH 04/20] fix --- client/src/lib/oauth-state-machine.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index a48b32e1a..ca473b493 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -34,11 +34,15 @@ export const oauthTransitions: Record = { metadata_discovery: { canTransition: async () => true, execute: async (context) => { - - try { - - } - const resourceMetadata = fetchProtectedResourceMetadata(serverUrl) + const authServerUrl = context.serverUrl; + // try { + // const resourceMetadata = await fetchProtectedResourceMetadata(context.serverUrl); + // if (resourceMetadata && resourceMetadata) { + // authServerUrl = resourceMetadata + // } + // } catch (_error) { + // // pass + // } const metadata = await discoverOAuthMetadata(authServerUrl); if (!metadata) { From b2fa99668efae6b1b0e82d3df854b6546755c765 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 10:19:39 +0100 Subject: [PATCH 05/20] bump typescript sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d46b3ef88..a5e09c6a5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@modelcontextprotocol/inspector-cli": "^0.13.0", "@modelcontextprotocol/inspector-client": "^0.13.0", "@modelcontextprotocol/inspector-server": "^0.13.0", - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.0", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", From 73fde03aa4e7d9f5b91bcc5c92d4fd9872b0158d Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 10:56:09 +0100 Subject: [PATCH 06/20] working metadata fetch --- client/src/App.tsx | 17 +----- client/src/components/AuthDebugger.tsx | 11 +--- client/src/components/OAuthFlowProgress.tsx | 14 ++++- .../__tests__/AuthDebugger.test.tsx | 15 +---- client/src/lib/auth-types.ts | 21 ++++++- client/src/lib/oauth-state-machine.ts | 39 +++++++------ package-lock.json | 58 ++----------------- 7 files changed, 63 insertions(+), 112 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 4708f08e5..55a4541e9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -19,7 +19,7 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; -import { AuthDebuggerState } from "./lib/auth-types"; +import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types"; import { cacheToolOutputSchemas } from "./utils/schemaUtils"; import React, { Suspense, @@ -121,20 +121,7 @@ const App = () => { const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); // Auth debugger state - const [authState, setAuthState] = useState({ - isInitiatingAuth: false, - oauthTokens: null, - loading: true, - oauthStep: "metadata_discovery", - oauthMetadata: null, - resourceMetadata: null, - oauthClientInfo: null, - authorizationUrl: null, - authorizationCode: "", - latestError: null, - statusMessage: null, - validationError: null, - }); + const [authState, setAuthState] = useState(EMPTY_DEBUGGER_STATE); // Helper function to update specific auth state properties const updateAuthState = (updates: Partial) => { diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 98d58e236..5565a77af 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { DebugInspectorOAuthClientProvider } from "../lib/auth"; import { AlertCircle } from "lucide-react"; -import { AuthDebuggerState } from "../lib/auth-types"; +import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types"; import { OAuthFlowProgress } from "./OAuthFlowProgress"; import { OAuthStateMachine } from "../lib/oauth-state-machine"; @@ -178,14 +178,7 @@ const AuthDebugger = ({ ); serverAuthProvider.clear(); updateAuthState({ - oauthTokens: null, - oauthStep: "metadata_discovery", - latestError: null, - oauthClientInfo: null, - authorizationCode: "", - authorizationUrl: "", - validationError: null, - oauthMetadata: null, + ...EMPTY_DEBUGGER_STATE, statusMessage: { type: "success", message: "OAuth tokens cleared successfully", diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 9f079dde0..6bd50bcbb 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -57,7 +57,6 @@ interface OAuthFlowProgressProps { } const steps: Array = [ - "resource_metadata_discovery", "metadata_discovery", "client_registration", "authorization_redirect", @@ -139,6 +138,19 @@ export const OAuthFlowProgress = ({ )} + {authState.resourceMetadataError && ( +
+

+ Failed to retrieve resource metadata, falling back to /.well-known/oauth-authorization-server: +

+

+ {authState.resourceMetadataError.message} + {authState.resourceMetadataError instanceof TypeError + ? " (This could indicate the endpoint doesn't exist or does not have CORS configured)" + : authState.resourceMetadataError['status'] && ` (${authState.resourceMetadataError['status']})`} +

+
+ )} {provider.getServerMetadata() && (
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 7c539661e..42a289ff9 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -51,6 +51,7 @@ import { auth, } from "@modelcontextprotocol/sdk/client/auth.js"; import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types"; // Type the mocked functions properly const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction< @@ -84,19 +85,7 @@ Object.defineProperty(window, "location", { }); describe("AuthDebugger", () => { - const defaultAuthState = { - isInitiatingAuth: false, - oauthTokens: null, - loading: false, - oauthStep: "metadata_discovery" as const, - oauthMetadata: null, - oauthClientInfo: null, - authorizationUrl: null, - authorizationCode: "", - latestError: null, - statusMessage: null, - validationError: null, - }; + const defaultAuthState = EMPTY_DEBUGGER_STATE; const defaultProps = { serverUrl: "https://example.com", diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index 2a9171f02..402626013 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -3,6 +3,7 @@ import { OAuthClientInformationFull, OAuthClientInformation, OAuthTokens, + OAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/shared/auth.js"; // OAuth flow steps @@ -28,8 +29,8 @@ export interface AuthDebuggerState { oauthTokens: OAuthTokens | null; loading: boolean; oauthStep: OAuthStep; - // TODO: use sdk type - resourceMetadata: object | null; + resourceMetadata: OAuthProtectedResourceMetadata | null; + resourceMetadataError: Error | { status: number; statusText: string; message: string } | null; oauthMetadata: OAuthMetadata | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; authorizationUrl: string | null; @@ -38,3 +39,19 @@ export interface AuthDebuggerState { statusMessage: StatusMessage | null; validationError: string | null; } + +export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { + isInitiatingAuth: false, + oauthTokens: null, + loading: true, + oauthStep: "metadata_discovery", + oauthMetadata: null, + resourceMetadata: null, + resourceMetadataError: null, + oauthClientInfo: null, + authorizationUrl: null, + authorizationCode: "", + latestError: null, + statusMessage: null, + validationError: null, +} diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index ca473b493..5b2925acf 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -5,8 +5,9 @@ import { registerClient, startAuthorization, exchangeAuthorization, + discoverOAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/client/auth.js"; -import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { OAuthMetadataSchema, OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; export interface StateMachineContext { state: AuthDebuggerState; @@ -20,29 +21,28 @@ export interface StateTransition { execute: (context: StateMachineContext) => Promise; } -const fetchProtectedResourceMetadata = async (serverUrl: string): Promise => { - // TODO: use sdk - const url = new URL("/.well-known/oauth-protected-resource", serverUrl); - const response = await fetch(url); - const resourceMetadata = await response.json(); - - return resourceMetadata; -} - // State machine transitions export const oauthTransitions: Record = { metadata_discovery: { canTransition: async () => true, execute: async (context) => { - const authServerUrl = context.serverUrl; - // try { - // const resourceMetadata = await fetchProtectedResourceMetadata(context.serverUrl); - // if (resourceMetadata && resourceMetadata) { - // authServerUrl = resourceMetadata - // } - // } catch (_error) { - // // pass - // } + let authServerUrl = context.serverUrl; + let resourceMetadata: OAuthProtectedResourceMetadata | null = null; + let resourceMetadataError: Error | null = null; + try { + resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl); + if (resourceMetadata && resourceMetadata.authorization_servers?.length) { + authServerUrl = resourceMetadata.authorization_servers[0]; + } + } catch (e) { + console.info(`Failed to find protected resource metadata: ${e}`); + console.log(e); + if (e instanceof Error) { + resourceMetadataError = e; + } else { + resourceMetadataError = new Error(String(e)); + } + } const metadata = await discoverOAuthMetadata(authServerUrl); if (!metadata) { @@ -52,6 +52,7 @@ export const oauthTransitions: Record = { context.provider.saveServerMetadata(parsedMetadata); context.updateState({ resourceMetadata, + resourceMetadataError, oauthMetadata: parsedMetadata, oauthStep: "client_registration", }); diff --git a/package-lock.json b/package-lock.json index 2920ca59d..6e5d73f09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.13.0", "@modelcontextprotocol/inspector-client": "^0.13.0", "@modelcontextprotocol/inspector-server": "^0.13.0", - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.0", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", @@ -2005,12 +2005,11 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz", - "integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==", - "license": "MIT", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", + "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", "dependencies": { - "ajv": "^8.17.1", + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -2026,28 +2025,6 @@ "node": ">=18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5719,22 +5696,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -9064,15 +9025,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", From 82858bed30fd5f93192f65c9dc7ea6e6deadbfdf Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 11:17:21 +0100 Subject: [PATCH 07/20] show proper authserverurl --- client/src/components/OAuthFlowProgress.tsx | 72 +++++++++++++-------- client/src/lib/auth-types.ts | 2 + client/src/lib/oauth-state-machine.ts | 5 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 6bd50bcbb..2e74bf68d 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -128,38 +128,54 @@ export const OAuthFlowProgress = ({ label="Metadata Discovery" {...getStepProps("metadata_discovery")} > - {authState.resourceMetadata && ( -
- - Retrieved OAuth Resource Metadata from {(new URL('/.well-known/oauth-protected-resource', serverUrl)).href} - -
-                {JSON.stringify(authState.resourceMetadata, null, 2)}
-              
-
- )} - {authState.resourceMetadataError && ( -
-

- Failed to retrieve resource metadata, falling back to /.well-known/oauth-authorization-server: -

-

- {authState.resourceMetadataError.message} - {authState.resourceMetadataError instanceof TypeError - ? " (This could indicate the endpoint doesn't exist or does not have CORS configured)" - : authState.resourceMetadataError['status'] && ` (${authState.resourceMetadataError['status']})`} -

-
- )} {provider.getServerMetadata() && (
- Retrieved OAuth Metadata from {serverUrl} - /.well-known/oauth-authorization-server + OAuth Metadata Sources + {!authState.resourceMetadata && " ℹ️"} -
-                {JSON.stringify(provider.getServerMetadata(), null, 2)}
-              
+ + {authState.resourceMetadata && ( +
+

Resource Metadata:

+

+ From {new URL('/.well-known/oauth-protected-resource', serverUrl).href} +

+
+                    {JSON.stringify(authState.resourceMetadata, null, 2)}
+                  
+
+ )} + + {authState.resourceMetadataError && ( +
+

+ ℹ️ No resource metadata available from {' '} + + {new URL('/.well-known/oauth-protected-resource', serverUrl).href} + +

+

+ Resource metadata was added in the 2025-DRAFT-v2 specification update +
+ {authState.resourceMetadataError.message} + {authState.resourceMetadataError instanceof TypeError + && " (This could indicate the endpoint doesn't exist or does not have CORS configured)"} +

+
+ )} + + {provider.getServerMetadata() && ( +
+

Authorization Server Metadata:

+ {authState.authServerUrl &&

+ From {new URL('/.well-known/oauth-authorization-server', authState.authServerUrl).href} +

} +
+                    {JSON.stringify(provider.getServerMetadata(), null, 2)}
+                  
+
+ )}
)} diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index 402626013..2541e8222 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -31,6 +31,7 @@ export interface AuthDebuggerState { oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; resourceMetadataError: Error | { status: number; statusText: string; message: string } | null; + authServerUrl: URL | null; oauthMetadata: OAuthMetadata | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; authorizationUrl: string | null; @@ -48,6 +49,7 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { oauthMetadata: null, resourceMetadata: null, resourceMetadataError: null, + authServerUrl: null, oauthClientInfo: null, authorizationUrl: null, authorizationCode: "", diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5b2925acf..939e5d6cd 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -26,13 +26,13 @@ export const oauthTransitions: Record = { metadata_discovery: { canTransition: async () => true, execute: async (context) => { - let authServerUrl = context.serverUrl; + let authServerUrl = new URL(context.serverUrl); let resourceMetadata: OAuthProtectedResourceMetadata | null = null; let resourceMetadataError: Error | null = null; try { resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl); if (resourceMetadata && resourceMetadata.authorization_servers?.length) { - authServerUrl = resourceMetadata.authorization_servers[0]; + authServerUrl = new URL(resourceMetadata.authorization_servers[0]); } } catch (e) { console.info(`Failed to find protected resource metadata: ${e}`); @@ -53,6 +53,7 @@ export const oauthTransitions: Record = { context.updateState({ resourceMetadata, resourceMetadataError, + authServerUrl, oauthMetadata: parsedMetadata, oauthStep: "client_registration", }); From 0dbf75a66427f1f430095d567595e373fcaf185b Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 11:50:09 +0100 Subject: [PATCH 08/20] store state before redirect --- client/src/App.tsx | 31 +++++++++++++++----- client/src/components/AuthDebugger.tsx | 6 ++++ client/src/components/OAuthDebugCallback.tsx | 19 ++++++++++-- client/src/lib/constants.ts | 1 + 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 55a4541e9..8be23a5be 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -234,21 +234,36 @@ const App = () => { ({ authorizationCode, errorMsg, + restoredState, }: { authorizationCode?: string; errorMsg?: string; + restoredState?: AuthDebuggerState; }) => { setIsAuthDebuggerVisible(true); - if (authorizationCode) { + + if (restoredState) { + // Restore the previous auth state updateAuthState({ - authorizationCode, - oauthStep: "token_request", - }); - } - if (errorMsg) { - updateAuthState({ - latestError: new Error(errorMsg), + ...restoredState, + // Update with the new authorization code if provided + authorizationCode: authorizationCode || restoredState.authorizationCode, + oauthStep: authorizationCode ? "token_request" : restoredState.oauthStep, + latestError: errorMsg ? new Error(errorMsg) : restoredState.latestError, }); + } else { + // Fallback to the original behavior if no state was restored + if (authorizationCode) { + updateAuthState({ + authorizationCode, + oauthStep: "token_request", + }); + } + if (errorMsg) { + updateAuthState({ + latestError: new Error(errorMsg), + }); + } } }, [], diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 5565a77af..0ef8e70b6 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -5,6 +5,7 @@ import { AlertCircle } from "lucide-react"; import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types"; import { OAuthFlowProgress } from "./OAuthFlowProgress"; import { OAuthStateMachine } from "../lib/oauth-state-machine"; +import { SESSION_KEYS } from "../lib/constants"; export interface AuthDebuggerProps { serverUrl: string; @@ -141,6 +142,11 @@ const AuthDebugger = ({ currentState.oauthStep === "authorization_code" && currentState.authorizationUrl ) { + // Store the current auth state before redirecting + sessionStorage.setItem( + SESSION_KEYS.AUTH_DEBUGGER_STATE, + JSON.stringify(currentState) + ); // Open the authorization URL automatically window.location.href = currentState.authorizationUrl; break; diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index 88d931c08..af9944ac0 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -9,9 +9,11 @@ interface OAuthCallbackProps { onConnect: ({ authorizationCode, errorMsg, + restoredState, }: { authorizationCode?: string; errorMsg?: string; + restoredState?: any; }) => void; } @@ -34,6 +36,19 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { } const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL); + + // Try to restore the auth state + const storedState = sessionStorage.getItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); + let restoredState = null; + if (storedState) { + try { + restoredState = JSON.parse(storedState); + // Clean up the stored state + sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); + } catch (e) { + console.error("Failed to parse stored auth state:", e); + } + } // ServerURL isn't set, this can happen if we've opened the // authentication request in a new tab, so we don't have the same @@ -50,8 +65,8 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { } // Instead of storing in sessionStorage, pass the code directly - // to the auth state manager through onConnect - onConnect({ authorizationCode: params.code }); + // to the auth state manager through onConnect, along with restored state + onConnect({ authorizationCode: params.code, restoredState }); }; handleCallback().finally(() => { diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 4c3e27aa3..3acbda91f 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -7,6 +7,7 @@ export const SESSION_KEYS = { TOKENS: "mcp_tokens", CLIENT_INFORMATION: "mcp_client_information", SERVER_METADATA: "mcp_server_metadata", + AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state", } as const; // Generate server-specific session storage keys From 20e961ce5c2286de5eee0503a85cc8e0005547ff Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 12:09:52 +0100 Subject: [PATCH 09/20] resumption working --- client/src/App.tsx | 72 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 8be23a5be..f3f1180e4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,6 +20,7 @@ import { import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types"; +import { OAuthStateMachine } from "./lib/oauth-state-machine"; import { cacheToolOutputSchemas } from "./utils/schemaUtils"; import React, { Suspense, @@ -231,7 +232,7 @@ const App = () => { // Update OAuth debug state during debug callback const onOAuthDebugConnect = useCallback( - ({ + async ({ authorizationCode, errorMsg, restoredState, @@ -241,29 +242,64 @@ const App = () => { restoredState?: AuthDebuggerState; }) => { setIsAuthDebuggerVisible(true); - - if (restoredState) { - // Restore the previous auth state + + if (errorMsg) { updateAuthState({ - ...restoredState, - // Update with the new authorization code if provided - authorizationCode: authorizationCode || restoredState.authorizationCode, - oauthStep: authorizationCode ? "token_request" : restoredState.oauthStep, - latestError: errorMsg ? new Error(errorMsg) : restoredState.latestError, + latestError: new Error(errorMsg), }); - } else { - // Fallback to the original behavior if no state was restored - if (authorizationCode) { - updateAuthState({ - authorizationCode, - oauthStep: "token_request", + return; + } + + if (restoredState && authorizationCode) { + // Restore the previous auth state and continue the OAuth flow + let currentState: AuthDebuggerState = { + ...restoredState, + authorizationCode, + oauthStep: "token_request", + isInitiatingAuth: true, + statusMessage: null, + latestError: null, + }; + + try { + // Create a new state machine instance to continue the flow + const stateMachine = new OAuthStateMachine(sseUrl, (updates) => { + currentState = { ...currentState, ...updates }; }); - } - if (errorMsg) { + + // Continue stepping through the OAuth flow from where we left off + while (currentState.oauthStep !== "complete" && currentState.oauthStep !== "authorization_code") { + await stateMachine.executeStep(currentState); + } + + if (currentState.oauthStep === "complete") { + // After the flow completes or reaches a user-input step, update the app state + updateAuthState({ + ...currentState, + statusMessage: { + type: "success", + message: "Authentication completed successfully", + }, + isInitiatingAuth: false, + }); + } + } catch (error) { + console.error("OAuth continuation error:", error); updateAuthState({ - latestError: new Error(errorMsg), + latestError: error instanceof Error ? error : new Error(String(error)), + statusMessage: { + type: "error", + message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`, + }, + isInitiatingAuth: false, }); } + } else if (authorizationCode) { + // Fallback to the original behavior if no state was restored + updateAuthState({ + authorizationCode, + oauthStep: "token_request", + }); } }, [], From 35eab580efa9010c88ff43dd91b483e61bcc43a1 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 12:26:53 +0100 Subject: [PATCH 10/20] switch to metadata from state --- client/src/components/AuthDebugger.tsx | 19 ++++++++++++++++++- client/src/components/OAuthFlowProgress.tsx | 6 +++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 0ef8e70b6..a90b10612 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { DebugInspectorOAuthClientProvider } from "../lib/auth"; import { AlertCircle } from "lucide-react"; @@ -60,6 +60,23 @@ const AuthDebugger = ({ authState, updateAuthState, }: AuthDebuggerProps) => { + // Initialize loading state + useEffect(() => { + if (authState.loading && serverUrl) { + // Check if we have existing tokens + const checkTokens = async () => { + const provider = new DebugInspectorOAuthClientProvider(serverUrl); + const existingTokens = await provider.tokens(); + + updateAuthState({ + loading: false, + oauthTokens: existingTokens || null, + }); + }; + + checkTokens(); + } + }, [serverUrl, authState.loading, updateAuthState]); const startOAuthFlow = useCallback(() => { if (!serverUrl) { updateAuthState({ diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 2e74bf68d..f86ceec8b 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -128,7 +128,7 @@ export const OAuthFlowProgress = ({ label="Metadata Discovery" {...getStepProps("metadata_discovery")} > - {provider.getServerMetadata() && ( + {authState.oauthMetadata && (
OAuth Metadata Sources @@ -165,14 +165,14 @@ export const OAuthFlowProgress = ({ )} - {provider.getServerMetadata() && ( + {authState.oauthMetadata && (

Authorization Server Metadata:

{authState.authServerUrl &&

From {new URL('/.well-known/oauth-authorization-server', authState.authServerUrl).href}

}
-                    {JSON.stringify(provider.getServerMetadata(), null, 2)}
+                    {JSON.stringify(authState.oauthMetadata, null, 2)}
                   
)} From cc49358f5ab7c037fd7349031512b4caac345795 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 13:42:54 +0100 Subject: [PATCH 11/20] add test + prettier --- client/src/App.tsx | 11 +++- client/src/components/AuthDebugger.tsx | 6 +- client/src/components/OAuthDebugCallback.tsx | 6 +- client/src/components/OAuthFlowProgress.tsx | 52 ++++++++++++--- .../__tests__/AuthDebugger.test.tsx | 66 +++++++++++++++++++ client/src/lib/auth-types.ts | 7 +- client/src/lib/oauth-state-machine.ts | 14 +++- 7 files changed, 139 insertions(+), 23 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index f3f1180e4..74cdca19d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -122,7 +122,8 @@ const App = () => { const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); // Auth debugger state - const [authState, setAuthState] = useState(EMPTY_DEBUGGER_STATE); + const [authState, setAuthState] = + useState(EMPTY_DEBUGGER_STATE); // Helper function to update specific auth state properties const updateAuthState = (updates: Partial) => { @@ -268,7 +269,10 @@ const App = () => { }); // Continue stepping through the OAuth flow from where we left off - while (currentState.oauthStep !== "complete" && currentState.oauthStep !== "authorization_code") { + while ( + currentState.oauthStep !== "complete" && + currentState.oauthStep !== "authorization_code" + ) { await stateMachine.executeStep(currentState); } @@ -286,7 +290,8 @@ const App = () => { } catch (error) { console.error("OAuth continuation error:", error); updateAuthState({ - latestError: error instanceof Error ? error : new Error(String(error)), + latestError: + error instanceof Error ? error : new Error(String(error)), statusMessage: { type: "error", message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`, diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index a90b10612..b88758d4b 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -67,13 +67,13 @@ const AuthDebugger = ({ const checkTokens = async () => { const provider = new DebugInspectorOAuthClientProvider(serverUrl); const existingTokens = await provider.tokens(); - + updateAuthState({ loading: false, oauthTokens: existingTokens || null, }); }; - + checkTokens(); } }, [serverUrl, authState.loading, updateAuthState]); @@ -162,7 +162,7 @@ const AuthDebugger = ({ // Store the current auth state before redirecting sessionStorage.setItem( SESSION_KEYS.AUTH_DEBUGGER_STATE, - JSON.stringify(currentState) + JSON.stringify(currentState), ); // Open the authorization URL automatically window.location.href = currentState.authorizationUrl; diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index af9944ac0..17705abb0 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -36,9 +36,11 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { } const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL); - + // Try to restore the auth state - const storedState = sessionStorage.getItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); + const storedState = sessionStorage.getItem( + SESSION_KEYS.AUTH_DEBUGGER_STATE, + ); let restoredState = null; if (storedState) { try { diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index f86ceec8b..b9b67c6c4 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -139,7 +139,13 @@ export const OAuthFlowProgress = ({

Resource Metadata:

- From {new URL('/.well-known/oauth-protected-resource', serverUrl).href} + From{" "} + { + new URL( + "/.well-known/oauth-protected-resource", + serverUrl, + ).href + }

                     {JSON.stringify(authState.resourceMetadata, null, 2)}
@@ -150,17 +156,35 @@ export const OAuthFlowProgress = ({
               {authState.resourceMetadataError && (
                 

- ℹ️ No resource metadata available from {' '} - - {new URL('/.well-known/oauth-protected-resource', serverUrl).href} + ℹ️ No resource metadata available from{" "} + + { + new URL( + "/.well-known/oauth-protected-resource", + serverUrl, + ).href + }

- Resource metadata was added in the 2025-DRAFT-v2 specification update + Resource metadata was added in the{" "} + + 2025-DRAFT-v2 specification update +
{authState.resourceMetadataError.message} - {authState.resourceMetadataError instanceof TypeError - && " (This could indicate the endpoint doesn't exist or does not have CORS configured)"} + {authState.resourceMetadataError instanceof TypeError && + " (This could indicate the endpoint doesn't exist or does not have CORS configured)"}

)} @@ -168,9 +192,17 @@ export const OAuthFlowProgress = ({ {authState.oauthMetadata && (

Authorization Server Metadata:

- {authState.authServerUrl &&

- From {new URL('/.well-known/oauth-authorization-server', authState.authServerUrl).href} -

} + {authState.authServerUrl && ( +

+ From{" "} + { + new URL( + "/.well-known/oauth-authorization-server", + authState.authServerUrl, + ).href + } +

+ )}
                     {JSON.stringify(authState.oauthMetadata, null, 2)}
                   
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 42a289ff9..fe5a94f95 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -40,6 +40,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ registerClient: jest.fn(), startAuthorization: jest.fn(), exchangeAuthorization: jest.fn(), + discoverOAuthProtectedResourceMetadata: jest.fn(), })); // Import the functions to get their types @@ -49,6 +50,7 @@ import { startAuthorization, exchangeAuthorization, auth, + discoverOAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/client/auth.js"; import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types"; @@ -67,6 +69,10 @@ const mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction< typeof exchangeAuthorization >; const mockAuth = auth as jest.MockedFunction; +const mockDiscoverOAuthProtectedResourceMetadata = + discoverOAuthProtectedResourceMetadata as jest.MockedFunction< + typeof discoverOAuthProtectedResourceMetadata + >; const sessionStorageMock = { getItem: jest.fn(), @@ -100,6 +106,7 @@ describe("AuthDebugger", () => { mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); + mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null); mockStartAuthorization.mockImplementation(async (_sseUrl, options) => { const authUrl = new URL("https://oauth.example.com/authorize"); @@ -421,4 +428,63 @@ describe("AuthDebugger", () => { }); }); }); + + describe("OAuth State Persistence", () => { + it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => { + const updateAuthState = jest.fn(); + + // Mock window.location.href setter + delete (window as any).location; + window.location = { href: "" } as any; + + // Setup mocks for OAuth flow + mockStartAuthorization.mockResolvedValue({ + authorizationUrl: new URL( + "https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug", + ), + codeVerifier: "test_verifier", + }); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { ...defaultAuthState, loading: false }, + }); + }); + + // Click Quick OAuth Flow + await act(async () => { + fireEvent.click(screen.getByText("Quick OAuth Flow")); + }); + + // Wait for the flow to reach the authorization step + await waitFor(() => { + expect(sessionStorage.setItem).toHaveBeenCalledWith( + SESSION_KEYS.AUTH_DEBUGGER_STATE, + expect.stringContaining('"oauthStep":"authorization_code"'), + ); + }); + + // Verify the stored state includes all the accumulated data + const storedStateCall = ( + sessionStorage.setItem as jest.Mock + ).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE); + + expect(storedStateCall).toBeDefined(); + const storedState = JSON.parse(storedStateCall![1]); + + expect(storedState).toMatchObject({ + oauthStep: "authorization_code", + authorizationUrl: expect.stringMatching( + /^https:\/\/oauth\.example\.com\/authorize/, + ), + oauthMetadata: expect.objectContaining({ + token_endpoint: "https://oauth.example.com/token", + }), + oauthClientInfo: expect.objectContaining({ + client_id: "test_client_id", + }), + }); + }); + }); }); diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index 2541e8222..e69dcd124 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -30,7 +30,10 @@ export interface AuthDebuggerState { loading: boolean; oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; - resourceMetadataError: Error | { status: number; statusText: string; message: string } | null; + resourceMetadataError: + | Error + | { status: number; statusText: string; message: string } + | null; authServerUrl: URL | null; oauthMetadata: OAuthMetadata | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; @@ -56,4 +59,4 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { latestError: null, statusMessage: null, validationError: null, -} +}; diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 939e5d6cd..d05369c41 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -7,7 +7,10 @@ import { exchangeAuthorization, discoverOAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/client/auth.js"; -import { OAuthMetadataSchema, OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { + OAuthMetadataSchema, + OAuthProtectedResourceMetadata, +} from "@modelcontextprotocol/sdk/shared/auth.js"; export interface StateMachineContext { state: AuthDebuggerState; @@ -30,8 +33,13 @@ export const oauthTransitions: Record = { let resourceMetadata: OAuthProtectedResourceMetadata | null = null; let resourceMetadataError: Error | null = null; try { - resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl); - if (resourceMetadata && resourceMetadata.authorization_servers?.length) { + resourceMetadata = await discoverOAuthProtectedResourceMetadata( + context.serverUrl, + ); + if ( + resourceMetadata && + resourceMetadata.authorization_servers?.length + ) { authServerUrl = new URL(resourceMetadata.authorization_servers[0]); } } catch (e) { From 03bb303bf9f1c07fba565c7771c5ad5d0c1ae70e Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 14:10:32 +0100 Subject: [PATCH 12/20] rm loading, prm test --- client/src/App.tsx | 2 - client/src/components/AuthDebugger.tsx | 26 ++-- .../__tests__/AuthDebugger.test.tsx | 123 +++++++++++++++++- client/src/lib/auth-types.ts | 2 - 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 74cdca19d..a332f9aa3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -329,8 +329,6 @@ const App = () => { } } catch (error) { console.error("Error loading OAuth tokens:", error); - } finally { - updateAuthState({ loading: false }); } }; diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index b88758d4b..9757af95a 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -60,23 +60,23 @@ const AuthDebugger = ({ authState, updateAuthState, }: AuthDebuggerProps) => { - // Initialize loading state + // Check for existing tokens on mount useEffect(() => { - if (authState.loading && serverUrl) { - // Check if we have existing tokens + if (serverUrl && !authState.oauthTokens) { const checkTokens = async () => { const provider = new DebugInspectorOAuthClientProvider(serverUrl); const existingTokens = await provider.tokens(); - - updateAuthState({ - loading: false, - oauthTokens: existingTokens || null, - }); + if (existingTokens) { + updateAuthState({ + oauthTokens: existingTokens, + oauthStep: "complete", + }); + } }; - checkTokens(); } - }, [serverUrl, authState.loading, updateAuthState]); + }, [serverUrl]); // Only run when serverUrl changes + const startOAuthFlow = useCallback(() => { if (!serverUrl) { updateAuthState({ @@ -241,10 +241,7 @@ const AuthDebugger = ({ )} - {authState.loading ? ( -

Loading authentication status...

- ) : ( -
+
{authState.oauthTokens && (

Access Token:

@@ -286,7 +283,6 @@ const AuthDebugger = ({ the standard automatic flow.

- )}
{ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); - mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null); + mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( + new Error("No protected resource metadata found") + ); mockStartAuthorization.mockImplementation(async (_sseUrl, options) => { const authUrl = new URL("https://oauth.example.com/authorize"); @@ -448,7 +450,7 @@ describe("AuthDebugger", () => { await act(async () => { renderAuthDebugger({ updateAuthState, - authState: { ...defaultAuthState, loading: false }, + authState: { ...defaultAuthState }, }); }); @@ -471,7 +473,7 @@ describe("AuthDebugger", () => { ).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE); expect(storedStateCall).toBeDefined(); - const storedState = JSON.parse(storedStateCall![1]); + const storedState = JSON.parse(storedStateCall![1] as string); expect(storedState).toMatchObject({ oauthStep: "authorization_code", @@ -487,4 +489,119 @@ describe("AuthDebugger", () => { }); }); }); + + describe("OAuth Protected Resource Metadata", () => { + it("should successfully fetch and display protected resource metadata", async () => { + const updateAuthState = jest.fn(); + const mockResourceMetadata = { + resource: "https://example.com/api", + authorization_servers: ["https://custom-auth.example.com"], + bearer_methods_supported: ["header", "body"], + resource_documentation: "https://example.com/api/docs", + resource_policy_uri: "https://example.com/api/policy", + }; + + // Mock successful metadata discovery + mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue( + mockResourceMetadata + ); + mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { ...defaultAuthState }, + }); + }); + + // Click Guided OAuth Flow to start the process + await act(async () => { + fireEvent.click(screen.getByText("Guided OAuth Flow")); + }); + + // Verify that the flow started with metadata discovery + expect(updateAuthState).toHaveBeenCalledWith({ + oauthStep: "metadata_discovery", + authorizationUrl: null, + statusMessage: null, + latestError: null, + }); + + // Click Continue to trigger metadata discovery + const continueButton = await screen.findByText("Continue"); + await act(async () => { + fireEvent.click(continueButton); + }); + + // Wait for the metadata to be fetched + await waitFor(() => { + expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( + "https://example.com" + ); + }); + + // Verify the state was updated with the resource metadata + await waitFor(() => { + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + resourceMetadata: mockResourceMetadata, + authServerUrl: new URL("https://custom-auth.example.com"), + oauthStep: "client_registration", + }) + ); + }); + }); + + it("should handle protected resource metadata fetch failure gracefully", async () => { + const updateAuthState = jest.fn(); + const mockError = new Error("Failed to fetch resource metadata"); + + // Mock failed metadata discovery + mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError); + // But OAuth metadata should still work with the original URL + mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { ...defaultAuthState }, + }); + }); + + // Click Guided OAuth Flow + await act(async () => { + fireEvent.click(screen.getByText("Guided OAuth Flow")); + }); + + // Click Continue to trigger metadata discovery + const continueButton = await screen.findByText("Continue"); + await act(async () => { + fireEvent.click(continueButton); + }); + + // Wait for the metadata fetch to fail + await waitFor(() => { + expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( + "https://example.com" + ); + }); + + // Verify the flow continues despite the error + await waitFor(() => { + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + resourceMetadataError: mockError, + // Should use the original server URL as fallback + authServerUrl: new URL("https://example.com"), + oauthStep: "client_registration", + }) + ); + }); + + // Verify that regular OAuth metadata discovery was still called + expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( + new URL("https://example.com") + ); + }); + }); }); diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index e69dcd124..daf1bc15f 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -27,7 +27,6 @@ export interface StatusMessage { export interface AuthDebuggerState { isInitiatingAuth: boolean; oauthTokens: OAuthTokens | null; - loading: boolean; oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; resourceMetadataError: @@ -47,7 +46,6 @@ export interface AuthDebuggerState { export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { isInitiatingAuth: false, oauthTokens: null, - loading: true, oauthStep: "metadata_discovery", oauthMetadata: null, resourceMetadata: null, From 11bc04364bfc012b2170be97ce530af1c118e547 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 14:11:01 +0100 Subject: [PATCH 13/20] whitespace --- client/src/components/AuthDebugger.tsx | 72 +++++++++---------- .../__tests__/AuthDebugger.test.tsx | 14 ++-- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 9757af95a..48c9cb008 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -242,47 +242,47 @@ const AuthDebugger = ({ )}
- {authState.oauthTokens && ( -
-

Access Token:

-
- {authState.oauthTokens.access_token.substring(0, 25)}... -
+ {authState.oauthTokens && ( +
+

Access Token:

+
+ {authState.oauthTokens.access_token.substring(0, 25)}...
- )} - -
- +
+ )} - +
+ - -
+ -

- Choose "Guided" for step-by-step instructions or "Quick" for - the standard automatic flow. -

+
+ +

+ Choose "Guided" for step-by-step instructions or "Quick" for + the standard automatic flow. +

+
{ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( - new Error("No protected resource metadata found") + new Error("No protected resource metadata found"), ); mockStartAuthorization.mockImplementation(async (_sseUrl, options) => { const authUrl = new URL("https://oauth.example.com/authorize"); @@ -503,7 +503,7 @@ describe("AuthDebugger", () => { // Mock successful metadata discovery mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue( - mockResourceMetadata + mockResourceMetadata, ); mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); @@ -536,7 +536,7 @@ describe("AuthDebugger", () => { // Wait for the metadata to be fetched await waitFor(() => { expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( - "https://example.com" + "https://example.com", ); }); @@ -547,7 +547,7 @@ describe("AuthDebugger", () => { resourceMetadata: mockResourceMetadata, authServerUrl: new URL("https://custom-auth.example.com"), oauthStep: "client_registration", - }) + }), ); }); }); @@ -582,7 +582,7 @@ describe("AuthDebugger", () => { // Wait for the metadata fetch to fail await waitFor(() => { expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( - "https://example.com" + "https://example.com", ); }); @@ -594,13 +594,13 @@ describe("AuthDebugger", () => { // Should use the original server URL as fallback authServerUrl: new URL("https://example.com"), oauthStep: "client_registration", - }) + }), ); }); // Verify that regular OAuth metadata discovery was still called expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - new URL("https://example.com") + new URL("https://example.com"), ); }); }); From e74f80db8591c288c394d7912b57e18bf93e4b78 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 14:26:29 +0100 Subject: [PATCH 14/20] fix tests --- client/src/components/AuthDebugger.tsx | 20 +++++++++++-------- .../__tests__/AuthDebugger.test.tsx | 9 +++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 48c9cb008..ec09963ec 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -64,18 +64,22 @@ const AuthDebugger = ({ useEffect(() => { if (serverUrl && !authState.oauthTokens) { const checkTokens = async () => { - const provider = new DebugInspectorOAuthClientProvider(serverUrl); - const existingTokens = await provider.tokens(); - if (existingTokens) { - updateAuthState({ - oauthTokens: existingTokens, - oauthStep: "complete", - }); + try { + const provider = new DebugInspectorOAuthClientProvider(serverUrl); + const existingTokens = await provider.tokens(); + if (existingTokens) { + updateAuthState({ + oauthTokens: existingTokens, + oauthStep: "complete", + }); + } + } catch (error) { + console.error("Failed to load existing OAuth tokens:", error); } }; checkTokens(); } - }, [serverUrl]); // Only run when serverUrl changes + }, [serverUrl, updateAuthState, authState.oauthTokens]); const startOAuthFlow = useCallback(() => { if (!serverUrl) { diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index b0df65ebc..b2fa4c614 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -202,7 +202,7 @@ describe("AuthDebugger", () => { // Should first discover and save OAuth metadata expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - "https://example.com", + new URL("https://example.com"), ); // Check that updateAuthState was called with the right info message @@ -314,6 +314,11 @@ describe("AuthDebugger", () => { }); expect(updateAuthState).toHaveBeenCalledWith({ + authServerUrl: null, + authorizationUrl: null, + isInitiatingAuth: false, + resourceMetadata: null, + resourceMetadataError: null, oauthTokens: null, oauthStep: "metadata_discovery", latestError: null, @@ -355,7 +360,7 @@ describe("AuthDebugger", () => { }); expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - "https://example.com", + new URL("https://example.com"), ); }); From 2d7936d10b7abeed521091cb3c3173230847abe7 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 2 Jun 2025 16:20:16 +0100 Subject: [PATCH 15/20] fix type errors --- client/src/components/OAuthDebugCallback.tsx | 3 ++- .../src/components/__tests__/AuthDebugger.test.tsx | 12 ++++++++++-- client/src/lib/auth-types.ts | 5 +---- package.json | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index 17705abb0..99774297f 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -4,6 +4,7 @@ import { generateOAuthErrorDescription, parseOAuthCallbackParams, } from "@/utils/oauthUtils.ts"; +import { AuthDebuggerState } from "@/lib/auth-types"; interface OAuthCallbackProps { onConnect: ({ @@ -13,7 +14,7 @@ interface OAuthCallbackProps { }: { authorizationCode?: string; errorMsg?: string; - restoredState?: any; + restoredState?: AuthDebuggerState; }) => void; } diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index b2fa4c614..772b3d435 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -441,8 +441,16 @@ describe("AuthDebugger", () => { const updateAuthState = jest.fn(); // Mock window.location.href setter - delete (window as any).location; - window.location = { href: "" } as any; + const originalLocation = window.location; + const locationMock = { + ...originalLocation, + href: "", + origin: "http://localhost:3000" + }; + Object.defineProperty(window, 'location', { + writable: true, + value: locationMock + }); // Setup mocks for OAuth flow mockStartAuthorization.mockResolvedValue({ diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index daf1bc15f..5e8113ef8 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -29,10 +29,7 @@ export interface AuthDebuggerState { oauthTokens: OAuthTokens | null; oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; - resourceMetadataError: - | Error - | { status: number; statusText: string; message: string } - | null; + resourceMetadataError: Error | null; authServerUrl: URL | null; oauthMetadata: OAuthMetadata | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; diff --git a/package.json b/package.json index a5e09c6a5..6ee5719e3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "test-cli": "cd cli && npm run test", "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", + "lint": "prettier --check . && cd client && npm run lint", "prepare": "npm run build", "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, From 98668507c798ce7010e209557ff35be2ebcad92e Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 2 Jun 2025 16:20:36 +0100 Subject: [PATCH 16/20] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ee5719e3..c0f7cf736 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@modelcontextprotocol/inspector-cli": "^0.13.0", "@modelcontextprotocol/inspector-client": "^0.13.0", "@modelcontextprotocol/inspector-server": "^0.13.0", - "@modelcontextprotocol/sdk": "^1.12.0", + "@modelcontextprotocol/sdk": "^1.12.1", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", From fa01fafa59e34285bd5e5580201dc4b3ec13ce18 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 2 Jun 2025 16:21:55 +0100 Subject: [PATCH 17/20] version bump + lint --- client/src/components/__tests__/AuthDebugger.test.tsx | 6 +++--- package-lock.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 772b3d435..3727128a7 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -445,11 +445,11 @@ describe("AuthDebugger", () => { const locationMock = { ...originalLocation, href: "", - origin: "http://localhost:3000" + origin: "http://localhost:3000", }; - Object.defineProperty(window, 'location', { + Object.defineProperty(window, "location", { writable: true, - value: locationMock + value: locationMock, }); // Setup mocks for OAuth flow diff --git a/package-lock.json b/package-lock.json index 6e5d73f09..365883541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.13.0", "@modelcontextprotocol/inspector-client": "^0.13.0", "@modelcontextprotocol/inspector-server": "^0.13.0", - "@modelcontextprotocol/sdk": "^1.12.0", + "@modelcontextprotocol/sdk": "^1.12.1", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", @@ -2005,9 +2005,9 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", - "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", From 9371006838de8224046eb8c0d3656b7eb9355af4 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Tue, 3 Jun 2025 11:02:34 +0100 Subject: [PATCH 18/20] less noisy errors --- client/src/components/__tests__/AuthDebugger.test.tsx | 7 +++++++ client/src/lib/oauth-state-machine.ts | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 3727128a7..2130e68b8 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -104,6 +104,9 @@ describe("AuthDebugger", () => { jest.clearAllMocks(); sessionStorageMock.getItem.mockReturnValue(null); + // Supress + jest.spyOn(console, "error").mockImplementation(() => {}); + mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( @@ -124,6 +127,10 @@ describe("AuthDebugger", () => { mockExchangeAuthorization.mockResolvedValue(mockOAuthTokens); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + const renderAuthDebugger = (props: Partial = {}) => { const mergedProps = { ...defaultProps, diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index d05369c41..101a33cfc 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -43,8 +43,6 @@ export const oauthTransitions: Record = { authServerUrl = new URL(resourceMetadata.authorization_servers[0]); } } catch (e) { - console.info(`Failed to find protected resource metadata: ${e}`); - console.log(e); if (e instanceof Error) { resourceMetadataError = e; } else { From e2d3989bb219cd34d249cb1d2cc4c5169e85ae30 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Tue, 3 Jun 2025 11:05:10 +0100 Subject: [PATCH 19/20] use scopes from PRM if available --- client/src/lib/oauth-state-machine.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 101a33cfc..5f10a7830 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -72,9 +72,13 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientMetadata = context.provider.clientMetadata; + // Prefer scopes from resource metadata if available + const scopesSupported = + context.state.resourceMetadata?.scopes_supported || + metadata.scopes_supported; // Add all supported scopes to client registration - if (metadata.scopes_supported) { - clientMetadata.scope = metadata.scopes_supported.join(" "); + if (scopesSupported) { + clientMetadata.scope = scopesSupported.join(" "); } const fullInformation = await registerClient(context.serverUrl, { From a387c6ad7e5f006a3138aff5235e42d187cbb8e9 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Tue, 3 Jun 2025 17:29:22 +0100 Subject: [PATCH 20/20] hook warning --- client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 00d3bb48c..2fbc7494e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -307,7 +307,7 @@ const App = () => { }); } }, - [], + [sseUrl], ); // Load OAuth tokens when sseUrl changes