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.
- changex_mcp-0.1.0/.gitignore +38 -0
- changex_mcp-0.1.0/LICENSE +21 -0
- changex_mcp-0.1.0/PKG-INFO +318 -0
- changex_mcp-0.1.0/README.md +291 -0
- changex_mcp-0.1.0/pyproject.toml +69 -0
- changex_mcp-0.1.0/src/changex_mcp/__init__.py +29 -0
- changex_mcp-0.1.0/src/changex_mcp/__main__.py +13 -0
- changex_mcp-0.1.0/src/changex_mcp/outline.py +111 -0
- changex_mcp-0.1.0/src/changex_mcp/provenance.py +200 -0
- changex_mcp-0.1.0/src/changex_mcp/server.py +274 -0
- changex_mcp-0.1.0/src/changex_mcp/session.py +130 -0
- changex_mcp-0.1.0/src/changex_mcp/tools.py +381 -0
- changex_mcp-0.1.0/src/changex_mcp/transport.py +344 -0
|
@@ -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.
|