Frontmatter
| id | 9837 |
| title | MCP: Output schemas reject responses with extra properties (additionalProperties:false drift) |
| state | Closed |
| labels | bugai |
| assignees | tobiu |
| createdAt | Apr 9, 2026, 10:33 PM |
| updatedAt | Apr 18, 2026, 10:26 PM |
| githubUrl | https://github.com/neomjs/neo/issues/9837 |
| author | tobiu |
| commentsCount | 2 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Apr 18, 2026, 9:20 PM |
MCP: Output schemas reject responses with extra properties (additionalProperties:false drift)

Input from Claude Opus 4.7 (Claude Code):
❋ Re-purposing audit trail (2026-04-18)
The original premise of this ticket — hardcoded
z.array(z.string())forcing all OpenAPI array inputs to string arrays — was effectively resolved by #10043's recursive refactor ofbuildZodSchemaFromNode, which now recurses intoitemsproperly. The original payload never landed as a direct PR.Rather than close a ticket that has graph edges already (cross-referenced by #9838, labeled
ai), and immediately file a near-identical new ticket, the title/body have been rewritten in place to cover the output-side manifestation of the same symptom class: strict MCP clients (notably GitHub Copilot) rejecting responses withadditionalProperties: falsein the declared outputSchema.Diagnosed via
self-repairskill protocol; evidence captured in the updated body above.

Input from Claude Opus 4.7 (Claude Code):
❋ Post-merge correction (2026-04-18): the proposed Step 3 audit should NOT be filed.
While discussing follow-up tickets after PR #10067 merged, tobiu flagged a blind spot in my original framing: I was treating MCP tool outputs as typed contracts (the REST-API Gold Standard), but a meaningful subset of them — Neural Link's runtime introspection tools — are fundamentally different.
get_vnode,get_component_tree,get_instance_properties, VDOM/VNode accessors, etc. surface live application state where the response shape is as open-ended as the app itself. VDOM nodes carry arbitrary HTML attrs (aria-*,data-*, handlers, styles); component instances expose hundreds of configs per class, multiplied across framework + app subclasses.Enumerating that in OpenAPI either produces "optional everything" schemas (strict-by-letter, useless-by-spirit) or artificially narrows responses and breaks introspection. For this class of tool, open shapes are the feature. The
.passthrough()that landed in PR #10067 is architecturally correct design, not a hotfix awaiting a stricter fix.Revised categorization
Category Examples Correct strategy Closed contracts healthcheck, GitHub workflow ops (create_issue,manage_issue_assignees…), KB queries with fixed result shapeNarrow schemas + drift auditing make sense. additionalProperties: falseis a legitimate contract.Runtime introspection Neural Link's get_vnode,get_component_tree,get_instance_properties,inspect_component_render_tree,query_vdom,query_componentOpen shapes by design. .passthrough()is correct, not a hotfix. Do NOT audit to narrow.What this means for this ticket's Step 3
The "Step 3 — schema-vs-implementation audit" bullet in this ticket's body should be read as superseded. Closing the full loop would require (a) identifying which tools fall into the closed-contract bucket and auditing those, and (b) explicitly documenting the runtime-introspection tools' openness so future agents don't mistakenly narrow them. If someone picks up the audit work later, the scope is materially smaller than the body implies and must be category-aware.
Saved this as durable feedback (
feedback_mcp_output_category_split.md) so future sessions don't re-propose the same audit trap.Cross-posted to PR #10067 for symmetry.
Problem
GitHub Copilot and other strict-JSON-Schema MCP clients crash when calling output-heavy tools on the Neo MCP servers. Example reported in the wild:
get_worker_topology→Error: Structured content does not match the tool's output schema: data/result/0 must NOT have additional propertiesConfirmed by a live session against devindex. Similar failures observed for
get_instance_properties,mutate_frontier,get_context_frontier.The error is not "the tool is broken" — it's that the declared JSON Schema for the tool's response has
additionalProperties: falseat nested levels, and the server returns fields not in that declared set. Clients with strict schema validation (Copilot) reject the payload; lenient clients (Claude Desktop, Cursor) pass it through silently.Forensic Trace (Three-Layer Interaction)
Schema drift in the OpenAPI definitions. Example:
ai/mcp/server/neural-link/openapi.yaml:905-939declaresget_worker_topologyitems with{sessionId, appWorkerId, isSharedWorker, userAgent, environment, connectedAt}. The actual service atai/mcp/server/neural-link/services/ConnectionService.mjs:312-317stores{appName, connectedAt, logs, sessionId}. Two returned fields (appName,logs) aren't declared; three declared fields are never populated.zod-to-json-schemaemitsadditionalProperties: falseon everyz.object(...)by default. Verified:zodToJsonSchema(z.object({a: z.string()})) // → {"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":false,...}Undeclared fields become JSON-Schema-forbidden.
#10043 inadvertently activated client-side strict validation. Diffed pre/post. Pre-#10043, top-level outputSchema for array responses was
{type: "array"}— which violates the MCP spec (requirestype: "object"). Clients likely skipped validation. #10043 correctly wrapped array responses in{result: [...]}for spec compliance, and from that point clients began validating — surfacing the latent drift that had been silently present.Why "the tools worked before"
Not a regression in validator code per se. #10043 made validation work correctly; the underlying schema drift has been latent for months. The drift was harmless only because no one validated it strictly.
Scope — Cross-Server Sweep
Emission counts from
buildOutputZodSchema+zodToJsonSchemafor every operation across all 5 servers:74/83 ops could silently reject a payload if the service ever returns a field the schema doesn't declare. Whether each one actually breaks today depends on implementation drift — the
get_worker_topology/mutate_frontier/get_context_frontier/get_instance_propertiesfailures are proof that real drift exists.Proposed Treatment (Option C: hotfix + audit)
Step 1 — Defense-in-depth in
ai/mcp/validation/OpenApiValidator.mjs(hotfix, restores all tools):In
buildZodSchemaFromNode, when emittingz.object(...)for output schemas, apply.passthrough():zodSchema = z.object(shape).passthrough(); // emits additionalProperties: trueInput side must stay strict (agents passing extra fields should still get validation errors — contract safety). This requires splitting the shared recursion to distinguish input vs output paths, OR adding a context parameter.
Verified:
zodToJsonSchema(z.object({...}).passthrough())emitsadditionalProperties: true. Restores tolerance for schema drift on the server response side without loosening input-side validation.Step 2 — Regression test: extend
test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjs(from #10064) with an assertion that every emitted outputSchema either (a) setsadditionalProperties: trueon object nodes, OR (b) is explicitly marked strict. Sibling to the existing array-has-items check.Step 3 — Audit ticket (follow-up, not this PR): sweep actual service returns vs declared OpenAPI schemas, realign them. Step 1 tolerates drift; Step 3 fixes the drift at the source.
Avoided Traps
zod-to-json-schemaconfig.additionalProperties: falseis correct for input schemas where contract violations should fail fast. The asymmetry (strict in, lenient out) matches the fault model: server implementations evolve independently of schemas; client contracts must stay pinned..passthrough()hotfix buys time for a proper audit without keeping Copilot users blocked.Related
items-missing issue — same MCP schema validation family, different facet)Origin Session ID
97343e6e-9d1c-4170-b3a5-6e58d41511b6(this session, diagnosed viaself-repairskill)