changex-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ venv/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+
12
+ # Node / Tauri
13
+ node_modules/
14
+ packages/viewer/dist/
15
+ packages/viewer/src-tauri/target/
16
+
17
+ # OS
18
+ .DS_Store
19
+
20
+ # Local editor/agent settings
21
+ .claude/settings.local.json
22
+
23
+ # ChangeX working artifacts
24
+ *.changex.tmp
25
+ examples/out/
26
+
27
+ # stray sample journals (regenerate via make demo)
28
+ examples/*.changex
29
+
30
+ # Tauri generated icons (regenerated by `tauri icon` / the desktop workflow)
31
+ packages/viewer/src-tauri/icons/32x32.png
32
+ packages/viewer/src-tauri/icons/128x128.png
33
+ packages/viewer/src-tauri/icons/128x128@2x.png
34
+ packages/viewer/src-tauri/icons/icon.icns
35
+ packages/viewer/src-tauri/icons/icon.ico
36
+ packages/viewer/src-tauri/icons/Square*.png
37
+ packages/viewer/src-tauri/icons/StoreLogo.png
38
+ packages/viewer/src-tauri/icons/_source.png
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ario Moniri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: changex-mcp
3
+ Version: 0.1.0
4
+ Summary: ChangeX MCP server: edit a .docx through an MCP client and get native Word revisions plus a portable .changex provenance journal.
5
+ Author: ChangeX
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: docx,fastmcp,mcp,provenance,track-changes
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Office/Business :: Office Suites
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: changex-core>=0.1.0
15
+ Requires-Dist: mcp[cli]>=1.8.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: httpx>=0.27; extra == 'dev'
18
+ Requires-Dist: mypy>=1.5; extra == 'dev'
19
+ Requires-Dist: pytest>=7.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.1; extra == 'dev'
21
+ Requires-Dist: starlette>=0.37; extra == 'dev'
22
+ Requires-Dist: uvicorn>=0.30; extra == 'dev'
23
+ Provides-Extra: http
24
+ Requires-Dist: starlette>=0.37; extra == 'http'
25
+ Requires-Dist: uvicorn>=0.30; extra == 'http'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # changex-mcp
29
+
30
+ The **ChangeX MCP server**: an MCP client (Claude Code/Desktop, OpenAI, Gemini
31
+ CLI, …) opens a `.docx`, makes small intent-named edits, and gets back
32
+
33
+ 1. a **Word file with native accept/reject revisions** authored by the model, and
34
+ 2. a portable, hash-chained **`.changex` provenance journal** recording what
35
+ changed, where, and (where known) why and by whom.
36
+
37
+ It is a thin [FastMCP](https://github.com/modelcontextprotocol/python-sdk) (stdio)
38
+ wrapper around the [`changex-core`](../core) spine. The server holds in-process,
39
+ single-session-per-handle state; the journal is flushed to disk on **every** edit,
40
+ and the edit sequence number is **server-assigned** so concurrent tool calls in one
41
+ turn stay ordered and race-free.
42
+
43
+ ## Install / run
44
+
45
+ ```bash
46
+ # zero-clone (recommended for end users):
47
+ uvx changex-mcp
48
+
49
+ # from this monorepo (dev): installs core from the workspace
50
+ uv sync
51
+ uv run changex-mcp
52
+
53
+ # or with pip:
54
+ pip install changex-mcp # pulls in changex-core
55
+ python -m changex_mcp # identical to the `changex-mcp` script
56
+ ```
57
+
58
+ All three forms start the same **stdio** server. Setup is under 10 minutes: install,
59
+ drop one of the config blocks below into your client, restart the client.
60
+
61
+ ## Remote HTTP transport (connector-URL clients)
62
+
63
+ Some clients don't spawn a local process — they connect to an MCP server over a
64
+ **URL**. claude.ai *custom connectors* and ChatGPT *app connectors* are both
65
+ URL-based. For those, run the same server over **Streamable HTTP** instead of stdio:
66
+
67
+ ```bash
68
+ # loopback only (default host 127.0.0.1, port 9000, path /mcp) — no token needed
69
+ changex-mcp --http
70
+ # → serves http://127.0.0.1:9000/mcp
71
+
72
+ # pick host / port / path
73
+ changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp
74
+ ```
75
+
76
+ Everything is also configurable by environment variable (CLI flags win over env):
77
+
78
+ | Env var | Meaning | Default |
79
+ |---------|---------|---------|
80
+ | `CHANGEX_MCP_TRANSPORT` | `stdio` \| `http` \| `sse` | `stdio` |
81
+ | `CHANGEX_MCP_HOST` | HTTP bind host | `127.0.0.1` |
82
+ | `CHANGEX_MCP_PORT` | HTTP bind port | `9000` |
83
+ | `CHANGEX_MCP_PATH` | HTTP endpoint path | `/mcp` |
84
+ | `CHANGEX_MCP_TOKEN` | Bearer token (see security) | *(none)* |
85
+ | `CHANGEX_MCP_PUBLIC` | `1` to acknowledge a non-loopback bind | *(off)* |
86
+
87
+ The HTTP deps (`starlette` + `uvicorn`) ship with the SDK's `cli` extra; if you
88
+ installed a minimal wheel, add them with `pip install "changex-mcp[http]"`.
89
+
90
+ ### Connector URL shape
91
+
92
+ ```
93
+ http://<host>:<port><path> e.g. http://127.0.0.1:9000/mcp
94
+ ```
95
+
96
+ That URL is exactly what you paste into a claude.ai custom connector or a ChatGPT
97
+ app connector. Authenticate with an `Authorization: Bearer <CHANGEX_MCP_TOKEN>`
98
+ header (required for any non-loopback bind; optional but recommended on loopback).
99
+
100
+ ### Security: this server edits local files
101
+
102
+ Because the tools write `.docx`/`.changex` files on the host, the bind policy is
103
+ **fail-closed**:
104
+
105
+ - **Default is loopback** (`127.0.0.1`). A loopback bind needs no token.
106
+ - **Binding to a non-loopback host or `0.0.0.0` is refused** unless you supply
107
+ **both**:
108
+ 1. the explicit `--public` flag (or `CHANGEX_MCP_PUBLIC=1`), **and**
109
+ 2. a bearer token in `CHANGEX_MCP_TOKEN`.
110
+
111
+ A public bind without a token aborts with a clear warning rather than silently
112
+ exposing file-editing tools to the network:
113
+
114
+ ```bash
115
+ CHANGEX_MCP_TOKEN=$(openssl rand -hex 32) changex-mcp --http --host 0.0.0.0 --public
116
+ ```
117
+
118
+ To reach a loopback HTTP server from a cloud client, put it behind a TLS reverse
119
+ proxy / tunnel and keep the bearer token on — never expose the raw port.
120
+
121
+ ## Tools
122
+
123
+ | Tool | Purpose |
124
+ |------|---------|
125
+ | `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. |
126
+ | `get_outline(handle, cursor?, limit?)` | Bounded, paginated paragraph list → discover `node_id`s. Returns `{nodes:[{node_id,kind,preview,style}], next_cursor, total}`. |
127
+ | `edit(handle, op, node_id, …)` | One small tracked edit. `op` ∈ `replace_text` / `insert_text_after` / `delete_text` / `set_paragraph_style`. Returns `{op_id, seq, node_id, provenance_source}`. |
128
+ | `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}`. |
129
+ | `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}`. |
130
+ | `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). |
131
+ | `get_changes(handle)` | The structured provenance journal: `{session_id, events:[…], count, verified}`. |
132
+ | `render_review(handle, fmt?)` | Human-readable redline; `fmt` ∈ `html` / `markdown`. Returns `{format, report}`. |
133
+
134
+ ### The `edit` contract (boundary-enforced, not just prompted)
135
+
136
+ `edit` is intent-dispatched on `op`; supply only that intent's fields:
137
+
138
+ ```
139
+ replace_text → node_id, before (exact current text), after
140
+ insert_text_after → node_id, anchor (exact text to insert after), text
141
+ delete_text → node_id, before (exact text to delete)
142
+ set_paragraph_style → node_id, style (new), before (current style name)
143
+ ```
144
+
145
+ The server **refuses**:
146
+
147
+ - **`before_mismatch`** — `before`/`anchor` must match the node's *current* text
148
+ exactly. This kills blind full-node overwrites.
149
+ - **`split_required`** — an op rewriting >50% of a paragraph is rejected with a
150
+ structured message instructing the model to split it into smaller `replace_text`
151
+ edits. The error *is* the prompt.
152
+
153
+ Errors are returned as `{"error": "<code>", "detail": "<message>"}`.
154
+
155
+ ## Provenance: observed vs declared (honest)
156
+
157
+ MCP tool calls do **not** carry the user's prompt, conversation turn, model id, or
158
+ vendor. So ChangeX splits provenance and labels each event with
159
+ `provenance_source`:
160
+
161
+ - **observed** (server-captured, not trusted from the agent): `ts`, `session_id`,
162
+ `tool_call_id` (transport request id), and `client_name` / `client_version` from
163
+ the MCP `clientInfo` handshake.
164
+ - **declared** (agent-supplied, optional, may be `null`): `agent` (model id) and
165
+ `vendor` — captured **once** at `open_tracked` via `agent_context`; plus optional
166
+ per-edit `rationale`, `prompt` (hashed to `prompt_sha256`, never stored verbatim),
167
+ and `turn_id`.
168
+
169
+ ## MCP client configuration (copy-paste)
170
+
171
+ > These use `uvx changex-mcp`. If you installed with pip, replace the command with
172
+ > `python` and args with `["-m", "changex_mcp"]`.
173
+
174
+ ### Claude Code
175
+
176
+ ```bash
177
+ claude mcp add changex -- uvx changex-mcp
178
+ ```
179
+
180
+ …or add to `~/.claude.json` (or the project `.mcp.json`):
181
+
182
+ ```json
183
+ {
184
+ "mcpServers": {
185
+ "changex": {
186
+ "command": "uvx",
187
+ "args": ["changex-mcp"]
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ ### Claude Desktop
194
+
195
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) /
196
+ `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
197
+
198
+ ```json
199
+ {
200
+ "mcpServers": {
201
+ "changex": {
202
+ "command": "uvx",
203
+ "args": ["changex-mcp"]
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### OpenAI (Agents SDK / `MCPServerStdio`)
210
+
211
+ ```python
212
+ from agents.mcp import MCPServerStdio
213
+
214
+ changex = MCPServerStdio(
215
+ params={
216
+ "command": "uvx",
217
+ "args": ["changex-mcp"],
218
+ },
219
+ cache_tools_list=True,
220
+ )
221
+ # then attach `changex` to your Agent(..., mcp_servers=[changex])
222
+ ```
223
+
224
+ For the OpenAI Responses API hosted-MCP shape, point a stdio bridge at
225
+ `uvx changex-mcp`.
226
+
227
+ ### Gemini CLI
228
+
229
+ `~/.gemini/settings.json`:
230
+
231
+ ```json
232
+ {
233
+ "mcpServers": {
234
+ "changex": {
235
+ "command": "uvx",
236
+ "args": ["changex-mcp"]
237
+ }
238
+ }
239
+ }
240
+ ```
241
+
242
+ ### claude.ai custom connector (remote, URL-based)
243
+
244
+ Custom connectors dial a **URL**, so run the HTTP transport first:
245
+
246
+ ```bash
247
+ export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
248
+ changex-mcp --http # → http://127.0.0.1:9000/mcp
249
+ ```
250
+
251
+ Then in claude.ai → **Settings → Connectors → Add custom connector**:
252
+
253
+ - **URL**: `http://127.0.0.1:9000/mcp` (or your TLS-tunneled public URL)
254
+ - **Authentication**: header `Authorization: Bearer <CHANGEX_MCP_TOKEN>`
255
+
256
+ A cloud client can't reach `127.0.0.1` on your laptop directly — front the
257
+ loopback server with a TLS tunnel/reverse proxy and use that HTTPS URL, keeping
258
+ the bearer token on. Any non-loopback bind **requires** `--public` + the token.
259
+
260
+ ### ChatGPT app connector (remote, URL-based)
261
+
262
+ Same server, same URL shape. Start it:
263
+
264
+ ```bash
265
+ export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
266
+ changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp
267
+ ```
268
+
269
+ In ChatGPT → **Settings → Connectors / Apps → Add** (developer mode for a custom
270
+ MCP app):
271
+
272
+ - **MCP server URL**: `https://<your-tunnel-host>/mcp` (point your tunnel at the
273
+ loopback `http://127.0.0.1:9000/mcp`)
274
+ - **Auth**: bearer token = `CHANGEX_MCP_TOKEN`
275
+
276
+ ## End-to-end example (what the model calls)
277
+
278
+ ```jsonc
279
+ // 1. open
280
+ open_tracked({ "path": "/abs/report.docx",
281
+ "agent_context": { "model": "claude-opus-4-8", "vendor": "anthropic" } })
282
+ // → { "handle": "ab12…", "summary": {…}, "baseline_sha256": "…", "session_id": "…" }
283
+
284
+ // 2. discover node_ids
285
+ get_outline({ "handle": "ab12…" })
286
+ // → { "nodes": [ { "node_id": "p:00000002", "preview": "The quick brown fox…" } ], … }
287
+
288
+ // 3. smallest edits
289
+ edit({ "handle": "ab12…", "op": "replace_text",
290
+ "node_id": "p:00000002", "before": "quick", "after": "swift",
291
+ "rationale": "tighter wording" })
292
+ edit({ "handle": "ab12…", "op": "set_paragraph_style",
293
+ "node_id": "p:00000001", "before": "Normal", "style": "Heading 1" })
294
+
295
+ // 4. review: reject (drop) or accept (restore) individual changes by op_id
296
+ reject({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
297
+ // → { "op_id": "…", "status": "rejected", "reverted": true, "active_ops": 1, "verified": true }
298
+ accept({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
299
+ // → { "op_id": "…", "status": "accepted", "reverted": false, "active_ops": 2, "verified": true }
300
+
301
+ // 5. save → native Word revisions + .changex (only non-reverted ops are rendered)
302
+ save_tracked({ "handle": "ab12…", "out": "/abs/report.tracked.docx" })
303
+ // → { "tracked_path": "…", "changex_path": "…/report.changex", "ops": 2, "verified": true }
304
+
305
+ // 6. review / audit
306
+ render_review({ "handle": "ab12…", "fmt": "markdown" })
307
+ get_changes({ "handle": "ab12…" })
308
+ ```
309
+
310
+ ## Notes / limits
311
+
312
+ - **docx only** in v0.1 (the frozen op set: text insert/delete/replace, paragraph
313
+ insert/delete, style change). xlsx/pptx/csv and `format.run` / `node.move` are
314
+ reserved for later versions.
315
+ - **Single-session per document.** Opening the same file twice in one server is
316
+ refused rather than left undefined.
317
+ - The hash chain is **tamper-evidence** for accidental corruption / naive tampering,
318
+ not adversarial integrity — signing is a later milestone.
@@ -0,0 +1,291 @@
1
+ # changex-mcp
2
+
3
+ The **ChangeX MCP server**: an MCP client (Claude Code/Desktop, OpenAI, Gemini
4
+ CLI, …) opens a `.docx`, makes small intent-named edits, and gets back
5
+
6
+ 1. a **Word file with native accept/reject revisions** authored by the model, and
7
+ 2. a portable, hash-chained **`.changex` provenance journal** recording what
8
+ changed, where, and (where known) why and by whom.
9
+
10
+ It is a thin [FastMCP](https://github.com/modelcontextprotocol/python-sdk) (stdio)
11
+ wrapper around the [`changex-core`](../core) spine. The server holds in-process,
12
+ single-session-per-handle state; the journal is flushed to disk on **every** edit,
13
+ and the edit sequence number is **server-assigned** so concurrent tool calls in one
14
+ turn stay ordered and race-free.
15
+
16
+ ## Install / run
17
+
18
+ ```bash
19
+ # zero-clone (recommended for end users):
20
+ uvx changex-mcp
21
+
22
+ # from this monorepo (dev): installs core from the workspace
23
+ uv sync
24
+ uv run changex-mcp
25
+
26
+ # or with pip:
27
+ pip install changex-mcp # pulls in changex-core
28
+ python -m changex_mcp # identical to the `changex-mcp` script
29
+ ```
30
+
31
+ All three forms start the same **stdio** server. Setup is under 10 minutes: install,
32
+ drop one of the config blocks below into your client, restart the client.
33
+
34
+ ## Remote HTTP transport (connector-URL clients)
35
+
36
+ Some clients don't spawn a local process — they connect to an MCP server over a
37
+ **URL**. claude.ai *custom connectors* and ChatGPT *app connectors* are both
38
+ URL-based. For those, run the same server over **Streamable HTTP** instead of stdio:
39
+
40
+ ```bash
41
+ # loopback only (default host 127.0.0.1, port 9000, path /mcp) — no token needed
42
+ changex-mcp --http
43
+ # → serves http://127.0.0.1:9000/mcp
44
+
45
+ # pick host / port / path
46
+ changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp
47
+ ```
48
+
49
+ Everything is also configurable by environment variable (CLI flags win over env):
50
+
51
+ | Env var | Meaning | Default |
52
+ |---------|---------|---------|
53
+ | `CHANGEX_MCP_TRANSPORT` | `stdio` \| `http` \| `sse` | `stdio` |
54
+ | `CHANGEX_MCP_HOST` | HTTP bind host | `127.0.0.1` |
55
+ | `CHANGEX_MCP_PORT` | HTTP bind port | `9000` |
56
+ | `CHANGEX_MCP_PATH` | HTTP endpoint path | `/mcp` |
57
+ | `CHANGEX_MCP_TOKEN` | Bearer token (see security) | *(none)* |
58
+ | `CHANGEX_MCP_PUBLIC` | `1` to acknowledge a non-loopback bind | *(off)* |
59
+
60
+ The HTTP deps (`starlette` + `uvicorn`) ship with the SDK's `cli` extra; if you
61
+ installed a minimal wheel, add them with `pip install "changex-mcp[http]"`.
62
+
63
+ ### Connector URL shape
64
+
65
+ ```
66
+ http://<host>:<port><path> e.g. http://127.0.0.1:9000/mcp
67
+ ```
68
+
69
+ That URL is exactly what you paste into a claude.ai custom connector or a ChatGPT
70
+ app connector. Authenticate with an `Authorization: Bearer <CHANGEX_MCP_TOKEN>`
71
+ header (required for any non-loopback bind; optional but recommended on loopback).
72
+
73
+ ### Security: this server edits local files
74
+
75
+ Because the tools write `.docx`/`.changex` files on the host, the bind policy is
76
+ **fail-closed**:
77
+
78
+ - **Default is loopback** (`127.0.0.1`). A loopback bind needs no token.
79
+ - **Binding to a non-loopback host or `0.0.0.0` is refused** unless you supply
80
+ **both**:
81
+ 1. the explicit `--public` flag (or `CHANGEX_MCP_PUBLIC=1`), **and**
82
+ 2. a bearer token in `CHANGEX_MCP_TOKEN`.
83
+
84
+ A public bind without a token aborts with a clear warning rather than silently
85
+ exposing file-editing tools to the network:
86
+
87
+ ```bash
88
+ CHANGEX_MCP_TOKEN=$(openssl rand -hex 32) changex-mcp --http --host 0.0.0.0 --public
89
+ ```
90
+
91
+ To reach a loopback HTTP server from a cloud client, put it behind a TLS reverse
92
+ proxy / tunnel and keep the bearer token on — never expose the raw port.
93
+
94
+ ## Tools
95
+
96
+ | Tool | Purpose |
97
+ |------|---------|
98
+ | `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. |
99
+ | `get_outline(handle, cursor?, limit?)` | Bounded, paginated paragraph list → discover `node_id`s. Returns `{nodes:[{node_id,kind,preview,style}], next_cursor, total}`. |
100
+ | `edit(handle, op, node_id, …)` | One small tracked edit. `op` ∈ `replace_text` / `insert_text_after` / `delete_text` / `set_paragraph_style`. Returns `{op_id, seq, node_id, provenance_source}`. |
101
+ | `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}`. |
102
+ | `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}`. |
103
+ | `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). |
104
+ | `get_changes(handle)` | The structured provenance journal: `{session_id, events:[…], count, verified}`. |
105
+ | `render_review(handle, fmt?)` | Human-readable redline; `fmt` ∈ `html` / `markdown`. Returns `{format, report}`. |
106
+
107
+ ### The `edit` contract (boundary-enforced, not just prompted)
108
+
109
+ `edit` is intent-dispatched on `op`; supply only that intent's fields:
110
+
111
+ ```
112
+ replace_text → node_id, before (exact current text), after
113
+ insert_text_after → node_id, anchor (exact text to insert after), text
114
+ delete_text → node_id, before (exact text to delete)
115
+ set_paragraph_style → node_id, style (new), before (current style name)
116
+ ```
117
+
118
+ The server **refuses**:
119
+
120
+ - **`before_mismatch`** — `before`/`anchor` must match the node's *current* text
121
+ exactly. This kills blind full-node overwrites.
122
+ - **`split_required`** — an op rewriting >50% of a paragraph is rejected with a
123
+ structured message instructing the model to split it into smaller `replace_text`
124
+ edits. The error *is* the prompt.
125
+
126
+ Errors are returned as `{"error": "<code>", "detail": "<message>"}`.
127
+
128
+ ## Provenance: observed vs declared (honest)
129
+
130
+ MCP tool calls do **not** carry the user's prompt, conversation turn, model id, or
131
+ vendor. So ChangeX splits provenance and labels each event with
132
+ `provenance_source`:
133
+
134
+ - **observed** (server-captured, not trusted from the agent): `ts`, `session_id`,
135
+ `tool_call_id` (transport request id), and `client_name` / `client_version` from
136
+ the MCP `clientInfo` handshake.
137
+ - **declared** (agent-supplied, optional, may be `null`): `agent` (model id) and
138
+ `vendor` — captured **once** at `open_tracked` via `agent_context`; plus optional
139
+ per-edit `rationale`, `prompt` (hashed to `prompt_sha256`, never stored verbatim),
140
+ and `turn_id`.
141
+
142
+ ## MCP client configuration (copy-paste)
143
+
144
+ > These use `uvx changex-mcp`. If you installed with pip, replace the command with
145
+ > `python` and args with `["-m", "changex_mcp"]`.
146
+
147
+ ### Claude Code
148
+
149
+ ```bash
150
+ claude mcp add changex -- uvx changex-mcp
151
+ ```
152
+
153
+ …or add to `~/.claude.json` (or the project `.mcp.json`):
154
+
155
+ ```json
156
+ {
157
+ "mcpServers": {
158
+ "changex": {
159
+ "command": "uvx",
160
+ "args": ["changex-mcp"]
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Claude Desktop
167
+
168
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) /
169
+ `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "changex": {
175
+ "command": "uvx",
176
+ "args": ["changex-mcp"]
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ ### OpenAI (Agents SDK / `MCPServerStdio`)
183
+
184
+ ```python
185
+ from agents.mcp import MCPServerStdio
186
+
187
+ changex = MCPServerStdio(
188
+ params={
189
+ "command": "uvx",
190
+ "args": ["changex-mcp"],
191
+ },
192
+ cache_tools_list=True,
193
+ )
194
+ # then attach `changex` to your Agent(..., mcp_servers=[changex])
195
+ ```
196
+
197
+ For the OpenAI Responses API hosted-MCP shape, point a stdio bridge at
198
+ `uvx changex-mcp`.
199
+
200
+ ### Gemini CLI
201
+
202
+ `~/.gemini/settings.json`:
203
+
204
+ ```json
205
+ {
206
+ "mcpServers": {
207
+ "changex": {
208
+ "command": "uvx",
209
+ "args": ["changex-mcp"]
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ ### claude.ai custom connector (remote, URL-based)
216
+
217
+ Custom connectors dial a **URL**, so run the HTTP transport first:
218
+
219
+ ```bash
220
+ export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
221
+ changex-mcp --http # → http://127.0.0.1:9000/mcp
222
+ ```
223
+
224
+ Then in claude.ai → **Settings → Connectors → Add custom connector**:
225
+
226
+ - **URL**: `http://127.0.0.1:9000/mcp` (or your TLS-tunneled public URL)
227
+ - **Authentication**: header `Authorization: Bearer <CHANGEX_MCP_TOKEN>`
228
+
229
+ A cloud client can't reach `127.0.0.1` on your laptop directly — front the
230
+ loopback server with a TLS tunnel/reverse proxy and use that HTTPS URL, keeping
231
+ the bearer token on. Any non-loopback bind **requires** `--public` + the token.
232
+
233
+ ### ChatGPT app connector (remote, URL-based)
234
+
235
+ Same server, same URL shape. Start it:
236
+
237
+ ```bash
238
+ export CHANGEX_MCP_TOKEN=$(openssl rand -hex 32)
239
+ changex-mcp --http --host 127.0.0.1 --port 9000 --path /mcp
240
+ ```
241
+
242
+ In ChatGPT → **Settings → Connectors / Apps → Add** (developer mode for a custom
243
+ MCP app):
244
+
245
+ - **MCP server URL**: `https://<your-tunnel-host>/mcp` (point your tunnel at the
246
+ loopback `http://127.0.0.1:9000/mcp`)
247
+ - **Auth**: bearer token = `CHANGEX_MCP_TOKEN`
248
+
249
+ ## End-to-end example (what the model calls)
250
+
251
+ ```jsonc
252
+ // 1. open
253
+ open_tracked({ "path": "/abs/report.docx",
254
+ "agent_context": { "model": "claude-opus-4-8", "vendor": "anthropic" } })
255
+ // → { "handle": "ab12…", "summary": {…}, "baseline_sha256": "…", "session_id": "…" }
256
+
257
+ // 2. discover node_ids
258
+ get_outline({ "handle": "ab12…" })
259
+ // → { "nodes": [ { "node_id": "p:00000002", "preview": "The quick brown fox…" } ], … }
260
+
261
+ // 3. smallest edits
262
+ edit({ "handle": "ab12…", "op": "replace_text",
263
+ "node_id": "p:00000002", "before": "quick", "after": "swift",
264
+ "rationale": "tighter wording" })
265
+ edit({ "handle": "ab12…", "op": "set_paragraph_style",
266
+ "node_id": "p:00000001", "before": "Normal", "style": "Heading 1" })
267
+
268
+ // 4. review: reject (drop) or accept (restore) individual changes by op_id
269
+ reject({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
270
+ // → { "op_id": "…", "status": "rejected", "reverted": true, "active_ops": 1, "verified": true }
271
+ accept({ "handle": "ab12…", "op_id": "…op-id-of-edit-2…" })
272
+ // → { "op_id": "…", "status": "accepted", "reverted": false, "active_ops": 2, "verified": true }
273
+
274
+ // 5. save → native Word revisions + .changex (only non-reverted ops are rendered)
275
+ save_tracked({ "handle": "ab12…", "out": "/abs/report.tracked.docx" })
276
+ // → { "tracked_path": "…", "changex_path": "…/report.changex", "ops": 2, "verified": true }
277
+
278
+ // 6. review / audit
279
+ render_review({ "handle": "ab12…", "fmt": "markdown" })
280
+ get_changes({ "handle": "ab12…" })
281
+ ```
282
+
283
+ ## Notes / limits
284
+
285
+ - **docx only** in v0.1 (the frozen op set: text insert/delete/replace, paragraph
286
+ insert/delete, style change). xlsx/pptx/csv and `format.run` / `node.move` are
287
+ reserved for later versions.
288
+ - **Single-session per document.** Opening the same file twice in one server is
289
+ refused rather than left undefined.
290
+ - The hash chain is **tamper-evidence** for accidental corruption / naive tampering,
291
+ not adversarial integrity — signing is a later milestone.