mcp-pager — Python
Token-aware response management for MCP servers.
pip install mcp-pager
from mcp.server.fastmcp import FastMCP
from mcp_pager import paginate
mcp = FastMCP("my-server")
paginate(mcp, max_tokens=4000)
@mcp.tool()
async def list_records(limit: int = 500) -> str:
records = await db.fetch(limit=limit) # could be huge
return json.dumps(records)
How it works
Same as the TypeScript version — one call wraps every tool you register. Oversized responses are chunked and delivered page by page with agent-readable metadata:
{
"hasMore": true,
"pageIndex": 0,
"totalPages": 12,
"remainingPages": 11,
"nextCursor": "eyJpZCI6Ijkx...",
"instruction": "Call `get_next_page` with nextCursor to get the next page. Repeat until hasMore is false."
}
Installation
# Core only
pip install mcp-pager
# With exact tiktoken token counting (recommended)
pip install "mcp-pager[tiktoken]"
# With Redis backend
pip install "mcp-pager[redis]"
# Both
pip install "mcp-pager[tiktoken,redis]"
Requirements: Python ≥ 3.10, mcp ≥ 1.0.0
API
paginate(mcp, **options)
| Parameter | Type | Default | Description |
|---|---|---|---|
mcp |
FastMCP |
— | The FastMCP server to wrap |
max_tokens |
int |
4000 |
Max tokens per page |
ttl_ms |
int |
600000 |
Cursor TTL in ms, sliding (10 min) |
token_counter |
Callable[[str], int] |
auto | Custom token counter |
page_tool_name |
str |
"get_next_page" |
Name of the injected pagination tool |
store |
StoreBackend |
MemoryBackend |
Custom storage backend |
signing_secret |
str |
None |
HMAC-sign cursors (sha256) |
on_paginate |
Callable[[PaginateEvent], None] |
None |
Lifecycle callback for logging |
Returns the same FastMCP instance.
Token counting
By default, mcp-pager auto-detects tiktoken and uses it for exact cl100k_base counts. If tiktoken is not installed it falls back to a content-aware heuristic (±10%).
pip install "mcp-pager[tiktoken]" # enables exact counting automatically
Custom counter:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
paginate(mcp, token_counter=lambda text: len(enc.encode(text)))
Storage backends
Default: in-memory
Works out of the box with sliding TTL. Not suitable for multi-process or serverless deployments.
Redis (production)
pip install "mcp-pager[redis]"
from redis.asyncio import Redis
from mcp_pager.backends.redis import RedisBackend
redis = Redis.from_url(os.environ["REDIS_URL"])
paginate(mcp, store=RedisBackend(redis), ttl_ms=10 * 60 * 1000)
Custom backend
from mcp_pager import StoreBackend
class DynamoBackend(StoreBackend):
async def get(self, id: str) -> list[str] | None: ...
async def set(self, id: str, chunks: list[str], ttl_ms: int) -> None: ...
async def delete(self, id: str) -> None: ...
async def refresh(self, id: str, ttl_ms: int) -> None: ...
paginate(mcp, store=DynamoBackend())
Observability — on_paginate
from mcp_pager import ChunkedEvent, PageFetchedEvent, CursorExpiredEvent
def handle_event(event):
if event.type == "chunked":
print(f"{event.tool_name} → {event.total_chunks} pages ({event.total_tokens} tokens)")
elif event.type == "page_fetched":
print(f"page {event.page_index + 1}/{event.total_pages} hasMore={event.has_more}")
elif event.type == "cursor_expired":
print("cursor expired")
paginate(mcp, on_paginate=handle_event)
Cursor signing (HMAC)
import os
paginate(mcp, signing_secret=os.environ["CURSOR_SECRET"])
Cursors are signed with HMAC-sha256. Tampered cursors are rejected before any store lookup.
LLM prompting guide
Add this to your system prompt so the LLM reliably follows pagination:
When a tool response contains a pagination cursor (hasMore: true), you MUST call
get_next_page with that cursor before answering. Keep calling until hasMore is false.
Never answer from partial results.
Demo server
python examples/demo_server.py
Tools: list_records, list_files, fetch_logs, get_next_page
Development
git clone https://github.com/SatishKakollu/mcp-pager
cd mcp-pager/python
pip install -e ".[dev]"
pytest tests/ -v
License
MIT
Metadata
Release files for mcp-pager 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_pager-0.8.0.tar.gz | 14.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_pager-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.2 kB
Release files / mcp_pager-0.8.0.tar.gz
| Download URL | mcp_pager-0.8.0.tar.gz |
|---|---|
| Size | 14.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af9f343a440ac4b6099e62d94f0303458aa34790d2c0764016a0291174342270
|
|
BLAKE2b-256 checksum How to use checksums |
e8a65e83ca6efe94dc7cbb9f41d83235472fcc993bd6da445297e795774e94cc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|
Release files / mcp_pager-0.8.0-py3-none-any.whl
| Download URL | mcp_pager-0.8.0-py3-none-any.whl |
|---|---|
| Size | 11.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
910aae668a4a71d279625982e185cbd0932b43b74316e10f6287272bab25537d
|
|
BLAKE2b-256 checksum How to use checksums |
01e0e02e23d52dde8d92b4b2b1c71243d377869b14da00e7b9cf17f63bb3341a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|