Skip to content

Commit ea9ab9f

Browse files
authored
Merge branch 'main' into fix-630
2 parents d3c168e + 6330cb4 commit ea9ab9f

File tree

13 files changed

+605
-49
lines changed

13 files changed

+605
-49
lines changed

cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-cli",
3-
"version": "0.16.3",
3+
"version": "0.16.4",
44
"description": "CLI for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -21,7 +21,7 @@
2121
},
2222
"devDependencies": {},
2323
"dependencies": {
24-
"@modelcontextprotocol/sdk": "^1.17.0",
24+
"@modelcontextprotocol/sdk": "^1.17.2",
2525
"commander": "^13.1.0",
2626
"spawn-rx": "^5.1.2"
2727
}

cli/src/transport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ function createStdioTransport(options: TransportOptions): Transport {
3232
const defaultEnv = getDefaultEnvironment();
3333

3434
const env: Record<string, string> = {
35-
...processEnv,
3635
...defaultEnv,
36+
...processEnv,
3737
};
3838

3939
const { cmd: actualCommand, args: actualArgs } = findActualExecutable(

client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.16.3",
3+
"version": "0.16.4",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -25,7 +25,7 @@
2525
"cleanup:e2e": "node e2e/global-teardown.js"
2626
},
2727
"dependencies": {
28-
"@modelcontextprotocol/sdk": "^1.17.0",
28+
"@modelcontextprotocol/sdk": "^1.17.2",
2929
"@radix-ui/react-checkbox": "^1.1.4",
3030
"@radix-ui/react-dialog": "^1.1.3",
3131
"@radix-ui/react-icons": "^1.3.0",

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

Lines changed: 179 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const mockOAuthMetadata = {
2525
token_endpoint: "https://oauth.example.com/token",
2626
response_types_supported: ["code"],
2727
grant_types_supported: ["authorization_code"],
28+
scopes_supported: ["read", "write"],
2829
};
2930

3031
const mockOAuthClientInfo = {
@@ -56,6 +57,57 @@ import {
5657
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
5758
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
5859

60+
// Mock local auth module
61+
jest.mock("@/lib/auth", () => ({
62+
DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
63+
tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
64+
clear: jest.fn().mockImplementation(() => {
65+
// Mock the real clear() behavior which removes items from sessionStorage
66+
sessionStorage.removeItem("[https://example.com/mcp] mcp_tokens");
67+
sessionStorage.removeItem("[https://example.com/mcp] mcp_client_info");
68+
sessionStorage.removeItem(
69+
"[https://example.com/mcp] mcp_server_metadata",
70+
);
71+
}),
72+
redirectUrl: "http://localhost:3000/oauth/callback/debug",
73+
clientMetadata: {
74+
redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
75+
token_endpoint_auth_method: "none",
76+
grant_types: ["authorization_code", "refresh_token"],
77+
response_types: ["code"],
78+
client_name: "MCP Inspector",
79+
},
80+
clientInformation: jest.fn().mockImplementation(async () => {
81+
const serverUrl = "https://example.com/mcp";
82+
const preregisteredKey = `[${serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`;
83+
const preregisteredData = sessionStorage.getItem(preregisteredKey);
84+
if (preregisteredData) {
85+
return JSON.parse(preregisteredData);
86+
}
87+
const dynamicKey = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
88+
const dynamicData = sessionStorage.getItem(dynamicKey);
89+
if (dynamicData) {
90+
return JSON.parse(dynamicData);
91+
}
92+
return undefined;
93+
}),
94+
saveClientInformation: jest.fn().mockImplementation((clientInfo) => {
95+
const serverUrl = "https://example.com/mcp";
96+
const key = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
97+
sessionStorage.setItem(key, JSON.stringify(clientInfo));
98+
}),
99+
saveTokens: jest.fn(),
100+
redirectToAuthorization: jest.fn(),
101+
saveCodeVerifier: jest.fn(),
102+
codeVerifier: jest.fn(),
103+
saveServerMetadata: jest.fn(),
104+
getServerMetadata: jest.fn(),
105+
})),
106+
discoverScopes: jest.fn().mockResolvedValue("read write" as never),
107+
}));
108+
109+
import { discoverScopes } from "@/lib/auth";
110+
59111
// Type the mocked functions properly
60112
const mockDiscoverAuthorizationServerMetadata =
61113
discoverAuthorizationServerMetadata as jest.MockedFunction<
@@ -75,6 +127,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
75127
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
76128
typeof discoverOAuthProtectedResourceMetadata
77129
>;
130+
const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
131+
typeof discoverScopes
132+
>;
78133

79134
const sessionStorageMock = {
80135
getItem: jest.fn(),
@@ -103,9 +158,15 @@ describe("AuthDebugger", () => {
103158
// Suppress console errors in tests to avoid JSDOM navigation noise
104159
jest.spyOn(console, "error").mockImplementation(() => {});
105160

106-
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
107-
mockOAuthMetadata,
108-
);
161+
// Set default mock behaviors with complete OAuth metadata
162+
mockDiscoverAuthorizationServerMetadata.mockResolvedValue({
163+
issuer: "https://oauth.example.com",
164+
authorization_endpoint: "https://oauth.example.com/authorize",
165+
token_endpoint: "https://oauth.example.com/token",
166+
response_types_supported: ["code"],
167+
grant_types_supported: ["authorization_code"],
168+
scopes_supported: ["read", "write"],
169+
});
109170
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
110171
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
111172
new Error("No protected resource metadata found"),
@@ -427,7 +488,24 @@ describe("AuthDebugger", () => {
427488
});
428489
});
429490

430-
it("should not include scope in authorization URL when scopes_supported is not present", async () => {
491+
it("should include scope in authorization URL when scopes_supported is not present", async () => {
492+
const updateAuthState =
493+
await setupAuthorizationUrlTest(mockOAuthMetadata);
494+
495+
// Wait for the updateAuthState to be called
496+
await waitFor(() => {
497+
expect(updateAuthState).toHaveBeenCalledWith(
498+
expect.objectContaining({
499+
authorizationUrl: expect.stringContaining("scope="),
500+
}),
501+
);
502+
});
503+
});
504+
505+
it("should omit scope from authorization URL when discoverScopes returns undefined", async () => {
506+
// Mock discoverScopes to return undefined (no scopes available)
507+
mockDiscoverScopes.mockResolvedValueOnce(undefined);
508+
431509
const updateAuthState =
432510
await setupAuthorizationUrlTest(mockOAuthMetadata);
433511

@@ -442,6 +520,103 @@ describe("AuthDebugger", () => {
442520
});
443521
});
444522

523+
describe("Client Registration behavior", () => {
524+
it("uses preregistered (static) client information without calling DCR", async () => {
525+
const preregClientInfo = {
526+
client_id: "static_client_id",
527+
client_secret: "static_client_secret",
528+
redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
529+
};
530+
531+
// Return preregistered client info for the server-specific key
532+
sessionStorageMock.getItem.mockImplementation((key) => {
533+
if (
534+
key ===
535+
`[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`
536+
) {
537+
return JSON.stringify(preregClientInfo);
538+
}
539+
return null;
540+
});
541+
542+
const updateAuthState = jest.fn();
543+
544+
await act(async () => {
545+
renderAuthDebugger({
546+
updateAuthState,
547+
authState: {
548+
...defaultAuthState,
549+
isInitiatingAuth: false,
550+
oauthStep: "client_registration",
551+
oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
552+
},
553+
});
554+
});
555+
556+
// Proceed from client_registration → authorization_redirect
557+
await act(async () => {
558+
fireEvent.click(screen.getByText("Continue"));
559+
});
560+
561+
// Should NOT attempt dynamic client registration
562+
expect(mockRegisterClient).not.toHaveBeenCalled();
563+
564+
// Should advance with the preregistered client info
565+
expect(updateAuthState).toHaveBeenCalledWith(
566+
expect.objectContaining({
567+
oauthClientInfo: expect.objectContaining({
568+
client_id: "static_client_id",
569+
}),
570+
oauthStep: "authorization_redirect",
571+
}),
572+
);
573+
});
574+
575+
it("falls back to DCR when no static client information is available", async () => {
576+
// No preregistered or dynamic client info present in session storage
577+
sessionStorageMock.getItem.mockImplementation(() => null);
578+
579+
// DCR returns a new client
580+
mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo);
581+
582+
const updateAuthState = jest.fn();
583+
584+
await act(async () => {
585+
renderAuthDebugger({
586+
updateAuthState,
587+
authState: {
588+
...defaultAuthState,
589+
isInitiatingAuth: false,
590+
oauthStep: "client_registration",
591+
oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
592+
},
593+
});
594+
});
595+
596+
await act(async () => {
597+
fireEvent.click(screen.getByText("Continue"));
598+
});
599+
600+
expect(mockRegisterClient).toHaveBeenCalledTimes(1);
601+
602+
// Should save and advance with the DCR client info
603+
expect(updateAuthState).toHaveBeenCalledWith(
604+
expect.objectContaining({
605+
oauthClientInfo: expect.objectContaining({
606+
client_id: "test_client_id",
607+
}),
608+
oauthStep: "authorization_redirect",
609+
}),
610+
);
611+
612+
// Verify the dynamically registered client info was persisted
613+
expect(sessionStorage.setItem).toHaveBeenCalledWith(
614+
`[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`,
615+
expect.any(String),
616+
);
617+
});
618+
});
619+
445620
describe("OAuth State Persistence", () => {
446621
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
447622
const updateAuthState = jest.fn();

0 commit comments

Comments
 (0)