bug(mcp): madt_api_call returns non-2xx responses as ordinary data - HTTP status discarded on the MCP surface #153

Closed
opened 2026-07-16 14:53:55 +00:00 by hexajon · 0 comments
hexajon commented 2026-07-16 14:53:55 +00:00 (Migrated from codeberg.org)

Observed (2026-07-16)

Codeberg's /repos/issues/search endpoint was hard-failing platform-wide (HTTP 500, Forgejo's empty-message ISE body {"message":"","url":"https://codeberg.org/api/swagger"} - reproduced with anonymous curl over both IPv4 and IPv6, and authenticated via madtea). The typed path surfaced it correctly: madt_issues action=search returned API error 500: ....

madt_api_call on the SAME failing endpoint returned a successful-looking result:

  • madt_api_call GET /repos/issues/search?q=whoami&limit=5 jq=length -> {"result": 2} - which is the key count of the error object, masquerading as a plausible search-result count.
  • Without jq, the raw error body is returned as StructuredContent data with no isError flag and no status code anywhere in the result.

This actively derailed the diagnosis of the search outage: the passthrough appeared to prove the endpoint healthy while every other path 500'd, sending the investigation toward phantom madtea-side differences (auth, headers, param order, token scopes) for the better part of an hour.

Mechanism

  • internal/service/api/call.go Call() captures HTTPStatusCode on CallResult but never treats >=400 as an error.
  • internal/mcp/tools_api.go handler reads result.Body and ignores result.HTTPStatusCode entirely: no isError, no status in the content, and when jq/template is set the error body is transformed before anyone could notice what it was.
  • Contrast: the typed client path (requestWithMeta, internal/gitea/client_http.go) converts >=400 into API error <code>: <body>.

Ask

  • madt_api_call surfaces non-2xx as a tool error (isError, message shaped like the typed path: API error <code>: <body>), evaluated BEFORE jq/template application so a filter can never transmute an error body into plausible data.
  • Decide whether an opt-out is wanted for deliberate status probing (e.g. a param that returns {status, body} structurally instead of erroring); default stays fail-loud. gh (the naming yardstick) exits non-zero on non-2xx from gh api.
  • Check the CLI madtea api for the same gap and align both surfaces.

Acceptance

  • A 500 (or any >=400) via madt_api_call -> tool error naming the HTTP status; with jq set, the error is returned instead of filtered output.
  • Conformance/unit test covering the jq-on-error-body case specifically (today's failure shape).
  • CLI behavior verified and consistent (or its divergence documented deliberately).
## Observed (2026-07-16) Codeberg's `/repos/issues/search` endpoint was hard-failing platform-wide (HTTP 500, Forgejo's empty-message ISE body `{"message":"","url":"https://codeberg.org/api/swagger"}` - reproduced with anonymous curl over both IPv4 and IPv6, and authenticated via madtea). The typed path surfaced it correctly: `madt_issues action=search` returned `API error 500: ...`. `madt_api_call` on the SAME failing endpoint returned a **successful-looking result**: - `madt_api_call GET /repos/issues/search?q=whoami&limit=5 jq=length` -> `{"result": 2}` - which is the **key count of the error object**, masquerading as a plausible search-result count. - Without jq, the raw error body is returned as StructuredContent data with no `isError` flag and no status code anywhere in the result. This actively derailed the diagnosis of the search outage: the passthrough appeared to prove the endpoint healthy while every other path 500'd, sending the investigation toward phantom madtea-side differences (auth, headers, param order, token scopes) for the better part of an hour. ## Mechanism - `internal/service/api/call.go` `Call()` captures `HTTPStatusCode` on `CallResult` but never treats >=400 as an error. - `internal/mcp/tools_api.go` handler reads `result.Body` and ignores `result.HTTPStatusCode` entirely: no `isError`, no status in the content, and when `jq`/`template` is set the error body is transformed before anyone could notice what it was. - Contrast: the typed client path (`requestWithMeta`, internal/gitea/client_http.go) converts >=400 into `API error <code>: <body>`. ## Ask - `madt_api_call` surfaces non-2xx as a tool error (isError, message shaped like the typed path: `API error <code>: <body>`), evaluated BEFORE jq/template application so a filter can never transmute an error body into plausible data. - Decide whether an opt-out is wanted for deliberate status probing (e.g. a param that returns `{status, body}` structurally instead of erroring); default stays fail-loud. gh (the naming yardstick) exits non-zero on non-2xx from `gh api`. - Check the CLI `madtea api` for the same gap and align both surfaces. ## Acceptance - A 500 (or any >=400) via `madt_api_call` -> tool error naming the HTTP status; with jq set, the error is returned instead of filtered output. - Conformance/unit test covering the jq-on-error-body case specifically (today's failure shape). - CLI behavior verified and consistent (or its divergence documented deliberately).
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#153
No description provided.