bug(mcp): tool results omit resultType required by protocol revision 2026-07-28 #740

Closed
opened 2026-10-09 15:38:46 +00:00 by hexajon · 2 comments
Owner

Some madtea MCP tool calls fail inside the client with a protocol error instead of a result. The client rejects the server's result envelope because it carries no resultType field. Protocol revision 2026-07-28 made resultType required on every tools/call result. madtea does not always send it.

The error

The client returns this on the failing calls:

missing required resultType - servers implementing protocol revision 2026-07-28 MUST include it (the absent-means-complete bridge applies only to earlier-revision servers)

The bridge that treats an absent resultType as "complete" applies only to servers on an earlier revision. A server on revision 2026-07-28 must send resultType.

What I saw

The error above came back on these calls:

  • madt_issues with the search action,
  • madt_finish,
  • madt_branch_delete.

These calls returned normal results:

  • madt_server_info,
  • madt_issues with the list action,
  • madt_prs with list, create, and merge,
  • madt_pull,
  • madt_fetch.

So the server sends resultType on some tool results and omits it on others. The failure is per handler or per code path, not server-wide.

Impact

A failing call looks like a tool error to the caller, even when the operation on the forge has partly or fully succeeded. The caller cannot read the real state from the envelope. madt_finish returned this error after it had already pushed the branch, which left the branch pushed with no pull request and forced a manual check of the forge to find the true state. An error that does not match the real outcome is a trap: a caller can retry a step that is not safe to repeat, or report a real success as a failure.

What I want

The MCP server sends a spec-conformant tools/call result for every tool and every action, with resultType set, under protocol revision 2026-07-28. If the result envelope comes from the MCP SDK, update the SDK to the revision the server negotiates. Do not pin the negotiated revision backwards to dodge the field.

Some madtea MCP tool calls fail inside the client with a protocol error instead of a result. The client rejects the server's result envelope because it carries no `resultType` field. Protocol revision 2026-07-28 made `resultType` required on every `tools/call` result. madtea does not always send it. ## The error The client returns this on the failing calls: ``` missing required resultType - servers implementing protocol revision 2026-07-28 MUST include it (the absent-means-complete bridge applies only to earlier-revision servers) ``` The bridge that treats an absent `resultType` as "complete" applies only to servers on an earlier revision. A server on revision 2026-07-28 must send `resultType`. ## What I saw The error above came back on these calls: - `madt_issues` with the `search` action, - `madt_finish`, - `madt_branch_delete`. These calls returned normal results: - `madt_server_info`, - `madt_issues` with the `list` action, - `madt_prs` with `list`, `create`, and `merge`, - `madt_pull`, - `madt_fetch`. So the server sends `resultType` on some tool results and omits it on others. The failure is per handler or per code path, not server-wide. ## Impact A failing call looks like a tool error to the caller, even when the operation on the forge has partly or fully succeeded. The caller cannot read the real state from the envelope. `madt_finish` returned this error after it had already pushed the branch, which left the branch pushed with no pull request and forced a manual check of the forge to find the true state. An error that does not match the real outcome is a trap: a caller can retry a step that is not safe to repeat, or report a real success as a failure. ## What I want The MCP server sends a spec-conformant `tools/call` result for every tool and every action, with `resultType` set, under protocol revision 2026-07-28. If the result envelope comes from the MCP SDK, update the SDK to the revision the server negotiates. Do not pin the negotiated revision backwards to dodge the field.
hexajon reopened this issue 2026-10-13 17:21:48 +00:00
Author
Owner

Reopening. v1.8.0 of the SDK fixed the handler path. One result path still omits resultType, so the acceptance ("every tool and every action") is not met yet.

Still failing

A call with an unknown argument returns the protocol error again. Example: madt_repos with a query argument (the search argument is q).

I called the server directly and captured the raw result for two calls.

Failing (unknown argument on madt_repos):

{"content":[{"type":"text","text":"madt_repos: unknown argument \"query\" ..."}],"isError":true}

Working (madt_issues list):

{"content":[...],"structuredContent":{...},"resultType":"complete"}

The success result carries resultType. The error result carries no resultType and no structuredContent.

Root cause

