fix: gate submission on unsendable field text, and on required tool args

A field that cannot turn its text into a value reports `undefined` — which is
exactly what an empty field reports. That made the two indistinguishable to
every caller of `SchemaForm`, so invalid text in an optional field was
submittable and the argument simply arrived absent (#2020).

`SchemaForm` now tracks per-field draft validity and reports it through
`onValidityChange`, covering both fields that hold text the form cannot send:
the JSON editor (unparseable text) and the number input (a magnitude JS cannot
hold exactly, which is dropped rather than silently rounded). The number field
also states it on itself, as the JSON editor already did. A nested object form
reports through the same channel, and both the field-level and form-level
effects report "valid" on cleanup so a draft cannot outlive the field that held
it — a remount under a new `resetKey`, or an unmounted panel.

`ToolDetailPanel` gated Execute on `isExecuting` alone, so it also had no
required-argument check at all — unlike the App and elicitation panels. It now
uses `hasMissingRequiredFields` like they do. `InlineElicitationRequest` had
neither gate and gets both.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-15 21:09:51 -04:00
commit 6002c2ce33

View file

@ -1,4 +1,5 @@
import { Button, Divider, ScrollArea, Stack, Text } from "@mantine/core";
import { useState } from "react";
import { MdPlayArrow } from "react-icons/md";
import type { Tool } from "@modelcontextprotocol/client";
import { SchemaForm } from "../SchemaForm/SchemaForm";
@ -58,7 +59,12 @@ export function AppDetailPanel({
/* v8 ignore next -- unreachable: Tool.inputSchema is always an object */
const formSchema = toFormSchema(inputSchema) ?? {};
const hasErrors = hasMissingRequiredFields(formSchema, formValues);
const disabled = isOpening || hasErrors;
// A field holding text it could not turn into a value (unparseable JSON, an
// unrepresentable number) reports `undefined`, which is indistinguishable
// from empty once it reaches `formValues` — so the form reports it directly
// and an optional argument can no longer be dropped silently (#2020).
const [hasInvalidDraft, setHasInvalidDraft] = useState(false);
const disabled = isOpening || hasErrors || hasInvalidDraft;
const hasFields = hasInputFields(tool);
return (
@ -82,6 +88,7 @@ export function AppDetailPanel({
// tool's name to drop another app's in-progress field text. See
// SchemaFormProps.resetKey.
resetKey={tool.name}
onValidityChange={setHasInvalidDraft}
/>
<OpenAppButton