bug(mcp): madt_api_call returns no structured content when a jq or template filter is empty #836

Closed
opened 2026-10-20 06:24:05 +00:00 by hexajon · 0 comments
Owner

Summary

madt_api_call fails with a protocol error whenever its response body is empty after filtering. A jq or template expression that matches nothing is the common trigger. The tool declares a JSON output schema, so the go-sdk rejects an empty structured result, and the caller sees a tool failure instead of an empty result.

Steps to reproduce

Run a call whose jq expression matches nothing, against any endpoint that returns a normal body:

madt_api_call GET /repos/sixfold-space/madtea/issues?limit=1  jq='.[] | select(.number==999999)'

The endpoint returns a 200 with a non-empty array. The jq filter selects nothing.

Expected

An empty result. The CLI (madtea api ... --jq ...) prints nothing and exits 0. The MCP tool should return a schema-valid empty result, for example {"result": ""}.

Actual

Tool madt_api_call has an output schema but did not return structured content

The call reports a tool error. The error text names only the output schema. It gives no hint that an empty filter result is the cause.

Why it happens

  • internal/service/api/call.go:144 applies jq. On no matches, applyJQ (internal/service/api/output.go:29) returns an empty string with no error, so Call returns CallResult{Body: []byte(""), Filtered: true}.
  • internal/mcp/tools_api.go:81 tests len(result.Body) == 0. Both empty-body branches, the tip-absent return at line 83 and the tip-present return at line 85, return a nil structured value.
  • madt_api_call declares a JSON output schema (internal/mcp/output_schema.go:200). The go-sdk requires structured content for a tool that declares one, so a nil structured value is rejected with the error above.

The same path triggers on any empty body. A 204 response hits it. A type=issues filter that leaves an empty array hits it too.

Scope

  • MCP madt_api_call only. The CLI renders the empty body fine, because it has no output-schema contract.
  • Independent of the endpoint being empty. The reproduction above uses a non-empty endpoint and an empty filter result.

Fix direction

madt_api_call is the raw passthrough escape hatch, and its JSON output schema is the declared design, so the fix stays inside that schema rather than dropping it. The empty-body branches in apiCall should return a schema-valid empty result, an empty result string or an empty JSON value, rather than nil. The built-in-command tip should ride as an added text block on top of that valid structured result, as it already does on the non-empty path.

Acceptance criteria

  • A madt_api_call whose jq or template filter matches nothing returns a schema-valid result such as {"result": ""}, not a "did not return structured content" error.
  • A 204 response, and any other empty response body, returns the same schema-valid empty result.
  • When a built-in-command tip applies to the endpoint, the tip still rides as a text block alongside the valid structured result.
  • A regression test drives an empty-filter call against a non-empty endpoint and asserts a schema-valid empty result with no protocol error.

Distinct from #153 and #181, which cover non-2xx error bodies. This is a 2xx response with an empty post-filter body.

Environment

  • Platform: Gitea 15.0.6.
  • madtea at main, reproduced on the build from commit 357ee888.
## Summary `madt_api_call` fails with a protocol error whenever its response body is empty after filtering. A jq or template expression that matches nothing is the common trigger. The tool declares a JSON output schema, so the go-sdk rejects an empty structured result, and the caller sees a tool failure instead of an empty result. ## Steps to reproduce Run a call whose jq expression matches nothing, against any endpoint that returns a normal body: madt_api_call GET /repos/sixfold-space/madtea/issues?limit=1 jq='.[] | select(.number==999999)' The endpoint returns a 200 with a non-empty array. The jq filter selects nothing. ## Expected An empty result. The CLI (`madtea api ... --jq ...`) prints nothing and exits 0. The MCP tool should return a schema-valid empty result, for example `{"result": ""}`. ## Actual Tool madt_api_call has an output schema but did not return structured content The call reports a tool error. The error text names only the output schema. It gives no hint that an empty filter result is the cause. ## Why it happens - `internal/service/api/call.go:144` applies jq. On no matches, `applyJQ` (`internal/service/api/output.go:29`) returns an empty string with no error, so `Call` returns `CallResult{Body: []byte(""), Filtered: true}`. - `internal/mcp/tools_api.go:81` tests `len(result.Body) == 0`. Both empty-body branches, the tip-absent return at line 83 and the tip-present return at line 85, return a `nil` structured value. - `madt_api_call` declares a JSON output schema (`internal/mcp/output_schema.go:200`). The go-sdk requires structured content for a tool that declares one, so a `nil` structured value is rejected with the error above. The same path triggers on any empty body. A 204 response hits it. A `type=issues` filter that leaves an empty array hits it too. ## Scope - MCP `madt_api_call` only. The CLI renders the empty body fine, because it has no output-schema contract. - Independent of the endpoint being empty. The reproduction above uses a non-empty endpoint and an empty filter result. ## Fix direction `madt_api_call` is the raw passthrough escape hatch, and its JSON output schema is the declared design, so the fix stays inside that schema rather than dropping it. The empty-body branches in `apiCall` should return a schema-valid empty result, an empty `result` string or an empty JSON value, rather than `nil`. The built-in-command tip should ride as an added text block on top of that valid structured result, as it already does on the non-empty path. ## Acceptance criteria - A `madt_api_call` whose jq or template filter matches nothing returns a schema-valid result such as `{"result": ""}`, not a "did not return structured content" error. - A 204 response, and any other empty response body, returns the same schema-valid empty result. - When a built-in-command tip applies to the endpoint, the tip still rides as a text block alongside the valid structured result. - A regression test drives an empty-filter call against a non-empty endpoint and asserts a schema-valid empty result with no protocol error. ## Related Distinct from #153 and #181, which cover non-2xx error bodies. This is a 2xx response with an empty post-filter body. ## Environment - Platform: Gitea 15.0.6. - madtea at main, reproduced on the build from commit 357ee888.
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.

Dependencies

No dependencies set.

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