Summary
A bundle tool whose argument is typed Optional[list[...]] (FastMCP/pydantic → JSON Schema anyOf: [{type: array}, {type: null}]) intermittently fails when the model emits that argument as a JSON-encoded string ("[{...}]") instead of a real array. The bundle's pydantic rejects it:
1 validation error for call[<tool>]
<arg>
Input should be a valid list [type=list_type, input_value='[{...}]', input_type=str]
Observed rate in one long document-editing session: ~2% of calls to a list-arg tool (self-recovered on retry).
Why this is surprising
The platform already has src/tools/coerce-input.ts, which is supposed to recover exactly this misencoding: for a property whose effective schema is array/object, it JSON.parses a string value before validation. The union-resolution path (effectiveSchemaFor) collapses anyOf: [array, null] to the array branch, so an Optional[list] arg should be coerced. And engine.ts runs coerceInputForSchema + validateToolInput before dispatch.
Yet the error that surfaced was the bundle's pydantic error, not the platform's Invalid tool input: — meaning the platform accepted the string and forwarded it. So either:
- the model's stringified value was malformed JSON (e.g. embedded content with unescaped quotes/braces), so
tryJsonParse returned undefined and coercion left the string — and platform validation then also passed it through (needs confirming how validateToolInput treats a string against anyOf:[array,null]), or
- the platform's view of the tool's input schema for this arg differs from what the bundle enforces (schema-shape mismatch), so neither coercion nor validation engaged.
Repro needed
The exact pre-coercion argument is not recoverable from event logs (the value is truncated in the pydantic error and the raw arg isn't persisted). To fix at the right layer, reproduce with the full stringified argument and confirm which of (1)/(2) applies:
- If malformed JSON: decide whether platform validation should reject (surface a clear
Invalid tool input) rather than forward a string to the bundle, and/or guide the model to emit a real array.
- If schema mismatch: align the platform's validated schema with the bundle's.
Acceptance
- A regression test in
test/unit/coerce-input*.test.ts covering an Optional[list[dict]] arg supplied as a JSON string (both well-formed → coerced, and malformed → rejected by the platform with a clear error, never forwarded as a raw string to the bundle).
Summary
A bundle tool whose argument is typed
Optional[list[...]](FastMCP/pydantic → JSON SchemaanyOf: [{type: array}, {type: null}]) intermittently fails when the model emits that argument as a JSON-encoded string ("[{...}]") instead of a real array. The bundle's pydantic rejects it:Observed rate in one long document-editing session: ~2% of calls to a list-arg tool (self-recovered on retry).
Why this is surprising
The platform already has
src/tools/coerce-input.ts, which is supposed to recover exactly this misencoding: for a property whose effective schema isarray/object, itJSON.parses a string value before validation. The union-resolution path (effectiveSchemaFor) collapsesanyOf: [array, null]to thearraybranch, so anOptional[list]arg should be coerced. Andengine.tsrunscoerceInputForSchema+validateToolInputbefore dispatch.Yet the error that surfaced was the bundle's pydantic error, not the platform's
Invalid tool input:— meaning the platform accepted the string and forwarded it. So either:tryJsonParsereturnedundefinedand coercion left the string — and platform validation then also passed it through (needs confirming howvalidateToolInputtreats a string againstanyOf:[array,null]), orRepro needed
The exact pre-coercion argument is not recoverable from event logs (the value is truncated in the pydantic error and the raw arg isn't persisted). To fix at the right layer, reproduce with the full stringified argument and confirm which of (1)/(2) applies:
Invalid tool input) rather than forward a string to the bundle, and/or guide the model to emit a real array.Acceptance
test/unit/coerce-input*.test.tscovering anOptional[list[dict]]arg supplied as a JSON string (both well-formed → coerced, and malformed → rejected by the platform with a clear error, never forwarded as a raw string to the bundle).