working metadata fetch

This commit is contained in:
Paul Carleton 2025-05-29 10:56:09 +01:00
commit 73fde03aa4
No known key found for this signature in database
7 changed files with 63 additions and 112 deletions

View file

@ -19,7 +19,7 @@ import {
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; 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 { cacheToolOutputSchemas } from "./utils/schemaUtils";
import React, { import React, {
Suspense, Suspense,
@ -121,20 +121,7 @@ const App = () => {
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
// Auth debugger state // Auth debugger state
const [authState, setAuthState] = useState<AuthDebuggerState>({ const [authState, setAuthState] = useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
isInitiatingAuth: false,
oauthTokens: null,
loading: true,
oauthStep: "metadata_discovery",
oauthMetadata: null,
resourceMetadata: null,
oauthClientInfo: null,
authorizationUrl: null,
authorizationCode: "",
latestError: null,
statusMessage: null,
validationError: null,
});
// Helper function to update specific auth state properties // Helper function to update specific auth state properties
const updateAuthState = (updates: Partial<AuthDebuggerState>) => { const updateAuthState = (updates: Partial<AuthDebuggerState>) => {

View file

@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DebugInspectorOAuthClientProvider } from "../lib/auth"; import { DebugInspectorOAuthClientProvider } from "../lib/auth";
import { AlertCircle } from "lucide-react"; 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 { OAuthFlowProgress } from "./OAuthFlowProgress";
import { OAuthStateMachine } from "../lib/oauth-state-machine"; import { OAuthStateMachine } from "../lib/oauth-state-machine";
@ -178,14 +178,7 @@ const AuthDebugger = ({
); );
serverAuthProvider.clear(); serverAuthProvider.clear();
updateAuthState({ updateAuthState({
oauthTokens: null, ...EMPTY_DEBUGGER_STATE,
oauthStep: "metadata_discovery",
latestError: null,
oauthClientInfo: null,
authorizationCode: "",
authorizationUrl: "",
validationError: null,
oauthMetadata: null,
statusMessage: { statusMessage: {
type: "success", type: "success",
message: "OAuth tokens cleared successfully", message: "OAuth tokens cleared successfully",

View file

@ -57,7 +57,6 @@ interface OAuthFlowProgressProps {
} }
const steps: Array<OAuthStep> = [ const steps: Array<OAuthStep> = [
"resource_metadata_discovery",
"metadata_discovery", "metadata_discovery",
"client_registration", "client_registration",
"authorization_redirect", "authorization_redirect",
@ -139,6 +138,19 @@ export const OAuthFlowProgress = ({
</pre> </pre>
</details> </details>
)} )}
{authState.resourceMetadataError && (
<div className="mt-2 p-3 border border-yellow-300 bg-yellow-50 rounded-md">
<p className="text-sm font-small text-yellow-700">
Failed to retrieve resource metadata, falling back to /.well-known/oauth-authorization-server:
</p>
<p className="text-xs text-yellow-600 mt-1">
{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']})`}
</p>
</div>
)}
{provider.getServerMetadata() && ( {provider.getServerMetadata() && (
<details className="text-xs mt-2"> <details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium"> <summary className="cursor-pointer text-muted-foreground font-medium">

View file

@ -51,6 +51,7 @@ import {
auth, auth,
} from "@modelcontextprotocol/sdk/client/auth.js"; } from "@modelcontextprotocol/sdk/client/auth.js";
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
// Type the mocked functions properly // Type the mocked functions properly
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction< const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
@ -84,19 +85,7 @@ Object.defineProperty(window, "location", {
}); });
describe("AuthDebugger", () => { describe("AuthDebugger", () => {
const defaultAuthState = { const defaultAuthState = EMPTY_DEBUGGER_STATE;
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 defaultProps = { const defaultProps = {
serverUrl: "https://example.com", serverUrl: "https://example.com",

View file

@ -3,6 +3,7 @@ import {
OAuthClientInformationFull, OAuthClientInformationFull,
OAuthClientInformation, OAuthClientInformation,
OAuthTokens, OAuthTokens,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js"; } from "@modelcontextprotocol/sdk/shared/auth.js";
// OAuth flow steps // OAuth flow steps
@ -28,8 +29,8 @@ export interface AuthDebuggerState {
oauthTokens: OAuthTokens | null; oauthTokens: OAuthTokens | null;
loading: boolean; loading: boolean;
oauthStep: OAuthStep; oauthStep: OAuthStep;
// TODO: use sdk type resourceMetadata: OAuthProtectedResourceMetadata | null;
resourceMetadata: object | null; resourceMetadataError: Error | { status: number; statusText: string; message: string } | null;
oauthMetadata: OAuthMetadata | null; oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
authorizationUrl: string | null; authorizationUrl: string | null;
@ -38,3 +39,19 @@ export interface AuthDebuggerState {
statusMessage: StatusMessage | null; statusMessage: StatusMessage | null;
validationError: string | 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,
}

View file

@ -5,8 +5,9 @@ import {
registerClient, registerClient,
startAuthorization, startAuthorization,
exchangeAuthorization, exchangeAuthorization,
discoverOAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/client/auth.js"; } 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 { export interface StateMachineContext {
state: AuthDebuggerState; state: AuthDebuggerState;
@ -20,29 +21,28 @@ export interface StateTransition {
execute: (context: StateMachineContext) => Promise<void>; execute: (context: StateMachineContext) => Promise<void>;
} }
const fetchProtectedResourceMetadata = async (serverUrl: string): Promise<object> => {
// 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 // State machine transitions
export const oauthTransitions: Record<OAuthStep, StateTransition> = { export const oauthTransitions: Record<OAuthStep, StateTransition> = {
metadata_discovery: { metadata_discovery: {
canTransition: async () => true, canTransition: async () => true,
execute: async (context) => { execute: async (context) => {
const authServerUrl = context.serverUrl; let authServerUrl = context.serverUrl;
// try { let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
// const resourceMetadata = await fetchProtectedResourceMetadata(context.serverUrl); let resourceMetadataError: Error | null = null;
// if (resourceMetadata && resourceMetadata) { try {
// authServerUrl = resourceMetadata resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl);
// } if (resourceMetadata && resourceMetadata.authorization_servers?.length) {
// } catch (_error) { authServerUrl = resourceMetadata.authorization_servers[0];
// // pass }
// } } 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); const metadata = await discoverOAuthMetadata(authServerUrl);
if (!metadata) { if (!metadata) {
@ -52,6 +52,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
context.provider.saveServerMetadata(parsedMetadata); context.provider.saveServerMetadata(parsedMetadata);
context.updateState({ context.updateState({
resourceMetadata, resourceMetadata,
resourceMetadataError,
oauthMetadata: parsedMetadata, oauthMetadata: parsedMetadata,
oauthStep: "client_registration", oauthStep: "client_registration",
}); });

58
package-lock.json generated
View file

@ -17,7 +17,7 @@
"@modelcontextprotocol/inspector-cli": "^0.13.0", "@modelcontextprotocol/inspector-cli": "^0.13.0",
"@modelcontextprotocol/inspector-client": "^0.13.0", "@modelcontextprotocol/inspector-client": "^0.13.0",
"@modelcontextprotocol/inspector-server": "^0.13.0", "@modelcontextprotocol/inspector-server": "^0.13.0",
"@modelcontextprotocol/sdk": "^1.11.5", "@modelcontextprotocol/sdk": "^1.12.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"open": "^10.1.0", "open": "^10.1.0",
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
@ -2005,12 +2005,11 @@
"link": true "link": true
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.11.5", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz",
"integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==", "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==",
"license": "MIT",
"dependencies": { "dependencies": {
"ajv": "^8.17.1", "ajv": "^6.12.6",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.5", "cross-spawn": "^7.0.5",
@ -2026,28 +2025,6 @@
"node": ">=18" "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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -5719,22 +5696,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -9064,15 +9025,6 @@
"node": ">=0.10.0" "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": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",