Skip to main content
Glama

xwiki-surgical-mcp

License: MIT

An MCP server that lets an LLM make section-level edits to a self-hosted XWiki instance, instead of rewriting the whole page.

Why this exists

XWiki's REST API has exactly one write operation for a page's content: PUT .../pages/{page} with a content field, which replaces the entire document. There is no partial-patch endpoint. That's a fine API for a human editing one page in a browser, but it's a bad handle to give an LLM — a full-document rewrite is one plausible-looking bug away from silently dropping half the page, mangling formatting, or reintroducing stale content it "helpfully" regenerated from a truncated context window.

This project makes section-level edits possible on top of that API: it parses a page into addressable sections client-side, lets the caller patch exactly one section's body in memory, and only then sends the full reconstructed document through the one write call XWiki actually exposes. The blast radius of a bad edit is one section, and the tool that validates expected_version refuses to write over someone else's concurrent change.

It was built because a general-purpose Claude session needed to edit real wiki pages without risking corruption of everything else on them — the constraint wasn't "can an LLM write XWiki syntax," it was "can an LLM be handed a scalpel instead of a fire axe."

Related MCP server: MediaWiki MCP Server

Quickstart

Requires Python 3.11+.

git clone https://github.com/danimoya/xwiki-surgical-mcp
cd xwiki-surgical-mcp
python3.11 -m venv .venv
source .venv/bin/activate
pip install -e .

cp .env.example .env
# edit .env: XWIKI_URL, XWIKI_MCP_SERVICE_USER, XWIKI_MCP_SERVICE_PASSWORD,
# MCP_BEARER_TOKEN — see "Security notes" below before choosing these.

python -m xwiki_mcp

The server speaks MCP over streamable HTTP (via FastMCP), bound by default to 127.0.0.1:8765. Point your MCP client at http://127.0.0.1:8765/mcp with the bearer token from MCP_BEARER_TOKEN.

Tools

Tool

Description

search_pages(query)

Full-text search across the wiki; returns matching page ids.

list_pages(space)

List page ids in a space, e.g. space="Main" or space="Projects.Alpha".

get_page(page_id)

Fetch a page's full content, version, and section map ({anchor, heading, level} per section). Always call this before preview_edit/apply_edit.

preview_edit(page_id, section_anchor, new_text)

Return a unified diff of replacing one section's body with new_text. Makes no write.

apply_edit(page_id, section_anchor, new_text, expected_version)

Write new_text as the replacement body of one section.

create_page(space, title, content)

Create a new page under a space with the given title and content.

The expected_version contract: apply_edit re-fetches the page immediately before writing and compares its live version against expected_version. If they don't match — because someone else edited the page since you called get_page — it raises VersionConflictError without writing anything. Call get_page again, re-derive your edit against the current content, and retry. This is optimistic concurrency control: cheap in the common case, and it means two callers can never silently stomp on each other's changes.

Architecture

Three modules, each with one job:

  • sections.py — parses XWiki Syntax 2.1 documents into a flat list of addressable Sections (heading text, level, and line range), and applies a targeted edit to one section's body without touching the rest of the document. This is pure text manipulation with no network calls, which is what makes it fuzz-testable and heavily unit-tested independent of a live wiki.

  • xwiki_client.py — a thin REST client: get_page, put_page_content (with the version-conflict check), create_page, list_pages, search_pages. No XWiki-syntax awareness lives here — it moves JSON and raises typed errors.

  • server.py — wires the two together into MCP tools via FastMCP, and owns the request/response shape each tool exposes to the LLM.

The core insight tying them together: XWiki has no partial-patch API, so "surgical" edits are computed client-side and always sent as one full-content write. sections.py is what makes that safe — it guarantees the reconstructed document is byte-identical to the original outside the target section's body (see the no-op-edit tests in tests/test_sections.py, which assert this against CRLF, NEL, and other non-obvious line-ending cases).

Security notes

  • Auth is a single static bearer token (MCP_BEARER_TOKEN), checked in auth.py. There's no per-tool scoping, rate limiting, or audit log — treat this token with the same care as the XWiki credential it guards.

  • Don't bind this to 0.0.0.0 or any publicly reachable address. The default (MCP_BIND_HOST=127.0.0.1) is deliberately localhost-only. This server holds an XWiki edit-capable credential behind one shared secret; if your MCP client runs on a different host, set MCP_BIND_HOST to a private network address (a VPN or overlay-network interface, a segment your reverse proxy doesn't expose), not a public one. See the comment on Config.from_env() in src/xwiki_mcp/config.py.

  • Use a low-privilege XWiki service account. XWIKI_MCP_SERVICE_USER should have Edit rights on the spaces it needs to touch, not Admin or superadmin. This server will faithfully execute whatever an LLM asks it to, including mistakes; scope the blast radius at the XWiki permissions layer, not just at the tool layer.

Testing

Unit tests (fast, no network):

pip install -e '.[dev]'
pytest tests/ -v

33 tests cover the section parser (including fuzz-adjacent edge cases — CRLF/NEL line endings, no-op edits, duplicate heading disambiguation), the REST client (mocked via responses), config loading, and the MCP tool wiring.

The integration test is separate and hits a real XWiki instance — it creates a page, edits it, verifies the edit, and deletes it. It's excluded from the default run (see the integration marker in pyproject.toml). Point .env at a disposable test wiki or sandbox space before running it:

pytest tests/test_integration_live.py -v -m integration

Known limitations

  • No Word/.docx import. Getting rich documents into XWiki content would need a LibreOffice/Office Importer conversion step ahead of this tool; out of scope for now.

  • The heading parser doesn't handle headings inside {{code}} or {{{verbatim}}} blocks, and doesn't reject asymmetric = marker counts (e.g. === Title ==) — see the comment above _HEADING_RE in sections.py. A heading-like line inside a code block will currently be parsed as a real section boundary.

  • create_page title validation is minimal. It rejects ., /, and ? (which would corrupt the page id), but doesn't URL-encode other exotic characters — very unusual titles may still produce an id XWiki rejects.

License

MIT

Related MCP Connectors

Related MCP Servers