fix: search_issues and list_issues return full issue bodies — a normal query blows the consumer's context window #8

Open
opened 2026-09-22 02:23:24 +00:00 by trtmn · 0 comments
Owner

Summary

search_issues — and, by the same code path, list_issues — return the complete Forgejo issue object for every hit, including the full body and the entire user object. On a repo with long ticket bodies this makes the tool unusable: payload size scales with body length × hit count, so even a 10-hit query can exceed an MCP client's tool-result limit. Narrowing the query barely helps.

Impact / evidence

Hit during a /forgejo-ticket duplicate check against trtmn/churchsms:

Call Result
search_issues {"query": "auth provider", "state": "all", "limit": 30} 260,857 characters across 2,292 lines
search_issues {"query": "OIDC identity provider", "limit": 10} (retried narrower) ~67.5 KB

Both exceeded the client's tool-result limit, so the harness persisted them to a file instead of returning them inline — nothing usable reached context, and the search had to be redone by another route (reading the persisted file and projecting it down with python3).

That defeats the tool's own stated purpose — "find all open issues mentioning authentication" — on the repo where cross-repo search is most needed.

Root cause

src/tools/issues.ts:74-79 — search_issues calls GET /repos/issues/search, then hands the raw response straight to respond() (src/tools/issues.ts:13-14):

{ type: "text", text: JSON.stringify(res.data, null, 2) }

There is no field projection at all. Forgejo's search endpoint returns full issue objects (body, user, repository, …), so response size is driven entirely by upstream body length times hit count.

list_issues (src/tools/issues.ts:45-52) goes through the identical respond() pass-through and has the same problem — it is simply reached less often. The fix belongs in the shared path, not only in search_issues.

Fix options (best first)

  1. Return bodies only from get_issue. Project list/search payloads down to number, title, state, labels, updated_at, html_url, and reserve body for a single-issue fetch. This is the standard MCP convention for list tools, and it removes the failure mode rather than bounding it.
  2. Opt-in full_body flag (default: summary) on both tools.
  3. Truncate body to N chars in respond() with a marker. Cheapest option, but still scales with hit count.

Whichever is chosen, add a test asserting the projected payload for a long-bodied issue stays small.

Operational notes

  • dist/ must be rebuilt. main is dist/index.js (the tsc output), and several src/ files are newer than their compiled counterparts. Editing src/ alone changes nothing for a running server — it needs npm run build plus an MCP reconnect.
  • Which branch? main is still the original plain-JS server (f36e6fb, src/tools/issues.js). The TypeScript rewrite currently being run is on feat/typescript-migration, which is not merged to main. Fixing the TS source means either merging that migration first or patching both trees.

Workaround until this lands

Use list_issues for title and duplicate scans (it is also the more precise tool for a single known repo — as its own description already says). If search_issues is genuinely required, keep limit low and project the client-persisted output file down to number/state/title before any of it enters context.

## Summary `search_issues` — and, by the same code path, `list_issues` — return the **complete** Forgejo issue object for every hit, including the full `body` and the entire `user` object. On a repo with long ticket bodies this makes the tool unusable: payload size scales with `body length × hit count`, so even a 10-hit query can exceed an MCP client's tool-result limit. Narrowing the query barely helps. ## Impact / evidence Hit during a `/forgejo-ticket` duplicate check against `trtmn/churchsms`: | Call | Result | |---|---| | `search_issues {"query": "auth provider", "state": "all", "limit": 30}` | **260,857 characters across 2,292 lines** | | `search_issues {"query": "OIDC identity provider", "limit": 10}` (retried narrower) | **~67.5 KB** | Both exceeded the client's tool-result limit, so the harness persisted them to a file instead of returning them inline — **nothing usable reached context**, and the search had to be redone by another route (reading the persisted file and projecting it down with `python3`). That defeats the tool's own stated purpose — "find all open issues mentioning authentication" — on the repo where cross-repo search is most needed. ## Root cause `src/tools/issues.ts:74-79` — `search_issues` calls `GET /repos/issues/search`, then hands the raw response straight to `respond()` (`src/tools/issues.ts:13-14`): ```ts { type: "text", text: JSON.stringify(res.data, null, 2) } ``` There is **no field projection at all**. Forgejo's search endpoint returns full issue objects (`body`, `user`, `repository`, …), so response size is driven entirely by upstream body length times hit count. `list_issues` (`src/tools/issues.ts:45-52`) goes through the identical `respond()` pass-through and has the same problem — it is simply reached less often. The fix belongs in the shared path, not only in `search_issues`. ## Fix options (best first) 1. **Return bodies only from `get_issue`.** Project list/search payloads down to `number`, `title`, `state`, `labels`, `updated_at`, `html_url`, and reserve `body` for a single-issue fetch. This is the standard MCP convention for list tools, and it removes the failure mode rather than bounding it. 2. **Opt-in `full_body` flag** (default: summary) on both tools. 3. **Truncate `body` to N chars** in `respond()` with a marker. Cheapest option, but still scales with hit count. Whichever is chosen, add a test asserting the projected payload for a long-bodied issue stays small. ## Operational notes - **`dist/` must be rebuilt.** `main` is `dist/index.js` (the `tsc` output), and several `src/` files are newer than their compiled counterparts. Editing `src/` alone changes nothing for a running server — it needs `npm run build` plus an MCP reconnect. - **Which branch?** `main` is still the original plain-JS server (`f36e6fb`, `src/tools/issues.js`). The TypeScript rewrite currently being run is on `feat/typescript-migration`, which is **not merged to `main`**. Fixing the TS source means either merging that migration first or patching both trees. ## Workaround until this lands Use `list_issues` for title and duplicate scans (it is also the more precise tool for a single known repo — as its own description already says). If `search_issues` is genuinely required, keep `limit` low and project the client-persisted output file down to `number`/`state`/`title` before any of it enters context.
Sign in to join this conversation.
No description provided.