Skip to content

fix(mcp): default list-tool pagesize and advertise it in schemas - #149

Merged
Tommel71 merged 5 commits into
developfrom
fix/mcp-pagesize-cap
Sep 15, 2026
Merged

Tommel71 merged 5 commits into
developfrom
fix/mcp-pagesize-cap

Conversation

@frederik-raphael

@frederik-raphael frederik-raphael commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When an MCP list-tool call omitted pagesize, the REST route received None. That selected the internal ceiling for list_txs_for and list_neighbors. For list_tx_flows, it disabled pagination entirely because the service slices only when both a page and a page size are present.

One measured list_txs_for call on an exchange hot wallet returned about 2.7 MB, or roughly 677k model tokens. Account-model transactions can likewise contain hundreds of flow events.

Fix

The MCP layer now uses a default page size of 25 when the caller omits pagesize:

  • Hand-written list tools apply the default in _params_from.
  • Auto-generated tools use FastMCP's ToolTransform with ArgTransformConfig(default=25). This both supplies the omitted value and advertises default: 25 in the MCP inputSchema.
  • A narrowly scoped middleware converts an explicit JSON null to 25 because a present null bypasses schema-default insertion.
  • Array and object values are rejected before HTTPX can serialize an empty container as an omitted query parameter.
  • Other invalid explicit values remain unchanged and reach the REST route's existing validation.

There is no second MCP-side maximum. Explicit valid page sizes pass through, and the REST API retains its existing upper bound of 5000.

The generated-tool set is collected while FastMCP.from_fastapi walks curated routes. It currently contains list_tx_flows. Consolidated tools remain outside this set because they apply their own defaults, and filtered list_neighbors uses pagesize as a target match count with a default of 50.

Generated OpenAPI output schemas remain present. Any output-schema size reduction belongs in a separate change with client-compatibility testing.

Pagination behavior

For list_tx_flows, omission and explicit null both produce a first page of 25 rows. Pass the returned next_page value back as page until the tail is reached. An explicit valid pagesize, including 5000, passes through unchanged.

Tests

Coverage includes:

  • The native default: 25 in the real list_tx_flows input schema.
  • Omitted, explicit-null, valid, and invalid page-size behavior through a FastMCP client.
  • Rejection of array and object values before HTTP serialization.
  • Middleware scoping to curated auto-generated tools.
  • A three-page walk over 60 flow events with no gaps or duplicates.
  • Consolidated-tool default handling.
  • Preservation of generated output schemas.

Verification run:

  • 100 focused MCP and database tests passed after the container-input fix.
  • All 150 MCP tests passed.
  • Ruff passed for the changed MCP source and tests.
  • The repository's complete pre-commit gate passed, including type checking, generated-code checks, and its pytest hook.

An omitted pagesize reached the upstream as no limit: a single
list_txs_for on an exchange hot wallet returned ~2.7 MB (~677k LLM
tokens). _params_from now defaults pagesize to 25 and caps it at 100
for every list tool; callers page onward via next_page.

Auto-generated tools inherited their OpenAPI response model as an MCP
outputSchema (graph_summary ~3.4k tokens, list_block_txs ~3.2k) that no
known client shows the model; component_fn drops it. tools/list payload
shrinks from ~18k to ~9k tokens.
- Add PagesizeCapMiddleware so auto-generated tools (list_tx_flows) get the default/ceiling instead of reaching the route with no limit
- Share the capped() helper in mcp/pagesize.py with the consolidated tools, keeping one policy
- Verify paging onward (next_page cursors) still reaches the tail of fan-out txs
)

logger.info(f"Redis lock {key} acquired.")
logger.info(f"Redis lock {key} acquired by {token}.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think github mistook "token" as an access token. this token here is no secret

try:
lock.release()
client.delete(_heartbeat_key(key), _alert_key(key))
logger.info(f"Redis lock {key} released.")
logger.info(f"Redis lock {key} released.")
except Exception as e:
# Never let a release problem mask what the body did or raised.
logger.warning(f"Redis lock {key} could not be released: {e}")
f"needs this lock. Check that host, then force-release with: "
f"redis-cli DEL {key}"
)
logger.warning(msg)
send_msg_to_topic(_ALERT_TOPIC, msg)
except Exception as e:
# Reporting must never turn lock contention into a crash.
logger.warning(f"Could not report stale holder of lock {key}: {e}")
@frederik-raphael
frederik-raphael changed the base branch from master to develop August 27, 2026 11:48
@frederik-raphael frederik-raphael changed the title feat(spark): write real 64-bit tx counts; cap MCP list pagesize; fix wrapped readers fix(mcp): cap list-tool pagesize and strip auto-generated outputSchema Aug 27, 2026
An explicit page size is the caller's decision and the route already
bounds it: PagesizeQuery declares ge=1, le=5000, and the Cassandra layer
clamps again to BIG_PAGE_SIZE / SMALL_PAGE_SIZE. Clamping a second time
at 100 in the MCP layer only stopped a model from asking for what the
API is willing to serve.

Omission is the part that needed fixing, and it still is. `None` reaches
the route as the internal ceiling (5000 rows for list_txs_for) or, for
list_tx_flows, as no pagination at all: the slice needs both a page and a
page size, so every flow event comes back with next_page null.

So MAX_PAGESIZE is gone and only the default remains. capped() becomes
resolve_pagesize() and PagesizeCapMiddleware becomes
PagesizeDefaultMiddleware, since neither clamps anything now. The
list_neighbors docstring names both defaults explicitly: 25 unfiltered,
50 as a match target when tag_filter is set, which are different
quantities that happened to read as one number. The round-trip test
sizes its fixture from DEFAULT_PAGESIZE so it keeps exercising three
pages if that default ever moves again.
@Tommel71

Copy link
Copy Markdown
Member

One thought on the middleware: as far as I can tell the model never actually sees the 25. It's in the docstrings of the hand-written tools, but for list_tx_flows (the one the middleware is for) it's not documented anywhere, since the description comes from the curation YAML. So the only hint that truncation happened is next_page.

Have you looked at fastmcp's ToolTransform with ArgTransformConfig(default=25)? If I understand it right, that would put default: 25 directly into the inputSchema so the model sees it natively.

@frederik-raphael frederik-raphael changed the title fix(mcp): cap list-tool pagesize and strip auto-generated outputSchema fix(mcp): default list-tool pagesize and advertise it in schemas Sep 15, 2026
@Tommel71
Tommel71 merged commit f70e34f into develop Sep 15, 2026
14 checks passed
@soad003
soad003 deleted the fix/mcp-pagesize-cap branch September 16, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants