Skip to content

fix(mcp): cross-process catalog write safety via optimistic concurrency#38

Merged
StefanSteiner merged 5 commits into
tableau:mainfrom
StefanSteiner:ssteiner/mcp-fix-race-condition
May 26, 2026
Merged

fix(mcp): cross-process catalog write safety via optimistic concurrency#38
StefanSteiner merged 5 commits into
tableau:mainfrom
StefanSteiner:ssteiner/mcp-fix-race-condition

Conversation

@StefanSteiner

@StefanSteiner StefanSteiner commented May 26, 2026

Copy link
Copy Markdown
Contributor

Title

fix(mcp): cross-process catalog safety, provenance tracking, and queryable-memory framing

Body

Summary

  • Replaces the DELETE+INSERT-in-transaction _table_catalog upsert with an optimistic UPDATE-then-conditional-INSERT pattern that is safe across multiple MCP server processes sharing the same hyperd via the daemon.
  • Adds provenance tracking (created_by / last_modified_by) to _table_catalog so users can see which MCP client created or last modified a table.
  • Reframes the persistent database as "queryable long-term memory" in both the LLM-facing README and the user-facing README, highlighting it as a differentiation feature.
  • Bumps CI test timeouts for flaky macOS TestDaemon startup.

Motivation

Optimistic concurrency: PR #26 introduced the single-instance daemon so multiple MCP clients share one hyperd. The old DELETE+INSERT upsert could interleave across processes, producing duplicate catalog rows. The new pattern eliminates this race without adding infrastructure.

Provenance: When multiple AI clients share a persistent database, users need to answer "who created this table?" or "who last updated it?" The initialize MCP handshake provides client_info.name + version which we now capture and store.

Queryable memory: Unlike flat-text memory, persistent data is queryable with SQL. LLMs can JOIN, filter, aggregate, and reason over stored context. The README changes make this capability front-and-center so LLMs naturally use it.

What changed

File Change
hyperdb-mcp/src/table_catalog.rs Optimistic upsert (UPDATE + conditional INSERT). Two new columns (created_by, last_modified_by). Schema migration via ALTER TABLE ADD COLUMN. use std::fmt::Write.
hyperdb-mcp/src/server.rs initialize override captures client_info into client_name field. after_ingest_catalog_update passes client name through. Removed stale #[expect(clippy::unused_self)].
hyperdb-mcp/src/watcher.rs Pass None for client_name (watchers have no client context).
hyperdb-mcp/src/readme.rs Reframed "Workspace model — queryable memory" section with trigger phrases and cross-reference examples.
hyperdb-mcp/README.md Added "Queryable Memory for AI" section, updated features list.
hyperdb-mcp/CHANGELOG.md New entries for optimistic concurrency and provenance.
hyperdb-mcp/tests/daemon_tests.rs TestDaemon timeout bumped to 150s (exceeds inner 60s callback timeout).
hyperdb-mcp/tests/per_tool_database_tests.rs Updated upsert_stub_in call sites for new 8th argument.

Test plan

  • cargo fmt --check && cargo clippy --workspace --tests -- -D warnings — clean
  • cargo test -p hyperdb-mcp — all green (195 tests)
  • cargo test -p hyperdb-mcp --test per_tool_database_tests — 30 pass
  • cargo test -p hyperdb-mcp --test end_to_end_mcp_tests — 10 pass
  • Verified provenance columns appear in describe output via live MCP session
  • Verified schema migration (ALTER TABLE ADD COLUMN) is idempotent on existing catalogs
  • Verified INSERT ... WHERE NOT EXISTS returns affected_rows: 0 on duplicate (no error)

Replace DELETE+INSERT-in-transaction upsert with UPDATE-then-conditional-INSERT.
Each statement is individually atomic at the hyperd level, preventing duplicate
_table_catalog rows when multiple MCP servers share one hyperd via the daemon.
Update both the LLM-facing readme (get_readme tool) and the user-facing
README.md to position the persistent database as structured, queryable
memory for AI — differentiating from flat-text memory systems by
emphasizing JOIN/filter/aggregate capabilities across sessions.
The macOS CI runner under load can exceed 60s for hyperd startup when
34 other tests run in the same suite. 90s gives HyperProcess::new's
internal 30s callback timeout ample room to surface real errors before
the outer assertion fires.
…d_by)

Capture which MCP client created and last modified each table catalog
entry. The `initialize` handshake provides `client_info.name` and
`version` which are stored as a combined label (e.g. "Claude Code 1.0.32").

- `created_by` is set once on INSERT, never overwritten
- `last_modified_by` updates on every explicit ingest (last writer wins)
- Reconciliation and watchers pass None to avoid clobbering provenance
- Schema migration via ALTER TABLE ADD COLUMN for pre-existing catalogs
…timeout

The internal `wait_for_callback` in HyperProcess::new has a 60s
timeout. On slow macOS CI runners, spawn latency can push the total
blocking time past the previous 90s outer limit, causing the test to
panic with a generic message instead of surfacing the real error from
the daemon thread. 150s ensures the daemon thread always has time to
finish and report the actual failure cause.
@StefanSteiner StefanSteiner merged commit 54e3f18 into tableau:main May 26, 2026
10 checks passed
This was referenced May 26, 2026
@StefanSteiner StefanSteiner deleted the ssteiner/mcp-fix-race-condition branch June 4, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant