fix: render nullable enum fields as a Select instead of a raw JSON textarea
A tool argument that is optional AND explicitly nullable — Zod's
`.nullish()` / `.nullable().optional()` — compiles to
`anyOf: [{ type: "string", enum: [...] }, { type: "null" }]`. Every branch
of the tool-call form's field dispatcher tests a single `type` string, so
such a field matched nothing and fell through to the raw-JSON fallback,
whose onChange stores the unparseable in-progress text back into state and
re-`JSON.stringify`s it on the next render — compounding a fresh layer of
escaping on every keystroke.
`normalizeUnionType` already existed for the FastMCP `string|null` shape,
but it only copied the surviving branch's `type` and dropped everything
else, so the `enum` never reached the dispatcher; and nothing called it on
a tool input schema — only elicitation requests, via
`resolveRefsInMessage`. So:
- Rewrite `normalizeUnionType` to hoist the non-null branch's own keywords
(`enum`, `items`, `properties`, `minimum`, …) onto the flattened schema,
as v1.x did. This also collapses nine near-identical blocks into two and
picks up the `object`/`array` cases the `type: [X, "null"]` form was
missing.
- Normalize each field in `SchemaForm.renderField`, so tool, elicitation,
and app forms all get it rather than just elicitation.
- Give a nullable enum `Select` a clear affordance, so a value can be set
back to `null`; a non-nullable enum keeps none, since it would reject it.
Adds a `record_shipment` preset and `nullable-fields-http.json` showcase
config serving all four nullable scalar shapes, for manual verification.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ebeaa142df
commit
937f1dd51b
2 changed files with 428 additions and 84 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
} from "./composable-test-server.js";
|
||||
import {
|
||||
createEchoTool,
|
||||
createNullableFieldsTool,
|
||||
createGetEnvTool,
|
||||
createAddTool,
|
||||
createGetSumTool,
|
||||
|
|
@ -91,6 +92,9 @@ function resolveToolPreset(
|
|||
switch (name) {
|
||||
case "echo":
|
||||
return createEchoTool();
|
||||
case "record_shipment":
|
||||
case "nullable_fields":
|
||||
return createNullableFieldsTool();
|
||||
case "get-env":
|
||||
case "get_env":
|
||||
return createGetEnvTool();
|
||||
|
|
|
|||
|
|
@ -162,6 +162,39 @@ export function createEchoTool(): ToolDefinition {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "record_shipment" tool whose arguments are all **nullable** — the
|
||||
* shape Zod's `.nullish()` / `.nullable()` compiles to, where the real type
|
||||
* sits inside `anyOf: [<branch>, { type: "null" }]` rather than at the top
|
||||
* level.
|
||||
*
|
||||
* `direction` is the case from #1928: a nullable *enum*, whose `enum` keyword
|
||||
* lives on the surviving branch. Before the fix nothing in the tool-call form
|
||||
* matched such a field, so it fell through to the raw-JSON textarea and
|
||||
* re-escaped its own value on every keystroke. The other three cover the
|
||||
* remaining scalar branches so a regression in one is visible next to a
|
||||
* working sibling.
|
||||
*/
|
||||
export function createNullableFieldsTool(): ToolDefinition {
|
||||
return {
|
||||
name: "record_shipment",
|
||||
description:
|
||||
"Record a shipment. Every argument is optional AND explicitly nullable (Zod .nullish()), so each compiles to an anyOf union with a null branch.",
|
||||
inputSchema: {
|
||||
direction: z
|
||||
.enum(["envio", "recebimento"])
|
||||
.nullish()
|
||||
.describe("Direction of the shipment (nullable enum — #1928)"),
|
||||
reference: z.string().nullish().describe("Free-text reference"),
|
||||
quantity: z.number().int().nullish().describe("Number of packages"),
|
||||
express: z.boolean().nullish().describe("Ship express"),
|
||||
},
|
||||
handler: async (params: Record<string, unknown>) => {
|
||||
return toToolResult(JSON.stringify(params, null, 2));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "get-env" tool matching @modelcontextprotocol/server-everything.
|
||||
* Returns the server process environment as pretty-printed JSON text.
|
||||
|
|
|
|||
Loading…
Reference in a new issue