From 46d3d844f8884f2099a5e3fb078b5bf337ab2a9f Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 16 May 2025 19:20:40 +0100 Subject: [PATCH 01/28] 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 d0b0432d..98d58e23 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/28] 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 cb411452..a30dcc9c 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 98d58e23..083fc39b 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 396af142..8167afa6 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/28] 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 a30dcc9c..4708f08e 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 083fc39b..98d58e23 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 8167afa6..9f079dde 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 87e8f8bc..2a9171f0 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 bef728d7..a48b32e1 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/28] 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 a48b32e1..ca473b49 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/28] bump typescript sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d46b3ef8..a5e09c6a 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/28] 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", From 82858bed30fd5f93192f65c9dc7ea6e6deadbfdf Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 11:17:21 +0100 Subject: [PATCH 07/28] 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 6bd50bcb..2e74bf68 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 40262601..2541e822 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 5b2925ac..939e5d6c 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/28] 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 55a4541e..8be23a5b 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 5565a77a..0ef8e70b 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 88d931c0..af9944ac 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 4c3e27aa..3acbda91 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/28] resumption working --- client/src/App.tsx | 78 +++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 8be23a5b..f3f1180e 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 { + 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 }; + }); + + // 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: 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 - if (authorizationCode) { - updateAuthState({ - authorizationCode, - oauthStep: "token_request", - }); - } - if (errorMsg) { - updateAuthState({ - latestError: new Error(errorMsg), - }); - } + 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/28] 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 0ef8e70b..a90b1061 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 2e74bf68..f86ceec8 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/28] 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 f3f1180e..74cdca19 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 a90b1061..b88758d4 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 af9944ac..17705abb 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 f86ceec8..b9b67c6c 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 42a289ff..fe5a94f9 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 2541e822..e69dcd12 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 939e5d6c..d05369c4 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/28] 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 74cdca19..a332f9aa 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 b88758d4..9757af95 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 e69dcd12..daf1bc15 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/28] whitespace --- client/src/components/AuthDebugger.tsx | 74 +++++++++---------- .../__tests__/AuthDebugger.test.tsx | 14 ++-- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index 9757af95..48c9cb00 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/28] 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 48c9cb00..ec09963e 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 b0df65eb..b2fa4c61 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 02aacb46a2851cf6c5066659f1bc3b56a44d3c76 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 29 May 2025 19:54:36 -0400 Subject: [PATCH 15/28] Send HTTP/DELETE when disconnecting client * In server/src/index.ts - add delete handler for /mcp endpoint - gets the server transport for the sessionId - calls terminateSession on it - removes the webapp and server transports for the sessionId from the maps - returns status 200 to the client. * In client/src/lib/hooks/useConnection.ts - import Transport - add useState for clientTransport, type Transport or null initialized to null - in connect() function - move creation of client transport down a ways, just before calling client.connect with it - immendiately efter calling client.connect, call setClientTransport with the connected transport (has to happen after, so that what's saved has the abort controller, etc. otherwise it doesn't work to save it prior. - in disconnect() function - immediately call clientTransport.terminateSession if transportType is "streamable-http" - setClientTransport to null --- client/src/lib/hooks/useConnection.ts | 32 +++++++++++++++++++-------- server/src/index.ts | 27 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9c825318..39da55cb 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,7 @@ import { } from "@/utils/configUtils"; import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; interface UseConnectionOptions { transportType: "stdio" | "sse" | "streamable-http"; @@ -83,6 +84,9 @@ export function useConnection({ const [serverCapabilities, setServerCapabilities] = useState(null); const [mcpClient, setMcpClient] = useState(null); + const [clientTransport, setClientTransport] = useState( + null, + ); const [requestHistory, setRequestHistory] = useState< { request: string; response?: string }[] >([]); @@ -377,14 +381,6 @@ export function useConnection({ transportType, ); - const clientTransport = - transportType === "streamable-http" - ? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, { - sessionId: undefined, - ...transportOptions, - }) - : new SSEClientTransport(mcpProxyServerUrl as URL, transportOptions); - if (onNotification) { [ CancelledNotificationSchema, @@ -414,7 +410,20 @@ export function useConnection({ let capabilities; try { - await client.connect(clientTransport); + const transport = + transportType === "streamable-http" + ? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, { + sessionId: undefined, + ...transportOptions, + }) + : new SSEClientTransport( + mcpProxyServerUrl as URL, + transportOptions, + ); + + await client.connect(transport as Transport); + + setClientTransport(transport); capabilities = client.getServerCapabilities(); const initializeRequest = { @@ -468,10 +477,15 @@ export function useConnection({ }; const disconnect = async () => { + if (transportType === "streamable-http") + await ( + clientTransport as StreamableHTTPClientTransport + ).terminateSession(); await mcpClient?.close(); const authProvider = new InspectorOAuthClientProvider(sseUrl); authProvider.clear(); setMcpClient(null); + setClientTransport(null); setConnectionStatus("disconnected"); setCompletionsSupported(false); setServerCapabilities(null); diff --git a/server/src/index.ts b/server/src/index.ts index 73288f67..1586d0a0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -226,6 +226,33 @@ app.post("/mcp", async (req, res) => { } }); +app.delete("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + console.log(`Received DELETE message for sessionId ${sessionId}`); + let serverTransport: Transport | undefined; + if (sessionId) { + try { + serverTransport = serverTransports.get( + sessionId, + ) as StreamableHTTPClientTransport; + if (!serverTransport) { + res.status(404).end("Transport not found for sessionId " + sessionId); + } else { + await ( + serverTransport as StreamableHTTPClientTransport + ).terminateSession(); + webAppTransports.delete(sessionId); + serverTransports.delete(sessionId); + console.log(`Transports removed for sessionId ${sessionId}`); + } + res.status(200).end(); + } catch (error) { + console.error("Error in /mcp route:", error); + res.status(500).json(error); + } + } +}); + app.get("/stdio", async (req, res) => { try { console.log("New connection"); From 2d7936d10b7abeed521091cb3c3173230847abe7 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 2 Jun 2025 16:20:16 +0100 Subject: [PATCH 16/28] 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 17705abb..99774297 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 b2fa4c61..772b3d43 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 daf1bc15..5e8113ef 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 a5e09c6a..6ee5719e 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 17/28] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ee5719e..c0f7cf73 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 18/28] 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 772b3d43..3727128a 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 6e5d73f0..36588354 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 19/28] 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 3727128a..2130e68b 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 d05369c4..101a33cf 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 20/28] 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 101a33cf..5f10a783 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 21/28] 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 00d3bb48..2fbc7494 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -307,7 +307,7 @@ const App = () => { }); } }, - [], + [sseUrl], ); // Load OAuth tokens when sseUrl changes From 2f50a4ba0eecc2774b4ac6544b10149452b96835 Mon Sep 17 00:00:00 2001 From: dr3s Date: Tue, 3 Jun 2025 13:58:33 -0400 Subject: [PATCH 22/28] fix: Resource template query parameters not appended to request URI #450 --- client/src/components/ResourcesTab.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 35742799..1293a0f8 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -16,6 +16,7 @@ import ListPane from "./ListPane"; import { useEffect, useState } from "react"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; import JsonView from "./JsonView"; +import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js"; const ResourcesTab = ({ resources, @@ -79,10 +80,10 @@ const ResourcesTab = ({ template: string, values: Record, ): string => { - return template.replace( - /{([^}]+)}/g, - (_, key) => values[key] || `{${key}}`, - ); + console.log("Expanding template:", template, "with values:", values); + const result = new UriTemplate(template).expand(values); + console.log("Filled template:", result); + return result; }; const handleTemplateValueChange = async (key: string, value: string) => { @@ -240,7 +241,8 @@ const ResourcesTab = ({ {selectedTemplate.uriTemplate .match(/{([^}]+)}/g) ?.map((param) => { - const key = param.slice(1, -1); + // Remove leading operator characters (?, &, /, #, ;, +, .) from variable name + const key = param.slice(1, -1).replace(/^[?&/#;+.]/, ""); return (
From f1650d06e3d3a835ecdafe65c8e7ce19f129c317 Mon Sep 17 00:00:00 2001 From: dr3s Date: Tue, 3 Jun 2025 13:58:33 -0400 Subject: [PATCH 23/28] use UriTemplate variable names --- client/src/components/ResourcesTab.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 1293a0f8..dd40d1b8 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -238,11 +238,8 @@ const ResourcesTab = ({

{selectedTemplate.description}

- {selectedTemplate.uriTemplate - .match(/{([^}]+)}/g) - ?.map((param) => { - // Remove leading operator characters (?, &, /, #, ;, +, .) from variable name - const key = param.slice(1, -1).replace(/^[?&/#;+.]/, ""); + {new UriTemplate(selectedTemplate.uriTemplate).variableNames + ?.map((key) => { return (
From 07f3e4593b8bb2ffc9cee99fb7813a08ca85f27b Mon Sep 17 00:00:00 2001 From: dr3s Date: Tue, 3 Jun 2025 13:58:33 -0400 Subject: [PATCH 24/28] remove logs --- client/src/components/ResourcesTab.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index dd40d1b8..8795e46b 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -80,10 +80,7 @@ const ResourcesTab = ({ template: string, values: Record, ): string => { - console.log("Expanding template:", template, "with values:", values); - const result = new UriTemplate(template).expand(values); - console.log("Filled template:", result); - return result; + return new UriTemplate(template).expand(values); }; const handleTemplateValueChange = async (key: string, value: string) => { From 26f744866aefde5acaec7a2da6314b8c915ec9be Mon Sep 17 00:00:00 2001 From: dr3s Date: Tue, 3 Jun 2025 13:58:33 -0400 Subject: [PATCH 25/28] formatting --- client/src/components/ResourcesTab.tsx | 41 +++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 8795e46b..8ec3f91b 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -235,26 +235,27 @@ const ResourcesTab = ({

{selectedTemplate.description}

- {new UriTemplate(selectedTemplate.uriTemplate).variableNames - ?.map((key) => { - return ( -
- - - handleTemplateValueChange(key, value) - } - onInputChange={(value) => - handleTemplateValueChange(key, value) - } - options={completions[key] || []} - /> -
- ); - })} + {new UriTemplate( + selectedTemplate.uriTemplate, + ).variableNames?.map((key) => { + return ( +
+ + + handleTemplateValueChange(key, value) + } + onInputChange={(value) => + handleTemplateValueChange(key, value) + } + options={completions[key] || []} + /> +
+ ); + })} + ), +})); + +describe("ResourcesTab - Template Query Parameters", () => { + const mockListResources = jest.fn(); + const mockClearResources = jest.fn(); + const mockListResourceTemplates = jest.fn(); + const mockClearResourceTemplates = jest.fn(); + const mockReadResource = jest.fn(); + const mockSetSelectedResource = jest.fn(); + const mockHandleCompletion = jest.fn(); + const mockSubscribeToResource = jest.fn(); + const mockUnsubscribeFromResource = jest.fn(); + + const mockResourceTemplate: ResourceTemplate = { + name: "Users API", + uriTemplate: "test://users{?name,limit,offset}", + description: "Fetch users with optional filtering and pagination", + }; + + const mockResource: Resource = { + uri: "test://users?name=john&limit=10&offset=0", + name: "Users Resource", + description: "Expanded users resource", + }; + + const defaultProps = { + resources: [], + resourceTemplates: [mockResourceTemplate], + listResources: mockListResources, + clearResources: mockClearResources, + listResourceTemplates: mockListResourceTemplates, + clearResourceTemplates: mockClearResourceTemplates, + readResource: mockReadResource, + selectedResource: null, + setSelectedResource: mockSetSelectedResource, + handleCompletion: mockHandleCompletion, + completionsSupported: true, + resourceContent: "", + nextCursor: undefined, + nextTemplateCursor: undefined, + error: null, + resourceSubscriptionsSupported: false, + resourceSubscriptions: new Set(), + subscribeToResource: mockSubscribeToResource, + unsubscribeFromResource: mockUnsubscribeFromResource, + }; + + const renderResourcesTab = (props = {}) => + render( + + + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should parse and display template variables from URI template", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Check that input fields are rendered for each template variable + expect(screen.getByTestId("combobox-name")).toBeInTheDocument(); + expect(screen.getByTestId("combobox-limit")).toBeInTheDocument(); + expect(screen.getByTestId("combobox-offset")).toBeInTheDocument(); + }); + + it("should display template description when template is selected", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + expect(screen.getByText("Fetch users with optional filtering and pagination")).toBeInTheDocument(); + }); + + it("should handle template value changes", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Find and fill template value inputs + const nameInput = screen.getByTestId("combobox-name"); + const limitInput = screen.getByTestId("combobox-limit"); + const offsetInput = screen.getByTestId("combobox-offset"); + + fireEvent.change(nameInput, { target: { value: "john" } }); + fireEvent.change(limitInput, { target: { value: "10" } }); + fireEvent.change(offsetInput, { target: { value: "0" } }); + + expect(nameInput).toHaveValue("john"); + expect(limitInput).toHaveValue("10"); + expect(offsetInput).toHaveValue("0"); + }); + + it("should expand template and read resource when Read Resource button is clicked", async () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Fill template values + const nameInput = screen.getByTestId("combobox-name"); + const limitInput = screen.getByTestId("combobox-limit"); + const offsetInput = screen.getByTestId("combobox-offset"); + + fireEvent.change(nameInput, { target: { value: "john" } }); + fireEvent.change(limitInput, { target: { value: "10" } }); + fireEvent.change(offsetInput, { target: { value: "0" } }); + + // Click Read Resource button + const readResourceButton = screen.getByText("Read Resource"); + expect(readResourceButton).not.toBeDisabled(); + + fireEvent.click(readResourceButton); + + // Verify that readResource was called with the expanded URI + expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john&limit=10&offset=0"); + + // Verify that setSelectedResource was called with the expanded resource + expect(mockSetSelectedResource).toHaveBeenCalledWith({ + uri: "test://users?name=john&limit=10&offset=0", + name: "test://users?name=john&limit=10&offset=0" + }); + }); + + it("should disable Read Resource button when no template values are provided", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Read Resource button should be disabled when no values are provided + const readResourceButton = screen.getByText("Read Resource"); + expect(readResourceButton).toBeDisabled(); + }); + + it("should handle partial template values correctly", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Fill only some template values + const nameInput = screen.getByTestId("combobox-name"); + fireEvent.change(nameInput, { target: { value: "john" } }); + + // Read Resource button should be enabled with partial values + const readResourceButton = screen.getByText("Read Resource"); + expect(readResourceButton).not.toBeDisabled(); + + fireEvent.click(readResourceButton); + + // Should expand with only the provided values + expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john"); + }); + + it("should handle special characters in template values", () => { + renderResourcesTab(); + + // Click on the resource template to select it + fireEvent.click(screen.getByText("Users API")); + + // Fill template values with special characters + const nameInput = screen.getByTestId("combobox-name"); + fireEvent.change(nameInput, { target: { value: "john doe" } }); + + fireEvent.click(screen.getByText("Read Resource")); + + // Should properly encode special characters + expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john%20doe"); + }); + + it("should clear template values when switching between templates", () => { + const anotherTemplate: ResourceTemplate = { + name: "Posts API", + uriTemplate: "test://posts{?author,category}", + description: "Fetch posts by author and category", + }; + + renderResourcesTab({ + resourceTemplates: [mockResourceTemplate, anotherTemplate] + }); + + // Select first template and fill values + fireEvent.click(screen.getByText("Users API")); + const nameInput = screen.getByTestId("combobox-name"); + fireEvent.change(nameInput, { target: { value: "john" } }); + + // Switch to second template + fireEvent.click(screen.getByText("Posts API")); + + // Should show new template fields and clear previous values + expect(screen.getByTestId("combobox-author")).toBeInTheDocument(); + expect(screen.getByTestId("combobox-category")).toBeInTheDocument(); + expect(screen.queryByTestId("combobox-name")).not.toBeInTheDocument(); + }); + + it("should display resource content when a resource is selected", () => { + const resourceContent = '{"users": [{"id": 1, "name": "John"}]}'; + + renderResourcesTab({ + selectedResource: mockResource, + resourceContent: resourceContent + }); + + expect(screen.getByTestId("json-view")).toBeInTheDocument(); + expect(screen.getByText(resourceContent)).toBeInTheDocument(); + }); + + it("should show alert when no resource or template is selected", () => { + renderResourcesTab(); + + expect(screen.getByText("Select a resource or template from the list to view its contents")).toBeInTheDocument(); + }); +}); From 9508cc46271b5965accda35e98c3a93f5cc4de96 Mon Sep 17 00:00:00 2001 From: dr3s Date: Wed, 4 Jun 2025 14:44:15 -0400 Subject: [PATCH 27/28] pretty --- .../__tests__/ResourcesTab.test.tsx | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/client/src/components/__tests__/ResourcesTab.test.tsx b/client/src/components/__tests__/ResourcesTab.test.tsx index dd12cced..d564b02c 100644 --- a/client/src/components/__tests__/ResourcesTab.test.tsx +++ b/client/src/components/__tests__/ResourcesTab.test.tsx @@ -20,7 +20,12 @@ jest.mock("../JsonView", () => { }); jest.mock("@/components/ui/combobox", () => ({ - Combobox: ({ id, value, onChange, placeholder }: { + Combobox: ({ + id, + value, + onChange, + placeholder, + }: { id: string; value: string; onChange: (value: string) => void; @@ -37,7 +42,10 @@ jest.mock("@/components/ui/combobox", () => ({ })); jest.mock("@/components/ui/label", () => ({ - Label: ({ htmlFor, children }: { + Label: ({ + htmlFor, + children, + }: { htmlFor: string; children: React.ReactNode; }) => ( @@ -48,7 +56,12 @@ jest.mock("@/components/ui/label", () => ({ })); jest.mock("@/components/ui/button", () => ({ - Button: ({ children, onClick, disabled, ...props }: { + Button: ({ + children, + onClick, + disabled, + ...props + }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; @@ -139,7 +152,9 @@ describe("ResourcesTab - Template Query Parameters", () => { // Click on the resource template to select it fireEvent.click(screen.getByText("Users API")); - expect(screen.getByText("Fetch users with optional filtering and pagination")).toBeInTheDocument(); + expect( + screen.getByText("Fetch users with optional filtering and pagination"), + ).toBeInTheDocument(); }); it("should handle template value changes", () => { @@ -180,16 +195,18 @@ describe("ResourcesTab - Template Query Parameters", () => { // Click Read Resource button const readResourceButton = screen.getByText("Read Resource"); expect(readResourceButton).not.toBeDisabled(); - + fireEvent.click(readResourceButton); // Verify that readResource was called with the expanded URI - expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john&limit=10&offset=0"); - + expect(mockReadResource).toHaveBeenCalledWith( + "test://users?name=john&limit=10&offset=0", + ); + // Verify that setSelectedResource was called with the expanded resource expect(mockSetSelectedResource).toHaveBeenCalledWith({ uri: "test://users?name=john&limit=10&offset=0", - name: "test://users?name=john&limit=10&offset=0" + name: "test://users?name=john&limit=10&offset=0", }); }); @@ -217,7 +234,7 @@ describe("ResourcesTab - Template Query Parameters", () => { // Read Resource button should be enabled with partial values const readResourceButton = screen.getByText("Read Resource"); expect(readResourceButton).not.toBeDisabled(); - + fireEvent.click(readResourceButton); // Should expand with only the provided values @@ -237,7 +254,9 @@ describe("ResourcesTab - Template Query Parameters", () => { fireEvent.click(screen.getByText("Read Resource")); // Should properly encode special characters - expect(mockReadResource).toHaveBeenCalledWith("test://users?name=john%20doe"); + expect(mockReadResource).toHaveBeenCalledWith( + "test://users?name=john%20doe", + ); }); it("should clear template values when switching between templates", () => { @@ -248,7 +267,7 @@ describe("ResourcesTab - Template Query Parameters", () => { }; renderResourcesTab({ - resourceTemplates: [mockResourceTemplate, anotherTemplate] + resourceTemplates: [mockResourceTemplate, anotherTemplate], }); // Select first template and fill values @@ -267,10 +286,10 @@ describe("ResourcesTab - Template Query Parameters", () => { it("should display resource content when a resource is selected", () => { const resourceContent = '{"users": [{"id": 1, "name": "John"}]}'; - + renderResourcesTab({ selectedResource: mockResource, - resourceContent: resourceContent + resourceContent: resourceContent, }); expect(screen.getByTestId("json-view")).toBeInTheDocument(); @@ -280,6 +299,10 @@ describe("ResourcesTab - Template Query Parameters", () => { it("should show alert when no resource or template is selected", () => { renderResourcesTab(); - expect(screen.getByText("Select a resource or template from the list to view its contents")).toBeInTheDocument(); + expect( + screen.getByText( + "Select a resource or template from the list to view its contents", + ), + ).toBeInTheDocument(); }); }); From 8ef91e359ecd5d51af8aa0dd315080b47389e19e Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 4 Jun 2025 16:13:35 -0400 Subject: [PATCH 28/28] Bump Inspector packages to 0.14.0, and sdk to 1.12.1 --- cli/package.json | 4 ++-- client/package.json | 4 ++-- package-lock.json | 22 +++++++++++----------- package.json | 8 ++++---- server/package.json | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cli/package.json b/cli/package.json index 55f12e15..bd027bfe 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.13.0", + "version": "0.14.0", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -21,7 +21,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/client/package.json b/client/package.json index ee54df3d..daf549fc 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.13.0", + "version": "0.14.0", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -23,7 +23,7 @@ "test:watch": "jest --config jest.config.cjs --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "@radix-ui/react-checkbox": "^1.1.4", "ajv": "^6.12.6", "@radix-ui/react-dialog": "^1.1.3", diff --git a/package-lock.json b/package-lock.json index 36588354..846d2eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.13.0", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.13.0", - "@modelcontextprotocol/inspector-client": "^0.13.0", - "@modelcontextprotocol/inspector-server": "^0.13.0", + "@modelcontextprotocol/inspector-cli": "^0.14.0", + "@modelcontextprotocol/inspector-client": "^0.14.0", + "@modelcontextprotocol/inspector-server": "^0.14.0", "@modelcontextprotocol/sdk": "^1.12.1", "concurrently": "^9.0.1", "open": "^10.1.0", @@ -40,10 +40,10 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "commander": "^13.1.0", "spawn-rx": "^5.1.2" }, @@ -63,10 +63,10 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -10920,10 +10920,10 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", diff --git a/package.json b/package.json index c0f7cf73..8742cc8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.13.0", + "version": "0.14.0", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -41,9 +41,9 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.13.0", - "@modelcontextprotocol/inspector-client": "^0.13.0", - "@modelcontextprotocol/inspector-server": "^0.13.0", + "@modelcontextprotocol/inspector-cli": "^0.14.0", + "@modelcontextprotocol/inspector-client": "^0.14.0", + "@modelcontextprotocol/inspector-server": "^0.14.0", "@modelcontextprotocol/sdk": "^1.12.1", "concurrently": "^9.0.1", "open": "^10.1.0", diff --git a/server/package.json b/server/package.json index a3333da0..50d74cfc 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.13.0", + "version": "0.14.0", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -27,7 +27,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.5", + "@modelcontextprotocol/sdk": "^1.12.1", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0",