forgejo-mcp/docs/design/output-bounding.md
Christoph Görn a53a792203 docs: 📝 codify output-bounding rule for MCP tools
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).
2026-05-10 16:11:52 +02:00

4.1 KiB
Raw Permalink Blame History

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, or has_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:

  1. The tool's mcp.NewTool() description (per-parameter doc).
  2. 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.