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.
- mcpstate-0.1.0/.github/workflows/ci.yml +26 -0
- mcpstate-0.1.0/.github/workflows/publish.yml +35 -0
- mcpstate-0.1.0/.gitignore +9 -0
- mcpstate-0.1.0/.shotlist.yaml +31 -0
- mcpstate-0.1.0/CHANGELOG.md +37 -0
- mcpstate-0.1.0/LICENSE +21 -0
- mcpstate-0.1.0/PKG-INFO +293 -0
- mcpstate-0.1.0/README.md +264 -0
- mcpstate-0.1.0/docs/concepts.md +145 -0
- mcpstate-0.1.0/docs/screenshots/01-cli-help.png +0 -0
- mcpstate-0.1.0/docs/screenshots/02-test-suite.png +0 -0
- mcpstate-0.1.0/docs/screenshots/03-session-one-mints.png +0 -0
- mcpstate-0.1.0/docs/screenshots/04-session-two-resumes.png +0 -0
- mcpstate-0.1.0/docs/screenshots/index.html +43 -0
- mcpstate-0.1.0/docs/screenshots/manifest.json +48 -0
- mcpstate-0.1.0/docs/superpowers/plans/2026-07-22-mcpstate.md +1669 -0
- mcpstate-0.1.0/docs/superpowers/specs/2026-07-22-mcpstate-design.md +201 -0
- mcpstate-0.1.0/docs/test-evidence.md +31 -0
- mcpstate-0.1.0/pyproject.toml +56 -0
- mcpstate-0.1.0/src/mcpstate/__init__.py +42 -0
- mcpstate-0.1.0/src/mcpstate/backends/__init__.py +3 -0
- mcpstate-0.1.0/src/mcpstate/backends/base.py +42 -0
- mcpstate-0.1.0/src/mcpstate/backends/redis.py +120 -0
- mcpstate-0.1.0/src/mcpstate/backends/sqlite.py +130 -0
- mcpstate-0.1.0/src/mcpstate/errors.py +71 -0
- mcpstate-0.1.0/src/mcpstate/fastmcp.py +70 -0
- mcpstate-0.1.0/src/mcpstate/ops.py +168 -0
- mcpstate-0.1.0/src/mcpstate/py.typed +0 -0
- mcpstate-0.1.0/src/mcpstate/server.py +200 -0
- mcpstate-0.1.0/src/mcpstate/store.py +384 -0
- mcpstate-0.1.0/tests/__init__.py +0 -0
- mcpstate-0.1.0/tests/test_backends.py +103 -0
- mcpstate-0.1.0/tests/test_cli.py +40 -0
- mcpstate-0.1.0/tests/test_errors.py +29 -0
- mcpstate-0.1.0/tests/test_fastmcp_helper.py +46 -0
- mcpstate-0.1.0/tests/test_from_url.py +45 -0
- mcpstate-0.1.0/tests/test_hardening.py +174 -0
- mcpstate-0.1.0/tests/test_ops.py +55 -0
- mcpstate-0.1.0/tests/test_races.py +53 -0
- mcpstate-0.1.0/tests/test_server.py +127 -0
- 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,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.
|
mcpstate-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|