Generalizes the issue surfaced in #124 (unbounded tool outputs blowing up context windows) into an architectural invariant: every MCP tool whose output size depends on data must expose a caller-controlled bound and a way to fetch the remainder. Three sub-rules: no silent truncation, bound by domain shape (line/per-file/page), always resumable. - Add docs/design/output-bounding.md with rule, sub-rules, parameter vocabulary table, documentation contract, and a checklist for new tools. - Update AGENTS.md "Adding a New Tool" to require the checklist for any tool whose output is data-proportional. Tracked as forgejo-mcp-e0j. Complementary to the OpenSpec change add-bounded-text-responses, which delivers the rule for the specific tools called out in #124 (get_pull_request_diff, get_file_content).
4.1 KiB
Output Bounding for MCP Tools
Architectural invariant for forgejo-mcp tool design. Any new tool that returns data
proportional to repository or upstream state MUST satisfy the rules below before
landing.
Why
MCP tool outputs flow into an LLM context window. A single unbounded response
(diff, file content, commit list, log stream) can blow the window or silently
truncate at the transport envelope. The caller then sees partial data with no
signal and no way to fetch the remainder. Issue
#124 surfaced this on
get_pull_request_diff, get_file_content, list_pull_request_files,
list_pull_reviews. This document generalizes the fix.
The Rule
Every tool output must be bounded by the caller, not the server. If the output size depends on data rather than tool semantics, the tool MUST expose at least one client-controlled bound AND a way to fetch the remainder.
A tool whose output is bounded by its own semantics (e.g. get_my_user_info
returns one fixed-shape user object) is exempt. Everything else is in scope.
Sub-rules
1. No silent truncation
A server-side envelope cap (e.g. 16 kB) without a caller-visible knob is a trap: the caller receives partial data with no signal. Either:
- Expose the cap as a parameter the caller can raise / lower, or
- Replace the cap with proper paging / range params (sub-rule 2), or
- Return an explicit truncation marker (sub-rule 3) when the cap fires.
Never silently drop bytes.
2. Bound by domain shape, not bytes
Pick the natural unit for the data type. Byte ranges are a last-resort fallback because they cut mid-token.
| Data type | Preferred bound | Parameter shape |
|---|---|---|
| Code / text file | Line range | start_line, end_line |
| Diff (multi-file) | Per-file slice (then optional paging) | file_path; index via list_*_files |
| List of entities | Page + limit | page, limit |
| Log stream | Tail / head + line or byte cap | tail_bytes (or tail_lines) + marker |
| Single binary blob | Byte range fallback | offset, max_bytes |
Reuse parameter names across tools — agents learn one vocabulary, not many.
3. Always resumable
When the caller hits the bound, the response must carry a continuation signal so a follow-up call can retrieve the rest. Acceptable shapes:
- Paging: response includes
page,total_count, orhas_next. - Range: response includes the range actually returned (e.g. lines 1–500 of 2300) so caller can issue the next slice.
- Truncation marker: a sentinel like
[truncated, N more bytes]for log / byte-range tails when paging is unsuitable. - Index tool: a sibling list tool (e.g.
list_pull_request_files) so the caller can enumerate slices before requesting any.
"Got 4 KB of N" beats "got 4 KB."
Documentation contract
Every bound parameter MUST appear in:
- The tool's
mcp.NewTool()description (per-parameter doc). - The README tool table.
An undocumented cap is the same trap as no cap.
Checklist for new tools
When adding a tool in operation/{domain}/, answer in the PR description:
- Is output size bounded by the tool's own semantics (one fixed-shape object)? If yes, exempt — note this and skip the rest.
- If no: which bound parameter(s) does the tool expose?
- Which sub-rule 2 row matches the data type?
- How does the caller resume / fetch the remainder?
- Are bound parameters documented in the tool description and the README tool table?
If any answer is "none" or "unclear", the tool is not ready to merge.
Retrofitting existing tools
Tracked as the umbrella in #124. Sub-issues should target one tool at a time and reference this document.