Title: [FEATURE] MCP tools return empty result sets on upstream HTTP errors instead of erroring · Issue #670 · stackql/stackql · GitHub
Open Graph Title: [FEATURE] MCP tools return empty result sets on upstream HTTP errors instead of erroring · Issue #670 · stackql/stackql
X Title: [FEATURE] MCP tools return empty result sets on upstream HTTP errors instead of erroring · Issue #670 · stackql/stackql
Description: Summary When a provider API call fails (401/403/404/429/5xx), run_select_query over MCP returns a successful tool result with {"rows": []}. The HTTP failure is only visible on the server process's stderr. MCP clients cannot distinguish "...
Open Graph Description: Summary When a provider API call fails (401/403/404/429/5xx), run_select_query over MCP returns a successful tool result with {"rows": []}. The HTTP failure is only visible on the server process's ...
X Description: Summary When a provider API call fails (401/403/404/429/5xx), run_select_query over MCP returns a successful tool result with {"rows": []}. The HTTP failure is only visible on the server ...
Opengraph URL: https://github.com/stackql/stackql/issues/670
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"[FEATURE] MCP tools return empty result sets on upstream HTTP errors instead of erroring","articleBody":"## Summary\n\nWhen a provider API call fails (401/403/404/429/5xx), `run_select_query`\nover MCP returns a successful tool result with `{\"rows\": []}`. The HTTP\nfailure is only visible on the server process's stderr. MCP clients cannot\ndistinguish \"query ran, zero rows\" from \"query failed\", and absence-based\nchecks (e.g. compliance controls of the form \"this query must return no\nrows\") silently false-pass.\n\nThis is fixable entirely within stackql/stackql - any-sdk already returns\nthe erroneous response to the caller (see analysis below).\n\n## Reproduction\n\nExhaust the unauthenticated GitHub rate limit (60 req/h/IP), then via MCP\nstdio with `--auth='{\"github\": {\"type\": \"null_auth\"}}'`:\n\n~~~json\n{\"name\": \"run_select_query\", \"arguments\": {\"sql\": \"SELECT name FROM github.repos.repos WHERE org = 'stackql'\"}}\n~~~\n\nServer stderr shows:\n\n~~~text\nhttp response status code: 403, response body: {\"message\":\"API rate limit exceeded for ...\"}\n~~~\n\nbut the tool result is `isError: false` with `structuredContent.rows: []`\nand a \"**no results**\" markdown rendering. Observed on v0.10.500.\n\n## Analysis\n\n- any-sdk's HTTP layer (`internal/anysdk/client.go`,\n `parseReponseBodyIfErroneous`) logs the error body to stderr but returns\n the full non-2xx `*http.Response` to stackql with the body re-buffered -\n the status code and body are available downstream. No any-sdk change is\n required.\n- The swallow points are in\n `internal/stackql/execution/mono_valent_execution.go`:\n - the plain-select path returns success with a nil payload on erroneous\n responses:\n\n ~~~go\n if httpResponse.StatusCode \u003e= 300 {\n return newHTTPProcessorResponse(nil, reversalStream, false, nil)\n }\n ~~~\n\n - similarly, `res.HasError()` routes the error text into the message\n handler and returns a nil error.\n\n By contrast the `isMaterialiseResponse` branch already generates an\n error for `StatusCode \u003e= 400`, so mutations/materialised ops surface\n failures - it is specifically selects that degrade to empty results.\n- `internal/stackql/mcpbackend.extractQueryResults` already checks\n `resp.GetError()` and would propagate a populated error; today it can\n only ever produce the opaque `failed to extract query results`.\n\n## Proposed fix (two layers, both in this repo)\n\n1. **Execution**: in the select path of `mono_valent_execution.go`, treat\n `StatusCode \u003e= 400` as an error (mirroring the materialise branch),\n carrying status code and response body into the `ExecutorOutput` error\n instead of returning `(nil payload, nil error)`.\n2. **MCP surface** (`pkg/mcp_server`): map backend errors to\n `CallToolResult` with `isError: true` and a structured error payload in\n the text/structured content, including a retryability classification:\n\n ~~~json\n {\n \"error\": {\n \"message\": \"GET https://api.github.com/... failed\",\n \"http_status\": 403,\n \"provider\": \"github\",\n \"retryable\": false\n }\n }\n ~~~\n\n Suggested classification: 408/429/5xx retryable; 400/401/403/404 not\n (with 403-rate-limit bodies ideally classified as retryable, since\n GitHub uses 403 for rate limiting).\n\n## Impact\n\nAny MCP client running absence-based checks gets false negatives today.\nThe interim mitigation we ship in auditron (stackql-mcp-rs demo) is a\ncanary control (`SELECT` that must return rows) whose failure flags a\nbroken run - workable, but it shouldn't be necessary, and it cannot\nattribute the failure or advise on retry.\n","author":{"url":"https://github.com/jeffreyaven","@type":"Person","name":"jeffreyaven"},"datePublished":"2026-06-13T01:11:16.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/670/stackql/issues/670"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:ffdd876e-0ee1-ac51-d8ed-4b53604c5125 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | BF9C:38840C:9DB9F2:D5E164:6A4D4F03 |
| html-safe-nonce | a7db8858a05ada7795cb18bd9860b8e559a2d3d9feafd82d39e580d04dcaf071 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCRjlDOjM4ODQwQzo5REI5RjI6RDVFMTY0OjZBNEQ0RjAzIiwidmlzaXRvcl9pZCI6Ijg4MDcxNjIxNjgzMjU1MjUyNTEiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 731287ea604bd288a3d29a52d5fb8fbc14a033dc3e7b925b1b74b770ae90b487 |
| hovercard-subject-tag | issue:4653701391 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/stackql/stackql/670/issue_layout |
| twitter:image | https://opengraph.githubassets.com/83a89cee622c4bfc206bea820ef795fdaaa5059cd0d439ccbefd2a155aa267a6/stackql/stackql/issues/670 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/83a89cee622c4bfc206bea820ef795fdaaa5059cd0d439ccbefd2a155aa267a6/stackql/stackql/issues/670 |
| og:image:alt | Summary When a provider API call fails (401/403/404/429/5xx), run_select_query over MCP returns a successful tool result with {"rows": []}. The HTTP failure is only visible on the server process's ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | jeffreyaven |
| hostname | github.com |
| expected-hostname | github.com |
| None | 92571a8944142227b7e19cd10918b1ddd06e5066c1ad5bc7e4769cf6140a87e6 |
| turbo-cache-control | no-preview |
| go-import | github.com/stackql/stackql git https://github.com/stackql/stackql.git |
| octolytics-dimension-user_id | 95105302 |
| octolytics-dimension-user_login | stackql |
| octolytics-dimension-repository_id | 443987542 |
| octolytics-dimension-repository_nwo | stackql/stackql |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 443987542 |
| octolytics-dimension-repository_network_root_nwo | stackql/stackql |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 56fc8347865a14e2ec811533d68f929cf4e0ec19 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width