Skip to main content

toolcap

Keep MCP tool results under the client's token limit, with pages the model can follow. Use it in your own server (one decorator), or as a proxy in front of any MCP server you didn't write.

A tool that returns 200 flow runs, a long log or a big page snapshot can blow past the client's limit (Claude Code rejects results over 25,000 tokens) or fill the context in one call. toolcap cuts any result into pages of whole items that fit a token budget, each with a next_cursor and a note telling the model how to get the next page, in one call or one decorator.

pip install toolcap

Python 3.9+, no dependencies. Works with any MCP framework (the official MCP Python SDK 1.x FastMCP and 2.x MCPServer, the standalone fastmcp) or none, because it only turns a value into text.

Fix a server you didn't write

MCP tool "..." response (61,234 tokens) exceeds maximum allowed tokens (25000) in Claude Code, or a client that silently fills its context with one giant snapshot? Wrap the server in toolcap proxy in your MCP config. It works with servers in any language:

{
  "mcpServers": {
    "filesystem": {
      "command": "uvx",
      "args": ["toolcap", "proxy", "--",
               "npx", "-y", "@modelcontextprotocol/server-filesystem", "/home/me/code"]
    }
  }
}

Or from the command line: claude mcp add filesystem -- uvx toolcap proxy -- npx -y @modelcontextprotocol/server-filesystem ~/code.

Everything after -- is the command you had before. The proxy adds an optional cursor parameter to every tool. A result that fits is passed through unchanged; one that doesn't is cut into pages exactly as described below. The proxy keeps the last 16 full results in memory, so the model's next pages are served from there without running the tool again. Tested on the reference filesystem server's directory_tree of /usr/share: a 1.1 million-token result became pages under 20,000 tokens.

Option
--max-tokens N budget for one result (default TOOLCAP_MAX_TOKENS, else 20,000)
--trim cut long results with a note instead of paging them; adds no parameter
--cursor-param NAME name of the added parameter (default cursor)
--quiet don't log cut results to stderr

Tools that already have a cursor parameter are left alone and their results are trimmed instead. outputSchema is removed from the tool list, because a page can't match it. Images and other non-text content stay on the first page. No uv? pip install toolcap and use "command": "toolcap".

In your own server

from mcp.server.fastmcp import FastMCP     # SDK 2.x: from mcp.server.mcpserver import MCPServer
from toolcap import Cap

mcp = FastMCP("prefect")
cap = Cap(max_tokens=20_000)

@mcp.tool()
@cap.paginate                      # adds a `cursor: str = ""` parameter for the model
def get_flow_runs(state: str = "COMPLETED", limit: int = 200) -> list:
    """List flow runs."""
    return api.flow_runs(state=state, limit=limit)    # any JSON-able value

If the 200 runs fit in 20,000 tokens the model gets them unchanged. If not, it gets as many whole runs as fit, plus a page object:

{"items": [{"id": "...", "name": "..."}, ...],
 "page": {"items": "1-37 of 200", "total": 200, "next_cursor": "MTozNzo1YzBmYTE5Mw",
          "note": "Showing items 1-37 of 200. Call this tool again with the same arguments and cursor=\"MTozNzo1YzBmYTE5Mw\" for the next page."}}

The model calls get_flow_runs(state="COMPLETED", limit=200, cursor="MTozNzo1YzBmYTE5Mw") and gets items 38 onward. The decorator also adds one sentence about next_cursor to the tool's description. It works on sync and async tools, keeps Context parameters hidden from the model as before, and resolves string annotations (from __future__ import annotations).

Without the decorator, call it yourself. This is the way to go when you want the cursor in your own signature, or to page only some of the result:

@mcp.tool(structured_output=False)
def read_log(run_id: str, cursor: str = "") -> str:
    """Read a flow run's log."""
    return cap.page(api.log_text(run_id), cursor)

What it does with each kind of result

