fix: address Copilot review — const precedence, TUI variant model, decline both-keyword unions

- rootUnion: decline a schema carrying BOTH oneOf and anyOf rather than
  reading one and silently dropping the other's constraints.
- SchemaForm: handle `const` before every type dispatch and for every
  constant type, so a numeric/boolean constant, or a schema carrying both
  `const` and `enum`, can no longer offer a forbidden value.
- collectSchemaDefaults / TUI initialValue: `const` outranks `default`,
  which is an annotation a schema may set to something its const rejects.
- convertToolParameters: resolve from the first branch's merged schema, so a
  branch's specialization of a root-declared property is what types the arg.
- TUI: ink-form keys values by field name across the whole form, so two
  branches' `kind` were one field and the later section's initial value
  decided what the earlier submitted. Branch fields now render under prefixed
  names behind a Variant select, and decodeFormValues translates back on
  submit, dropping every branch but the chosen one.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 01:01:54 -04:00
commit 2782ed75d1

View file

@ -5,7 +5,7 @@ import { InspectorClient } from "@inspector/core/mcp/index.js";
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
import type { Tool, CallToolResult } from "@modelcontextprotocol/client";
import type { JsonValue } from "@inspector/core/mcp/index.js";
import { schemaToForm } from "../utils/schemaToForm.js";
import { decodeFormValues, schemaToForm } from "../utils/schemaToForm.js";
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
interface ToolTestModalProps {
@ -114,9 +114,15 @@ export function ToolTestModal({
{ isActive: true },
);
const handleFormSubmit = async (values: Record<string, JsonValue>) => {
const handleFormSubmit = async (rawValues: Record<string, JsonValue>) => {
if (!inspectorClient || !tool) return;
// A root union renders every alternative as its own section, under prefixed
// field names, because ink-form scopes values by name across the whole form
// (#2123). This turns them back into the arguments the server declared:
// the base fields plus the chosen branch's, and nothing from the others.
const values = decodeFormValues(tool.inputSchema, rawValues);
setState("loading");
const startTime = Date.now();