Skip to content

Commit 8da07c8

Browse files
authored
Merge branch 'main' into bump-version
2 parents ca913eb + 2feac76 commit 8da07c8

File tree

13 files changed

+399
-32
lines changed

13 files changed

+399
-32
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,22 @@ interface DynamicJsonFormProps {
1515
maxDepth?: number;
1616
}
1717

18+
const isTypeSupported = (
19+
type: JsonSchemaType["type"],
20+
supportedTypes: string[],
21+
): boolean => {
22+
if (Array.isArray(type)) {
23+
return type.every((t) => supportedTypes.includes(t));
24+
}
25+
return typeof type === "string" && supportedTypes.includes(type);
26+
};
27+
1828
const isSimpleObject = (schema: JsonSchemaType): boolean => {
1929
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
20-
if (schema.type && supportedTypes.includes(schema.type)) return true;
30+
if (schema.type && isTypeSupported(schema.type, supportedTypes)) return true;
2131
if (schema.type === "object") {
2232
return Object.values(schema.properties ?? {}).every(
23-
(prop) => prop.type && supportedTypes.includes(prop.type),
33+
(prop) => prop.type && isTypeSupported(prop.type, supportedTypes),
2434
);
2535
}
2636
if (schema.type === "array") {
@@ -181,7 +191,13 @@ const DynamicJsonForm = ({
181191
const isRequired =
182192
parentSchema?.required?.includes(propertyName || "") ?? false;
183193

184-
switch (propSchema.type) {
194+
let fieldType = propSchema.type;
195+
if (Array.isArray(fieldType)) {
196+
// Of the possible types, find the first non-null type to determine the control to render
197+
fieldType = fieldType.find((t) => t !== "null") ?? fieldType[0];
198+
}
199+
200+
switch (fieldType) {
185201
case "string": {
186202
if (
187203
propSchema.oneOf &&
@@ -337,6 +353,8 @@ const DynamicJsonForm = ({
337353
required={isRequired}
338354
/>
339355
);
356+
case "null":
357+
return null;
340358
case "object":
341359
if (!propSchema.properties) {
342360
return (

client/src/components/ToolsTab.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import { TabsContent } from "@/components/ui/tabs";
77
import { Textarea } from "@/components/ui/textarea";
88
import DynamicJsonForm from "./DynamicJsonForm";
99
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
10-
import { generateDefaultValue, isPropertyRequired } from "@/utils/schemaUtils";
10+
import {
11+
generateDefaultValue,
12+
isPropertyRequired,
13+
normalizeUnionType,
14+
} from "@/utils/schemaUtils";
1115
import {
1216
CompatibilityCallToolResult,
1317
ListToolsResult,
@@ -104,7 +108,7 @@ const ToolsTab = ({
104108
</p>
105109
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
106110
([key, value]) => {
107-
const prop = value as JsonSchemaType;
111+
const prop = normalizeUnionType(value as JsonSchemaType);
108112
const inputSchema =
109113
selectedTool.inputSchema as JsonSchemaType;
110114
const required = isPropertyRequired(key, inputSchema);
@@ -148,7 +152,10 @@ const ToolsTab = ({
148152
onChange={(e) =>
149153
setParams({
150154
...params,
151-
[key]: e.target.value,
155+
[key]:
156+
e.target.value === ""
157+
? undefined
158+
: e.target.value,
152159
})
153160
}
154161
className="mt-1"

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const mockOAuthClientInfo = {
3636
// Mock MCP SDK functions - must be before imports
3737
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
3838
auth: jest.fn(),
39-
discoverOAuthMetadata: jest.fn(),
39+
discoverAuthorizationServerMetadata: jest.fn(),
4040
registerClient: jest.fn(),
4141
startAuthorization: jest.fn(),
4242
exchangeAuthorization: jest.fn(),
@@ -46,7 +46,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
4646

4747
// Import the functions to get their types
4848
import {
49-
discoverOAuthMetadata,
49+
discoverAuthorizationServerMetadata,
5050
registerClient,
5151
startAuthorization,
5252
exchangeAuthorization,
@@ -57,9 +57,10 @@ import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
5757
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
5858

5959
// Type the mocked functions properly
60-
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
61-
typeof discoverOAuthMetadata
62-
>;
60+
const mockDiscoverAuthorizationServerMetadata =
61+
discoverAuthorizationServerMetadata as jest.MockedFunction<
62+
typeof discoverAuthorizationServerMetadata
63+
>;
6364
const mockRegisterClient = registerClient as jest.MockedFunction<
6465
typeof registerClient
6566
>;
@@ -102,7 +103,9 @@ describe("AuthDebugger", () => {
102103
// Suppress console errors in tests to avoid JSDOM navigation noise
103104
jest.spyOn(console, "error").mockImplementation(() => {});
104105

105-
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
106+
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
107+
mockOAuthMetadata,
108+
);
106109
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
107110
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
108111
new Error("No protected resource metadata found"),
@@ -203,7 +206,7 @@ describe("AuthDebugger", () => {
203206
});
204207

205208
// Should first discover and save OAuth metadata
206-
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
209+
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
207210
new URL("https://example.com/"),
208211
);
209212

@@ -216,7 +219,7 @@ describe("AuthDebugger", () => {
216219
});
217220

218221
it("should show error when quick OAuth flow fails to discover metadata", async () => {
219-
mockDiscoverOAuthMetadata.mockRejectedValue(
222+
mockDiscoverAuthorizationServerMetadata.mockRejectedValue(
220223
new Error("Metadata discovery failed"),
221224
);
222225

@@ -362,7 +365,7 @@ describe("AuthDebugger", () => {
362365
fireEvent.click(screen.getByText("Continue"));
363366
});
364367

365-
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
368+
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
366369
new URL("https://example.com/"),
367370
);
368371
});
@@ -509,7 +512,9 @@ describe("AuthDebugger", () => {
509512
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
510513
mockResourceMetadata,
511514
);
512-
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
515+
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
516+
mockOAuthMetadata,
517+
);
513518

514519
await act(async () => {
515520
renderAuthDebugger({
@@ -563,7 +568,9 @@ describe("AuthDebugger", () => {
563568
// Mock failed metadata discovery
564569
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
565570
// But OAuth metadata should still work with the original URL
566-
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
571+
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
572+
mockOAuthMetadata,
573+
);
567574

568575
await act(async () => {
569576
renderAuthDebugger({
@@ -603,7 +610,7 @@ describe("AuthDebugger", () => {
603610
});
604611

605612
// Verify that regular OAuth metadata discovery was still called
606-
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
613+
expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
607614
new URL("https://example.com/"),
608615
);
609616
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ describe("DynamicJsonForm String Fields", () => {
3535
const input = screen.getByRole("textbox");
3636
expect(input).toHaveProperty("type", "text");
3737
});
38+
39+
it("should handle a union type of string and null", () => {
40+
const schema: JsonSchemaType = {
41+
type: ["string", "null"],
42+
description: "Test string or null field",
43+
};
44+
render(
45+
<DynamicJsonForm schema={schema} value={null} onChange={jest.fn()} />,
46+
);
47+
const input = screen.getByRole("textbox");
48+
expect(input).toHaveProperty("type", "text");
49+
});
3850
});
3951

4052
describe("Format Support", () => {

client/src/lib/auth.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
OAuthMetadata,
99
} from "@modelcontextprotocol/sdk/shared/auth.js";
1010
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
11+
import { generateOAuthState } from "@/utils/oauthUtils";
1112

1213
export const getClientInformationFromSessionStorage = async ({
1314
serverUrl,
@@ -86,6 +87,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
8687
};
8788
}
8889

90+
state(): string | Promise<string> {
91+
return generateOAuthState();
92+
}
93+
8994
async clientInformation() {
9095
// Try to get the preregistered client information from session storage first
9196
const preregisteredClientInformation =
@@ -129,6 +134,12 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
129134
}
130135

131136
redirectToAuthorization(authorizationUrl: URL) {
137+
if (
138+
authorizationUrl.protocol !== "http:" &&
139+
authorizationUrl.protocol !== "https:"
140+
) {
141+
throw new Error("Authorization URL must be HTTP or HTTPS");
142+
}
132143
window.location.href = authorizationUrl.href;
133144
}
134145

client/src/lib/hooks/useConnection.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ export function useConnection({
351351
return;
352352
}
353353

354+
let lastRequest = "";
354355
try {
355356
// Inject auth manually instead of using SSEClientTransport, because we're
356357
// proxying through the inspector server first.
@@ -564,7 +565,9 @@ export function useConnection({
564565
}
565566

566567
if (capabilities?.logging && defaultLoggingLevel) {
568+
lastRequest = "logging/setLevel";
567569
await client.setLoggingLevel(defaultLoggingLevel);
570+
lastRequest = "";
568571
}
569572

570573
if (onElicitationRequest) {
@@ -578,6 +581,17 @@ export function useConnection({
578581
setMcpClient(client);
579582
setConnectionStatus("connected");
580583
} catch (e) {
584+
if (
585+
lastRequest === "logging/setLevel" &&
586+
e instanceof McpError &&
587+
e.code === ErrorCode.MethodNotFound
588+
) {
589+
toast({
590+
title: "Error",
591+
description: `Server declares logging capability but doesn't implement method: "${lastRequest}"`,
592+
variant: "destructive",
593+
});
594+
}
581595
console.error(e);
582596
setConnectionStatus("error");
583597
}

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OAuthStep, AuthDebuggerState } from "./auth-types";
22
import { DebugInspectorOAuthClientProvider } from "./auth";
33
import {
4-
discoverOAuthMetadata,
4+
discoverAuthorizationServerMetadata,
55
registerClient,
66
startAuthorization,
77
exchangeAuthorization,
@@ -12,6 +12,7 @@ import {
1212
OAuthMetadataSchema,
1313
OAuthProtectedResourceMetadata,
1414
} from "@modelcontextprotocol/sdk/shared/auth.js";
15+
import { generateOAuthState } from "@/utils/oauthUtils";
1516

1617
export interface StateMachineContext {
1718
state: AuthDebuggerState;
@@ -56,7 +57,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
5657
resourceMetadata ?? undefined,
5758
);
5859

59-
const metadata = await discoverOAuthMetadata(authServerUrl);
60+
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
6061
if (!metadata) {
6162
throw new Error("Failed to discover OAuth metadata");
6263
}
@@ -113,21 +114,14 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
113114
scope = metadata.scopes_supported.join(" ");
114115
}
115116

116-
// Generate a random state
117-
const array = new Uint8Array(32);
118-
crypto.getRandomValues(array);
119-
const state = Array.from(array, (byte) =>
120-
byte.toString(16).padStart(2, "0"),
121-
).join("");
122-
123117
const { authorizationUrl, codeVerifier } = await startAuthorization(
124118
context.serverUrl,
125119
{
126120
metadata,
127121
clientInformation,
128122
redirectUrl: context.provider.redirectUrl,
129123
scope,
130-
state: state,
124+
state: generateOAuthState(),
131125
resource: context.state.resource ?? undefined,
132126
},
133127
);

client/src/utils/__tests__/oauthUtils.ts renamed to client/src/utils/__tests__/oauthUtils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
generateOAuthErrorDescription,
33
parseOAuthCallbackParams,
4+
generateOAuthState,
45
} from "@/utils/oauthUtils.ts";
56

67
describe("parseOAuthCallbackParams", () => {
@@ -75,4 +76,11 @@ describe("generateOAuthErrorDescription", () => {
7576
"Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.",
7677
);
7778
});
79+
80+
describe("generateOAuthState", () => {
81+
it("Returns a string", () => {
82+
expect(generateOAuthState()).toBeDefined();
83+
expect(generateOAuthState()).toHaveLength(64);
84+
});
85+
});
7886
});

0 commit comments

Comments
 (0)