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

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables LLM clients to interact with any MediaWiki wiki, supporting page creation/editing, search, file uploads, category browsing, and page history retrieval. Supports multiple wikis with OAuth2 or bot password authentication for both public and private wikis.
    1,323 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables LLM clients to read, search, compare, create, edit, move, protect, and delete wiki pages, manage files, inspect site info and history, and query extension data from NeoWiki, Semantic MediaWiki, Cargo, Bucket, and Wikibase. Supports multiple wikis with optional OAuth authentication and per-wiki tool availability.
    8 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables LLM clients to search, read, compare, and edit pages on any MediaWiki wiki, including multi-wiki setups with OAuth-authenticated writes. It also queries extension data such as Wikibase entities, Semantic MediaWiki and Cargo results, and Bucket Lua queries.
    8 npm
    MIT