Context
Split out of #16250 after an external contributor (@novice-22) corrected the scope I was about to give this — issuecomment-5156710968. The correction is the reason this ticket exists in this shape rather than the wrong one.
The Problem
ai/mcp/ToolService.mjs:107-111 dispatches one of two ways:
if (tool.passAsObject) {
return tool.handler(validatedArgs);
}
const handlerArgs = tool.argNames.map(name => validatedArgs[name]);
return tool.handler(...handlerArgs);Without x-pass-as-object, arguments arrive positionally, in contract order. Whether that is correct depends entirely on the handler signature, which the contract never references. #16250 proved two operations where it is wrong and fails silently — get_ingestion_progress destructures a bare Number (every key undefined, = {} suppresses the throw, defaults win) and get_pull_request_diff binds one parameter out of four declared.
Nobody knows how many others there are, and the obvious way to count them is wrong.
The Architectural Reality — two questions, two different sources of truth
This is the distinction the correction turns on, and getting it backwards produces a census of the wrong population:
| question |
source of truth |
count |
| which operations are unannotated and take arguments? |
the OpenAPI contracts alone |
9 (contributor's table) |
| which handler signatures silently degrade under positional dispatch? |
the handler signatures themselves |
unknown — this ticket |
"My nine include seven that are genuinely positional and correct, so the overlap is smaller than it looks." — @novice-22
So the annotation table is a starting set, not the defect set. A contract-only sweep cannot answer this: the contract does not know whether the handler destructures an object, declares one parameter, or accepts N positional ones in the declared order. Only reading the handler answers it.
The three-way split #16250 established:
- handler takes positional params in contract order → correct without the annotation
- handler destructures a single object → silently degrades (defaults win, no throw)
- handler declares fewer params than the contract → silently truncates (extras dropped)
Classes 2 and 3 are the defect. Class 1 is the majority and must not be "fixed".
The Fix
(Prescription — the census is the deliverable; repairs are downstream.)
For every operation across all MCP servers' openapi.yaml, resolve its handler through the server's toolService.mjs binding table and classify it 1/2/3. Report the class-2 and class-3 members. Annotating them is separate work, and each one is a behavior change on a live tool surface.
Worth considering as an outcome rather than assuming it: if the classification is mechanically derivable, this belongs in a lint rather than a one-time sweep — a new operation whose handler destructures an object and carries no annotation is the same bug re-filed. OpenApiValidatorCompliance.spec.mjs is the existing home for contract-vs-implementation checks.
Acceptance Criteria
Out of Scope
- Repairing the members found — each is a live tool-surface behavior change and needs its own ticket and review.
- The two in #16250, fixed in PR #16330.
- The
getPullRequestDiff legacy bare-number tolerance — every in-repo call site passes an object and the only bare-number producer is the dispatcher itself, so it becomes dead once #16330 lands. Its removal is a named decision, not part of a census.
Avoided Traps
- Censusing the annotation table. Nine unannotated-with-arguments operations, seven of them correct. Starting there means starting from a population that mostly is not the bug — the exact scope error this ticket was corrected out of before it was filed.
- Answering from the contracts. The contract cannot see the handler signature, and the handler signature is what decides. A contract-only sweep would produce a confident, complete-looking, wrong answer.
- Treating "takes arguments" as "takes an object". Class 1 is the majority: positional handlers matching contract order work correctly with no annotation and must be left alone.
Related
- #16250 / PR #16330 — the two proven members and their fix
- #16231 — established that a missing annotation can break a call; this establishes that the handler signature decides whether it does
Live latest-open sweep at 2026-08-02T09:33Z; no equivalent found.
Origin Session ID: 713db0da-2239-44ea-ba5b-931be90d34fc
Retrieval Hint: query_raw_memories("MCP positional dispatch handler signature census x-pass-as-object three-way split")
Context
Split out of #16250 after an external contributor (@novice-22) corrected the scope I was about to give this — issuecomment-5156710968. The correction is the reason this ticket exists in this shape rather than the wrong one.
The Problem
ai/mcp/ToolService.mjs:107-111dispatches one of two ways:if (tool.passAsObject) { return tool.handler(validatedArgs); } const handlerArgs = tool.argNames.map(name => validatedArgs[name]); return tool.handler(...handlerArgs);Without
x-pass-as-object, arguments arrive positionally, in contract order. Whether that is correct depends entirely on the handler signature, which the contract never references. #16250 proved two operations where it is wrong and fails silently —get_ingestion_progressdestructures a bareNumber(every keyundefined,= {}suppresses the throw, defaults win) andget_pull_request_diffbinds one parameter out of four declared.Nobody knows how many others there are, and the obvious way to count them is wrong.
The Architectural Reality — two questions, two different sources of truth
This is the distinction the correction turns on, and getting it backwards produces a census of the wrong population:
So the annotation table is a starting set, not the defect set. A contract-only sweep cannot answer this: the contract does not know whether the handler destructures an object, declares one parameter, or accepts N positional ones in the declared order. Only reading the handler answers it.
The three-way split #16250 established:
Classes 2 and 3 are the defect. Class 1 is the majority and must not be "fixed".
The Fix
(Prescription — the census is the deliverable; repairs are downstream.)
For every operation across all MCP servers'
openapi.yaml, resolve its handler through the server'stoolService.mjsbinding table and classify it 1/2/3. Report the class-2 and class-3 members. Annotating them is separate work, and each one is a behavior change on a live tool surface.Worth considering as an outcome rather than assuming it: if the classification is mechanically derivable, this belongs in a lint rather than a one-time sweep — a new operation whose handler destructures an object and carries no annotation is the same bug re-filed.
OpenApiValidatorCompliance.spec.mjsis the existing home for contract-vs-implementation checks.Acceptance Criteria
get_ingestion_progress,get_pull_request_diff) appear in the defect set — a census that misses its own known positives has not been validated.Out of Scope
getPullRequestDifflegacy bare-number tolerance — every in-repo call site passes an object and the only bare-number producer is the dispatcher itself, so it becomes dead once #16330 lands. Its removal is a named decision, not part of a census.Avoided Traps
Related
Live latest-open sweep at 2026-08-02T09:33Z; no equivalent found.
Origin Session ID: 713db0da-2239-44ea-ba5b-931be90d34fc
Retrieval Hint:
query_raw_memories("MCP positional dispatch handler signature census x-pass-as-object three-way split")