fix: render nested/Optional arrays as structured forms & align string field style with ToolsTab#1258
Conversation
- Replace <Input type="text"> with <Textarea> for plain-text string properties in DynamicJsonForm, matching the height and style of direct-parameter string inputs in ToolsTab - Special-format strings (email, uri, date, date-time) keep <Input> with their appropriate type attribute - Boolean fields already used <Switch>; update tests to query role="switch" / aria-checked instead of role="checkbox" - Update test assertions to reflect the textarea element type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Overview
The PR fixes two real, user-visible issues in DynamicJsonForm:
- FastMCP/Pydantic v2 schemas (
List[Model],Optional[List[T]],List[Optional[T]]) were falling back to a raw JSON editor instead of getting structured Add/Remove controls. The fix routes those schemas through the existingnormalizeUnionTypehelper before type dispatch. - Plain string fields in
DynamicJsonFormrendered as single-line<Input>whileToolsTabrenders direct-parameter strings as<Textarea>— a visible mismatch. Now both use<Textarea>.
Also swaps the boolean checkbox for shadcn <Switch> and tidies description whitespace.
The core approach is sound: normalizing anyOf:[X,null] at the top of renderFormFields and on propSchema.items cleanly handles all three Optional/nested patterns with a single helper instead of scattering special-cases through the type switch.
What's done well
normalizeUnionTypeis applied at exactly the right entry points — top ofrenderFormFieldsand onpropSchema.items— soOptional[List[T]],List[Optional[T]], and top-levelOptional[Array]all converge on the same code paths.- The new array case correctly distinguishes "items are an object with properties" from
isSimpleObjectitems, and the per-card layout with a bottom-right Remove button is reasonable. - The new tests are targeted and meaningful — the four cases in "Complex Array Rendering" precisely cover the bugs the PR claims to fix, and the union-type test (
anyOf:[array,null]) is the kind of test that would have caught this regression in the first place. - Style alignment with
ToolsTab.tsxis verified — that component does renderprop.type === "string"as<Textarea>, so this PR brings the nested form into line.
Issues to address
1. Regression: pattern validation silently dropped for plain strings (correctness)
Plain-text strings switched from <Input pattern={...}> to <Textarea>, which has no pattern attribute. The PR removes the corresponding test with a comment explaining <textarea> doesn't support it, but schema.pattern constraints from server-side tool schemas will now be ignored client-side — a real loss of validation that isn't called out in the PR description.
Suggested fix: keep <Input type=\"text\"> when propSchema.pattern is present, or implement pattern checking manually (onBlur validation against new RegExp(propSchema.pattern)).
2. Regression: top-level array description disappears
The diff removes these blocks from the structured array case:
- {propSchema.description && (
- <p className=\"text-sm text-gray-600\">{propSchema.description}</p>
- )}
- {propSchema.items?.description && (
- <p className=\"text-sm text-gray-500\">Items: {propSchema.items.description}</p>
- )}Two test assertions for \"Test array field\" and \"Items: Array item\" were correspondingly deleted. The PR description doesn't mention this. For top-level array schemas (where there's no parent object label upstream to surface the description), users will lose this context.
Suggested fix: restore both blocks above the items list, or move into the per-card object header.
3. required attribute dropped on booleans (minor)
Previous <Input type=\"checkbox\" required={isRequired}> is replaced with <Switch>, which doesn't accept required. Usually inconsequential for booleans (false is a valid value), but inconsistent with the rest of the form. Probably acceptable, but worth a deliberate decision rather than a silent drop.
4. Parameter reassignment style (minor)
In renderFormFields, both propSchema = normalizeUnionType(propSchema) and the description-trim block reassign the function parameter. Functionally fine, but a local const normalizedSchema = ... would read more clearly given the function is ~300 lines long.
5. "True"/"False" label (minor UX)
The new <span> next to the Switch reads \"True\" : \"False\". Most toggle UIs either omit a value label entirely (relying on the toggle's on/off visual + a separate field label) or use lowercase/sentence-case. Capitalized "True"/"False" reads more like a Python repr than a UI element.
Smaller observations
- The
specialFormatMaprefactor (typedRecord<SpecialFormat, string>) is a nice tightening — restrictsinputTypeto known good values. - Description-trimming is reasonable defensive handling for Pydantic docstrings (
\"\"\"\n text \n\"\"\"). Allocating a new schema object on every render in a recursive function is wasteful but negligible at typical form sizes; not worth optimizing. canRenderTopLevelFormnow usesnormalizeUnionTypecorrectly. Note this meansOptional[primitive]at the top level becomes form-capable — that's the desired behavior but worth confirming with a test (none added).- Test changes around
role=\"switch\"/aria-checkedare the canonical RTL approach for shadcn Switch.
Risk summary
- Behavioral — Two silent regressions (pattern validation, array description) that aren't reflected in the PR description. The pattern one in particular has correctness implications if tool authors rely on
patternfor input-shape constraints. - Test coverage — Good for the bugs being fixed; insufficient for the regressions introduced (no test that
patternis enforced; assertion for the array description was deleted rather than relocated). - Performance — No concerns.
- Security — Loss of client-side
patternvalidation is not a security boundary (server should always re-validate), but it does weaken UX-level input guardrails.
Recommendation
The core fix is correct and valuable, but the PR should preserve pattern validation and the array description. Items 3–5 are polish and not blocking.
Plain-text strings render as <Textarea> to match ToolsTab, but <textarea> has no `pattern` attribute, so a string schema's `pattern` constraint was silently dropped client-side. Fall back to <Input type="text"> when `propSchema.pattern` is present so the constraint is preserved (minLength/maxLength/required carried along). Adds a regression test asserting a pattern-constrained string renders an <input> carrying the pattern attribute. Addresses review item modelcontextprotocol#1 on modelcontextprotocol#1258. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structured array case dropped the array-level description and the "Items: <item description>" line. For top-level array schemas there is no parent object label upstream, so users lost that context. Restore both blocks above the items list. Re-adds the assertions (relocated into the existing test) verifying the array description and the per-item "Items:" line render. Addresses review item modelcontextprotocol#2 on modelcontextprotocol#1258. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PR had swapped the boolean <Input type="checkbox"> for a shadcn <Switch>, which dropped the `required` attribute (Switch had none) and added a capitalized "True"/"False" label. Revert to the native checkbox that the Inspector used previously: it natively supports `required` (restoring parity with every other field) and removes the label, so the control is consistent with the rest of the form again. Reverts the boolean test assertions from role="switch"/aria-checked back to role="checkbox" in both component test files. Addresses review items modelcontextprotocol#3 (required dropped) and modelcontextprotocol#5 (True/False label) on modelcontextprotocol#1258 by removing the Switch change that introduced them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renderFormFields reassigned its `propSchema` parameter (both for normalizeUnionType and the description trim). Rename the parameter to `rawSchema` and bind a local `propSchema` instead, so the incoming parameter is never mutated. Pure refactor; no behavior change. Addresses review item modelcontextprotocol#4 on modelcontextprotocol#1258. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three small improvements that make the structured array forms usable
out of the box:
- getArrayItemDefault now builds object items via generateDefaultValue,
so "Add Item" on an object array seeds required fields instead of {}.
- Switching an empty structured array to JSON seeds one template item so
the expected shape is visible instead of a bare [].
- generateDefaultValue returns [] for a root-level array schema (not
undefined), so a top-level array renders an empty form with an Add
button rather than falling back.
Adds tests for the object-item default, the JSON template seeding, and
updates the root-level array default-value test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review! Addressed all points:
I also folded in a few small array-default improvements (with tests): All client lint/typecheck and the full test suite pass locally, and the branch is rebased on the latest |




Problem
Two related rendering gaps in
DynamicJsonForm:List[T]→{ type: "array", items: { type: "object", properties: {...} } }Optional[List[str]]→{ anyOf: [{ type: "array", items: {...} }, { type: "null" }] }List[Optional[T]]→ items wrapped inanyOf: [object, null]All three fell back to a raw JSON editor instead of structured Add/Remove controls.
DynamicJsonFormrendered plain-text strings as<Input>(single-line, 36 px), whileToolsTabrenders direct-parameter strings as<Textarea>— visually inconsistent.Solution
DynamicJsonForm.tsxcanRenderTopLevelForm— callsnormalizeUnionType(s)first soanyOf:[array,null]top-level schemas enable form mode.renderFormFields— callsnormalizeUnionType(...)at the top of every recursive call soanyOf:[X,null]fields resolve to their real type before any switch/depth check. Binds a local from arawSchemaparameter instead of mutating the parameter.case "array"— normalizespropSchema.itemsthroughnormalizeUnionType; gates structured rendering onisSimpleObject(itemSchema) || (itemSchema.type === "object" && !!itemSchema.properties)so arrays of objects get Add/Remove controls instead of falling back to JSON. The array-level description and the per-itemItems: …description render above the items list.case "string"— plain text now renders as<Textarea>(matchingToolsTab); special formats (email,uri,date,date-time) keep<Input type="...">. A string that declares apatternkeeps<Input type="text" pattern=…>so the constraint is preserved client-side (<textarea>has nopattern).propSchema.descriptionto strip leading/trailing whitespace from Python triple-quoted docstrings.Add Itemon an object array seeds required fields (viagenerateDefaultValue) instead of{}; switching an empty structured array to JSON seeds one template item so the shape is visible; a top-level array schema defaults to[].Tests
DynamicJsonForm.test.tsx— plain-text strings asserttype="textarea"; a pattern-constrained string asserts<input type="text">carrying thepatternattribute.DynamicJsonForm.array.test.tsx— targeted cases for object arrays, untyped fallback,Optional[List[X]], andList[Optional[X]]; array/item description assertions; object-item default and JSON template-seeding tests.schemaUtils.test.ts— a root-level array schema defaults to[].Test plan
cd client && npm test— full client suite passesnpm run lintpasses with no new warningsList[SomeModel]parameter → fields render as a structured form with Add/RemoveOptional[List[str]]→ same structured form, not a JSON boxemail,date, etc.) still render as typed<input>elementsUpdate — addressed review feedback
<Input type="text" pattern>so the constraint is preserved (regression test added).requiredon booleans + "True"/"False" label — reverted the boolean field from the shadcn<Switch>back to the native<Input type="checkbox" required>the Inspector used previously: it natively supportsrequired(restoring parity with the other fields) and drops the capitalized label. Boolean tests reverted torole="checkbox".renderFormFieldsno longer mutates itspropSchemaparameter.