Skip to content

Cap the upstream response size of a tool call - #1279

Merged
helpcodeai merged 2 commits into
mainfrom
fix/tool-response-size-cap
Oct 26, 2026
Merged

helpcodeai merged 2 commits into
mainfrom
fix/tool-response-size-cap

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

A tool call could load an upstream response of any size. In production one JSON-RPC call (odoo_search_read) returned about 55 MB. Parsing and serializing it blocked the Node event loop for about 2.6 s, and every other request waited.

This PR caps the upstream response body a tool call reads. The default is 10 MB. TOOL_MAX_RESPONSE_BYTES changes it. When a body is larger, the call fails with our own error and does not load the body.

How it works:

  • The REST engine (which also carries JSON-RPC and OData), the GraphQL engine and the SOAP engine set axios maxContentLength from toolMaxResponseBytes().
  • axios counts the decompressed bytes while it reads the stream and aborts at the limit. So the body is never buffered in full, a missing Content-Length does not help, and a gzip body is counted after decompression.
  • All three engines already send through the outbound adapter in common/outbound-http.ts. The adapter turns axios' maxContentLength size of N exceeded into a ResponseTooLargeError with code TOOL_RESPONSE_TOO_LARGE. It has no HTTP status, so the model and the call log can tell it apart from an upstream error. The message says what to do: add filters, ask for fewer fields, set a lower limit or page size, or fetch smaller pages.
  • The adapter only restates the error when a caller set maxContentLength. Today only the tool engines do, so token, login, spec import and alert requests behave as before.

What the MCP client sees:

{
  "error": "The API response is larger than 10 MB, the limit for one tool call, so AnythingMCP stopped reading it and returned nothing. Narrow the request: add filters, ask for fewer fields, set a lower limit or page size, or fetch the data in smaller pages. An administrator can raise the limit with TOOL_MAX_RESPONSE_BYTES.",
  "code": "TOOL_RESPONSE_TOO_LARGE"
}

Cloud and self-hosted: same behavior and same 10 MB default in both. The env var is passed through in docker-compose.yml and docker-compose.cloud.yml. An empty or invalid value falls back to the default.

Why 10 MB: it matches the existing 10 MB caps (OUTBOUND_FETCH_DEFAULTS.maxBytes for file downloads and HARD_MAX_BINARY_BYTES for attached files). So no file that could be attached today is lost.

Why there is no second cap on the text sent to the MCP client: the rendered text comes from a body that is now at most 10 MB. A second limit on the pretty-printed text would either fail responses that passed the first limit (pretty printing adds whitespace) or repeat it. The opt-in responseMapping.transform.maxBytes still shapes output per tool.

Changes

  • packages/backend/src/common/response-size-limit.ts (new): default, env reader, ResponseTooLargeError, restateResponseTooLarge.
  • packages/backend/src/common/outbound-http.ts: the adapter restates axios' size error.
  • packages/backend/src/connectors/engines/rest.engine.ts, graphql.engine.ts, soap.engine.ts: set maxContentLength. SOAP passes the error through instead of wrapping it.
  • Tests: common/response-size-limit.spec.ts, connectors/engines/response-size-limit.engines.spec.ts (real local HTTP server through each engine), one case in mcp-server/dynamic-mcp-tools.spec.ts for the tool result.
  • Docs: .env.example, docs/deployment.md env table, docs/tool-definition.md (how transform.maxBytes and binary.maxBytes relate to the new limit). Compose files pass the variable through.

Behavior changes to note:

  • A binary file over 10 MB used to come back as a short "file too large, not attached" note. It now fails with the size error. Files up to 10 MB are unchanged.
  • A very large OData $metadata document requested through a tool now fails above 10 MB. An operator can raise the limit.

Not covered here (possible follow-ups): the MCP client engine (proxy to a remote MCP server) reads through the MCP SDK with fetch, and database engines already cap rows at 1000 (MAX_ROWS). Neither goes through axios.

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

The engine spec runs a real HTTP server on 127.0.0.1 and covers: a 1 MB JSON-RPC body passes under the default; an 11 MB body fails with ResponseTooLargeError and the 10 MB message; TOOL_MAX_RESPONSE_BYTES lowers and raises the limit; a body exactly at the limit passes and one byte less fails; a gzip body is counted after decompression; GraphQL and SOAP pass under the limit and fail over it. I checked that the REST cases fail when the maxContentLength line is removed (4 of the 5 failed, as expected).

Commands and results (Node v22, macOS):

Directory Command Result
packages/backend DATABASE_URL=postgresql://u:p@localhost:5433/x npx prisma generate OK
packages/backend npm run lint 0 errors, 11 existing warnings. eslint --fix also rewrote two unrelated spec files; I reverted them
packages/backend npx tsc --noEmit -p tsconfig.json exit 0
packages/backend npm test 775 suites passed, 4 skipped; 23088 tests passed, 1581 skipped, 0 failed
repo root node scripts/validate-adapters.mjs --warn exit 0, 1442 existing warnings, no adapter changed

No baseline failures.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test)
  • This PR has a descriptive title

Related Issues

No issue. The maintainer approved a cap on tool response size after the production incident above. No Prisma schema change, no auth change, nothing under ee/.

A single tool call could load an upstream body of any size. One JSON-RPC
response of about 55 MB took some 2.6 s to parse and serialize, and the
event loop served no other request in that time.

The REST (also JSON-RPC and OData), GraphQL and SOAP engines now pass a
limit to axios as maxContentLength. axios counts the decompressed bytes
while it reads and stops at the limit, so an oversized body is never
buffered in full. The outbound adapter restates axios' error as our own
ResponseTooLargeError (code TOOL_RESPONSE_TOO_LARGE), whose message asks
the model to narrow the request with filters, fewer fields or a limit.

The limit is 10 MB by default on Cloud and self-hosted, and
TOOL_MAX_RESPONSE_BYTES overrides it.
@helpcodeai
helpcodeai enabled auto-merge (squash) October 26, 2026 12:52
@helpcodeai
helpcodeai merged commit e503545 into main Oct 26, 2026
14 checks passed
@helpcodeai
helpcodeai deleted the fix/tool-response-size-cap branch October 26, 2026 13:12
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant