From 73fde03aa4e7d9f5b91bcc5c92d4fd9872b0158d Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 10:56:09 +0100 Subject: [PATCH] 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 4708f08e..55a4541e 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 98d58e23..5565a77a 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 9f079dde..6bd50bcb 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 7c539661..42a289ff 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 2a9171f0..40262601 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 ca473b49..5b2925ac 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 2920ca59..6e5d73f0 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",