toolcap 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toolcap-0.1.0/.gitignore +12 -0
- toolcap-0.1.0/CHANGELOG.md +20 -0
- toolcap-0.1.0/LICENSE +21 -0
- toolcap-0.1.0/PKG-INFO +158 -0
- toolcap-0.1.0/README.md +131 -0
- toolcap-0.1.0/pyproject.toml +43 -0
- toolcap-0.1.0/src/toolcap/__init__.py +49 -0
- toolcap-0.1.0/src/toolcap/core.py +489 -0
- toolcap-0.1.0/src/toolcap/errors.py +34 -0
- toolcap-0.1.0/tests/test_decorator.py +93 -0
- toolcap-0.1.0/tests/test_page.py +219 -0
- toolcap-0.1.0/tests/test_readme.py +38 -0
- toolcap-0.1.0/tests/test_sdk.py +159 -0
toolcap-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-11)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `Cap(max_tokens).page(data, cursor)`: returns any tool result as text that fits a token
|
|
8
|
+
budget. Lists (or the largest list inside a dict, or the one at `key=`) are split into pages
|
|
9
|
+
of whole items with a `page` object (`items`, `total`, `next_cursor`, `note`); text is split
|
|
10
|
+
at line breaks with a footer; one item bigger than the budget is cut and paging moves on;
|
|
11
|
+
values without lists keep every key and get long strings and lists cut.
|
|
12
|
+
- Stateless cursors (offset + fingerprint of the full result). Results that changed between
|
|
13
|
+
pages are flagged in the note; cursors toolcap did not issue raise `CursorError`, an MCP SDK
|
|
14
|
+
`ToolError` whose text reaches the model.
|
|
15
|
+
- `Cap.trim(data)`: the same fitting without cursors, with a note on what was left out.
|
|
16
|
+
- `@cap.paginate` decorator for sync and async tools: adds a `cursor` parameter (resolving
|
|
17
|
+
string annotations, keeping `Context` parameters), mentions it in the description, and
|
|
18
|
+
declares no output schema so pages are not duplicated as `structuredContent`.
|
|
19
|
+
- `TOOLCAP_MAX_TOKENS` environment variable, custom token counters, JSON indent setting.
|
|
20
|
+
- Tested with the MCP Python SDK 1.10, 1.x and 2.x.
|
toolcap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sri Krishna Lingamaneni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
toolcap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: toolcap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keep MCP tool results under the client's token limit (Claude Code's 25,000): cursor pagination of long lists and text, or safe truncation, in one call or decorator
|
|
5
|
+
Project-URL: Homepage, https://github.com/srikrishnaL/kitforge/tree/main/packages/toolcap
|
|
6
|
+
Project-URL: Source, https://github.com/srikrishnaL/kitforge
|
|
7
|
+
Project-URL: Issues, https://github.com/srikrishnaL/kitforge/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/srikrishnaL/kitforge/blob/main/packages/toolcap/CHANGELOG.md
|
|
9
|
+
Author: Sri Krishna Lingamaneni
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,context window,cursor,fastmcp,llm,max_mcp_output_tokens,mcp,mcp server,model context protocol,pagination,response size,token limit,tool result,truncate
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: anyio>=4; extra == 'test'
|
|
23
|
+
Requires-Dist: mcp>=1.10; (python_version >= '3.10') and extra == 'test'
|
|
24
|
+
Requires-Dist: pydantic>=2; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# toolcap
|
|
29
|
+
|
|
30
|
+
Keep MCP tool results under the client's token limit, with pages the model can follow.
|
|
31
|
+
|
|
32
|
+
A tool that returns 200 flow runs, a long log or a big page snapshot can blow past the client's
|
|
33
|
+
limit (Claude Code rejects results over 25,000 tokens) or fill the context in one call. toolcap
|
|
34
|
+
cuts any result into pages of whole items that fit a token budget, each with a `next_cursor`
|
|
35
|
+
and a note telling the model how to get the next page, in one call or one decorator.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install toolcap
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python 3.9+, no dependencies. Works with any MCP framework (the official MCP Python SDK 1.x
|
|
42
|
+
`FastMCP` and 2.x `MCPServer`, the standalone `fastmcp`) or none, because it only turns a value
|
|
43
|
+
into text.
|
|
44
|
+
|
|
45
|
+
## Example
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from mcp.server.fastmcp import FastMCP # SDK 2.x: from mcp.server.mcpserver import MCPServer
|
|
49
|
+
from toolcap import Cap
|
|
50
|
+
|
|
51
|
+
mcp = FastMCP("prefect")
|
|
52
|
+
cap = Cap(max_tokens=20_000)
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
@cap.paginate # adds a `cursor: str = ""` parameter for the model
|
|
56
|
+
def get_flow_runs(state: str = "COMPLETED", limit: int = 200) -> list:
|
|
57
|
+
"""List flow runs."""
|
|
58
|
+
return api.flow_runs(state=state, limit=limit) # any JSON-able value
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If the 200 runs fit in 20,000 tokens the model gets them unchanged. If not, it gets as many
|
|
62
|
+
whole runs as fit, plus a `page` object:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{"items": [{"id": "...", "name": "..."}, ...],
|
|
66
|
+
"page": {"items": "1-37 of 200", "total": 200, "next_cursor": "MTozNzo1YzBmYTE5Mw",
|
|
67
|
+
"note": "Showing items 1-37 of 200. Call this tool again with the same arguments and cursor=\"MTozNzo1YzBmYTE5Mw\" for the next page."}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The model calls `get_flow_runs(state="COMPLETED", limit=200, cursor="MTozNzo1YzBmYTE5Mw")` and
|
|
71
|
+
gets items 38 onward. The decorator also adds one sentence about `next_cursor` to the tool's
|
|
72
|
+
description. It works on sync and async tools, keeps `Context` parameters hidden from the model
|
|
73
|
+
as before, and resolves string annotations (`from __future__ import annotations`).
|
|
74
|
+
|
|
75
|
+
Without the decorator, call it yourself. This is the way to go when you want the cursor in your
|
|
76
|
+
own signature, or to page only some of the result:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@mcp.tool(structured_output=False)
|
|
80
|
+
def read_log(run_id: str, cursor: str = "") -> str:
|
|
81
|
+
"""Read a flow run's log."""
|
|
82
|
+
return cap.page(api.log_text(run_id), cursor)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## What it does with each kind of result
|
|
86
|
+
|
|
87
|
+
| Result | What the model gets when it is too big |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| A list | A page of whole items, wrapped as `{"items": [...], "page": {...}}` |
|
|
90
|
+
| 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"` |
|
|
91
|
+
| 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="..."]` |
|
|
92
|
+
| 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 |
|
|
93
|
+
| Anything else (a dict without lists, a huge string field) | Every key kept, long strings and lists cut until it fits |
|
|
94
|
+
|
|
95
|
+
`cap.trim(data)` does the same without cursors, for tools where paging makes no sense: the
|
|
96
|
+
model gets the first part and a note saying how many items were left out and to narrow the
|
|
97
|
+
request. Pydantic models, dataclasses, tuples and sets are converted to JSON first.
|
|
98
|
+
|
|
99
|
+
## Settings
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
Cap(
|
|
103
|
+
max_tokens=None, # default: the TOOLCAP_MAX_TOKENS env var, else 20,000
|
|
104
|
+
count=None, # text -> token count; default is a high estimate (UTF-8 bytes / 3)
|
|
105
|
+
indent=None, # JSON indent; None (compact) uses the fewest tokens
|
|
106
|
+
cursor_param="cursor", # name of the parameter the decorator adds
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For an exact fit, pass a real tokenizer: `Cap(count=lambda s: len(enc.encode(s)))`. The default
|
|
111
|
+
estimate counts one token per 3 bytes of UTF-8, which is above what real tokenizers give for
|
|
112
|
+
English, JSON, code and CJK text, so pages come out a little smaller than they could be but
|
|
113
|
+
never over the limit. Users of your server can change the budget with `TOOLCAP_MAX_TOKENS`
|
|
114
|
+
without code changes (match it to `MAX_MCP_OUTPUT_TOKENS` if they raised that in Claude Code).
|
|
115
|
+
|
|
116
|
+
Module-level shortcuts with default settings: `toolcap.page(data, cursor)`,
|
|
117
|
+
`toolcap.trim(data)` and `@toolcap.paginate`.
|
|
118
|
+
|
|
119
|
+
## How paging works, and its limits
|
|
120
|
+
|
|
121
|
+
- Cursors are stateless: an offset plus a short fingerprint of the full result. The server runs
|
|
122
|
+
the tool again on every page and slices it, so nothing is stored between calls and it works
|
|
123
|
+
across processes and restarts. The cost is that each page re-runs the query; for an expensive
|
|
124
|
+
backend, cache the result yourself or pass the cursor's offset on to the backend.
|
|
125
|
+
- If the result changed between pages (new rows came in), the page's note says so and that
|
|
126
|
+
items may be repeated or missed. Give results a stable sort order.
|
|
127
|
+
- A cursor that toolcap did not issue is rejected with an error the model can act on ("call the
|
|
128
|
+
tool without a cursor to start from the beginning"). It is the SDK's own `ToolError`, so the
|
|
129
|
+
text reaches the model on SDK 2.x too.
|
|
130
|
+
- The decorated tool returns text and declares no output schema, so the SDKs don't also copy
|
|
131
|
+
the page into `structuredContent` (which would double its size, and some clients skip their
|
|
132
|
+
truncation for structured content). Use `structured_output=False` on hand-written `-> str`
|
|
133
|
+
tools for the same reason.
|
|
134
|
+
- The budget covers one result. It does not know how full the model's context already is.
|
|
135
|
+
- Lists are paged at one level (the largest list reached through dict keys). Lists inside list
|
|
136
|
+
items are cut, not paged.
|
|
137
|
+
|
|
138
|
+
## Why this exists
|
|
139
|
+
|
|
140
|
+
MCP servers keep running into the client's result-size limit, and each one fixes it by hand
|
|
141
|
+
with ad-hoc `limit` parameters or a second "minimal" tool:
|
|
142
|
+
|
|
143
|
+
- [strands-agents/harness-sdk#5862](https://github.com/strands-agents/harness-sdk/issues/5862):
|
|
144
|
+
"enforce a response size cap with truncation and continuation"
|
|
145
|
+
- [microsoft/playwright-mcp#395](https://github.com/microsoft/playwright-mcp/issues/395) (14 👍):
|
|
146
|
+
"the snapshot length is too long, add pagination to it"
|
|
147
|
+
- [OpsLevel/opslevel-mcp#47](https://github.com/OpsLevel/opslevel-mcp/issues/47) (13 👍):
|
|
148
|
+
filters to avoid response size errors
|
|
149
|
+
- [PrefectHQ/prefect-mcp-server#125](https://github.com/PrefectHQ/prefect-mcp-server/issues/125):
|
|
150
|
+
`get_flow_runs` returning ~261k tokens
|
|
151
|
+
- [eyaltoledano/claude-task-master#1137](https://github.com/eyaltoledano/claude-task-master/issues/1137):
|
|
152
|
+
pagination for `get_tasks`
|
|
153
|
+
- [mui/material-ui#46778](https://github.com/mui/material-ui/issues/46778): MCP tool "returning
|
|
154
|
+
too many tokens for Claude Code"
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
toolcap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# toolcap
|
|
2
|
+
|
|
3
|
+
Keep MCP tool results under the client's token limit, with pages the model can follow.
|
|
4
|
+
|
|
5
|
+
A tool that returns 200 flow runs, a long log or a big page snapshot can blow past the client's
|
|
6
|
+
limit (Claude Code rejects results over 25,000 tokens) or fill the context in one call. toolcap
|
|
7
|
+
cuts any result into pages of whole items that fit a token budget, each with a `next_cursor`
|
|
8
|
+
and a note telling the model how to get the next page, in one call or one decorator.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install toolcap
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python 3.9+, no dependencies. Works with any MCP framework (the official MCP Python SDK 1.x
|
|
15
|
+
`FastMCP` and 2.x `MCPServer`, the standalone `fastmcp`) or none, because it only turns a value
|
|
16
|
+
into text.
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from mcp.server.fastmcp import FastMCP # SDK 2.x: from mcp.server.mcpserver import MCPServer
|
|
22
|
+
from toolcap import Cap
|
|
23
|
+
|
|
24
|
+
mcp = FastMCP("prefect")
|
|
25
|
+
cap = Cap(max_tokens=20_000)
|
|
26
|
+
|
|
27
|
+
@mcp.tool()
|
|
28
|
+
@cap.paginate # adds a `cursor: str = ""` parameter for the model
|
|
29
|
+
def get_flow_runs(state: str = "COMPLETED", limit: int = 200) -> list:
|
|
30
|
+
"""List flow runs."""
|
|
31
|
+
return api.flow_runs(state=state, limit=limit) # any JSON-able value
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the 200 runs fit in 20,000 tokens the model gets them unchanged. If not, it gets as many
|
|
35
|
+
whole runs as fit, plus a `page` object:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{"items": [{"id": "...", "name": "..."}, ...],
|
|
39
|
+
"page": {"items": "1-37 of 200", "total": 200, "next_cursor": "MTozNzo1YzBmYTE5Mw",
|
|
40
|
+
"note": "Showing items 1-37 of 200. Call this tool again with the same arguments and cursor=\"MTozNzo1YzBmYTE5Mw\" for the next page."}}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The model calls `get_flow_runs(state="COMPLETED", limit=200, cursor="MTozNzo1YzBmYTE5Mw")` and
|
|
44
|
+
gets items 38 onward. The decorator also adds one sentence about `next_cursor` to the tool's
|
|
45
|
+
description. It works on sync and async tools, keeps `Context` parameters hidden from the model
|
|
46
|
+
as before, and resolves string annotations (`from __future__ import annotations`).
|
|
47
|
+
|
|
48
|
+
Without the decorator, call it yourself. This is the way to go when you want the cursor in your
|
|
49
|
+
own signature, or to page only some of the result:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
@mcp.tool(structured_output=False)
|
|
53
|
+
def read_log(run_id: str, cursor: str = "") -> str:
|
|
54
|
+
"""Read a flow run's log."""
|
|
55
|
+
return cap.page(api.log_text(run_id), cursor)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What it does with each kind of result
|
|
59
|
+
|
|
60
|
+
| Result | What the model gets when it is too big |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| A list | A page of whole items, wrapped as `{"items": [...], "page": {...}}` |
|
|
63
|
+
| 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"` |
|
|
64
|
+
| 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="..."]` |
|
|
65
|
+
| 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 |
|
|
66
|
+
| Anything else (a dict without lists, a huge string field) | Every key kept, long strings and lists cut until it fits |
|
|
67
|
+
|
|
68
|
+
`cap.trim(data)` does the same without cursors, for tools where paging makes no sense: the
|
|
69
|
+
model gets the first part and a note saying how many items were left out and to narrow the
|
|
70
|
+
request. Pydantic models, dataclasses, tuples and sets are converted to JSON first.
|
|
71
|
+
|
|
72
|
+
## Settings
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
Cap(
|
|
76
|
+
max_tokens=None, # default: the TOOLCAP_MAX_TOKENS env var, else 20,000
|
|
77
|
+
count=None, # text -> token count; default is a high estimate (UTF-8 bytes / 3)
|
|
78
|
+
indent=None, # JSON indent; None (compact) uses the fewest tokens
|
|
79
|
+
cursor_param="cursor", # name of the parameter the decorator adds
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For an exact fit, pass a real tokenizer: `Cap(count=lambda s: len(enc.encode(s)))`. The default
|
|
84
|
+
estimate counts one token per 3 bytes of UTF-8, which is above what real tokenizers give for
|
|
85
|
+
English, JSON, code and CJK text, so pages come out a little smaller than they could be but
|
|
86
|
+
never over the limit. Users of your server can change the budget with `TOOLCAP_MAX_TOKENS`
|
|
87
|
+
without code changes (match it to `MAX_MCP_OUTPUT_TOKENS` if they raised that in Claude Code).
|
|
88
|
+
|
|
89
|
+
Module-level shortcuts with default settings: `toolcap.page(data, cursor)`,
|
|
90
|
+
`toolcap.trim(data)` and `@toolcap.paginate`.
|
|
91
|
+
|
|
92
|
+
## How paging works, and its limits
|
|
93
|
+
|
|
94
|
+
- Cursors are stateless: an offset plus a short fingerprint of the full result. The server runs
|
|
95
|
+
the tool again on every page and slices it, so nothing is stored between calls and it works
|
|
96
|
+
across processes and restarts. The cost is that each page re-runs the query; for an expensive
|
|
97
|
+
backend, cache the result yourself or pass the cursor's offset on to the backend.
|
|
98
|
+
- If the result changed between pages (new rows came in), the page's note says so and that
|
|
99
|
+
items may be repeated or missed. Give results a stable sort order.
|
|
100
|
+
- A cursor that toolcap did not issue is rejected with an error the model can act on ("call the
|
|
101
|
+
tool without a cursor to start from the beginning"). It is the SDK's own `ToolError`, so the
|
|
102
|
+
text reaches the model on SDK 2.x too.
|
|
103
|
+
- The decorated tool returns text and declares no output schema, so the SDKs don't also copy
|
|
104
|
+
the page into `structuredContent` (which would double its size, and some clients skip their
|
|
105
|
+
truncation for structured content). Use `structured_output=False` on hand-written `-> str`
|
|
106
|
+
tools for the same reason.
|
|
107
|
+
- The budget covers one result. It does not know how full the model's context already is.
|
|
108
|
+
- Lists are paged at one level (the largest list reached through dict keys). Lists inside list
|
|
109
|
+
items are cut, not paged.
|
|
110
|
+
|
|
111
|
+
## Why this exists
|
|
112
|
+
|
|
113
|
+
MCP servers keep running into the client's result-size limit, and each one fixes it by hand
|
|
114
|
+
with ad-hoc `limit` parameters or a second "minimal" tool:
|
|
115
|
+
|
|
116
|
+
- [strands-agents/harness-sdk#5862](https://github.com/strands-agents/harness-sdk/issues/5862):
|
|
117
|
+
"enforce a response size cap with truncation and continuation"
|
|
118
|
+
- [microsoft/playwright-mcp#395](https://github.com/microsoft/playwright-mcp/issues/395) (14 👍):
|
|
119
|
+
"the snapshot length is too long, add pagination to it"
|
|
120
|
+
- [OpsLevel/opslevel-mcp#47](https://github.com/OpsLevel/opslevel-mcp/issues/47) (13 👍):
|
|
121
|
+
filters to avoid response size errors
|
|
122
|
+
- [PrefectHQ/prefect-mcp-server#125](https://github.com/PrefectHQ/prefect-mcp-server/issues/125):
|
|
123
|
+
`get_flow_runs` returning ~261k tokens
|
|
124
|
+
- [eyaltoledano/claude-task-master#1137](https://github.com/eyaltoledano/claude-task-master/issues/1137):
|
|
125
|
+
pagination for `get_tasks`
|
|
126
|
+
- [mui/material-ui#46778](https://github.com/mui/material-ui/issues/46778): MCP tool "returning
|
|
127
|
+
too many tokens for Claude Code"
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toolcap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Keep MCP tool results under the client's token limit (Claude Code's 25,000): cursor pagination of long lists and text, or safe truncation, in one call or decorator"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Sri Krishna Lingamaneni" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp", "model context protocol", "mcp server", "tool result", "response size", "token limit",
|
|
16
|
+
"max_mcp_output_tokens", "pagination", "cursor", "truncate", "context window", "fastmcp",
|
|
17
|
+
"llm", "agents",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = ["pytest>=7", "pydantic>=2", "anyio>=4", "mcp>=1.10; python_version >= '3.10'"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/srikrishnaL/kitforge/tree/main/packages/toolcap"
|
|
35
|
+
Source = "https://github.com/srikrishnaL/kitforge"
|
|
36
|
+
Issues = "https://github.com/srikrishnaL/kitforge/issues"
|
|
37
|
+
Changelog = "https://github.com/srikrishnaL/kitforge/blob/main/packages/toolcap/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/toolcap"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""toolcap: keep MCP tool results under the client's token limit, with cursor pagination.
|
|
2
|
+
|
|
3
|
+
>>> from toolcap import Cap
|
|
4
|
+
>>> cap = Cap(max_tokens=20_000)
|
|
5
|
+
>>> text = cap.page(rows, cursor) # a page of whole items + next_cursor
|
|
6
|
+
>>> text = cap.trim(rows) # first part only, with a note on what was cut
|
|
7
|
+
|
|
8
|
+
Or as a decorator under ``@mcp.tool()``: ``@cap.paginate``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Callable, Optional, Sequence, Union
|
|
14
|
+
|
|
15
|
+
from .core import DEFAULT_MAX_TOKENS, Cap, decode_cursor, encode_cursor, estimate_tokens, jsonable
|
|
16
|
+
from .errors import CursorError
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Cap",
|
|
22
|
+
"CursorError",
|
|
23
|
+
"DEFAULT_MAX_TOKENS",
|
|
24
|
+
"decode_cursor",
|
|
25
|
+
"encode_cursor",
|
|
26
|
+
"estimate_tokens",
|
|
27
|
+
"jsonable",
|
|
28
|
+
"page",
|
|
29
|
+
"paginate",
|
|
30
|
+
"trim",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
Key = Union[str, Sequence[str], None]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def page(data: Any, cursor: Optional[str] = "", *, max_tokens: Optional[int] = None, key: Key = None) -> str:
|
|
37
|
+
"""``Cap(max_tokens).page(data, cursor)``: one page of ``data`` that fits the budget."""
|
|
38
|
+
return Cap(max_tokens).page(data, cursor, key=key)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def trim(data: Any, *, max_tokens: Optional[int] = None, key: Key = None) -> str:
|
|
42
|
+
"""``Cap(max_tokens).trim(data)``: ``data`` cut down to fit, with a note on what was left out."""
|
|
43
|
+
return Cap(max_tokens).trim(data, key=key)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def paginate(fn: Optional[Callable] = None, *, max_tokens: Optional[int] = None, key: Key = None):
|
|
47
|
+
"""``@paginate`` / ``@paginate(max_tokens=...)``: decorator form of :func:`page`."""
|
|
48
|
+
cap = Cap(max_tokens)
|
|
49
|
+
return cap.paginate(fn, key=key)
|