Skip to content

Editing a section larger than the content cap silently drops the section's tail #536

Description

@alistair3149

get-page applies the content cap to a section read exactly as it does to a whole page. When the requested section is itself larger than the cap, the caller receives a prefix of it and has no way to fetch the remainder. Writing that prefix back with update-page section=N replaces the section in full, so everything past the cap is deleted.

The path that loses data

update-page's description recommends this loop (src/tools/update-page.ts:86):

section=N replaces one section and, paired with get-page section=N, reads, changes and writes back a single section, which is also how to add content in the middle of a page

It is safe only while the section fits under the cap.

  1. get-page section=N sets rvsection (src/tools/get-page.ts:83-85), then truncates whatever comes back (src/tools/get-page.ts:113-114) at MCP_CONTENT_MAX_BYTES, default 50000 (src/results/truncation.ts:1-13, README.md:133).
  2. The caller revises the prefix it received.
  3. update-page section=N sends it as text alongside section (src/tools/update-page.ts:69-79), and MediaWiki replaces the whole section.

Measured against MediaWiki 1.43.6, on a page whose section 1 is 60009 bytes:

rvsection=1 content bytes: 60009
truncated: true | returnedBytes: 50000 | totalBytes: 60009
lost bytes: 10009

One revision, no error, no conflict, 10009 bytes gone.

What the caller sees

The truncation field renders into the text block as:

Truncation:
  Reason: content-truncated
  Returned bytes: 50000
  Total bytes: 60009
  Item noun: wikitext
  Tool name: get-page
  Sections:
  - (empty)
  - Big
  - Small
  Remedy hint: To read a specific section, call get-page again with section=N.

That the content is incomplete is stated plainly. The remedy is not: the caller already passed section=N, and the hint (src/tools/get-page.ts:127) sends it back to the same call. The section list is the whole page's outline, re-fetched at src/tools/get-page.ts:116-119, so it never names a target narrower than the one that just truncated.

(ResponseFormatterImpl.truncationMarker at src/results/response.ts:68-76, which would number those sections, is called by no tool. The block above is what ships.)

Nothing catches it

update-page has no notion of where its source came from. The handler (src/tools/update-page.ts:98-122) forwards the argument to the API. A truncated read and a complete one are indistinguishable to it.

latestId does not help, for the same reason it does not help in #530: it detects edits by other users, and a caller chaining its own read and write passes conflict detection every time. This is a correct edit of the wrong content, not an edit conflict.

The caller also cannot see it coming. The outline keeps only each heading's text (src/services/sectionService.ts:22), so it carries no byte offsets, and under metadata=true the size field is the revision's — the full page, 60042 bytes on the probe page — not the section's. The section's real size arrives only as totalBytes, after the truncated read.

Interaction with #530

The natural remedy for an oversized section is to target a narrower subsection instead. That requires knowing which headings are nested beneath it, which is exactly the structure #530 reports the outline discards. So where the oversized section has subsections, this workaround is blocked on #530.

Where it has none, there is no narrower target at all and fixing #530 would not produce one.

Routes that do work today

  • Raising MCP_CONTENT_MAX_BYTES. Operator-side, so unavailable to a caller mid-task.
  • update-page with section=N and mode='append'/'prepend'. This never reads the section: MediaWiki extracts the section server-side and concatenates around it (ApiEditPage.php, the getSection branch of the append/prepend path). Verified on 1.43.6 — appending with section=1 landed inside that section, ahead of the next heading. It covers additions, not changes to existing text. Two caveats: nothing tells a caller the two compose, since the mode and source descriptions present them as alternatives (src/tools/update-page.ts:25-29, :49-54); and prependtext lands before the section's heading rather than after it, so it appends to the previous section visually.
  • get-revision applies no cap to source at all (src/tools/get-revision.ts:100-101), so fetching the latest revision by ID returns full text that get-page would have cut. That inconsistency probably deserves its own issue; closing it would remove this route.

Options

Not ranked, and several combine.

  1. Refuse the unsafe write — mark truncated content on the read and have update-page reject a whole-section replace it cannot confirm is complete. Needs some way to carry that fact between two calls, and the server handles each call independently.
  2. Continuation for content reads — an offset parameter on get-page so an oversized section can be read in segments. Only closes the loop if the write side can reassemble.
  3. A splice write — let update-page replace a span within a section instead of the entire section.
  4. Documentation and steering — state that section=N composes with mode='append'/'prepend', and that a truncated section read must not be written back wholesale. Cheapest, and enforces nothing.
  5. Put section sizes in the outline — byteoffset is already in the API response per get-page's section outline drops heading levels, so section=N edits silently delete subsections #530 — so a caller can avoid the trap before reading.
  6. Exempt section reads from the cap, or raise it for them.

Scope

Pre-existing, not caused by a recent change. Section reads and the byte cap arrived together in 2067277, first released in v0.7.0. #533 (8fa0cff) touched only src/tools/update-page.ts and its tests and docs, leaving get-page, the truncation module and the section service untouched.


AI-authored — Claude Code, Opus 5 1M (ultracode); noticed while reviewing PR #533 and written up at @alistair3149's request; not human-reviewed; every file:line and behavioural claim above was verified against master at 8fa0cff, with the MediaWiki behaviour reproduced against a local MediaWiki 1.43.6 rather than cited.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions