tokenmizer 0.2.4__tar.gz → 0.2.6__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.
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/CHANGELOG.md +30 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/PKG-INFO +11 -12
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/README.md +10 -11
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/pyproject.toml +3 -4
- tokenmizer-0.2.6/scripts/mcp_e2e_check.py +158 -0
- tokenmizer-0.2.6/server.json +29 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_graph_persistence.py +84 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/__init__.py +1 -1
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/app.py +1 -1
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/graph.py +26 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/mcp/server.py +12 -2
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/marketplace.json +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/plugin.json +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/analyze/SKILL.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/checkpoint/SKILL.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/resume/SKILL.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.claude-plugin/skills/stats/SKILL.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/ISSUE_TEMPLATE/extraction_miss.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/workflows/ci.yml +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.github/workflows/release.yml +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.gitignore +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/.mcp.json +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/CONTRIBUTING.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/Dockerfile +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/LICENSE +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/SECURITY.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/TESTING.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/USAGE.md +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner_v2.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/checkpoint_accuracy/runner_v3.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/graph_retrieval/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/graph_retrieval/runner.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/latency/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/latency/runner.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/benchmarks/resume_quality/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docker-compose.yml +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docs/assets/architecture.svg +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/docs/assets/logo.svg +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/examples/basic_usage.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/install.sh +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/run_stdlib_tests.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/setup.sh +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/scripts/static_audit.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/chaos/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/chaos/test_recovery.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/conftest.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/test_api_endpoint.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/integration/test_checkpoint.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/memory_accuracy/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/memory_accuracy/test_retention.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_cache.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_compression_correctness.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_decision_cache_async.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_file_intelligence.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_graph.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_hybrid_extractor.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_rate_limiter.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_security.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_tokenizer.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tests/unit/test_validator.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/agents/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/analytics/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/analytics/engine.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/api/rate_limiter.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/checkpoints/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/checkpoints/manager.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/cli.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/engine.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/output_trimmer.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/compression/window.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/config/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/config/settings.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/dto.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/errors.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/core/tokenizer.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/dashboard/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/dashboard/page.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/filters/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/filters/file_intelligence.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/decision_tracker.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/helpers.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/hybrid_extractor.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/types.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/validator.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/graph_memory/visualization.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/mcp/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/providers/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/providers/providers.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/auth.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/middleware.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/security/redaction.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/semantic_cache/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/semantic_cache/cache.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/state/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/state/backend.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer/storage/__init__.py +0 -0
- {tokenmizer-0.2.4 → tokenmizer-0.2.6}/tokenmizer.yaml +0 -0
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.6] — 2026-07-02 — MCP registry readiness
|
|
4
|
+
|
|
5
|
+
- NEW console script `tokenmizer-mcp` — runs the MCP stdio server directly
|
|
6
|
+
(no `python3 -m tokenmizer.mcp.server` incantation needed in client configs).
|
|
7
|
+
- README carries the `mcp-name` ownership marker required by the official
|
|
8
|
+
MCP registry's PyPI package validation.
|
|
9
|
+
|
|
10
|
+
## [0.2.5] — 2026-07-02 — restart no longer breaks checkpoints
|
|
11
|
+
|
|
12
|
+
### Critical
|
|
13
|
+
- **`graph_memory/graph.py`:** `_load()` rebuilt nodes/edges from SQLite
|
|
14
|
+
without converting `type`/`status` strings back to their str-Enum types.
|
|
15
|
+
Str-Enum equality kept passing (which hid the bug from every unit test),
|
|
16
|
+
but any `.value` access crashed — concretely, **after a server restart,
|
|
17
|
+
checkpointing any reloaded session returned HTTP 500**, and resume paths
|
|
18
|
+
touching `.value` were equally broken. Enums are now restored on load;
|
|
19
|
+
nodes/edges with unrecognized types (forward-compat) are skipped with a
|
|
20
|
+
warning instead of killing the whole load. Found by the new MCP e2e
|
|
21
|
+
check, not unit tests — reloaded graphs were never checkpoint-ed in tests
|
|
22
|
+
before. Four regression tests added.
|
|
23
|
+
|
|
24
|
+
### MCP server
|
|
25
|
+
- `serverInfo.version` no longer hardcoded (was stale "0.2.3").
|
|
26
|
+
- `resume_session` no longer claims "No checkpoint found" when a checkpoint
|
|
27
|
+
exists but its graph was empty — the two cases now produce distinct,
|
|
28
|
+
accurate messages.
|
|
29
|
+
- NEW `scripts/mcp_e2e_check.py`: boots the real proxy in-process, spawns
|
|
30
|
+
the real MCP stdio server, and exercises handshake + all 5 tools
|
|
31
|
+
end-to-end. This is the check that caught the restart bug.
|
|
32
|
+
|
|
3
33
|
## [0.2.4] — 2026-07-02 — launch-readiness fixes
|
|
4
34
|
|
|
5
35
|
### Critical — the endpoint did not work
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenmizer
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Reduce AI context loss by 2x. Graph-backed checkpoint and resume for any LLM session.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Shweta-Mishra-ai/tokenmizer
|
|
6
6
|
Project-URL: Repository, https://github.com/Shweta-Mishra-ai/tokenmizer
|
|
@@ -86,6 +86,8 @@ Description-Content-Type: text/markdown
|
|
|
86
86
|
</p>
|
|
87
87
|
|
|
88
88
|
<p>
|
|
89
|
+
<a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/v/tokenmizer?color=7c6af7&style=flat-square" alt="PyPI"/></a>
|
|
90
|
+
<a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/pyversions/tokenmizer?color=5ee7c8&style=flat-square"/></a>
|
|
89
91
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-4ade80?style=flat-square"/></a>
|
|
90
92
|
<a href="https://github.com/Shweta-Mishra-ai/tokenmizer/actions"><img src="https://img.shields.io/github/actions/workflow/status/Shweta-Mishra-ai/tokenmizer/ci.yml?branch=main&style=flat-square&color=4ade80"/></a>
|
|
91
93
|
<img src="https://img.shields.io/badge/Claude%20Code-Plugin-7c6af7?style=flat-square&logo=anthropic"/>
|
|
@@ -146,22 +148,16 @@ History is **never deleted**. "Why did we switch from React to Next.js?" — alw
|
|
|
146
148
|
|
|
147
149
|
### 1. Install
|
|
148
150
|
|
|
149
|
-
> Not yet published to PyPI — install from source (PyPI release is on the
|
|
150
|
-
> roadmap; the `pip install tokenmizer` short form will work after that).
|
|
151
|
-
|
|
152
151
|
```bash
|
|
153
|
-
git clone https://github.com/Shweta-Mishra-ai/tokenmizer
|
|
154
|
-
cd tokenmizer
|
|
155
|
-
|
|
156
152
|
# Recommended
|
|
157
|
-
pip install
|
|
153
|
+
pip install "tokenmizer[anthropic,cache]"
|
|
158
154
|
|
|
159
155
|
# All providers
|
|
160
|
-
pip install
|
|
156
|
+
pip install "tokenmizer[anthropic,openai,gemini,cohere,cache]"
|
|
161
157
|
|
|
162
158
|
# No key? Use Ollama (free, local)
|
|
163
159
|
brew install ollama && ollama pull llama3
|
|
164
|
-
pip install
|
|
160
|
+
pip install tokenmizer
|
|
165
161
|
```
|
|
166
162
|
|
|
167
163
|
### 2. Set your API key
|
|
@@ -231,20 +227,23 @@ Then use skills directly:
|
|
|
231
227
|
|
|
232
228
|
### Option B — MCP server
|
|
233
229
|
|
|
230
|
+
mcp-name: io.github.Shweta-Mishra-ai/tokenmizer
|
|
231
|
+
|
|
234
232
|
Add to `~/.claude/settings.json`:
|
|
235
233
|
|
|
236
234
|
```json
|
|
237
235
|
{
|
|
238
236
|
"mcpServers": {
|
|
239
237
|
"tokenmizer": {
|
|
240
|
-
"command": "
|
|
241
|
-
"args": ["-m", "tokenmizer.mcp.server"],
|
|
238
|
+
"command": "tokenmizer-mcp",
|
|
242
239
|
"env": { "TOKENMIZER_URL": "http://localhost:8000" }
|
|
243
240
|
}
|
|
244
241
|
}
|
|
245
242
|
}
|
|
246
243
|
```
|
|
247
244
|
|
|
245
|
+
(`tokenmizer-mcp` is installed with the package; `python3 -m tokenmizer.mcp.server` also works.)
|
|
246
|
+
|
|
248
247
|
---
|
|
249
248
|
|
|
250
249
|
## Other Tools
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p>
|
|
14
|
+
<a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/v/tokenmizer?color=7c6af7&style=flat-square" alt="PyPI"/></a>
|
|
15
|
+
<a href="https://pypi.org/project/tokenmizer"><img src="https://img.shields.io/pypi/pyversions/tokenmizer?color=5ee7c8&style=flat-square"/></a>
|
|
14
16
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-4ade80?style=flat-square"/></a>
|
|
15
17
|
<a href="https://github.com/Shweta-Mishra-ai/tokenmizer/actions"><img src="https://img.shields.io/github/actions/workflow/status/Shweta-Mishra-ai/tokenmizer/ci.yml?branch=main&style=flat-square&color=4ade80"/></a>
|
|
16
18
|
<img src="https://img.shields.io/badge/Claude%20Code-Plugin-7c6af7?style=flat-square&logo=anthropic"/>
|
|
@@ -71,22 +73,16 @@ History is **never deleted**. "Why did we switch from React to Next.js?" — alw
|
|
|
71
73
|
|
|
72
74
|
### 1. Install
|
|
73
75
|
|
|
74
|
-
> Not yet published to PyPI — install from source (PyPI release is on the
|
|
75
|
-
> roadmap; the `pip install tokenmizer` short form will work after that).
|
|
76
|
-
|
|
77
76
|
```bash
|
|
78
|
-
git clone https://github.com/Shweta-Mishra-ai/tokenmizer
|
|
79
|
-
cd tokenmizer
|
|
80
|
-
|
|
81
77
|
# Recommended
|
|
82
|
-
pip install
|
|
78
|
+
pip install "tokenmizer[anthropic,cache]"
|
|
83
79
|
|
|
84
80
|
# All providers
|
|
85
|
-
pip install
|
|
81
|
+
pip install "tokenmizer[anthropic,openai,gemini,cohere,cache]"
|
|
86
82
|
|
|
87
83
|
# No key? Use Ollama (free, local)
|
|
88
84
|
brew install ollama && ollama pull llama3
|
|
89
|
-
pip install
|
|
85
|
+
pip install tokenmizer
|
|
90
86
|
```
|
|
91
87
|
|
|
92
88
|
### 2. Set your API key
|
|
@@ -156,20 +152,23 @@ Then use skills directly:
|
|
|
156
152
|
|
|
157
153
|
### Option B — MCP server
|
|
158
154
|
|
|
155
|
+
mcp-name: io.github.Shweta-Mishra-ai/tokenmizer
|
|
156
|
+
|
|
159
157
|
Add to `~/.claude/settings.json`:
|
|
160
158
|
|
|
161
159
|
```json
|
|
162
160
|
{
|
|
163
161
|
"mcpServers": {
|
|
164
162
|
"tokenmizer": {
|
|
165
|
-
"command": "
|
|
166
|
-
"args": ["-m", "tokenmizer.mcp.server"],
|
|
163
|
+
"command": "tokenmizer-mcp",
|
|
167
164
|
"env": { "TOKENMIZER_URL": "http://localhost:8000" }
|
|
168
165
|
}
|
|
169
166
|
}
|
|
170
167
|
}
|
|
171
168
|
```
|
|
172
169
|
|
|
170
|
+
(`tokenmizer-mcp` is installed with the package; `python3 -m tokenmizer.mcp.server` also works.)
|
|
171
|
+
|
|
173
172
|
---
|
|
174
173
|
|
|
175
174
|
## Other Tools
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tokenmizer"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "Reduce AI context loss by 2x. Graph-backed checkpoint and resume for any LLM session."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -73,6 +73,7 @@ all = [
|
|
|
73
73
|
|
|
74
74
|
[project.scripts]
|
|
75
75
|
tokenmizer = "tokenmizer.cli:app"
|
|
76
|
+
tokenmizer-mcp = "tokenmizer.mcp.server:run_stdio_server"
|
|
76
77
|
|
|
77
78
|
[project.urls]
|
|
78
79
|
Homepage = "https://github.com/Shweta-Mishra-ai/tokenmizer"
|
|
@@ -108,6 +109,4 @@ omit = [
|
|
|
108
109
|
[tool.coverage.report]
|
|
109
110
|
# Honest floor over ALL product code (previously 70, but with api/app.py,
|
|
110
111
|
# auth.py, providers and other core modules excluded — coverage theater).
|
|
111
|
-
# Single source of truth: CI
|
|
112
|
-
fail_under = 50
|
|
113
|
-
show_missing = true
|
|
112
|
+
# Single source of truth: CI m
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP server end-to-end check.
|
|
3
|
+
|
|
4
|
+
Spawns the real MCP stdio server as a subprocess, performs the MCP
|
|
5
|
+
handshake (initialize → initialized → tools/list), then exercises real
|
|
6
|
+
tool calls against a running TokenMizer proxy (TOKENMIZER_URL) and a
|
|
7
|
+
local file analysis (no proxy needed).
|
|
8
|
+
|
|
9
|
+
Run: python scripts/mcp_e2e_check.py
|
|
10
|
+
Exit code 0 = all checks passed.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
20
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
|
|
23
|
+
FAILURES: list[str] = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check(name: str, cond: bool, detail: str = ""):
|
|
27
|
+
status = "PASS" if cond else "FAIL"
|
|
28
|
+
print(f"[{status}] {name}" + (f" — {detail}" if detail and not cond else ""))
|
|
29
|
+
if not cond:
|
|
30
|
+
FAILURES.append(name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _start_proxy_in_thread(port: int) -> None:
|
|
34
|
+
"""Run the real FastAPI app via uvicorn in a daemon thread."""
|
|
35
|
+
import threading
|
|
36
|
+
import time
|
|
37
|
+
|
|
38
|
+
import uvicorn
|
|
39
|
+
|
|
40
|
+
from tokenmizer.api.app import app
|
|
41
|
+
|
|
42
|
+
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
|
|
43
|
+
server = uvicorn.Server(config)
|
|
44
|
+
t = threading.Thread(target=server.run, daemon=True)
|
|
45
|
+
t.start()
|
|
46
|
+
for _ in range(50): # wait up to 5s for startup
|
|
47
|
+
if server.started:
|
|
48
|
+
return
|
|
49
|
+
time.sleep(0.1)
|
|
50
|
+
raise RuntimeError("proxy did not start within 5s")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _seed_session_graph(session_id: str) -> None:
|
|
54
|
+
"""Put real nodes into the session graph BEFORE the proxy starts, using
|
|
55
|
+
the same storage dir the proxy will read — so checkpoint/resume exercise
|
|
56
|
+
the full real pipeline instead of an empty graph."""
|
|
57
|
+
from tokenmizer.config.settings import get_settings
|
|
58
|
+
from tokenmizer.graph_memory.graph import GraphMemory
|
|
59
|
+
|
|
60
|
+
storage = get_settings().graph_checkpoint.storage_dir
|
|
61
|
+
g = GraphMemory(session_id, storage_dir=storage)
|
|
62
|
+
g.extract_from_messages([
|
|
63
|
+
{"role": "user", "content": "Let's build a FastAPI auth service with JWT and PostgreSQL"},
|
|
64
|
+
{"role": "assistant", "content": "Decided: PostgreSQL for storage. Completed: project setup. Working on: login endpoint in api/auth.py"},
|
|
65
|
+
], incremental=False)
|
|
66
|
+
g._persist(force=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main() -> int:
|
|
70
|
+
port = 8765
|
|
71
|
+
_seed_session_graph("mcp-e2e-test")
|
|
72
|
+
_start_proxy_in_thread(port)
|
|
73
|
+
os.environ["TOKENMIZER_URL"] = f"http://127.0.0.1:{port}"
|
|
74
|
+
print(f"proxy up on :{port}")
|
|
75
|
+
|
|
76
|
+
proc = subprocess.Popen(
|
|
77
|
+
[sys.executable, "-m", "tokenmizer.mcp.server"],
|
|
78
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
79
|
+
text=True, encoding="utf-8",
|
|
80
|
+
env={**os.environ},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def rpc(method: str, params: dict | None = None, req_id: int | None = 1):
|
|
84
|
+
msg: dict = {"jsonrpc": "2.0", "method": method}
|
|
85
|
+
if req_id is not None:
|
|
86
|
+
msg["id"] = req_id
|
|
87
|
+
if params is not None:
|
|
88
|
+
msg["params"] = params
|
|
89
|
+
proc.stdin.write(json.dumps(msg) + "\n")
|
|
90
|
+
proc.stdin.flush()
|
|
91
|
+
if req_id is None:
|
|
92
|
+
return None
|
|
93
|
+
line = proc.stdout.readline()
|
|
94
|
+
return json.loads(line)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# 1. Handshake
|
|
98
|
+
r = rpc("initialize", {"protocolVersion": "2024-11-05",
|
|
99
|
+
"capabilities": {}, "clientInfo": {"name": "e2e", "version": "0"}})
|
|
100
|
+
check("initialize returns serverInfo",
|
|
101
|
+
r and r.get("result", {}).get("serverInfo", {}).get("name") == "tokenmizer")
|
|
102
|
+
check("initialize reports current version",
|
|
103
|
+
r.get("result", {}).get("serverInfo", {}).get("version") not in ("", "0.2.3"),
|
|
104
|
+
str(r.get("result", {}).get("serverInfo")))
|
|
105
|
+
rpc("notifications/initialized", req_id=None)
|
|
106
|
+
|
|
107
|
+
# 2. tools/list
|
|
108
|
+
r = rpc("tools/list", req_id=2)
|
|
109
|
+
tools = {t["name"] for t in r.get("result", {}).get("tools", [])}
|
|
110
|
+
expected = {"checkpoint_session", "resume_session", "get_graph_stats",
|
|
111
|
+
"analyze_file", "get_savings_stats"}
|
|
112
|
+
check("tools/list exposes all 5 tools", tools == expected, str(tools))
|
|
113
|
+
|
|
114
|
+
# 3. Local tool (no proxy): analyze_file on a real CSV
|
|
115
|
+
with tempfile.NamedTemporaryFile("w", suffix=".csv", delete=False,
|
|
116
|
+
encoding="utf-8") as f:
|
|
117
|
+
f.write("region,revenue\nnorth,100\nsouth,250\neast,90\nwest,410\n")
|
|
118
|
+
csv_path = f.name
|
|
119
|
+
r = rpc("tools/call", {"name": "analyze_file",
|
|
120
|
+
"arguments": {"file_path": csv_path}}, req_id=3)
|
|
121
|
+
text = r.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
122
|
+
check("analyze_file returns analysis", "File Analysis" in text, text[:120])
|
|
123
|
+
os.unlink(csv_path)
|
|
124
|
+
|
|
125
|
+
# 4. Proxy-backed tools (require TOKENMIZER_URL server running)
|
|
126
|
+
r = rpc("tools/call", {"name": "get_savings_stats", "arguments": {}}, req_id=4)
|
|
127
|
+
text = r.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
128
|
+
check("get_savings_stats reaches proxy", "Savings Report" in text, text[:120])
|
|
129
|
+
|
|
130
|
+
r = rpc("tools/call", {"name": "checkpoint_session",
|
|
131
|
+
"arguments": {"session_id": "mcp-e2e-test"}}, req_id=5)
|
|
132
|
+
text = r.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
133
|
+
check("checkpoint_session creates checkpoint", "checkpointed" in text, text[:160])
|
|
134
|
+
|
|
135
|
+
r = rpc("tools/call", {"name": "resume_session",
|
|
136
|
+
"arguments": {"session_id": "mcp-e2e-test"}}, req_id=6)
|
|
137
|
+
text = r.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
138
|
+
check("resume_session returns context", "TokenMizer Resume" in text, text[:160])
|
|
139
|
+
|
|
140
|
+
# 5. Unknown method → JSON-RPC error, not crash
|
|
141
|
+
r = rpc("bogus/method", {}, req_id=7)
|
|
142
|
+
check("unknown method returns -32601 error",
|
|
143
|
+
r.get("error", {}).get("code") == -32601)
|
|
144
|
+
|
|
145
|
+
finally:
|
|
146
|
+
proc.stdin.close()
|
|
147
|
+
proc.terminate()
|
|
148
|
+
|
|
149
|
+
print()
|
|
150
|
+
if FAILURES:
|
|
151
|
+
print(f"E2E RESULT: {len(FAILURES)} FAILURE(S): {FAILURES}")
|
|
152
|
+
return 1
|
|
153
|
+
print("E2E RESULT: ALL PASS")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
sys.exit(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.Shweta-Mishra-ai/tokenmizer",
|
|
4
|
+
"description": "An MCP server that provides [describe what your server does]",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/Shweta-Mishra-ai/tokenmizer",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "pypi",
|
|
13
|
+
"identifier": "tokenmizer\"\r",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{
|
|
20
|
+
"description": "Your API key for the service",
|
|
21
|
+
"isRequired": true,
|
|
22
|
+
"format": "string",
|
|
23
|
+
"isSecret": true,
|
|
24
|
+
"name": "YOUR_API_KEY"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -198,3 +198,87 @@ class TestDirectMutationRequiresForce:
|
|
|
198
198
|
"force=True is required after any direct node mutation, or the "
|
|
199
199
|
"dirty-flag optimization silently drops the write"
|
|
200
200
|
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestEnumRestorationOnLoad:
|
|
204
|
+
"""
|
|
205
|
+
Regression tests for the restart-breaks-everything bug (v0.2.4 e2e find):
|
|
206
|
+
|
|
207
|
+
asdict() serializes NodeType/NodeStatus/EdgeType (str-Enums) to plain
|
|
208
|
+
strings; _load() rebuilt nodes WITHOUT converting back. str-Enum equality
|
|
209
|
+
(`n.type == NodeType.TASK`) still passed on reloaded graphs — hiding the
|
|
210
|
+
bug from every existing test — but `.type.value` crashed with
|
|
211
|
+
"'str' object has no attribute 'value'". Net effect: after any server
|
|
212
|
+
restart, checkpointing a reloaded session returned HTTP 500.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def test_reloaded_node_types_are_real_enums(self, graph, tmp_path):
|
|
216
|
+
graph.add_node(NodeType.DECISION, "Use PostgreSQL", status=NodeStatus.COMPLETED)
|
|
217
|
+
graph._persist(force=True)
|
|
218
|
+
|
|
219
|
+
reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
|
|
220
|
+
node = next(iter(reloaded._nodes.values()))
|
|
221
|
+
assert isinstance(node.type, NodeType), type(node.type)
|
|
222
|
+
assert isinstance(node.status, NodeStatus), type(node.status)
|
|
223
|
+
# The exact access pattern that crashed checkpoint creation:
|
|
224
|
+
assert node.type.value == "decision"
|
|
225
|
+
assert node.status.value == "completed"
|
|
226
|
+
|
|
227
|
+
def test_reloaded_edge_types_are_real_enums(self, graph, tmp_path):
|
|
228
|
+
n1 = graph.add_node(NodeType.TASK, "Implement login")
|
|
229
|
+
n2 = graph.add_node(NodeType.FILE, "src/auth.py")
|
|
230
|
+
graph.add_edge(n1, n2, EdgeType.IMPLEMENTS)
|
|
231
|
+
graph._persist(force=True)
|
|
232
|
+
|
|
233
|
+
reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
|
|
234
|
+
assert reloaded._edges, "edge did not survive reload"
|
|
235
|
+
assert isinstance(reloaded._edges[0].type, EdgeType)
|
|
236
|
+
assert reloaded._edges[0].type.value == "implements"
|
|
237
|
+
|
|
238
|
+
def test_checkpoint_of_reloaded_graph_does_not_crash(self, graph, tmp_path):
|
|
239
|
+
"""Full restart simulation: persist -> fresh GraphMemory ->
|
|
240
|
+
CheckpointManager.create() — the exact path that 500'd."""
|
|
241
|
+
from tokenmizer.checkpoints.manager import CheckpointManager
|
|
242
|
+
|
|
243
|
+
graph.add_node(NodeType.GOAL, "Build auth service")
|
|
244
|
+
graph.add_node(NodeType.DECISION, "Use PostgreSQL",
|
|
245
|
+
status=NodeStatus.COMPLETED)
|
|
246
|
+
graph._persist(force=True)
|
|
247
|
+
|
|
248
|
+
reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
|
|
249
|
+
mgr = CheckpointManager(storage_dir=str(tmp_path))
|
|
250
|
+
ckpt = mgr.create(
|
|
251
|
+
session_id="dirty-flag-test-session",
|
|
252
|
+
messages=[], graph=reloaded, context_pct=0.5, trigger="manual",
|
|
253
|
+
)
|
|
254
|
+
assert ckpt.checkpoint_id
|
|
255
|
+
snapshot_types = {n["type"] for n in ckpt.graph_snapshot["nodes"]}
|
|
256
|
+
assert "decision" in snapshot_types
|
|
257
|
+
|
|
258
|
+
def test_unknown_node_type_is_skipped_not_fatal(self, graph, tmp_path):
|
|
259
|
+
"""Forward-compat: a node with an unrecognized type (e.g. written by
|
|
260
|
+
a newer version) must not take down the whole graph load."""
|
|
261
|
+
import json as _json
|
|
262
|
+
import sqlite3 as _sqlite3
|
|
263
|
+
|
|
264
|
+
graph.add_node(NodeType.TASK, "Implement login")
|
|
265
|
+
graph._persist(force=True)
|
|
266
|
+
|
|
267
|
+
# Corrupt one node's type directly in SQLite
|
|
268
|
+
conn = _sqlite3.connect(str(graph._db_path))
|
|
269
|
+
row = conn.execute(
|
|
270
|
+
"SELECT nodes_json FROM graphs WHERE session_id=?",
|
|
271
|
+
("dirty-flag-test-session",),
|
|
272
|
+
).fetchone()
|
|
273
|
+
nodes = _json.loads(row[0])
|
|
274
|
+
nodes.append({**nodes[0], "id": "corrupt01", "type": "from_the_future"})
|
|
275
|
+
conn.execute(
|
|
276
|
+
"UPDATE graphs SET nodes_json=? WHERE session_id=?",
|
|
277
|
+
(_json.dumps(nodes), "dirty-flag-test-session"),
|
|
278
|
+
)
|
|
279
|
+
conn.commit()
|
|
280
|
+
conn.close()
|
|
281
|
+
|
|
282
|
+
reloaded = GraphMemory("dirty-flag-test-session", storage_dir=str(tmp_path))
|
|
283
|
+
assert "corrupt01" not in reloaded._nodes # bad node skipped
|
|
284
|
+
assert len(reloaded._nodes) == 1 # good node survived
|
|
@@ -315,11 +315,37 @@ class GraphMemory:
|
|
|
315
315
|
)
|
|
316
316
|
self._processed_hashes = set()
|
|
317
317
|
|
|
318
|
+
# CRITICAL: restore enum types on load. asdict() serializes
|
|
319
|
+
# NodeType/NodeStatus (str-Enums) to plain strings; without
|
|
320
|
+
# converting back, every reloaded node has type/status as `str`.
|
|
321
|
+
# Because they're str-Enums, equality checks still pass — which
|
|
322
|
+
# HID this bug — but any `.value` access crashes ('str' object
|
|
323
|
+
# has no attribute 'value'). Concretely: after a server restart,
|
|
324
|
+
# every checkpoint of a reloaded session returned HTTP 500.
|
|
325
|
+
# Found by the MCP e2e check, not by unit tests, because unit
|
|
326
|
+
# tests reloaded graphs but never then called `.type.value`.
|
|
318
327
|
for nd in nodes_data:
|
|
319
328
|
nd.pop("_evicted", None)
|
|
329
|
+
try:
|
|
330
|
+
nd["type"] = NodeType(nd["type"])
|
|
331
|
+
nd["status"] = NodeStatus(nd["status"])
|
|
332
|
+
except (ValueError, KeyError) as conv_err:
|
|
333
|
+
logger.warning(
|
|
334
|
+
f"Skipping node with unknown type/status during load "
|
|
335
|
+
f"for {self.session_id}: {conv_err} — {nd.get('id')}"
|
|
336
|
+
)
|
|
337
|
+
continue
|
|
320
338
|
n = MemoryNode(**{k: v for k, v in nd.items() if k != "_evicted"})
|
|
321
339
|
self._nodes[n.id] = n
|
|
322
340
|
for ed in edges_data:
|
|
341
|
+
try:
|
|
342
|
+
ed["type"] = EdgeType(ed["type"])
|
|
343
|
+
except (ValueError, KeyError) as conv_err:
|
|
344
|
+
logger.warning(
|
|
345
|
+
f"Skipping edge with unknown type during load "
|
|
346
|
+
f"for {self.session_id}: {conv_err}"
|
|
347
|
+
)
|
|
348
|
+
continue
|
|
323
349
|
self._edges.append(MemoryEdge(**ed))
|
|
324
350
|
except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
|
|
325
351
|
logger.warning(f"Corrupted DB for {self.session_id} — starting fresh: {e}")
|
|
@@ -207,7 +207,16 @@ def handle_resume_session(args: dict) -> str:
|
|
|
207
207
|
ctx = result.get("resume_context", "")
|
|
208
208
|
tokens = result.get("token_count", 0)
|
|
209
209
|
if not ctx:
|
|
210
|
-
|
|
210
|
+
# A checkpoint EXISTS (the API 404s via "error" above when none does) —
|
|
211
|
+
# its graph was just empty at checkpoint time. Saying "no checkpoint
|
|
212
|
+
# found" here would be wrong and send the user debugging the wrong
|
|
213
|
+
# thing.
|
|
214
|
+
return (
|
|
215
|
+
f"Checkpoint {result.get('checkpoint_id', '?')} exists for "
|
|
216
|
+
f"'{session_id}' but its graph was empty (no session activity "
|
|
217
|
+
f"had been recorded when it was created). Chat through the proxy "
|
|
218
|
+
f"with this session_id, then checkpoint again."
|
|
219
|
+
)
|
|
211
220
|
return (
|
|
212
221
|
f"[TokenMizer Resume — session: {session_id} — {tokens} tokens]\n\n"
|
|
213
222
|
f"{ctx}\n\n"
|
|
@@ -333,12 +342,13 @@ def run_stdio_server():
|
|
|
333
342
|
params = req.get("params", {})
|
|
334
343
|
|
|
335
344
|
if method == "initialize":
|
|
345
|
+
from tokenmizer import __version__
|
|
336
346
|
send({
|
|
337
347
|
"jsonrpc": "2.0", "id": req_id,
|
|
338
348
|
"result": {
|
|
339
349
|
"protocolVersion": "2024-11-05",
|
|
340
350
|
"capabilities": {"tools": {}},
|
|
341
|
-
"serverInfo": {"name": "tokenmizer", "version":
|
|
351
|
+
"serverInfo": {"name": "tokenmizer", "version": __version__},
|
|
342
352
|
},
|
|
343
353
|
})
|
|
344
354
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|