Skip to content

fix(mcp): follow the protocol rules clients check and keep answers under 25,000 characters - #31

Merged
jackulau merged 12 commits into
mainfrom
fix/mcp-audit
Nov 12, 2026
Merged

jackulau merged 12 commits into
mainfrom
fix/mcp-audit

Conversation

@jackulau

Copy link
Copy Markdown
Owner

The MCP server now follows the rules clients check, and no tool answer goes over 25,000 characters.

Protocol

  • stdout carries protocol messages only. server/discover (2026-07-28) is answered and cached for 5 minutes.
  • Cancellation is honoured for queued and running requests. A cancel for initialize is ignored.
  • A bad cursor is an error, not the full list. A notifications/* message with id: null gets no reply; other null-id requests get -32600.
  • eval_init and score_add are marked destructive, and score_add is listed as a writing tool.

Answer size

  • Every tool answer stays under 25,000 characters. A long page returns fewer rows with limitUsed; tools with a cursor also return nextCursor.
  • session_replay takes whole rows first. Text is cut only when one row, or the step itself, is over the limit alone, and each cut is flagged (<key>Truncated, truncated, note). Paths, ids and cursors are never cut.
  • Measured on a 300-file session: steps 1 to 300 went from 4,997,641 characters to under 25,000, and paging reaches every file once.

Tests

  • New: test/mcp/replay-limit.test.ts, test/mcp/fit-answer.test.ts, test/mcp/fit-page.test.ts, plus protocol cases for null-id notifications, cancel for initialize, surrogate-safe cuts and the discover TTL.
  • The new tests fail on the commit before each fix.
  • npm run check: 6,248 tests, 2 failures, both speed tests in test/viewer/model.test.ts, which this branch doesn't touch; that file passes 12 of 12 alone (load average about 71). The last commit's MCP tests pass 117 of 117. tsc and lint are clean, and the site builds and passes 135 of 135.

Docs: apps/site/src/pages/docs/mcp.astro and CHANGELOG.

Results of server/discover and the list and read methods carry ttlMs and
cacheScope. A request cancelled with notifications/cancelled gets no reply,
also when a synchronous read held the event loop while the cancel arrived.
A cursor on tools/list, prompts/list or resources/templates/list is -32602,
a request with a null id gets -32600 instead of no answer, a 2025-03-26
session accepts batches, and error text that quotes a huge argument is cut
to 4,000 characters around the middle.
A read tool with a limit runs again with fewer rows while its text is over
25,000 characters, and the answer carries limitUsed. Rows are dropped, never
cut, so nextCursor still starts at the first row left out. session_get at
limit 200 was 40,000 characters on a long session and session_replay's step
list 227,000; session_replay now shows 16 KB of a file or diff (was 64 KB),
and eval_candidates returns at most limit rejected sessions.
eval_init with force replaces a suite, and score_add replaces the score
stored for the same target, name, source and person, so both now say
destructiveHint: true. score_list's error for a bad since names the
argument instead of the CLI's --since.
…ialize, cache discover for 5 minutes, cut text on a character boundary, and wait on signals in the cancel test
The step list, a step, a step with a file, a range and a fork page their files with limit and cursor and cut the text that does not fit, saying so with truncated. fitPage returns the largest page that fits, or the page whole with truncated when the fixed part is the long one. benchmarks has a default limit, and every tool that can return limitUsed declares it.
Cut only when the answer does not fit: unpaged lists (a step's own files, edits in a tool input) keep their first items with a Total, the longest strings are cut to a common length, and the note and flags are counted in the length. A page takes rows by their size, so a small diff is not cut by the default limit. A cut inside a tool input is flagged on detail.
… first

A page takes whole rows by their measured size and sets limitUsed when it has fewer than asked. Only a row or the rest of the answer that is still too long is cut, by a top-down allocation that shares the room among the longest children at any depth, counts JSON escapes, keeps ids and paths whole, flags each cut, and ends with a guaranteed fit.
@jackulau
jackulau merged commit bf788e4 into main Nov 12, 2026
29 checks passed
@jackulau
jackulau deleted the fix/mcp-audit branch November 12, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant