Files
Tirth Kanani f9b1cee476 feat(tools): bound every MCP tool response, not just get_affected_flows
#849 found get_affected_flows returning ~247k tokens inside a workflow
documented as "5 tool calls, 800 tokens total". PR #853 capped that one
tool. Measuring all 30 registered tools against a real 5.6k-node graph
found the same class of bug in ten more places, several of them on the
default path:

  list_communities            206,858 tokens with DEFAULT arguments
  get_community               134,781 default / 535,618 with members
  get_architecture_overview   625,012 in standard mode
  refactor dead_code           47,312 / suggest 38,246
  detect_changes               46,089 for a ONE-file diff
  get_surprising_connections 1,287,174 at top_n=10**6
  get_hub_nodes              555,848 / get_bridge_nodes 317,202
  get_review_context        4,720,622 on a whole-repo diff

PR #853's own fix was also only half a fix: standard mode carries a full
steps list per flow (~980 tokens each), so its 50-flow cap still produced
~49k tokens, and max_flows=0 disabled the limit entirely.

Every list-returning tool now follows one contract, the one #853
established: `total` (or a per-list `*_total`) always reports the
untruncated count, `truncated` marks the cut, and the summary line says
how many of how many are shown. Bounds are validated the way query.py
validates max_results - booleans rejected, values below 1 rejected.
detail_level="minimal" was added to the analysis and refactor tools,
projecting to the same compact field sets their siblings use.

Where a count alone cannot bound a response, a shared budget does:
get_flow and get_affected_flows spend a step budget, get_review_context
and detect_changes spend a source-line budget. Ceilings that depend on
payload size depend on detail_level, mirroring query.py capping
minimal-mode results at five.

Two behaviour changes are deliberate and their #853 tests are updated in
this commit: get_affected_flows standard mode now caps at 25 flows
(minimal at 500), and max_flows=0 keeps its "no caller limit" meaning
while still obeying the ceiling - an escape hatch that returns a quarter
of a million tokens is the bug, not a feature.

Default behaviour stays backward compatible in shape: an existing caller
passing nothing still gets a valid response, just bounded.

tests/test_token_budget.py records the measured budget table as
reviewable data and pins it three ways: per-tool default and worst-case
token ceilings, exact truncated list lengths against the imported
ceiling constants, and the ceiling constants themselves. Removing a cap,
raising a ceiling, or adding an unbounded field fails it. The fixture
graph builds once per module and the whole file runs in ~7s, offline.

Reported, not fixed: code_review_graph/tools/query.py is owned elsewhere
and four of its tools remain unbounded - get_impact_radius (3.4M tokens;
changed_nodes and edges ignore max_results, which is not even exposed on
the MCP signature), find_large_functions (737k), traverse_graph (385k)
and semantic_search_nodes, whose limit/token_budget are neither
validated nor capped. Their default budgets are still asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3
2026-08-19 20:14:19 +01:00
..