Skip to main content
Glama
README.md
# xwiki-surgical-mcp

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

An [MCP](https://modelcontextprotocol.io) server that lets an LLM make
**section-level edits** to a self-hosted [XWiki](https://www.xwiki.org/)
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."

## Quickstart

Requires Python 3.11+.

```bash
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](https://gofastmcp.com/)),
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 `Section`s (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):

```bash
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`](https://github.com/getsentry/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:

```bash
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](LICENSE)