forgejo-mcp/demos/bounded-responses.md
Christoph Görn 4baeb3030e docs: 📝 add showboat demos for v2.22.0 (org labels, bounded responses)
- demos/org-labels.md: list_org_labels + merged list_repo_labels with
  scope field, opt-out via include_org_labels=false
- demos/bounded-responses.md: file_path on get_pull_request_diff and
  start_line/end_line on get_file_content, with measured size reductions
2026-05-12 12:43:04 +02:00

5.5 KiB
Raw Permalink Blame History

Demo: bounded responses for get_pull_request_diff + get_file_content

2026-05-12T10:50:00Z by Showboat 0.6.1

Background

Two existing tools returned arbitrarily large payloads:

  • get_pull_request_diff — the full unified diff for the PR, every file concatenated. A 30-file PR easily blows past 30 kB.
  • get_file_content (plain-text mode) — the entire file, no matter how large.

The architectural rule for this MCP server (docs/design/output-bounding.md) says every data-proportional response must be bounded by the caller. Issue #124 made it the law for these two.

v2.22.0 adds optional, additive parameters:

  • get_pull_request_diff → file_path selects a single file's hunks.
  • get_file_content → start_line + end_line select a 1-indexed inclusive line range. Out-of-range values clamp.

Default behavior is unchanged when the new params are omitted.

Setup

export FORGEJO_URL=https://codeberg.org
export FORGEJO_ACCESS_TOKEN=<your-token>
make build

1. get_pull_request_diff — full vs. per-file

Full diff of PR #131 (the PR that added this very feature):

./forgejo-mcp --cli get_pull_request_diff \
  --args '{"owner":"goern","repo":"forgejo-mcp","index":131}' 2>/dev/null | python3 -c "
import sys, json
text = json.load(sys.stdin)[0]['text']
print(f'Full diff: {len(text):,} bytes, {text.count(chr(10)):,} lines')
"
Full diff: 44,720 bytes, 988 lines

Now slice just one file:

./forgejo-mcp --cli get_pull_request_diff \
  --args '{"owner":"goern","repo":"forgejo-mcp","index":131,"file_path":"pkg/diff/splitter.go"}' 2>/dev/null | python3 -c "
import sys, json
text = json.load(sys.stdin)[0]['text']
print(f'Sliced: {len(text):,} bytes, {text.count(chr(10)):,} lines')
print()
print(chr(10).join(text.split(chr(10))[:8]))
"
Sliced: 2,731 bytes, 86 lines

diff --git a/pkg/diff/splitter.go b/pkg/diff/splitter.go
new file mode 100644
index 0000000..f05ad55
--- /dev/null
+++ b/pkg/diff/splitter.go
@@ -0,0 +1,81 @@
+// Package diff offers small helpers for slicing unified-diff strings.
+// It exists so MCP tools that return diff payloads can satisfy the

16× smaller payload for the file the agent actually wanted to review.

Missing path → tool error

./forgejo-mcp --cli get_pull_request_diff \
  --args '{"owner":"goern","repo":"forgejo-mcp","index":131,"file_path":"nonexistent/file.go"}' 2>&1 | tail -1
Error: tool execution failed: file_path "nonexistent/file.go" not found in pull request diff

The caller should use list_pull_request_files first to discover the exact paths in the PR.

2. get_file_content — full vs. line range

Full CHANGELOG.md:

./forgejo-mcp --cli get_file_content \
  --args '{"owner":"goern","repo":"forgejo-mcp","ref":"main","filePath":"CHANGELOG.md"}' 2>/dev/null | python3 -c "
import sys, json
r = json.load(sys.stdin)
text = json.loads(r[0]['text']).get('Result', '')
print(f'Full CHANGELOG: {len(text):,} chars, {text.count(chr(10)):,} lines')
"
Full CHANGELOG: 38,774 chars, 545 lines

Slice lines 1–10:

./forgejo-mcp --cli get_file_content \
  --args '{"owner":"goern","repo":"forgejo-mcp","ref":"main","filePath":"CHANGELOG.md","start_line":1,"end_line":10}' 2>/dev/null | python3 -c "
import sys, json
r = json.load(sys.stdin)
text = json.loads(r[0]['text']).get('Result', '')
print(f'Sliced: {len(text):,} chars, {text.count(chr(10))+1:,} lines')
print()
print(text)
"
Sliced: 936 chars, 10 lines

## [2.22.0](https://codeberg.org/goern/forgejo-mcp/compare/v2.21.0...v2.22.0) (2026-05-12)

### :sparkles: Features

* add list_org_labels + merge org labels in list_repo_labels (#130)
* bounded responses for get_pull_request_diff + get_file_content (#131)
...

41× smaller payload for the section the agent wanted to read.

Range shortcuts

Args Behavior
(neither set) Full file (unchanged contract)
start_line=N From line N to EOF
end_line=N From line 1 to N
start_line=A, end_line=B Inclusive [A..B], clamped to extent
start_line > end_line Tool error

Slicing applies only to plain-text mode. with_metadata=true returns the SDK ContentsResponse unchanged (base64-encoded content + sha) — the slice would break the encoding, so the range params are silently ignored when metadata mode is set.

3. End-to-end: agent code-review workflow

Reviewing a PR no longer means pulling its entire diff and burning context window on files the agent does not care about:

  1. list_pull_request_files — discover filenames.
  2. For each file, decide whether to read it.
  3. get_pull_request_diff with file_path=<filename> — just that file's hunks.
  4. If the agent needs the surrounding source, get_file_content with start_line + end_line around the hunk — not the whole file.

Per-call payloads stay proportional to what the agent actually inspects, not to the size of the PR or the file. This is the rule codified in docs/design/output-bounding.md and now enforced for both tools.