Frontmatter
| title | fix(mcp): emit items for array schemas to satisfy strict validators (#10064) |
| author | tobiu |
| state | Merged |
| createdAt | Apr 18, 2026, 7:51 PM |
| updatedAt | Apr 18, 2026, 7:58 PM |
| closedAt | Apr 18, 2026, 7:58 PM |
| mergedAt | Apr 18, 2026, 7:58 PM |
| branches | dev ← claude/admiring-easley-fc4b56 |
| url | https://github.com/neomjs/neo/pull/10065 |
Merged

tobiu
commented on Apr 18, 2026, 7:57 PM
Input from Claude Opus 4.7 (Claude Code):
❋ # PR Review Summary
Status: Comment (Self-Review)
Self-Review Opening: Self-review of #10064. I chose a two-layer fix (defense-in-depth in
OpenApiValidator+ source-of-truth in the YAML) over either one in isolation, because mid-verification I discovered the YAML fix alone was insufficient —items: {}recurses throughbuildZodSchemaFromNodeto the catch-all, which was stillz.any(). That forced the Layer 1 scope to cover both the array fallback and the final catch-all. Key trade-offs and gaps noted below.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 88 — Respects the OpenAPI-as-source-of-intent principle (YAML is primary contract, validator is safety net).z.unknown()overz.any()aligns with the "prefer the stricter type when runtime behavior is identical" principle already established in the file.[CONTENT_COMPLETENESS]: 82 — Both inline comments in the validator explain the why (zod-to-json-schema openApi3 quirk) not just the what. JSDoc on the new test helper cites the exact error and ticket. The validator module itself lacks a file-level@summaryAnchor — not touched here since it predates this PR, but worth flagging (see[KB_GAP]).[EXECUTION_QUALITY]: 85 — 6/6 tests green; sweep covers 34 operations across all 5 servers; caught the two-stage fallback during verification rather than post-merge. Loss: test couples directly tojs-yaml+zod-to-json-schemarather than exercisingToolService.listTools()end-to-end. See[TOOLING_GAP].[PRODUCTIVITY]: 95 — Ticket goal achieved: Copilot'scall_methodschema now emitsitems: {}. Zero scope creep into #9837 (adjacent ticket in the same file).[IMPACT]: 70 — Unblocks an entire MCP host (GitHub Copilot) and hardens the validator against any future YAML gap in any of the five servers. Not framework-critical, but a real integration-surface fix.[COMPLEXITY]: 35 — Three small touchpoints (+96/-3). The cognitive load was in diagnosing the three-stage pipeline interaction, not in the patch itself.[EFFORT_PROFILE]: Quick Win — high ROI (restores a major harness), low code complexity.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #10064
- Related Graph Nodes: #10043 (closed — input schema hardcoding fix), #9837 (open — nested array items). This PR is the orthogonal third case and does not subsume #9837.
- Origin Session ID:
97343e6e-9d1c-4170-b3a5-6e58d41511b6
🧠 Graph Ingestion Notes
[KB_GAP]:ai/mcp/validation/OpenApiValidator.mjshas per-function JSDoc but no file/module-level Anchor describing the three-stage YAML→Zod→JSON-Schema pipeline. Future agents hitting a similar class of bug (serialization quirk) would benefit from an Anchor at the top calling out "this file bridges OpenAPI to MCP-consumable JSON Schema; emission quirks live here". Out-of-scope for this fix.[TOOLING_GAP]: (1) The regression test replicates the server's dependency chain (js-yaml+zod-to-json-schema) rather than exercisingToolService.listTools()directly. If the server swapped emitters, the test would still pass against the old library while shipping a broken schema. A stronger oracle would bootToolServiceand validate the emitted tools with Ajv in strict mode. (2)test/playwright/unit/ai/mcp/server/memory-core/managers/ChromaManager.spec.mjsfails to importai/mcp/server/memory-core/config.mjsin this worktree — unrelated to this PR but surfaces that config scaffolding is gitignored without a template fallback for worktree-isolated harnesses. (3)Authorization.spec.mjshas a pre-existing timeout on the "401 when no token provided" case — also unrelated.[RETROSPECTIVE]: Two takeaways from my own process on this one. (a) Assign before implementing. I filed #10064 and started coding without runningmanage_issue_assignees add @me; tobiu flagged it as a minor process flaw. Feedback memory saved — next session will fold assignment into the same Ticket-First step as creation. (b) Trust-but-verify fallback paths. My first validator patch only touched the array-missing-items branch. If I had stopped there and relied on the YAML fix, theitems: {}sentinel would have slipped through via the unrelated catch-all — an invisible regression. Running the emission throughbuildZodSchemaend-to-end (not just reading the diff) caught it. Worth codifying: any time you change a schema-emission path, re-emit the actual artifact and diff it, don't just reason about it.
📋 Required Actions
- Human QA confirms Copilot no longer errors on
call_method(the user-facing trigger for this ticket).- (Optional, follow-up) File a lightweight ticket for the stronger Ajv-strict oracle test (
[TOOLING_GAP]#1). Scope is small enough to bundle with whoever picks up #9837.No block-level changes requested — the fix itself is complete and tested.
Handoff: awaiting human merge per
pull-request-workflow.md§5 (Human-in-the-Loop). I will not self-merge.
Summary
GitHub Copilot's MCP client rejected
mcp_neo_mjs-neura_call_methodwith:Root-caused to a three-stage interaction:
neural-link/openapi.yamldeclaredCallMethodRequest.argsastype: arraywithout anitemsfield (four total occurrences across neural-link; other four MCP servers clean).OpenApiValidator.mjshandled the missingitemsby producingz.array(z.any()).zod-to-json-schemawithtarget: 'openApi3'silently stripsitemsfromz.array(z.any()), emitting{"type":"array"}. Strict validators (Copilot) reject this; permissive ones (Claude, Cursor) accept it, which is why the bug was invisible until a Copilot user surfaced it.Fix (two layers)
Layer 1 —
ai/mcp/validation/OpenApiValidator.mjsBoth the array fallback and the catch-all now returnz.unknown()instead ofz.any(). Runtime validation is identical (both accept anything); the only observable difference is inzod-to-json-schemaemission —z.unknown()preservesitems: {}, satisfying strict validators.Layer 2 —
ai/mcp/server/neural-link/openapi.yamlCallMethodRequest.args→ addeditems: {}(heterogeneous method args)/data/store/inspectresponsefilters,sorters,items→ addeditems: {type: object}Verification
New regression test
test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjs:z.array(z.unknown())emitsitemsunder theopenApi3target (direct guard on Layer 1).items.Orthogonality to prior work
z.array(z.string())— unrelated prior regression.itemskey at all.Avoided Traps
z.array(z.string())fallback — would regress typed args.items— adds a second serialization layer whenz.unknown()achieves the same outcome natively in Zod.Origin Session ID
97343e6e-9d1c-4170-b3a5-6e58d41511b6Resolves #10064