Releases: fuwasegu/exocortex
Release list
1.2.1
Fixes a regression in 1.2.0 that broke stdio proxy mode. Closes #30.
Who this affects
Anyone running --mode proxy (the recommended setup for Cursor and for multiple editor instances) on 1.2.0. Cursor showed the server as errored and no exo_* tool reached the agent:
Invalid input: expected object, received undefined
path: ["tools", N, "inputSchema"]
Direct --mode server was never affected — the SSE server always emitted correct MCP. Only the proxy corrupted the payload on its way to stdout.
Upgrade to 1.2.1. In proxy mode the server restarts itself on the version change.
What went wrong
The proxy re-serializes results from the SSE server before writing them to stdout. Under mcp 1.x the Pydantic field names were the protocol names, so a plain model_dump() happened to produce valid MCP. mcp 2.0 renamed the fields to snake_case and moved the protocol names to aliases:
| mcp 1.x | mcp 2.0 | |
|---|---|---|
| field | inputSchema |
input_schema (alias inputSchema) |
plain model_dump() |
inputSchema ✅ |
input_schema ❌ |
So the proxy started emitting a key the MCP schema does not define, and strict clients rejected the entire response. This came in with the mcp 2.x migration in 1.2.0.
inputSchema was not the only casualty — outputSchema, mimeType and _meta are aliased too, and were equally broken.
Serialization now goes through a single helper that applies by_alias=True, so no branch can omit it.
Why it was not caught
The 1.2.0 migration verified the server end thoroughly — tool registration, input schemas, concurrency, ASGI mounts — but nothing covered the proxy's re-serialization, which is the only place SDK models are re-dumped by hand. That gap is now closed by tests asserting the general rule: no field may be sent under its Python name when it has a differing wire alias. That will catch the next rename, not just this one.
Verification
Against a real stdio proxy bridging a live SSE server: all 19 tools carry camelCase inputSchema, none leak input_schema. 340 tests pass on Python 3.10, 3.11 and 3.12.
Thanks to @novr for the diagnosis, the exact root cause, and a working patch.
Full Changelog: 1.2.0...1.2.1
1.2.0
Migrates to the mcp 2.x server API. Closes #27.
Upgrading
Nothing to do — the MCP tools and their arguments are unchanged. In proxy mode the server restarts itself on the version change.
mcp>=2.0.0,<3 is now required. 1.1.0 capped mcp at <2 as a stopgap because mcp 2.0 removed mcp.server.fastmcp; this release moves to the new API properly.
Slow tools no longer block the server
mcp 1.x invoked sync tool functions directly on the asyncio event loop, one at a time, which is why a single slow operation could stall everything — the complaint behind #25. mcp 2.x dispatches them on worker threads, so tools run in parallel:
pings served while a recall was in flight: 6244
That property is now structural rather than something Exocortex has to maintain.
The part that needed care
Parallel dispatch made several check-then-write guards reachable for the first time. exo_link_memories reads "does this link already exist?" before creating it; under 1.x nothing could interleave between those two statements, under 2.x it can. Eight concurrent identical calls produced four duplicate links.
Database-touching tools are now serialized with an operation-level lock, restoring the guarantee those guards were written against while leaving the event loop free. KùzuDB permits a single writer anyway, so little is given up. exo_ping is deliberately excluded — it never touches the database, and involving the manager would load the embedding model just to answer a health check.
Verified after the fix: eight concurrent exo_link_memories → exactly one link.
Reproducible builds
uv.lock is now committed, and mcp carries a <3 cap.
An unpinned dependency with no lockfile is what let an upstream major release break every fresh install and all of CI with no change in this repository — and it went unnoticed because main had not run CI in months. The lock also pins ruff, whose newer releases began formatting Python inside Markdown code fences and failing lint on untouched files.
The lock covers CI and local development; uvx --from git+... resolves from package metadata, so the version specifiers are what protect end users from the next major bump.
Verification
331 tests pass on Python 3.10, 3.11 and 3.12 against mcp 2.0.0. The new concurrency guard was mutation-tested: removing the serialization makes it fail with six duplicate links.
Full Changelog: 1.1.0...1.2.0
1.1.0
Fixes a bug where writing a single memory could freeze the server for minutes and grow the database to hundreds of megabytes. Closes #25.
Upgrading
If your database directory is far larger than the data it holds, it was bloated by the old write path. Fixing the cause does not shrink an existing database — rebuild it once:
uvx --from git+https://github.com/fuwasegu/exocortex exocortex --mode compactOr ask your assistant to call the new exo_compact tool. Your previous database is kept under ~/.exocortex/backups/.
The bug
exo_store_memory opened and closed a kuzu.Database for every write query, not once per write. Closing one runs a full synchronous StorageManager::checkpoint(), so a single store with five tags paid 13 checkpoints. Because MCP tools run directly on the event loop, that froze every other request — exo_ping included.
The RELATED_TO.reason corruption reported in #25 turned out to be the same bug, not a second one: every checkpoint rewrote relationship column chunks without reclaiming pages. Identical 700-relationship inserts:
| on-disk size | time | |
|---|---|---|
| before | 265 MiB | 78.7 s |
| after | 5.6 MiB | 6.2 s |
47× the disk from the same data. Once a column chunk outgrew the buffer pool, reads of it failed outright with Buffer manager exception: Unable to allocate memory!.
Results
| before | after | |
|---|---|---|
store_memory (5 tags) |
1513 ms, 13 DB opens | 209 ms, 1 open |
store_memory (no tags) |
314 ms, 3 opens | 24 ms, 0 opens |
link_memories |
153 ms, 2 opens | 12 ms, 0 opens |
| 700 relationships | 115.6 s, 280 MiB | 6.9 s, 13.9 MiB |
| test suite | 371 s | 70 s |
What's new
exo_compact/--mode compact— rebuilds a bloated database and reclaims the space. Relationship text that is already unreadable cannot be recovered; those columns are reported and rebuilt empty so the rest of the graph survives.Memorycontent is never dropped — compaction fails loudly rather than discarding memories.exo_sleepnow works in proxy mode. The server hands its database lock over before spawning the dream worker. This was previously documented as unsupported; the cause was the server holding a long-lived read-only database, which blocks writers.--mode dashboardopens read-only, so the standalone dashboard shares the database instead of competing for the exclusive lock.- New settings:
EXOCORTEX_DB_IDLE_TIMEOUT(default 5s — how long the database may sit idle before the lock is handed back) andEXOCORTEX_READ_ONLY.
Also in this release
mcpis now capped at<2. mcp 2.0.0 removedmcp.server.fastmcp, which made a fresh install fail to import entirely. Migration to the 2.x API is tracked in #27.- Fixes found by an adversarial review pass before merge: a stale connection could reopen a database the manager no longer tracked and leak its file lock; a lazy query result could be closed mid-consumption; compaction could leak the source handle and swallow a failed rollback.
Full Changelog: 1.0.1...1.1.0