Skip to content

Commit 5db6426

Browse files
authored
[auth] Add resource compatibility to debugger (RFC 8707) (#526)
* pass resource in auth debugger, reusing server url * throw error on resource url mismatch to match regular auth flow * lint & tests * add test * make authserver url make more sense * use latest typescript sdk * more lenient resource checking * pick resource, and store in state * fixing tests and types * [auth] use sdk function for selecting resource (#537) * use sdk function * bump typescript sdk * fix some tests
1 parent 2ef41ab commit 5db6426

File tree

6 files changed

+34
-22
lines changed

6 files changed

+34
-22
lines changed

client/src/components/OAuthFlowProgress.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export const OAuthFlowProgress = ({
156156
{authState.resourceMetadataError && (
157157
<div className="mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md">
158158
<p className="text-sm font-medium text-blue-700">
159-
ℹ️ No resource metadata available from{" "}
159+
ℹ️ Problem with resource metadata from{" "}
160160
<a
161161
href={
162162
new URL(

client/src/components/__tests__/AuthDebugger.test.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
4141
startAuthorization: jest.fn(),
4242
exchangeAuthorization: jest.fn(),
4343
discoverOAuthProtectedResourceMetadata: jest.fn(),
44+
selectResourceURL: jest.fn(),
4445
}));
4546

4647
// Import the functions to get their types
@@ -88,7 +89,7 @@ describe("AuthDebugger", () => {
8889
const defaultAuthState = EMPTY_DEBUGGER_STATE;
8990

9091
const defaultProps = {
91-
serverUrl: "https://example.com",
92+
serverUrl: "https://example.com/mcp",
9293
onBack: jest.fn(),
9394
authState: defaultAuthState,
9495
updateAuthState: jest.fn(),
@@ -203,7 +204,7 @@ describe("AuthDebugger", () => {
203204

204205
// Should first discover and save OAuth metadata
205206
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
206-
new URL("https://example.com"),
207+
new URL("https://example.com/"),
207208
);
208209

209210
// Check that updateAuthState was called with the right info message
@@ -320,6 +321,7 @@ describe("AuthDebugger", () => {
320321
isInitiatingAuth: false,
321322
resourceMetadata: null,
322323
resourceMetadataError: null,
324+
resource: null,
323325
oauthTokens: null,
324326
oauthStep: "metadata_discovery",
325327
latestError: null,
@@ -361,7 +363,7 @@ describe("AuthDebugger", () => {
361363
});
362364

363365
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
364-
new URL("https://example.com"),
366+
new URL("https://example.com/"),
365367
);
366368
});
367369

@@ -496,11 +498,11 @@ describe("AuthDebugger", () => {
496498
it("should successfully fetch and display protected resource metadata", async () => {
497499
const updateAuthState = jest.fn();
498500
const mockResourceMetadata = {
499-
resource: "https://example.com/api",
501+
resource: "https://example.com/mcp",
500502
authorization_servers: ["https://custom-auth.example.com"],
501503
bearer_methods_supported: ["header", "body"],
502-
resource_documentation: "https://example.com/api/docs",
503-
resource_policy_uri: "https://example.com/api/policy",
504+
resource_documentation: "https://example.com/mcp/docs",
505+
resource_policy_uri: "https://example.com/mcp/policy",
504506
};
505507

506508
// Mock successful metadata discovery
@@ -538,7 +540,7 @@ describe("AuthDebugger", () => {
538540
// Wait for the metadata to be fetched
539541
await waitFor(() => {
540542
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
541-
"https://example.com",
543+
"https://example.com/mcp",
542544
);
543545
});
544546

@@ -584,7 +586,7 @@ describe("AuthDebugger", () => {
584586
// Wait for the metadata fetch to fail
585587
await waitFor(() => {
586588
expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
587-
"https://example.com",
589+
"https://example.com/mcp",
588590
);
589591
});
590592

@@ -594,15 +596,15 @@ describe("AuthDebugger", () => {
594596
expect.objectContaining({
595597
resourceMetadataError: mockError,
596598
// Should use the original server URL as fallback
597-
authServerUrl: new URL("https://example.com"),
599+
authServerUrl: new URL("https://example.com/"),
598600
oauthStep: "client_registration",
599601
}),
600602
);
601603
});
602604

603605
// Verify that regular OAuth metadata discovery was still called
604606
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
605-
new URL("https://example.com"),
607+
new URL("https://example.com/"),
606608
);
607609
});
608610
});

client/src/lib/auth-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface AuthDebuggerState {
3030
oauthStep: OAuthStep;
3131
resourceMetadata: OAuthProtectedResourceMetadata | null;
3232
resourceMetadataError: Error | null;
33+
resource: URL | null;
3334
authServerUrl: URL | null;
3435
oauthMetadata: OAuthMetadata | null;
3536
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
@@ -47,6 +48,7 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
4748
oauthMetadata: null,
4849
resourceMetadata: null,
4950
resourceMetadataError: null,
51+
resource: null,
5052
authServerUrl: null,
5153
oauthClientInfo: null,
5254
authorizationUrl: null,

client/src/lib/oauth-state-machine.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
startAuthorization,
77
exchangeAuthorization,
88
discoverOAuthProtectedResourceMetadata,
9+
selectResourceURL,
910
} from "@modelcontextprotocol/sdk/client/auth.js";
1011
import {
1112
OAuthMetadataSchema,
@@ -29,17 +30,15 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
2930
metadata_discovery: {
3031
canTransition: async () => true,
3132
execute: async (context) => {
32-
let authServerUrl = new URL(context.serverUrl);
33+
// Default to discovering from the server's URL
34+
let authServerUrl = new URL("/", context.serverUrl);
3335
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
3436
let resourceMetadataError: Error | null = null;
3537
try {
3638
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
3739
context.serverUrl,
3840
);
39-
if (
40-
resourceMetadata &&
41-
resourceMetadata.authorization_servers?.length
42-
) {
41+
if (resourceMetadata?.authorization_servers?.length) {
4342
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
4443
}
4544
} catch (e) {
@@ -50,6 +49,13 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
5049
}
5150
}
5251

52+
const resource: URL | undefined = await selectResourceURL(
53+
context.serverUrl,
54+
context.provider,
55+
// we default to null, so swap it for undefined if not set
56+
resourceMetadata ?? undefined,
57+
);
58+
5359
const metadata = await discoverOAuthMetadata(authServerUrl);
5460
if (!metadata) {
5561
throw new Error("Failed to discover OAuth metadata");
@@ -58,6 +64,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
5864
context.provider.saveServerMetadata(parsedMetadata);
5965
context.updateState({
6066
resourceMetadata,
67+
resource,
6168
resourceMetadataError,
6269
authServerUrl,
6370
oauthMetadata: parsedMetadata,
@@ -113,6 +120,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
113120
clientInformation,
114121
redirectUrl: context.provider.redirectUrl,
115122
scope,
123+
resource: context.state.resource ?? undefined,
116124
},
117125
);
118126

@@ -163,6 +171,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
163171
authorizationCode: context.state.authorizationCode,
164172
codeVerifier,
165173
redirectUri: context.provider.redirectUrl,
174+
resource: context.state.resource ?? undefined,
166175
});
167176

168177
context.provider.saveTokens(tokens);

package-lock.json

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@modelcontextprotocol/inspector-cli": "^0.14.3",
4747
"@modelcontextprotocol/inspector-client": "^0.14.3",
4848
"@modelcontextprotocol/inspector-server": "^0.14.3",
49-
"@modelcontextprotocol/sdk": "^1.13.0",
49+
"@modelcontextprotocol/sdk": "^1.13.1",
5050
"concurrently": "^9.0.1",
5151
"open": "^10.1.0",
5252
"shell-quote": "^1.8.2",

0 commit comments

Comments
 (0)