From fd99d879c5075c5fda8eb05a18ac735752b5b7cf Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 2 Dec 2025 19:37:28 -0500 Subject: [PATCH 1/4] DynamicJsonForm improvements and full enum schema support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In DynamicJsonForm.tsx, - Support all five enum shapes used by MCP SDK: - Titled single-select via `oneOf`/`anyOf` with `{ const, title }` - Untitled single-select via `enum` - Titled legacy single-select with optional `enumNames` for labels - Titled multi-select via `items.anyOf`/`oneOf` - Untitled multi-select via `items.enum` - Show field descriptions for string select fields and boolean checkboxes (consistent help text above inputs). - Prefer schema property `title` for object field labels; fall back to the JSON key when missing. - Allow top‑level form rendering for objects with properties, arrays with items, and primitive types (was overly strict before). - Multi-select UI: sensible list size and `minItems`/`maxItems` helper text when present. - JSON editor fallback, copy/format controls, and debounced parsing preserved. - apply defaults for optional fields during form init In schemaUtils.ts - `generateDefaultValue` now includes optional properties that declare a `default`, ensuring fields like strings show their defaults on first render. In jsonUtils.ts - Add legacy `enumNames?: string[]` and array constraints `minItems?`/`maxItems?`. --- client/src/components/DynamicJsonForm.tsx | 235 ++++++++++++++++------ client/src/utils/jsonUtils.ts | 5 + client/src/utils/schemaUtils.ts | 6 +- 3 files changed, 184 insertions(+), 62 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 685401ab9..af5f4def4 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -76,7 +76,38 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => { const DynamicJsonForm = forwardRef( ({ schema, value, onChange, maxDepth = 3 }, ref) => { - const isOnlyJSON = !isSimpleObject(schema); + // Determine if we can render a form at the top level. + // This is more permissive than isSimpleObject(): + // - Objects with any properties are form-capable (individual complex fields may still fallback to JSON) + // - Arrays with defined items are form-capable + // - Primitive types are form-capable + const canRenderTopLevelForm = (s: JsonSchemaType): boolean => { + const primitiveTypes = ["string", "number", "integer", "boolean", "null"]; + + const hasType = Array.isArray(s.type) ? s.type.length > 0 : !!s.type; + if (!hasType) return false; + + const includesType = (t: string) => + Array.isArray(s.type) ? s.type.includes(t as any) : s.type === t; + + // Primitive at top-level + if (primitiveTypes.some(includesType)) return true; + + // Object with properties + if (includesType("object")) { + const keys = Object.keys(s.properties ?? {}); + return keys.length > 0; + } + + // Array with items + if (includesType("array")) { + return !!s.items; + } + + return false; + }; + + const isOnlyJSON = !canRenderTopLevelForm(schema); const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [jsonError, setJsonError] = useState(); const [copiedJson, setCopiedJson] = useState(false); @@ -267,63 +298,76 @@ const DynamicJsonForm = forwardRef( switch (fieldType) { case "string": { - if ( - propSchema.oneOf && - propSchema.oneOf.every( - (option) => - typeof option.const === "string" && - typeof option.title === "string", - ) - ) { + // Titled single-select using oneOf/anyOf with const/title pairs + const titledOptions = (propSchema.oneOf ?? propSchema.anyOf)?.filter( + (opt) => (opt as any).const !== undefined, + ) as { const: string; title?: string }[] | undefined; + + if (titledOptions && titledOptions.length > 0) { return ( - +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + +
); } + // Untitled single-select using enum (with optional legacy enumNames for labels) if (propSchema.enum) { + const names = Array.isArray((propSchema as any).enumNames) + ? (propSchema as any).enumNames + : undefined; return ( - +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + +
); } @@ -413,13 +457,20 @@ const DynamicJsonForm = forwardRef( case "boolean": return ( - handleFieldChange(path, e.target.checked)} - className="w-4 h-4" - required={isRequired} - /> +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + handleFieldChange(path, e.target.checked)} + className="w-4 h-4" + required={isRequired} + /> +
); case "null": return null; @@ -449,7 +500,7 @@ const DynamicJsonForm = forwardRef( {Object.entries(propSchema.properties).map(([key, subSchema]) => (