The gap is the unknown-argument rejection in internal/mcp/arg_suggest.go. installUnknownArgumentRejection is a receiving middleware. On an unknown top-level argument it returns a bare CallToolResult with IsError: true at arg_suggest.go:268. This return is short of the SDK layer that stamps resultType, so the envelope leaves without the field.

A known argument used on the wrong action takes the normal route: it reaches the handler and returns through the path that stamps resultType. That is why madt_issues with a misused labels argument comes back clean, and madt_repos with an unknown query argument does not.

Why the guard missed it

TestToolResultCarriesResultType in internal/mcp/result_type_test.go checks the success path only. It fails on any error result (result_type_test.go:42) and never calls the unknown-argument path. So the residual does not show in the gate.

Impact

Every unknown-argument rejection is a non-conformant result. The caller sees an opaque "malformed result" instead of the "did you mean" text the rejection carries. The redirect that is built to help the caller self-correct never reaches the caller.

Acceptance

  • An unknown-argument rejection carries resultType under revision 2026-07-28.
  • The guard test exercises an error result and the unknown-argument middleware path, not the success path alone.
  • No negotiated revision is pinned backwards to dodge the field.

This blocks v0.20.0.

Reopening. v1.8.0 of the SDK fixed the handler path. One result path still omits `resultType`, so the acceptance ("every tool and every action") is not met yet. ## Still failing A call with an unknown argument returns the protocol error again. Example: `madt_repos` with a `query` argument (the search argument is `q`). I called the server directly and captured the raw result for two calls. Failing (unknown argument on `madt_repos`): ```json {"content":[{"type":"text","text":"madt_repos: unknown argument \"query\" ..."}],"isError":true} ``` Working (`madt_issues` list): ```json {"content":[...],"structuredContent":{...},"resultType":"complete"} ``` The success result carries `resultType`. The error result carries no `resultType` and no `structuredContent`. ## Root cause The gap is the unknown-argument rejection in `internal/mcp/arg_suggest.go`. `installUnknownArgumentRejection` is a receiving middleware. On an unknown top-level argument it returns a bare `CallToolResult` with `IsError: true` at `arg_suggest.go:268`. This return is short of the SDK layer that stamps `resultType`, so the envelope leaves without the field. A known argument used on the wrong action takes the normal route: it reaches the handler and returns through the path that stamps `resultType`. That is why `madt_issues` with a misused `labels` argument comes back clean, and `madt_repos` with an unknown `query` argument does not. ## Why the guard missed it `TestToolResultCarriesResultType` in `internal/mcp/result_type_test.go` checks the success path only. It fails on any error result (`result_type_test.go:42`) and never calls the unknown-argument path. So the residual does not show in the gate. ## Impact Every unknown-argument rejection is a non-conformant result. The caller sees an opaque "malformed result" instead of the "did you mean" text the rejection carries. The redirect that is built to help the caller self-correct never reaches the caller. ## Acceptance - An unknown-argument rejection carries `resultType` under revision 2026-07-28. - The guard test exercises an error result and the unknown-argument middleware path, not the success path alone. - No negotiated revision is pinned backwards to dodge the field. This blocks v0.20.0.
hexajon reopened this issue 2026-10-14 22:05:02 +00:00
Author
Owner

Resolved on main. The unknown-argument rejection now carries resultType under
protocol 2026-07-28: 93edff67 removed the pre-empting middleware, so the SDK
input-schema validator rejects an unknown top-level argument in band and
callTool stamps resultType on the result; the did-you-mean enrichment mutates
only the content and preserves the stamp. This is not a backward revision pin.
The guard test now covers the error path: result_type_rejection_test.go checks
a focused madt_repos case and every tool's rejection (39 tools, 0 skipped),
alongside the existing success-path test. Fixed by 93edff67 and 8e85f20f.

Resolved on main. The unknown-argument rejection now carries resultType under protocol 2026-07-28: 93edff67 removed the pre-empting middleware, so the SDK input-schema validator rejects an unknown top-level argument in band and callTool stamps resultType on the result; the did-you-mean enrichment mutates only the content and preserves the stamp. This is not a backward revision pin. The guard test now covers the error path: result_type_rejection_test.go checks a focused madt_repos case and every tool's rejection (39 tools, 0 skipped), alongside the existing success-path test. Fixed by 93edff67 and 8e85f20f.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#740
No description provided.