Skip to main content

ChangeX MCP server: edit a .docx through an MCP client and get native Word revisions plus a portable .changex provenance journal.

Project description

changex-mcp

The ChangeX MCP server: an MCP client (Claude Code/Desktop, OpenAI, Gemini CLI, …) opens a .docx, makes small intent-named edits, and gets back

  1. a Word file with native accept/reject revisions authored by the model, and
  2. a portable, hash-chained .changex provenance journal recording what changed, where, and (where known) why and by whom.

It is a thin FastMCP (stdio) wrapper around the changex-core spine. The server holds in-process, single-session-per-handle state; the journal is flushed to disk on every edit, and the edit sequence number is server-assigned so concurrent tool calls in one turn stay ordered and race-free.

Install / run

# zero-clone (recommended for end users):
uvx changex-mcp

# from this monorepo (dev): installs core from the workspace
uv sync
uv run changex-mcp

# or with pip:
pip install changex-mcp        # pulls in changex-core
python -m changex_mcp          # identical to the `changex-mcp` script

All three forms start the same stdio server. Setup is under 10 minutes: install, drop one of the config blocks below into your client, restart the client.

Remote HTTP transport (connector-URL clients)

Some clients don't spawn a local process — they connect to an MCP server over a URL. claude.ai custom connectors and ChatGPT app connectors are both URL-based. For those, run the same server over Streamable HTTP instead of stdio:

# loopback only (default host 127.0.0.1, port 9000, path /mcp) — no token needed
changex-mcp --http
# → serves http://127.0.0.1:9000/mcp

# pick host / port / path
changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp

Everything is also configurable by environment variable (CLI flags win over env):

Env var Meaning Default
CHANGEX_MCP_TRANSPORT stdio | http | sse stdio
CHANGEX_MCP_HOST HTTP bind host 127.0.0.1
CHANGEX_MCP_PORT HTTP bind port 9000
CHANGEX_MCP_PATH HTTP endpoint path /mcp
CHANGEX_MCP_TOKEN Bearer token (see security) (none)
CHANGEX_MCP_PUBLIC 1 to acknowledge a non-loopback bind (off)

The HTTP deps (starlette + uvicorn) ship with the SDK's cli extra; if you installed a minimal wheel, add them with pip install "changex-mcp[http]".

Connector URL shape

http://<host>:<port><path>          e.g.  http://127.0.0.1:9000/mcp

That URL is exactly what you paste into a claude.ai custom connector or a ChatGPT app connector. Authenticate with an Authorization: Bearer <CHANGEX_MCP_TOKEN> header (required for any non-loopback bind; optional but recommended on loopback).

Security: this server edits local files

Because the tools write .docx/.changex files on the host, the bind policy is fail-closed:

  • Default is loopback (127.0.0.1). A loopback bind needs no token.

  • Binding to a non-loopback host or 0.0.0.0 is refused unless you supply both:

    1. the explicit --public flag (or CHANGEX_MCP_PUBLIC=1), and
    2. a bearer token in CHANGEX_MCP_TOKEN.

    A public bind without a token aborts with a clear warning rather than silently exposing file-editing tools to the network:

    CHANGEX_MCP_TOKEN=$(openssl rand -hex 32) changex-mcp --http --host 0.0.0.0 --public
    

To reach a loopback HTTP server from a cloud client, put it behind a TLS reverse proxy / tunnel and keep the bearer token on — never expose the raw port.

Tools

Tool Purpose
open_tracked(path, agent_context?, author?) Open a .docx; returns {handle, summary, baseline_sha256, session_id}. Pass agent_context={"model","vendor"} so revisions are authored by your model.
get_outline(handle, cursor?, limit?) Bounded, paginated paragraph list → discover node_ids. Returns {nodes:[{node_id,kind,preview,style}], next_cursor, total}.
edit(handle, op, node_id, …) One small tracked edit. opreplace_text / insert_text_after / delete_text / set_paragraph_style. Returns {op_id, seq, node_id, provenance_source}.
reject(handle, op_id) Reject a change by op_id: the op is non-destructively reverted (the rejection itself is audited) and excluded from the next save_tracked, so its revision is genuinely absent from the saved .docx. Returns {op_id, status, reverted, active_ops, verified}.
accept(handle, op_id) Accept (un-reject) a previously rejected op_id so its revision is kept and reappears on the next save_tracked. Returns {op_id, status, reverted, active_ops, verified}.
save_tracked(handle, out) Write the native-revisions .docx as a pure projection of the journal's non-reverted events; returns {tracked_path, changex_path, ops, verified} (ops = active op count).
get_changes(handle) The structured provenance journal: {session_id, events:[…], count, verified}.
render_review(handle, fmt?) Human-readable redline; fmthtml / markdown. Returns {format, report}.

The edit contract (boundary-enforced, not just prompted)

edit is intent-dispatched on op; supply only that intent's fields:

replace_text        → node_id, before (exact current text), after
insert_text_after   → node_id, anchor (exact text to insert after), text
delete_text         → node_id, before (exact text to delete)
set_paragraph_style → node_id, style (new), before (current style name)

The server refuses:

  • before_mismatchbefore/anchor must match the node's current text exactly. This kills blind full-node overwrites.
  • split_required — an op rewriting >50% of a paragraph is rejected with a structured message instructing the model to split it into smaller replace_text edits. The error is the prompt.

Errors are returned as {"error": "<code>", "detail": "<message>"}.

Provenance: observed vs declared (honest)

