From 46d3d844f8884f2099a5e3fb078b5bf337ab2a9f Mon Sep 17 00:00:00 2001
From: Paul Carleton
+ {JSON.stringify(authState.resourceMetadata, null, 2)}
+
+ + 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']})`} +
+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)}
Resource Metadata:
- From {new URL('/.well-known/oauth-protected-resource', serverUrl).href} + From{" "} + { + new URL( + "/.well-known/oauth-protected-resource", + serverUrl, + ).href + }
{JSON.stringify(authState.resourceMetadata, null, 2)}
@@ -150,17 +156,35 @@ export const OAuthFlowProgress = ({
{authState.resourceMetadataError && (
- ℹ️ No resource metadata available from {' '}
-
- {new URL('/.well-known/oauth-protected-resource', serverUrl).href}
+ ℹ️ No resource metadata available from{" "}
+
+ {
+ new URL(
+ "/.well-known/oauth-protected-resource",
+ serverUrl,
+ ).href
+ }
- Resource metadata was added in the 2025-DRAFT-v2 specification update
+ Resource metadata was added in the{" "}
+
+ 2025-DRAFT-v2 specification update
+
{authState.resourceMetadataError.message}
- {authState.resourceMetadataError instanceof TypeError
- && " (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
+ {authState.resourceMetadataError instanceof TypeError &&
+ " (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
)}
@@ -168,9 +192,17 @@ export const OAuthFlowProgress = ({
{authState.oauthMetadata && (
Authorization Server Metadata:
- {authState.authServerUrl &&
- From {new URL('/.well-known/oauth-authorization-server', authState.authServerUrl).href}
-
}
+ {authState.authServerUrl && (
+
+ From{" "}
+ {
+ new URL(
+ "/.well-known/oauth-authorization-server",
+ authState.authServerUrl,
+ ).href
+ }
+
+ )}
{JSON.stringify(authState.oauthMetadata, null, 2)}
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index 42a289ff9..fe5a94f95 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -40,6 +40,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
registerClient: jest.fn(),
startAuthorization: jest.fn(),
exchangeAuthorization: jest.fn(),
+ discoverOAuthProtectedResourceMetadata: jest.fn(),
}));
// Import the functions to get their types
@@ -49,6 +50,7 @@ import {
startAuthorization,
exchangeAuthorization,
auth,
+ discoverOAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/client/auth.js";
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
@@ -67,6 +69,10 @@ const mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction<
typeof exchangeAuthorization
>;
const mockAuth = auth as jest.MockedFunction;
+const mockDiscoverOAuthProtectedResourceMetadata =
+ discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
+ typeof discoverOAuthProtectedResourceMetadata
+ >;
const sessionStorageMock = {
getItem: jest.fn(),
@@ -100,6 +106,7 @@ describe("AuthDebugger", () => {
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
+ mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null);
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
const authUrl = new URL("https://oauth.example.com/authorize");
@@ -421,4 +428,63 @@ describe("AuthDebugger", () => {
});
});
});
+
+ describe("OAuth State Persistence", () => {
+ it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
+ const updateAuthState = jest.fn();
+
+ // Mock window.location.href setter
+ delete (window as any).location;
+ window.location = { href: "" } as any;
+
+ // Setup mocks for OAuth flow
+ mockStartAuthorization.mockResolvedValue({
+ authorizationUrl: new URL(
+ "https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug",
+ ),
+ codeVerifier: "test_verifier",
+ });
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: { ...defaultAuthState, loading: false },
+ });
+ });
+
+ // Click Quick OAuth Flow
+ await act(async () => {
+ fireEvent.click(screen.getByText("Quick OAuth Flow"));
+ });
+
+ // Wait for the flow to reach the authorization step
+ await waitFor(() => {
+ expect(sessionStorage.setItem).toHaveBeenCalledWith(
+ SESSION_KEYS.AUTH_DEBUGGER_STATE,
+ expect.stringContaining('"oauthStep":"authorization_code"'),
+ );
+ });
+
+ // Verify the stored state includes all the accumulated data
+ const storedStateCall = (
+ sessionStorage.setItem as jest.Mock
+ ).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
+
+ expect(storedStateCall).toBeDefined();
+ const storedState = JSON.parse(storedStateCall![1]);
+
+ expect(storedState).toMatchObject({
+ oauthStep: "authorization_code",
+ authorizationUrl: expect.stringMatching(
+ /^https:\/\/oauth\.example\.com\/authorize/,
+ ),
+ oauthMetadata: expect.objectContaining({
+ token_endpoint: "https://oauth.example.com/token",
+ }),
+ oauthClientInfo: expect.objectContaining({
+ client_id: "test_client_id",
+ }),
+ });
+ });
+ });
});
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts
index 2541e8222..e69dcd124 100644
--- a/client/src/lib/auth-types.ts
+++ b/client/src/lib/auth-types.ts
@@ -30,7 +30,10 @@ export interface AuthDebuggerState {
loading: boolean;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
- resourceMetadataError: Error | { status: number; statusText: string; message: string } | null;
+ resourceMetadataError:
+ | Error
+ | { status: number; statusText: string; message: string }
+ | null;
authServerUrl: URL | null;
oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
@@ -56,4 +59,4 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
latestError: null,
statusMessage: null,
validationError: null,
-}
+};
diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts
index 939e5d6cd..d05369c41 100644
--- a/client/src/lib/oauth-state-machine.ts
+++ b/client/src/lib/oauth-state-machine.ts
@@ -7,7 +7,10 @@ import {
exchangeAuthorization,
discoverOAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/client/auth.js";
-import { OAuthMetadataSchema, OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
+import {
+ OAuthMetadataSchema,
+ OAuthProtectedResourceMetadata,
+} from "@modelcontextprotocol/sdk/shared/auth.js";
export interface StateMachineContext {
state: AuthDebuggerState;
@@ -30,8 +33,13 @@ export const oauthTransitions: Record = {
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
let resourceMetadataError: Error | null = null;
try {
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl);
- if (resourceMetadata && resourceMetadata.authorization_servers?.length) {
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(
+ context.serverUrl,
+ );
+ if (
+ resourceMetadata &&
+ resourceMetadata.authorization_servers?.length
+ ) {
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
}
} catch (e) {
From 03bb303bf9f1c07fba565c7771c5ad5d0c1ae70e Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Thu, 29 May 2025 14:10:32 +0100
Subject: [PATCH 12/20] rm loading, prm test
---
client/src/App.tsx | 2 -
client/src/components/AuthDebugger.tsx | 26 ++--
.../__tests__/AuthDebugger.test.tsx | 123 +++++++++++++++++-
client/src/lib/auth-types.ts | 2 -
4 files changed, 131 insertions(+), 22 deletions(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 74cdca19d..a332f9aa3 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -329,8 +329,6 @@ const App = () => {
}
} catch (error) {
console.error("Error loading OAuth tokens:", error);
- } finally {
- updateAuthState({ loading: false });
}
};
diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx
index b88758d4b..9757af95a 100644
--- a/client/src/components/AuthDebugger.tsx
+++ b/client/src/components/AuthDebugger.tsx
@@ -60,23 +60,23 @@ const AuthDebugger = ({
authState,
updateAuthState,
}: AuthDebuggerProps) => {
- // Initialize loading state
+ // Check for existing tokens on mount
useEffect(() => {
- if (authState.loading && serverUrl) {
- // Check if we have existing tokens
+ if (serverUrl && !authState.oauthTokens) {
const checkTokens = async () => {
const provider = new DebugInspectorOAuthClientProvider(serverUrl);
const existingTokens = await provider.tokens();
-
- updateAuthState({
- loading: false,
- oauthTokens: existingTokens || null,
- });
+ if (existingTokens) {
+ updateAuthState({
+ oauthTokens: existingTokens,
+ oauthStep: "complete",
+ });
+ }
};
-
checkTokens();
}
- }, [serverUrl, authState.loading, updateAuthState]);
+ }, [serverUrl]); // Only run when serverUrl changes
+
const startOAuthFlow = useCallback(() => {
if (!serverUrl) {
updateAuthState({
@@ -241,10 +241,7 @@ const AuthDebugger = ({
)}
- {authState.loading ? (
- Loading authentication status...
- ) : (
-
+
{authState.oauthTokens && (
Access Token:
@@ -286,7 +283,6 @@ const AuthDebugger = ({
the standard automatic flow.
- )}
{
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
- mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null);
+ mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
+ new Error("No protected resource metadata found")
+ );
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
const authUrl = new URL("https://oauth.example.com/authorize");
@@ -448,7 +450,7 @@ describe("AuthDebugger", () => {
await act(async () => {
renderAuthDebugger({
updateAuthState,
- authState: { ...defaultAuthState, loading: false },
+ authState: { ...defaultAuthState },
});
});
@@ -471,7 +473,7 @@ describe("AuthDebugger", () => {
).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
expect(storedStateCall).toBeDefined();
- const storedState = JSON.parse(storedStateCall![1]);
+ const storedState = JSON.parse(storedStateCall![1] as string);
expect(storedState).toMatchObject({
oauthStep: "authorization_code",
@@ -487,4 +489,119 @@ describe("AuthDebugger", () => {
});
});
});
+
+ describe("OAuth Protected Resource Metadata", () => {
+ it("should successfully fetch and display protected resource metadata", async () => {
+ const updateAuthState = jest.fn();
+ const mockResourceMetadata = {
+ resource: "https://example.com/api",
+ authorization_servers: ["https://custom-auth.example.com"],
+ bearer_methods_supported: ["header", "body"],
+ resource_documentation: "https://example.com/api/docs",
+ resource_policy_uri: "https://example.com/api/policy",
+ };
+
+ // Mock successful metadata discovery
+ mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
+ mockResourceMetadata
+ );
+ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: { ...defaultAuthState },
+ });
+ });
+
+ // Click Guided OAuth Flow to start the process
+ await act(async () => {
+ fireEvent.click(screen.getByText("Guided OAuth Flow"));
+ });
+
+ // Verify that the flow started with metadata discovery
+ expect(updateAuthState).toHaveBeenCalledWith({
+ oauthStep: "metadata_discovery",
+ authorizationUrl: null,
+ statusMessage: null,
+ latestError: null,
+ });
+
+ // Click Continue to trigger metadata discovery
+ const continueButton = await screen.findByText("Continue");
+ await act(async () => {
+ fireEvent.click(continueButton);
+ });
+
+ // Wait for the metadata to be fetched
+ await waitFor(() => {
+ expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
+ "https://example.com"
+ );
+ });
+
+ // Verify the state was updated with the resource metadata
+ await waitFor(() => {
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ resourceMetadata: mockResourceMetadata,
+ authServerUrl: new URL("https://custom-auth.example.com"),
+ oauthStep: "client_registration",
+ })
+ );
+ });
+ });
+
+ it("should handle protected resource metadata fetch failure gracefully", async () => {
+ const updateAuthState = jest.fn();
+ const mockError = new Error("Failed to fetch resource metadata");
+
+ // Mock failed metadata discovery
+ mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
+ // But OAuth metadata should still work with the original URL
+ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: { ...defaultAuthState },
+ });
+ });
+
+ // Click Guided OAuth Flow
+ await act(async () => {
+ fireEvent.click(screen.getByText("Guided OAuth Flow"));
+ });
+
+ // Click Continue to trigger metadata discovery
+ const continueButton = await screen.findByText("Continue");
+ await act(async () => {
+ fireEvent.click(continueButton);
+ });
+
+ // Wait for the metadata fetch to fail
+ await waitFor(() => {
+ expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
+ "https://example.com"
+ );
+ });
+
+ // Verify the flow continues despite the error
+ await waitFor(() => {
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ resourceMetadataError: mockError,
+ // Should use the original server URL as fallback
+ authServerUrl: new URL("https://example.com"),
+ oauthStep: "client_registration",
+ })
+ );
+ });
+
+ // Verify that regular OAuth metadata discovery was still called
+ expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
+ new URL("https://example.com")
+ );
+ });
+ });
});
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts
index e69dcd124..daf1bc15f 100644
--- a/client/src/lib/auth-types.ts
+++ b/client/src/lib/auth-types.ts
@@ -27,7 +27,6 @@ export interface StatusMessage {
export interface AuthDebuggerState {
isInitiatingAuth: boolean;
oauthTokens: OAuthTokens | null;
- loading: boolean;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
resourceMetadataError:
@@ -47,7 +46,6 @@ export interface AuthDebuggerState {
export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
isInitiatingAuth: false,
oauthTokens: null,
- loading: true,
oauthStep: "metadata_discovery",
oauthMetadata: null,
resourceMetadata: null,
From 11bc04364bfc012b2170be97ce530af1c118e547 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Thu, 29 May 2025 14:11:01 +0100
Subject: [PATCH 13/20] whitespace
---
client/src/components/AuthDebugger.tsx | 72 +++++++++----------
.../__tests__/AuthDebugger.test.tsx | 14 ++--
2 files changed, 43 insertions(+), 43 deletions(-)
diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx
index 9757af95a..48c9cb008 100644
--- a/client/src/components/AuthDebugger.tsx
+++ b/client/src/components/AuthDebugger.tsx
@@ -242,47 +242,47 @@ const AuthDebugger = ({
)}
- {authState.oauthTokens && (
-
- Access Token:
-
- {authState.oauthTokens.access_token.substring(0, 25)}...
-
+ {authState.oauthTokens && (
+
+ Access Token:
+
+ {authState.oauthTokens.access_token.substring(0, 25)}...
- )}
-
-
-
+
+ )}
-
+
+
-
-
+
-
- Choose "Guided" for step-by-step instructions or "Quick" for
- the standard automatic flow.
-
+
+
+
+ Choose "Guided" for step-by-step instructions or "Quick" for
+ the standard automatic flow.
+
+
{
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
- new Error("No protected resource metadata found")
+ new Error("No protected resource metadata found"),
);
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
const authUrl = new URL("https://oauth.example.com/authorize");
@@ -503,7 +503,7 @@ describe("AuthDebugger", () => {
// Mock successful metadata discovery
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
- mockResourceMetadata
+ mockResourceMetadata,
);
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
@@ -536,7 +536,7 @@ describe("AuthDebugger", () => {
// Wait for the metadata to be fetched
await waitFor(() => {
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
- "https://example.com"
+ "https://example.com",
);
});
@@ -547,7 +547,7 @@ describe("AuthDebugger", () => {
resourceMetadata: mockResourceMetadata,
authServerUrl: new URL("https://custom-auth.example.com"),
oauthStep: "client_registration",
- })
+ }),
);
});
});
@@ -582,7 +582,7 @@ describe("AuthDebugger", () => {
// Wait for the metadata fetch to fail
await waitFor(() => {
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
- "https://example.com"
+ "https://example.com",
);
});
@@ -594,13 +594,13 @@ describe("AuthDebugger", () => {
// Should use the original server URL as fallback
authServerUrl: new URL("https://example.com"),
oauthStep: "client_registration",
- })
+ }),
);
});
// Verify that regular OAuth metadata discovery was still called
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
- new URL("https://example.com")
+ new URL("https://example.com"),
);
});
});
From e74f80db8591c288c394d7912b57e18bf93e4b78 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Thu, 29 May 2025 14:26:29 +0100
Subject: [PATCH 14/20] fix tests
---
client/src/components/AuthDebugger.tsx | 20 +++++++++++--------
.../__tests__/AuthDebugger.test.tsx | 9 +++++++--
2 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx
index 48c9cb008..ec09963ec 100644
--- a/client/src/components/AuthDebugger.tsx
+++ b/client/src/components/AuthDebugger.tsx
@@ -64,18 +64,22 @@ const AuthDebugger = ({
useEffect(() => {
if (serverUrl && !authState.oauthTokens) {
const checkTokens = async () => {
- const provider = new DebugInspectorOAuthClientProvider(serverUrl);
- const existingTokens = await provider.tokens();
- if (existingTokens) {
- updateAuthState({
- oauthTokens: existingTokens,
- oauthStep: "complete",
- });
+ try {
+ const provider = new DebugInspectorOAuthClientProvider(serverUrl);
+ const existingTokens = await provider.tokens();
+ if (existingTokens) {
+ updateAuthState({
+ oauthTokens: existingTokens,
+ oauthStep: "complete",
+ });
+ }
+ } catch (error) {
+ console.error("Failed to load existing OAuth tokens:", error);
}
};
checkTokens();
}
- }, [serverUrl]); // Only run when serverUrl changes
+ }, [serverUrl, updateAuthState, authState.oauthTokens]);
const startOAuthFlow = useCallback(() => {
if (!serverUrl) {
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index b0df65ebc..b2fa4c614 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -202,7 +202,7 @@ describe("AuthDebugger", () => {
// Should first discover and save OAuth metadata
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
- "https://example.com",
+ new URL("https://example.com"),
);
// Check that updateAuthState was called with the right info message
@@ -314,6 +314,11 @@ describe("AuthDebugger", () => {
});
expect(updateAuthState).toHaveBeenCalledWith({
+ authServerUrl: null,
+ authorizationUrl: null,
+ isInitiatingAuth: false,
+ resourceMetadata: null,
+ resourceMetadataError: null,
oauthTokens: null,
oauthStep: "metadata_discovery",
latestError: null,
@@ -355,7 +360,7 @@ describe("AuthDebugger", () => {
});
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
- "https://example.com",
+ new URL("https://example.com"),
);
});
From 2d7936d10b7abeed521091cb3c3173230847abe7 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Mon, 2 Jun 2025 16:20:16 +0100
Subject: [PATCH 15/20] fix type errors
---
client/src/components/OAuthDebugCallback.tsx | 3 ++-
.../src/components/__tests__/AuthDebugger.test.tsx | 12 ++++++++++--
client/src/lib/auth-types.ts | 5 +----
package.json | 1 +
4 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx
index 17705abb0..99774297f 100644
--- a/client/src/components/OAuthDebugCallback.tsx
+++ b/client/src/components/OAuthDebugCallback.tsx
@@ -4,6 +4,7 @@ import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
} from "@/utils/oauthUtils.ts";
+import { AuthDebuggerState } from "@/lib/auth-types";
interface OAuthCallbackProps {
onConnect: ({
@@ -13,7 +14,7 @@ interface OAuthCallbackProps {
}: {
authorizationCode?: string;
errorMsg?: string;
- restoredState?: any;
+ restoredState?: AuthDebuggerState;
}) => void;
}
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index b2fa4c614..772b3d435 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -441,8 +441,16 @@ describe("AuthDebugger", () => {
const updateAuthState = jest.fn();
// Mock window.location.href setter
- delete (window as any).location;
- window.location = { href: "" } as any;
+ const originalLocation = window.location;
+ const locationMock = {
+ ...originalLocation,
+ href: "",
+ origin: "http://localhost:3000"
+ };
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: locationMock
+ });
// Setup mocks for OAuth flow
mockStartAuthorization.mockResolvedValue({
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts
index daf1bc15f..5e8113ef8 100644
--- a/client/src/lib/auth-types.ts
+++ b/client/src/lib/auth-types.ts
@@ -29,10 +29,7 @@ export interface AuthDebuggerState {
oauthTokens: OAuthTokens | null;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
- resourceMetadataError:
- | Error
- | { status: number; statusText: string; message: string }
- | null;
+ resourceMetadataError: Error | null;
authServerUrl: URL | null;
oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
diff --git a/package.json b/package.json
index a5e09c6a5..6ee5719e3 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"test-cli": "cd cli && npm run test",
"prettier-fix": "prettier --write .",
"prettier-check": "prettier --check .",
+ "lint": "prettier --check . && cd client && npm run lint",
"prepare": "npm run build",
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
},
From 98668507c798ce7010e209557ff35be2ebcad92e Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Mon, 2 Jun 2025 16:20:36 +0100
Subject: [PATCH 16/20] version bump
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 6ee5719e3..c0f7cf736 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"@modelcontextprotocol/inspector-cli": "^0.13.0",
"@modelcontextprotocol/inspector-client": "^0.13.0",
"@modelcontextprotocol/inspector-server": "^0.13.0",
- "@modelcontextprotocol/sdk": "^1.12.0",
+ "@modelcontextprotocol/sdk": "^1.12.1",
"concurrently": "^9.0.1",
"open": "^10.1.0",
"shell-quote": "^1.8.2",
From fa01fafa59e34285bd5e5580201dc4b3ec13ce18 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Mon, 2 Jun 2025 16:21:55 +0100
Subject: [PATCH 17/20] version bump + lint
---
client/src/components/__tests__/AuthDebugger.test.tsx | 6 +++---
package-lock.json | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index 772b3d435..3727128a7 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -445,11 +445,11 @@ describe("AuthDebugger", () => {
const locationMock = {
...originalLocation,
href: "",
- origin: "http://localhost:3000"
+ origin: "http://localhost:3000",
};
- Object.defineProperty(window, 'location', {
+ Object.defineProperty(window, "location", {
writable: true,
- value: locationMock
+ value: locationMock,
});
// Setup mocks for OAuth flow
diff --git a/package-lock.json b/package-lock.json
index 6e5d73f09..365883541 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@modelcontextprotocol/inspector-cli": "^0.13.0",
"@modelcontextprotocol/inspector-client": "^0.13.0",
"@modelcontextprotocol/inspector-server": "^0.13.0",
- "@modelcontextprotocol/sdk": "^1.12.0",
+ "@modelcontextprotocol/sdk": "^1.12.1",
"concurrently": "^9.0.1",
"open": "^10.1.0",
"shell-quote": "^1.8.2",
@@ -2005,9 +2005,9 @@
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz",
- "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz",
+ "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==",
"dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5",
From 9371006838de8224046eb8c0d3656b7eb9355af4 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Tue, 3 Jun 2025 11:02:34 +0100
Subject: [PATCH 18/20] less noisy errors
---
client/src/components/__tests__/AuthDebugger.test.tsx | 7 +++++++
client/src/lib/oauth-state-machine.ts | 2 --
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index 3727128a7..2130e68b8 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -104,6 +104,9 @@ describe("AuthDebugger", () => {
jest.clearAllMocks();
sessionStorageMock.getItem.mockReturnValue(null);
+ // Supress
+ jest.spyOn(console, "error").mockImplementation(() => {});
+
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
@@ -124,6 +127,10 @@ describe("AuthDebugger", () => {
mockExchangeAuthorization.mockResolvedValue(mockOAuthTokens);
});
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
const renderAuthDebugger = (props: Partial = {}) => {
const mergedProps = {
...defaultProps,
diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts
index d05369c41..101a33cfc 100644
--- a/client/src/lib/oauth-state-machine.ts
+++ b/client/src/lib/oauth-state-machine.ts
@@ -43,8 +43,6 @@ export const oauthTransitions: Record = {
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
}
} catch (e) {
- console.info(`Failed to find protected resource metadata: ${e}`);
- console.log(e);
if (e instanceof Error) {
resourceMetadataError = e;
} else {
From e2d3989bb219cd34d249cb1d2cc4c5169e85ae30 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Tue, 3 Jun 2025 11:05:10 +0100
Subject: [PATCH 19/20] use scopes from PRM if available
---
client/src/lib/oauth-state-machine.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts
index 101a33cfc..5f10a7830 100644
--- a/client/src/lib/oauth-state-machine.ts
+++ b/client/src/lib/oauth-state-machine.ts
@@ -72,9 +72,13 @@ export const oauthTransitions: Record = {
const metadata = context.state.oauthMetadata!;
const clientMetadata = context.provider.clientMetadata;
+ // Prefer scopes from resource metadata if available
+ const scopesSupported =
+ context.state.resourceMetadata?.scopes_supported ||
+ metadata.scopes_supported;
// Add all supported scopes to client registration
- if (metadata.scopes_supported) {
- clientMetadata.scope = metadata.scopes_supported.join(" ");
+ if (scopesSupported) {
+ clientMetadata.scope = scopesSupported.join(" ");
}
const fullInformation = await registerClient(context.serverUrl, {
From a387c6ad7e5f006a3138aff5235e42d187cbb8e9 Mon Sep 17 00:00:00 2001
From: Paul Carleton
Date: Tue, 3 Jun 2025 17:29:22 +0100
Subject: [PATCH 20/20] hook warning
---
client/src/App.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 00d3bb48c..2fbc7494e 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -307,7 +307,7 @@ const App = () => {
});
}
},
- [],
+ [sseUrl],
);
// Load OAuth tokens when sseUrl changes