mcpstate 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.
Files changed (41) hide show
  1. mcpstate-0.1.0/.github/workflows/ci.yml +26 -0
  2. mcpstate-0.1.0/.github/workflows/publish.yml +35 -0
  3. mcpstate-0.1.0/.gitignore +9 -0
  4. mcpstate-0.1.0/.shotlist.yaml +31 -0
  5. mcpstate-0.1.0/CHANGELOG.md +37 -0
  6. mcpstate-0.1.0/LICENSE +21 -0
  7. mcpstate-0.1.0/PKG-INFO +293 -0
  8. mcpstate-0.1.0/README.md +264 -0
  9. mcpstate-0.1.0/docs/concepts.md +145 -0
  10. mcpstate-0.1.0/docs/screenshots/01-cli-help.png +0 -0
  11. mcpstate-0.1.0/docs/screenshots/02-test-suite.png +0 -0
  12. mcpstate-0.1.0/docs/screenshots/03-session-one-mints.png +0 -0
  13. mcpstate-0.1.0/docs/screenshots/04-session-two-resumes.png +0 -0
  14. mcpstate-0.1.0/docs/screenshots/index.html +43 -0
  15. mcpstate-0.1.0/docs/screenshots/manifest.json +48 -0
  16. mcpstate-0.1.0/docs/superpowers/plans/2026-07-22-mcpstate.md +1669 -0
  17. mcpstate-0.1.0/docs/superpowers/specs/2026-07-22-mcpstate-design.md +201 -0
  18. mcpstate-0.1.0/docs/test-evidence.md +31 -0
  19. mcpstate-0.1.0/pyproject.toml +56 -0
  20. mcpstate-0.1.0/src/mcpstate/__init__.py +42 -0
  21. mcpstate-0.1.0/src/mcpstate/backends/__init__.py +3 -0
  22. mcpstate-0.1.0/src/mcpstate/backends/base.py +42 -0
  23. mcpstate-0.1.0/src/mcpstate/backends/redis.py +120 -0
  24. mcpstate-0.1.0/src/mcpstate/backends/sqlite.py +130 -0
  25. mcpstate-0.1.0/src/mcpstate/errors.py +71 -0
  26. mcpstate-0.1.0/src/mcpstate/fastmcp.py +70 -0
  27. mcpstate-0.1.0/src/mcpstate/ops.py +168 -0
  28. mcpstate-0.1.0/src/mcpstate/py.typed +0 -0
  29. mcpstate-0.1.0/src/mcpstate/server.py +200 -0
  30. mcpstate-0.1.0/src/mcpstate/store.py +384 -0
  31. mcpstate-0.1.0/tests/__init__.py +0 -0
  32. mcpstate-0.1.0/tests/test_backends.py +103 -0
  33. mcpstate-0.1.0/tests/test_cli.py +40 -0
  34. mcpstate-0.1.0/tests/test_errors.py +29 -0
  35. mcpstate-0.1.0/tests/test_fastmcp_helper.py +46 -0
  36. mcpstate-0.1.0/tests/test_from_url.py +45 -0
  37. mcpstate-0.1.0/tests/test_hardening.py +174 -0
  38. mcpstate-0.1.0/tests/test_ops.py +55 -0
  39. mcpstate-0.1.0/tests/test_races.py +53 -0
  40. mcpstate-0.1.0/tests/test_server.py +127 -0
  41. mcpstate-0.1.0/tests/test_store.py +194 -0
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: python -m pip install -e ".[dev]"
21
+ - name: Lint
22
+ run: python -m ruff check src tests
23
+ - name: Types
24
+ run: python -m mypy src/mcpstate
25
+ - name: Tests
26
+ run: python -m pytest -q
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.12"
15
+ - name: Build distributions
16
+ run: |
17
+ python -m pip install build
18
+ python -m build
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write # OIDC Trusted Publishing - no API tokens stored
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ .DS_Store
@@ -0,0 +1,31 @@
1
+ # Visual test evidence for mcpstate. Regenerate any time with: shotlist run
2
+ output:
3
+ dir: docs/screenshots
4
+
5
+ shots:
6
+ - name: cli-help
7
+ kind: cli
8
+ command: "~/Library/Python/3.11/bin/mcpstate --help"
9
+ style: rendered
10
+ cols: 90
11
+ rows: 14
12
+ alt: "mcpstate --help showing the serve subcommand"
13
+
14
+ - name: test-suite
15
+ kind: cli
16
+ command: "python3 -m pytest -q"
17
+ style: rendered
18
+ cols: 90
19
+ rows: 8
20
+ alt: "full pytest suite green"
21
+
22
+ - name: hand-off-demo
23
+ kind: session
24
+ clear_between: true
25
+ steps:
26
+ - name: session-one-mints
27
+ command: "python3 -c \"from mcpstate import HandleStore; s = HandleStore.from_url('sqlite:///demo-state.db'); h = s.mint('research', {'sources': ['arxiv/2401.00001'], 'notes': 'phase 1 done'}, user='varma'); open('demo-handle.txt', 'w').write(h); print('session 1 minted:', h)\""
28
+ alt: "Session 1 mints a research handle and saves state"
29
+ - name: session-two-resumes
30
+ command: "python3 -c \"from mcpstate import HandleStore; h = open('demo-handle.txt').read(); s = HandleStore.from_url('sqlite:///demo-state.db'); print('session 2 sees handles:', [i.handle for i in s.list('varma')]); print('session 2 resumed state:', s.get(h, user='varma').state)\""
31
+ alt: "A separate process lists and resumes the same state - the hand-off"
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## 0.1.0 - 2026-07-22
7
+
8
+ ### Added
9
+
10
+ - `HandleStore`: mint, get, versioned save, commutative patch, list, revoke,
11
+ and sweep — durable state behind opaque handles, scoped to a user, with
12
+ per-handle TTL and lazy expiry.
13
+ - Conflict kit: compare-and-swap saves with agent-legible `StaleWrite`
14
+ rejections carrying the full current snapshot; `Append`/`SetKey`/`DelKey`/
15
+ `Merge` patch ops that apply without version checks; freshness metadata
16
+ (version, updated_at, last_writer) on every read.
17
+ - Backends: SQLite (zero-config default) and Redis (shared reach for
18
+ multi-instance and cross-device sync), both behind a six-method protocol
19
+ verified by a shared contract test suite and concurrency race proofs.
20
+ - Structured error hierarchy: `handle_not_found` vs `handle_expired`
21
+ distinguished, `patch_error` with path diagnostics, `backend_error`.
22
+ - FastMCP helpers: `store_from_env()` (`MCPSTATE_BACKEND`) and
23
+ `current_user()` (OAuth subject → `MCPSTATE_USER` → `"local"`).
24
+ - Flagship MCP server (`mcpstate serve`): `state_save`, `state_load`,
25
+ `state_list`, `state_patch`, `state_delete` tools built on the public
26
+ library API; stdio and HTTP transports.
27
+ - Documentation: README with architecture and conflict-flow diagrams,
28
+ concepts guide (hand-off sync, the conflict ladder, honest limits).
29
+ - Hardening: Redis keys percent-encoded against namespace collision from
30
+ hostile user/handle strings; credentials redacted from every error message;
31
+ early JSON-serializability validation; 1 MiB state size guard with
32
+ structured `state_too_large` error; explicit SQLite busy timeout for
33
+ multi-process use; writer attribution (`MCPSTATE_WRITER`/hostname) on all
34
+ flagship writes; selective `state_load(path=...)` subtree reads;
35
+ opportunistic expiry sweep during `state_list`; `py.typed` marker with a
36
+ clean mypy pass; CI (ruff, mypy, pytest on 3.11/3.12) and PyPI Trusted
37
+ Publishing workflows.
mcpstate-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Varma Budharaju
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,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcpstate
3
+ Version: 0.1.0
4
+ Summary: Durable, user-keyed state for stateless MCP servers — handles, hand-off sync, agent-mediated conflict resolution.
5
+ Project-URL: Homepage, https://github.com/varmabudharaju/mcpstate
6
+ Author: Varma Budharaju
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agents,mcp,model-context-protocol,session,state,sync
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.11
16
+ Provides-Extra: dev
17
+ Requires-Dist: fakeredis>=2.23; extra == 'dev'
18
+ Requires-Dist: fastmcp>=2.9; extra == 'dev'
19
+ Requires-Dist: mypy>=1.10; extra == 'dev'
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8; extra == 'dev'
22
+ Requires-Dist: redis>=5; extra == 'dev'
23
+ Requires-Dist: ruff>=0.4; extra == 'dev'
24
+ Provides-Extra: fastmcp
25
+ Requires-Dist: fastmcp>=2.9; extra == 'fastmcp'
26
+ Provides-Extra: redis
27
+ Requires-Dist: redis>=5; extra == 'redis'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # mcpstate
31
+
32
+ **Durable, user-keyed state for stateless MCP servers.**
33
+
34
+ *State that follows the user, not the session.*
35
+
36
+ `mcpstate` is a Python library — plus a ready-to-run MCP server — that gives
37
+ agents state which survives the end of a conversation, a switch to a different
38
+ MCP client, or a move to a different device. Start a research session in
39
+ Claude Desktop on your laptop; continue it tomorrow from Claude Code, or from
40
+ your phone.
41
+
42
+ ## Why this exists
43
+
44
+ The MCP specification revision of **2026-07-28** made the protocol stateless:
45
+ `Mcp-Session-Id`, the initialize handshake, and SSE resumability were all
46
+ removed ([changelog](https://modelcontextprotocol.io/specification/draft/changelog),
47
+ [announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/)).
48
+ Sessions fought load balancers; the spec chose horizontal scale.
49
+
50
+ The spec's official answer for stateful servers is the **handle pattern**:
51
+ mint an explicit handle (a `basket_id`, a `research_id`) from a tool, and have
52
+ the model pass it back as an ordinary argument. How a server *persists* what a
53
+ handle points to is explicitly out of scope — every stateful MCP server now
54
+ needs a durable, user-scoped, expiring handle store, and nothing standardizes
55
+ one.
56
+
57
+ `mcpstate` is that store: minting, persistence, versioning, TTL, identity
58
+ scoping, and conflict handling behind one small API — with the storage backend
59
+ deciding how far your state can travel.
60
+
61
+ ## The three axes of continuity
62
+
63
+ ```mermaid
64
+ flowchart LR
65
+ subgraph yesterday [Monday, laptop]
66
+ A[Claude Desktop\nconversation 1]
67
+ end
68
+ subgraph today [Tuesday, laptop]
69
+ B[Claude Code\nconversation 2]
70
+ end
71
+ subgraph tonight [Tuesday night, phone]
72
+ C[Claude app\nconversation 3]
73
+ end
74
+ A -- "research_k3v9x2mq" --> S[(mcpstate\nhandle store)]
75
+ S -- resume --> B
76
+ B -- save --> S
77
+ S -- resume --> C
78
+ ```
79
+
80
+ | Axis | Scenario | Backend needed |
81
+ |---|---|---|
82
+ | Across conversations | Context window filled up; next conversation resumes the work | SQLite (default, zero config) |
83
+ | Across clients | Started in Claude Desktop, continuing in Claude Code or Cursor | SQLite (default, zero config) |
84
+ | Across devices | Laptop to phone, desk to server | Redis (any shared instance) |
85
+
86
+ The first axis is the everyday one: today, every MCP server forgets everything
87
+ between conversations. With `mcpstate`, work products survive by default.
88
+
89
+ ## Quickstart: end users (the flagship server)
90
+
91
+ Install and add one entry to your MCP client config — every agent you run
92
+ gains durable memory:
93
+
94
+ ```bash
95
+ pip install "mcpstate[fastmcp]"
96
+ ```
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "state": {
102
+ "command": "mcpstate",
103
+ "args": ["serve"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ The server exposes five tools:
110
+
111
+ | Tool | What the agent uses it for |
112
+ |---|---|
113
+ | `state_save` | Create durable state (mints a handle) or update it (versioned) |
114
+ | `state_load` | Load state by handle (optionally just a subtree via `path`) |
115
+ | `state_list` | "What was I working on?" — list this user's handles |
116
+ | `state_patch` | Additive edits that can never conflict (append, set key, merge) |
117
+ | `state_delete` | Permanently remove state |
118
+
119
+ For cross-device reach, point every device at a shared Redis:
120
+
121
+ ```json
122
+ { "args": ["serve", "--backend", "redis://your-redis-host:6379/0"] }
123
+ ```
124
+
125
+ ## Quickstart: server authors (the library)
126
+
127
+ ```bash
128
+ pip install mcpstate
129
+ ```
130
+
131
+ ```python
132
+ from mcpstate import HandleStore, Append, StaleWrite
133
+
134
+ store = HandleStore.from_url() # default: sqlite:///~/.mcpstate/state.db
135
+
136
+ # Mint: create durable state, get back an opaque handle for the model to carry.
137
+ handle = store.mint("research", {"sources": [], "notes": ""}, user="alice", ttl_days=7)
138
+
139
+ # Read: state plus the freshness metadata you need to write it back.
140
+ snap = store.get(handle, user="alice")
141
+
142
+ # Versioned save: declare which version you read. If another session wrote
143
+ # in between, you get a StaleWrite carrying the current state - hand it to
144
+ # your model to merge and retry.
145
+ try:
146
+ store.save(handle, {**snap.state, "notes": "arm64 wins"}, user="alice",
147
+ expect_version=snap.version, writer="laptop/claude-code")
148
+ except StaleWrite as conflict:
149
+ current = conflict.details["current"] # full current snapshot, agent-legible
150
+
151
+ # Commutative patch: additive edits skip version checks entirely - two
152
+ # devices appending at the same moment both land.
153
+ store.patch(handle, [Append("sources", "https://arxiv.org/abs/...")],
154
+ user="alice", writer="phone/claude")
155
+
156
+ # Resume, any session later: what was this user working on?
157
+ for info in store.list("alice", kind="research"):
158
+ print(info.handle, info.updated_at, info.last_writer)
159
+ ```
160
+
161
+ ## The conflict model
162
+
163
+ `mcpstate` implements **hand-off sync**: state moves between sessions like a
164
+ relay baton — one active writer at a time is the expected case, and the rare
165
+ overlap is *detected and surfaced*, never silently clobbered.
166
+
167
+ The design bet: **your client is an LLM.** Traditional sync systems need
168
+ CRDTs because their clients cannot reason about a conflict. An agent can. So a
169
+ losing write gets back a structured rejection containing the winner's state
170
+ and an instruction to re-read and re-apply — and the model performs a
171
+ *semantic* merge, which is better than a structural one:
172
+
173
+ ```mermaid
174
+ sequenceDiagram
175
+ participant L as Laptop agent
176
+ participant S as mcpstate
177
+ participant P as Phone agent
178
+ L->>S: get(handle) -> version 4
179
+ P->>S: get(handle) -> version 4
180
+ P->>S: save(state', expect_version=4)
181
+ S-->>P: ok, now version 5
182
+ L->>S: save(state'', expect_version=4)
183
+ S-->>L: StaleWrite: modified by phone, now v5, here is the current state
184
+ L->>L: merge intent with phone's state
185
+ L->>S: save(merged, expect_version=5)
186
+ S-->>L: ok, now version 6
187
+ ```
188
+
189
+ Three mechanisms, cheapest first:
190
+
191
+ 1. **Versioned saves** — every snapshot carries a version; `save` declares the
192
+ version it read; mismatches raise `StaleWrite` with the current snapshot
193
+ inside.
194
+ 2. **Commutative patches** — `Append` / `SetKey` / `DelKey` / `Merge` commute
195
+ with each other, so they apply without version checks and cannot conflict.
196
+ Most agent-state mutations are additive; most writes never see a conflict.
197
+ 3. **Freshness metadata** — every read returns version, `updated_at`, and
198
+ `last_writer`, so a resuming session knows what happened while it was away.
199
+
200
+ ## Backends
201
+
202
+ ```mermaid
203
+ flowchart TD
204
+ T[flagship server tools] --> HS[HandleStore]
205
+ LIB[your server's own tools] --> HS
206
+ HS --> B{backend URL}
207
+ B -- "sqlite:///..." --> SQ[(SQLite\none machine, zero config)]
208
+ B -- "redis://..." --> RD[(Redis\nshared: multi-instance,\ncross-device)]
209
+ ```
210
+
211
+ | | SQLite (default) | Redis |
212
+ |---|---|---|
213
+ | URL | `sqlite:///~/.mcpstate/state.db` | `redis://host:6379/0` |
214
+ | Reach | one machine: conversations + clients | anywhere the Redis is reachable |
215
+ | Setup | none | `pip install "mcpstate[redis]"` + a Redis |
216
+ | Concurrency | atomic compare-and-swap via SQL | optimistic WATCH/MULTI transactions |
217
+
218
+ Configuration is three environment variables:
219
+
220
+ - `MCPSTATE_BACKEND` — backend URL (or pass `--backend` to `mcpstate serve`).
221
+ - `MCPSTATE_USER` — identity override for local/stdio use. Remote servers
222
+ running with OAuth resolve the user from the access token instead; stdio
223
+ servers default to `"local"`.
224
+ - `MCPSTATE_WRITER` — the label recorded as `last_writer` on every write
225
+ (e.g. `laptop/claude-code`); defaults to the machine's hostname, so
226
+ cross-device hand-offs are attributable out of the box.
227
+
228
+ Security defaults worth knowing: states are capped at 1 MiB
229
+ (`HandleStore(max_state_bytes=...)` to change; oversized saves get a
230
+ structured `state_too_large` error), credentials never appear in error
231
+ messages, and `mcpstate serve --transport http` has no authentication of its
232
+ own — keep it on localhost or behind an authenticating proxy; multi-user
233
+ identity requires running under FastMCP OAuth.
234
+
235
+ ## API reference
236
+
237
+ ### `HandleStore`
238
+
239
+ | Method | Behavior | Raises |
240
+ |---|---|---|
241
+ | `from_url(url=None)` | Construct from a backend URL; `None` uses the SQLite default | `ValueError`, `BackendError` |
242
+ | `mint(kind, state, *, user, ttl_days=None, writer=None) -> str` | Create state, return opaque handle `{kind}_{8 chars}` | `ValueError` |
243
+ | `get(handle, *, user) -> Snapshot` | State + version + timestamps + last writer | `HandleNotFound`, `HandleExpired` |
244
+ | `save`/`mint`/`patch` size guard | States over `max_state_bytes` (default 1 MiB) are rejected | `StateTooLarge` |
245
+ | `save(handle, state, *, user, expect_version, writer=None) -> Snapshot` | Versioned full replace; returns the new snapshot | `StaleWrite`, `HandleNotFound`, `HandleExpired` |
246
+ | `patch(handle, ops, *, user, writer=None) -> Snapshot` | Apply commutative ops; no version needed | `PatchError`, `HandleNotFound`, `HandleExpired` |
247
+ | `list(user, *, kind=None, include_expired=False) -> list[HandleInfo]` | Metadata only, most recently updated first | — |
248
+ | `revoke(handle, *, user)` | Delete | `HandleNotFound` |
249
+ | `sweep(user) -> int` | Physically remove expired records | — |
250
+
251
+ ### Patch ops
252
+
253
+ | Op | Wire form (for `state_patch`) |
254
+ |---|---|
255
+ | `Append(path, value)` | `{"op": "append", "path": "sources", "value": ...}` |
256
+ | `SetKey(path, key, value)` | `{"op": "set_key", "path": "profile", "key": "name", "value": ...}` |
257
+ | `DelKey(path, key)` | `{"op": "del_key", "path": "", "key": "draft"}` |
258
+ | `Merge(mapping)` | `{"op": "merge", "mapping": {...}}` |
259
+
260
+ `path` is a dotted path into the state (`"profile.tags"`); `""` is the root.
261
+
262
+ ### Errors
263
+
264
+ Every error carries `.code` and `.to_payload()` — a structured dict written
265
+ for a model to read: `stale_write` includes the full current snapshot;
266
+ `handle_expired` is distinguished from `handle_not_found` and says when it
267
+ expired and what the TTL was.
268
+
269
+ ## Design notes
270
+
271
+ Read [docs/concepts.md](docs/concepts.md) for the full conceptual story:
272
+ the relay-baton model, why hand-off (not CRDTs) is the right v1 for agent
273
+ systems, the conflict ladder, and the honest limits.
274
+
275
+ ## Roadmap
276
+
277
+ Deliberately out of v1, in rough order:
278
+
279
+ - Append-only changelog and `changes_since(handle, version)` — richer resume
280
+ UX and the substrate for op-based merging.
281
+ - Advisory activity leases — "another session is working on this right now."
282
+ - Merge hooks / CRDTs behind the same handle API — true concurrent sync.
283
+ - Push via MCP resource subscriptions, where client support allows.
284
+ - Postgres backend; sidecar service for non-Python servers.
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ python3 -m pip install -e ".[dev]"
290
+ python3 -m pytest
291
+ ```
292
+
293
+ MIT licensed.