fix: return a schema-valid empty result from madt_api_call #849

Merged
hexajon merged 3 commits from issue-836-api-call-empty-structured into main 2026-10-20 17:19:06 +00:00
Owner

Summary

madt_api_call failed with "has an output schema but did not return structured content" whenever its body was empty. The common trigger is a jq or template filter that matches nothing, and a 204 hits the same path. The tool declares a JSON output schema, and both empty-body branches in apiCall returned a nil structured value, so the SDK rejected the result.

The change:

  • The code that turns a CallResult into the MCP result moved into apiCallToolResult in internal/mcp/tools_api.go, so it can be tested on its own.
  • An empty or nil body now gives the schema-valid result {"result": ""}. It then takes the same path as a non-empty body. When a built-in-command tip applies, the tip rides as a second text block after the JSON block.
  • The output schema, the service layer, and the write-guard path do not change.

One result changes for a caller. An empty-body result with a tip now has two content blocks and structured content, not one tip block. TestAPICallNudge_EmptyBodyWithTip now expects that shape.

Tests:

  • apiCallToolResult is tested with an empty filter result, a 204 with a nil body, and an empty 200, each with and without a tip. Each result is checked against the tool's declared output schema.
  • An in-memory MCP server and client check that the result carries structured content that validates against the schema.
  • New TestApplyJQ and template rows lock the step that a filter which matches nothing returns an empty string.

./scripts/gate.sh passes on this commit.

Closes #836

## Summary `madt_api_call` failed with "has an output schema but did not return structured content" whenever its body was empty. The common trigger is a jq or template filter that matches nothing, and a 204 hits the same path. The tool declares a JSON output schema, and both empty-body branches in `apiCall` returned a nil structured value, so the SDK rejected the result. The change: - The code that turns a `CallResult` into the MCP result moved into `apiCallToolResult` in `internal/mcp/tools_api.go`, so it can be tested on its own. - An empty or nil body now gives the schema-valid result `{"result": ""}`. It then takes the same path as a non-empty body. When a built-in-command tip applies, the tip rides as a second text block after the JSON block. - The output schema, the service layer, and the write-guard path do not change. One result changes for a caller. An empty-body result with a tip now has two content blocks and structured content, not one tip block. `TestAPICallNudge_EmptyBodyWithTip` now expects that shape. Tests: - `apiCallToolResult` is tested with an empty filter result, a 204 with a nil body, and an empty 200, each with and without a tip. Each result is checked against the tool's declared output schema. - An in-memory MCP server and client check that the result carries structured content that validates against the schema. - New `TestApplyJQ` and template rows lock the step that a filter which matches nothing returns an empty string. `./scripts/gate.sh` passes on this commit. Closes #836
A jq or template filter that matched nothing, or a 204 response, gave an
empty body. The tool then returned no structured content, but it declares
a JSON output schema, so the client rejected the call with a protocol
error.

The result shaping now lives in apiCallToolResult. An empty body gives the
empty envelope {"result": ""}. A built-in-command tip rides as an added
text block after the JSON text block, as on the non-empty path.

Refs #836
test: lock the empty result of a filter that matches nothing
Some checks failed
ci / gate (pull_request) Has been cancelled
ci / mcp-conformance (pull_request) Has been cancelled
drift / govulncheck (pull_request) Has been cancelled
drift / surface-diff (pull_request) Has been cancelled
drift / forgejo-smoke (pull_request) Has been cancelled
6a7ef4022a
A jq filter or a template that matches nothing now has a test that expects an empty string and no error. The madt_api_call schema comment says the tool always returns a JSONOutput on success. The wire test has a new name and doc comment that say it builds the empty filtered result by hand and does not run jq.
Merge branch 'main' into issue-836-api-call-empty-structured
Some checks failed
ci / mcp-conformance (pull_request) Successful in 3m41s
drift / govulncheck (pull_request) Has been skipped
drift / surface-diff (pull_request) Successful in 54s
drift / forgejo-smoke (pull_request) Has been skipped
ci / mcp-conformance (push) Successful in -37s
drift / govulncheck (push) Has been skipped
drift / surface-diff (push) Successful in -43s
drift / forgejo-smoke (push) Has been skipped
ci / gate (push) Failing after 3m23s
ci / gate (pull_request) Failing after 13m39s
e95d322bc0
Sign in to join this conversation.
No reviewers
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!849
No description provided.