MCP tool calls do not carry the user's prompt, conversation turn, model id, or vendor. So ChangeX splits provenance and labels each event with provenance_source:

  • observed (server-captured, not trusted from the agent): ts, session_id, tool_call_id (transport request id), and client_name / client_version from the MCP clientInfo handshake.
  • declared (agent-supplied, optional, may be null): agent (model id) and vendor — captured once at open_tracked via agent_context; plus optional per-edit rationale, prompt (hashed to prompt_sha256, never stored verbatim), and turn_id.

MCP client configuration (copy-paste)

These use uvx changex-mcp. If you installed with pip, replace the command with python and args with ["-m", "changex_mcp"].

Claude Code

claude mcp add changex -- uvx changex-mcp

…or add to ~/.claude.json (or the project .mcp.json):

{
  "mcpServers": {
    "changex": {
      "command": "uvx",
      "args": ["changex-mcp"]
    }
  }
}

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json (macOS) / %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "changex": {
      "command": "uvx",
      "args": ["changex-mcp"]
    }
  }
}

OpenAI (Agents SDK / MCPServerStdio)

from agents.mcp import MCPServerStdio

changex = MCPServerStdio(
    params={
        "command": "uvx",
        "args": ["changex-mcp"],
    },
    cache_tools_list=True,
)
# then attach `changex` to your Agent(..., mcp_servers=[changex])

For the OpenAI Responses API hosted-MCP shape, point a stdio bridge at uvx changex-mcp.

Gemini CLI

~/.gemini/settings.json:

{
  "mcpServers": {
    "changex": {
      "command": "uvx",
      "args": ["changex-mcp"]
    }
  }
}

claude.ai custom connector (remote, URL-based)

Custom connectors dial a URL, so run the HTTP transport first:

export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
changex-mcp --http            # → http://127.0.0.1:9000/mcp

Then in claude.ai → Settings → Connectors → Add custom connector:

  • URL: http://127.0.0.1:9000/mcp (or your TLS-tunneled public URL)
  • Authentication: header Authorization: Bearer <CHANGEX_MCP_TOKEN>

A cloud client can't reach 127.0.0.1 on your laptop directly — front the loopback server with a TLS tunnel/reverse proxy and use that HTTPS URL, keeping the bearer token on. Any non-loopback bind requires --public + the token.

ChatGPT app connector (remote, URL-based)

Same server, same URL shape. Start it:

export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp

In ChatGPT → Settings → Connectors / Apps → Add (developer mode for a custom MCP app):

  • MCP server URL: https://<your-tunnel-host>/mcp (point your tunnel at the loopback http://127.0.0.1:9000/mcp)
  • Auth: bearer token = CHANGEX_MCP_TOKEN

End-to-end example (what the model calls)

// 1. open
open_tracked({ "path": "/abs/report.docx",
               "agent_context": { "model": "claude-opus-4-8", "vendor": "anthropic" } })
// → { "handle": "ab12…", "summary": {…}, "baseline_sha256": "…", "session_id": "…" }

// 2. discover node_ids
get_outline({ "handle": "ab12…" })
// → { "nodes": [ { "node_id": "p:00000002", "preview": "The quick brown fox…" } ], … }

// 3. smallest edits
edit({ "handle": "ab12…", "op": "replace_text",
       "node_id": "p:00000002", "before": "quick", "after": "swift",
       "rationale": "tighter wording" })
edit({ "handle": "ab12…", "op": "set_paragraph_style",
       "node_id": "p:00000001", "before": "Normal", "style": "Heading 1" })

// 4. review: reject (drop) or accept (restore) individual changes by op_id
reject({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
// → { "op_id": "…", "status": "rejected", "reverted": true, "active_ops": 1, "verified": true }
accept({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
// → { "op_id": "…", "status": "accepted", "reverted": false, "active_ops": 2, "verified": true }

// 5. save → native Word revisions + .changex (only non-reverted ops are rendered)
save_tracked({ "handle": "ab12…", "out": "/abs/report.tracked.docx" })
// → { "tracked_path": "…", "changex_path": "…/report.changex", "ops": 2, "verified": true }

// 6. review / audit
render_review({ "handle": "ab12…", "fmt": "markdown" })
get_changes({ "handle": "ab12…" })

Notes / limits

  • docx only — the MCP server opens .docx files. Within docx the full op set is available: text insert/delete/replace, paragraph insert/delete, style change, and (since v0.1.1) format.run (run-property revision) and node.move (paragraph move). The other formats (xlsx/csv/pptx/md) are tracked via the changex track CLI rather than this server.
  • Single-session per document. Opening the same file twice in one server is refused rather than left undefined.
  • The hash chain is tamper-evidence for accidental corruption / naive tampering, not adversarial integrity — signing is a later milestone.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

changex_mcp-0.1.2.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

changex_mcp-0.1.2-py3-none-any.whl (27.7 kB view details)

Uploaded Python 3

File details

Details for the file changex_mcp-0.1.2.tar.gz.

File metadata

  • Download URL: changex_mcp-0.1.2.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for changex_mcp-0.1.2.tar.gz
Algorithm Hash digest
SHA256 731451ff0469d7913e135a5bab63e13b06d6956b86dfb3c2e913f4a5b5389b9c
MD5 a180ffe47af1ec6dfc0b1059f63bc76f
BLAKE2b-256 3fb104348e0208fd30b96e1677a9a7dbfaafbaa839f30e42726dc52e8000bb92

See more details on using hashes here.

File details

Details for the file changex_mcp-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: changex_mcp-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 27.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for changex_mcp-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 04ca43b954947521d5e8e60e1d02ef7713d0491ff09b530a3d1645cfe29f71dc
MD5 dda8d3095ae9af91f12fccc4e738b19d
BLAKE2b-256 3d6439067b09b1e9086141a65421be63c5e794811b69adc6b01f5ad656522f2a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page