Result What the model gets when it is too big
A list A page of whole items, wrapped as {"items": [...], "page": {...}}
A dict with a list inside ({"count": 500, "results": [...]}) The same dict with the largest list paged, the other fields kept, and a page key next to it (_page if the dict already has a page). Pick the list with key="data.items"
Text A chunk cut at a line break (or a space), then [Output cut: showing characters 1-5850 of 65000. Call this tool again ... cursor="..."]
One item bigger than the whole budget That item with its long strings (then long lists) cut, marked ... [N more characters cut], and paging moves on to the next item
Anything else (a dict without lists, a huge string field) Every key kept, long strings and lists cut until it fits

cap.trim(data) does the same without cursors, for tools where paging makes no sense: the model gets the first part and a note saying how many items were left out and to narrow the request. Pydantic models, dataclasses, tuples and sets are converted to JSON first.

Settings

Cap(
    max_tokens=None,        # default: the TOOLCAP_MAX_TOKENS env var, else 20,000
    count=None,             # text -> token count; default is a high estimate (UTF-8 bytes / 3)
    indent=None,            # JSON indent; None (compact) uses the fewest tokens
    cursor_param="cursor",  # name of the parameter the decorator adds
)

For an exact fit, pass a real tokenizer: Cap(count=lambda s: len(enc.encode(s))). The default estimate counts one token per 3 bytes of UTF-8, which is above what real tokenizers give for English, JSON, code and CJK text, so pages come out a little smaller than they could be but never over the limit. Users of your server can change the budget with TOOLCAP_MAX_TOKENS without code changes (match it to MAX_MCP_OUTPUT_TOKENS if they raised that in Claude Code).

Module-level shortcuts with default settings: toolcap.page(data, cursor), toolcap.trim(data) and @toolcap.paginate.

How paging works, and its limits

  • Cursors are stateless: an offset plus a short fingerprint of the full result. The server runs the tool again on every page and slices it, so nothing is stored between calls and it works across processes and restarts. The cost is that each page re-runs the query; for an expensive backend, cache the result yourself or pass the cursor's offset on to the backend.
  • If the result changed between pages (new rows came in), the page's note says so and that items may be repeated or missed. Give results a stable sort order.
  • A cursor that toolcap did not issue is rejected with an error the model can act on ("call the tool without a cursor to start from the beginning"). It is the SDK's own ToolError, so the text reaches the model on SDK 2.x too.
  • The decorated tool returns text and declares no output schema, so the SDKs don't also copy the page into structuredContent (which would double its size, and some clients skip their truncation for structured content). Use structured_output=False on hand-written -> str tools for the same reason.
  • The budget covers one result. It does not know how full the model's context already is.
  • Lists are paged at one level (the largest list reached through dict keys). Lists inside list items are cut, not paged.

Why this exists

MCP servers keep running into the client's result-size limit, and each one fixes it by hand with ad-hoc limit parameters or a second "minimal" tool:

License

MIT

Metadata

Release files for toolcap 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toolcap 0.2.0
File Size Uploaded Provenance Actions
toolcap-0.2.0.tar.gz 24.5 kB Without provenance Details for toolcap-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for toolcap 0.2.0
File Interpreter ABI Platform Actions
toolcap-0.2.0-py3-none-any.whl Python 3 none any Details for toolcap-0.2.0-py3-none-any.whl

Total release size: 44.5 kB

Release files / toolcap-0.2.0.tar.gz

Download URL toolcap-0.2.0.tar.gz
Size 24.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c7d4ad29712822a756ae3b0357b955829e6a7c0342f4d08b49ed7658f3c59cd4
BLAKE2b-256 checksum
How to use checksums
11f60fea0396a820e329acce74759fe5f07a9abbfdcbe7420acc972778e9b306
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / toolcap-0.2.0-py3-none-any.whl

Download URL toolcap-0.2.0-py3-none-any.whl
Size 20.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0246838c7d12a174a8c995381773ea63fff10763dbac9a2982ce0e9ef12bb25
BLAKE2b-256 checksum
How to use checksums
381a78a6cb89b3dbcb08cee7746a4c3df10df7ce4302f42285d1e00047172d4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for toolcap 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page