diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 96b03b3a..685401ab 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -113,8 +113,8 @@ const DynamicJsonForm = forwardRef( setJsonError(errorMessage); // Reset to default for clearly invalid JSON (not just incomplete typing) - const trimmed = jsonString.trim(); - if (trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { + const trimmed = jsonString?.trim(); + if (trimmed && trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { onChange(generateDefaultValue(schema)); } } @@ -155,7 +155,7 @@ const DynamicJsonForm = forwardRef( const formatJson = () => { try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) { return; } @@ -171,7 +171,7 @@ const DynamicJsonForm = forwardRef( const validateJson = () => { if (!isJsonMode) return { isValid: true, error: null }; try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) return { isValid: true, error: null }; const parsed = JSON.parse(jsonStr); // Clear any pending debounced update and immediately update parent diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 740cc5ed..26f988e9 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -18,6 +18,7 @@ import { generateDefaultValue, isPropertyRequired, normalizeUnionType, + resolveRef, } from "@/utils/schemaUtils"; import { CompatibilityCallToolResult, @@ -90,14 +91,21 @@ const ToolsTab = ({ useEffect(() => { const params = Object.entries( selectedTool?.inputSchema.properties ?? [], - ).map(([key, value]) => [ - key, - generateDefaultValue( + ).map(([key, value]) => { + // First resolve any $ref references + const resolvedValue = resolveRef( value as JsonSchemaType, - key, selectedTool?.inputSchema as JsonSchemaType, - ), - ]); + ); + return [ + key, + generateDefaultValue( + resolvedValue, + key, + selectedTool?.inputSchema as JsonSchemaType, + ), + ]; + }); setParams(Object.fromEntries(params)); // Reset validation errors when switching tools @@ -154,7 +162,12 @@ const ToolsTab = ({

{Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { - const prop = normalizeUnionType(value as JsonSchemaType); + // First resolve any $ref references + const resolvedValue = resolveRef( + value as JsonSchemaType, + selectedTool.inputSchema as JsonSchemaType, + ); + const prop = normalizeUnionType(resolvedValue); const inputSchema = selectedTool.inputSchema as JsonSchemaType; const required = isPropertyRequired(key, inputSchema); @@ -181,16 +194,18 @@ const ToolsTab = ({ ...params, [key]: checked ? null - : prop.default !== null - ? prop.default - : prop.type === "boolean" - ? false - : prop.type === "string" - ? "" - : prop.type === "number" || - prop.type === "integer" - ? undefined - : undefined, + : prop.type === "array" + ? undefined + : prop.default !== null + ? prop.default + : prop.type === "boolean" + ? false + : prop.type === "string" + ? "" + : prop.type === "number" || + prop.type === "integer" + ? undefined + : undefined, }) } /> diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index 3a9336f1..334dafc2 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -48,6 +48,7 @@ export type JsonSchemaType = { const?: JsonValue; oneOf?: (JsonSchemaType | JsonSchemaConst)[]; anyOf?: (JsonSchemaType | JsonSchemaConst)[]; + $ref?: string; }; export type JsonObject = { [key: string]: JsonValue }; @@ -84,8 +85,9 @@ export function tryParseJson(str: string): { success: boolean; data: JsonValue; } { - const trimmed = str.trim(); + const trimmed = str?.trim(); if ( + trimmed && !(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]")) ) { diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 73137805..36587021 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -145,6 +145,50 @@ export function isPropertyRequired( return schema.required?.includes(propertyName) ?? false; } +/** + * Resolves $ref references in JSON schema + * @param schema The schema that may contain $ref + * @param rootSchema The root schema to resolve references against + * @returns The resolved schema without $ref + */ +export function resolveRef( + schema: JsonSchemaType, + rootSchema: JsonSchemaType, +): JsonSchemaType { + if (!("$ref" in schema) || !schema.$ref) { + return schema; + } + + const ref = schema.$ref; + + // Handle simple #/properties/name references + if (ref.startsWith("#/")) { + const path = ref.substring(2).split("/"); + let current: unknown = rootSchema; + + for (const segment of path) { + if ( + current && + typeof current === "object" && + current !== null && + segment in current + ) { + current = (current as Record)[segment]; + } else { + // If reference cannot be resolved, return the original schema + console.warn(`Could not resolve $ref: ${ref}`); + return schema; + } + } + + return current as JsonSchemaType; + } + + // For other types of references, return the original schema + console.warn(`Unsupported $ref format: ${ref}`); + return schema; +} + /** * Normalizes union types (like string|null from FastMCP) to simple types for form rendering * @param schema The JSON schema to normalize @@ -191,6 +235,16 @@ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { return { ...schema, type: "integer", anyOf: undefined, nullable: true }; } + // Handle anyOf with exactly array and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.length === 2 && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "array", anyOf: undefined, nullable: true }; + } + // Handle array type with exactly string and null if ( Array.isArray(schema.type) &&