Skip to content

Commit 5042d80

Browse files
docs(mcp): surface KV store on crates.io README + fix error mapping (follow-up to #185) (#188)
* fix(mcp): map caller-fixable identifier errors to INVALID_ARGUMENT An invalid KV `store`/`key` (a disallowed byte or over the 512-byte limit) surfaced from the hyperdb-api layer as `Error::InvalidName` and fell through the catch-all arm to `INTERNAL_ERROR` — telling an LLM caller "server bug, give up" for something it supplied and can fix. Map `InvalidName` and `InvalidTableDefinition` to `INVALID_ARGUMENT`, which carries a self-correction suggestion; the human-readable message naming the offending byte or length is unchanged. `InvalidOperation` is deliberately left out: it is hyperdb-api caller-API misuse where the caller is this MCP's own Rust code, not the LLM, so it correctly stays `INTERNAL_ERROR`. A regression test locks in that distinction. Caught during the KV MCP smoke run. * docs(mcp): add on-demand KV smoke-test guide Capture the manual, tool-driven smoke tests that exercise the eight `kv_*` tools against a live MCP server: CRUD/upsert, listing and discovery, value fidelity, destructive semantics, input validation (now INVALID_ARGUMENT), read-only mode, database routing/isolation, the LEFT JOIN enrichment pattern, the hidden backing table, and single-process atomicity. Includes a safety section: the persistent database may hold real data, so scratch names are `smoke_`-prefixed and every run ends by verifying the persistent DB is untouched. The existence-check safety rule is scoped to the target database — a bare `describe` inspects only the ephemeral primary and `status` never lists table names, so neither alone protects a persistent write. Cross-linked from DEVELOPMENT.md's Test section. * docs(mcp): surface the KV store across all doc surfaces The key-value scratchpad tools shipped without documentation in the user-facing MCP README, the LLM-facing get_readme, or the workspace READMEs. Add coverage in the right places so an LLM told "remember X" considers kv_set instead of always reaching for CREATE TABLE: - hyperdb-mcp/README.md: new "### Key-Value Store" tool section; a "Table or key-value store?" decision rule in Queryable Memory for AI; the hyper://schema/kv Resources-table row; KV tools in the Features bullet; and the KV mutator/reader split across all three read-only enumerations (flag table, Allowed, Blocked). - src/readme.rs (get_readme): "KV store vs. a custom table" decision subsection; kv_pop lexicographic-order clarification; a describe persistent-DB signpost (status reports counts, not names); a back-link completing the bidirectional cross-link. - src/server.rs: kv_pop and kv_list_stores #[tool(description)] strings clarify lexicographic pop order and the no-store-registry behavior. - README.md (top-level): KvStore / AsyncKvStore Key Features bullet. Also fix a pre-existing false claim on the --read-only flag-table row (carried forward on a line this change edited): Hyper-format export is NOT disabled in read-only mode (export has no writable gate; it's a read-only file copy), matching the Read-Only "Allowed" list and the HyperMcpServer::new doc comment.
1 parent 18dc014 commit 5042d80

9 files changed

Lines changed: 525 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ database files (`.hyper`) without any C library dependencies.
3333
- **Typed Row Mapping**`#[derive(FromRow)]` structs, including streaming `stream_as` for constant-memory typed queries
3434
- **Compile-time SQL Validation** — opt-in `query_as!` macro checks SQL against your schema at build time (red squigglies in VS Code)
3535
- **Connection Pooling** — async pooling via `deadpool` for high-concurrency applications
36+
- **Key-Value Store** — string-native `KvStore` / `AsyncKvStore` backed by a single fixed table
3637
- **Arrow Integration** — insert and read data in Arrow IPC stream format
3738
- **gRPC Transport** — read-only access with Arrow IPC and load balancing support
3839
- **Full Type Support** — all Hyper types including Numeric, Geography, Intervals

hyperdb-mcp/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
2727

2828
### Fixed
2929

30+
- **Caller-fixable argument errors now return `INVALID_ARGUMENT`, not
31+
`INTERNAL_ERROR`.** An invalid identifier from the `hyperdb-api` layer —
32+
such as a KV `store`/`key` containing a disallowed byte or exceeding the
33+
512-byte limit — previously fell through to the catch-all `INTERNAL_ERROR`
34+
code, mislabeling a validation failure the caller can fix as a server-side
35+
bug. These (`InvalidName`, `InvalidTableDefinition`) now map to
36+
`INVALID_ARGUMENT`, which carries a self-correction suggestion. The
37+
human-readable message (naming the offending byte or the length) is
38+
unchanged.
39+
- **README `--read-only` flag table no longer claims Hyper-format export is
40+
disabled.** The `--read-only` row wrongly listed "Hyper-format export" among
41+
the disabled operations, contradicting both the actual behavior (`export`
42+
has no read-only gate — a `.hyper` export is a harmless read-only file copy
43+
and stays allowed) and the same document's Read-Only Mode "Allowed" list.
44+
Docs-only correction; no behavior change.
3045
- **TCP keepalive on the `hyperd` connection.** Connections to `hyperd` now
3146
enable TCP keepalive (60s idle, 10s interval, ~90s to declare a dead peer)
3247
instead of relying on the OS default 2-hour idle timeout. Without it, a

hyperdb-mcp/DEVELOPMENT.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ cargo test -p hyperdb-mcp
128128

129129
Most tests require a running `hyperd`. Set `HYPERD_PATH` before running.
130130

131+
For manual, tool-driven checks against a **live** MCP server (useful after a
132+
build or before a release — they exercise real session state the unit tests
133+
don't), see [SMOKE_TESTS.md](SMOKE_TESTS.md).
134+
131135
### Lint
132136

133137
```bash

hyperdb-mcp/README.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This means an LLM can:
2525

2626
The ephemeral database is scratch space (think: a whiteboard). The persistent database is long-term memory (think: a filing cabinet you can query). Multiple AI clients sharing the same daemon see the same persistent data — so Claude Code, Cursor, and VS Code Copilot can all read from and contribute to the same knowledge base.
2727

28+
**Table or key-value store?** For a handful of small facts, notes, or flags, prefer the built-in key-value store (`kv_set` with `persist: true`) over `CREATE TABLE` + `load_data` — it needs no schema and no DDL. Reach for a real table when you need typed columns, JOINs, or aggregation. See [Working with both databases](#working-with-both-databases) for the `persist` / `database` mechanics that apply to both paths.
29+
2830
---
2931

3032
## Features
@@ -48,6 +50,7 @@ The ephemeral database is scratch space (think: a whiteboard). The persistent da
4850
- **Partial schema overrides** — supply just the columns you want to correct (e.g. `{"population":"BIGINT"}`) — the rest keep their inferred type
4951
- **Rich resource surface** — workspace readme, per-table JSON and CSV samples, and one JSON + one CSV resource per table so LLMs can orient themselves via `resources/list` without any tool calls
5052
- **Saved queries** — register named read-only SQL with `save_query`; each query becomes `hyper://queries/{name}/definition` (metadata) + `hyper://queries/{name}/result` (live re-run). Persisted in the persistent attachment, session-only when `--ephemeral-only`
53+
- **Key-value scratchpad** — lightweight `kv_set` / `kv_get` / `kv_list` / `kv_delete` / `kv_pop` / `kv_size` / `kv_clear` / `kv_list_stores` store for small notes and state without a `CREATE TABLE`. Ephemeral by default (lost on restart); pass `persist: true` (or `database: "persistent"`) to make a store durable across sessions
5154
- **Live resource-update notifications** — MCP clients can `resources/subscribe` to any `hyper://...` URI; the server fires `notifications/resources/updated` after every ingest, DDL, watcher event, or saved-query mutation
5255

5356
---
@@ -270,7 +273,7 @@ If hyperd repeatedly fails to start (3 attempts within 60 seconds — e.g., misc
270273

271274
| Flag | Behavior |
272275
|---|---|
273-
| `--read-only` | Disables `execute`, `load_data`, `load_file`, `watch_directory`, `save_query`, `delete_query`, and Hyper-format export. See [Read-Only Mode](#read-only-mode). |
276+
| `--read-only` | Disables `execute`, `load_data`, `load_file`, `watch_directory`, `save_query`, `delete_query`, and the KV mutators (`kv_set`, `kv_delete`, `kv_pop`, `kv_clear`). Export (including `.hyper`) stays allowed — it's a read-only file copy. See [Read-Only Mode](#read-only-mode). |
274277

275278
---
276279

@@ -507,6 +510,42 @@ delete_query(name: 'top_5_customers')
507510
Returns `{ "deleted": true }` when the query existed, `{ "deleted": false }`
508511
when it did not (no error on unknown names). Disabled in read-only mode.
509512

513+
### Key-Value Store
514+
515+
Lightweight named scratchpad for stashing a value under `store` + `key` and
516+
recalling it later — remember a variable, a summary, a JSON config, or a
517+
work-queue entry without creating a table or running `load_data`.
518+
519+
> **Stores default to the EPHEMERAL database and are LOST on server restart.**
520+
> Pass `database="persistent"` (or `persist=true`) to make a store durable
521+
> across restarts, or an attached alias to target that database. Each database
522+
> has its own isolated set of stores; a store in one database is invisible from
523+
> another.
524+
525+
Eight tools cover the surface:
526+
527+
| Tool | Purpose | Parameters |
528+
|---|---|---|
529+
| `kv_set` | Write/overwrite a value (upsert) | `store`, `key`, `value`, `database`, `persist` |
530+
| `kv_get` | Read a value by store + key (`value` is null when absent, not an error) | `store`, `key`, `database`, `persist` |
531+
| `kv_delete` | Remove one key (`{deleted: true/false}`, no error on unknown key) | `store`, `key`, `database`, `persist` |
532+
| `kv_list` | List all keys in a store, sorted ascending | `store`, `database`, `persist` |
533+
| `kv_list_stores` | List store namespaces that currently hold data | `database`, `persist` |
534+
| `kv_size` | Count keys in a store | `store`, `database`, `persist` |
535+
| `kv_pop` | Destructively read-and-remove the lowest-keyed entry (atomic) | `store`, `database`, `persist` |
536+
| `kv_clear` | Delete all keys in a store (returns count removed) | `store`, `database`, `persist` |
537+
538+
```
539+
kv_set(store: 'session', key: 'last_report', value: '{"rows": 4210}', database: 'persistent')
540+
kv_get(store: 'session', key: 'last_report')
541+
```
542+
543+
Key properties:
544+
- **Read-only mode** — the four mutators (`kv_set`, `kv_delete`, `kv_pop`, `kv_clear`) are disabled and return `READ_ONLY_VIOLATION`; the four readers (`kv_get`, `kv_list`, `kv_size`, `kv_list_stores`) always work.
545+
- **Pop order**`kv_pop` removes and returns the **lowest-keyed** entry in lexicographic key order (not insertion order), making a store usable as a simple work queue.
546+
- **No store registry** — a store that becomes empty simply **drops out** of `kv_list_stores`; there is no separate registry of store names.
547+
- **Backing table** — values live in `_hyperdb_kv_store(store_name, key, value)`, which is indexless (Hyper has no indexes) and hidden from `describe` by its `_hyperdb_` prefix, but is directly queryable — e.g. `LEFT JOIN` it to enrich an analytical table (always filter on `kv.store_name`). Uniqueness of `(store_name, key)` is enforced by the tool layer's upsert, atomic within a single server process. See the `hyper://schema/kv` resource for the schema and join pattern.
548+
510549
### Export Tools
511550

512551
#### `export`
@@ -601,6 +640,7 @@ can route it appropriately (LLM context vs. file download vs. chart).
601640
| `hyper://tables/{name}/csv-sample` | `text/csv` | First 20 rows of a table as CSV, header-first |
602641
| `hyper://queries/{name}/definition` | `application/json` | Stored SQL + metadata for a saved query |
603642
| `hyper://queries/{name}/result` | `application/json` | Live result of a saved query — re-runs on every read |
643+
| `hyper://schema/kv` | `text/plain` | KV scratchpad schema: the `_hyperdb_kv_store(store_name, key, value)` backing table, its indexless shape, the ephemeral-vs-persistent durability rule, and the `LEFT JOIN` enrichment pattern |
604644

605645
Resource templates (discoverable via `resources/templates/list`):
606646

@@ -666,8 +706,8 @@ Four guided analytical workflows registered as MCP **Prompts**.
666706
hyperdb-mcp --persistent-db ~/analytics.hyper --read-only
667707
```
668708

669-
- **Allowed:** `query`, `query_data`, `query_file`, `describe`, `sample`, `inspect_file`, `status`, `export`
670-
- **Blocked:** `execute`, `load_data`, `load_file`, `watch_directory`, `save_query`, `delete_query` — return `READ_ONLY_VIOLATION`
709+
- **Allowed:** `query`, `query_data`, `query_file`, `describe`, `sample`, `inspect_file`, `status`, `export`, and the KV readers `kv_get`, `kv_list`, `kv_size`, `kv_list_stores`
710+
- **Blocked:** `execute`, `load_data`, `load_file`, `watch_directory`, `save_query`, `delete_query`, and the KV mutators `kv_set`, `kv_delete`, `kv_pop`, `kv_clear` — return `READ_ONLY_VIOLATION`
671711
- **Resources, prompts, and resource subscriptions** work normally — read-only clients can still subscribe to `hyper://...` URIs and receive notifications when other (non-read-only) connections mutate state
672712

673713
The `query` tool also enforces read-only at the SQL level — only `SELECT`/`WITH`/`EXPLAIN`/`SHOW`/`VALUES` are accepted.

0 commit comments

Comments
 (0)