Summary
Five improvements to the MCP KV tools, surfaced by dogfooding the new API in a real Claude Code session (storing and querying a 70 KB ~/.claude.json blob). Each item below is backed by concrete friction hit during that session, with a proposed fix and a code citation. Ranked by impact.
Context: this is follow-up feedback from the same collaboration that designed and implemented the KV API + MCP tools (Opus 4.8 + operator). The tools work well; these are refinements, not defects.
1. kv_set cannot load a value from a file (highest impact)
Problem. value is inline-only. Storing a 70 KB file as a single KV entry required a 5-step workaround: jq-wrap the file as a JSON string → load_file into a staging table → SQL UPDATE to copy the value into the kv_set-created row → DROP TABLE → hash-verify round-trip. Every step is a corruption opportunity; the only reason I trusted the result was an explicit MD5 check against the original.
Proposed fix. Add an optional value_path parameter to kv_set (mutually exclusive with value), read server-side. The server already has filesystem access — load_file/load_files take absolute paths — so this reuses existing plumbing and collapses the entire workaround into one call.
Cite. KvSetParams at hyperdb-mcp/src/server.rs:845; handler fn kv_set at hyperdb-mcp/src/server.rs:3123.
2. kv_set gives no insert-vs-overwrite signal
Problem. Writing first then second to the same key both return the identical {"stored": true, ...}. The README calls it an "upsert," but the caller cannot tell whether they created a key or silently clobbered an existing value. For a memory/config store, a typo'd store or key name overwrites data with zero signal — an invisible footgun.
Proposed fix. Return {"stored": true, "created": true|false} (optionally previous_value or overwritten: true). The upsert path already performs an existence check internally, so the information is on hand — it's just discarded. Also, perhaps have parameter to kv_set to fail if the key already exists.
Cite. Response is hard-coded at hyperdb-mcp/src/server.rs:3136:
Ok(()) => Self::ok_content(json!({ "stored": true, "store": p.store, "key": p.key })). The library set at hyperdb-api/src/async_kv_store.rs:105 returns Result<()>, so the created/overwritten distinction would need to be threaded up from there (or derived via a pre-check in the handler).
3. No size reporting or guardrail
Problem. kv_set accepted 70 KB silently. Nothing reports a value's byte size, and kv_size counts keys, not bytes — so there's no way to see storage weight without dropping to SQL (octet_length(value) on _hyperdb_kv_store). The README's own "a few scraps" framing implies a soft ceiling that is surfaced nowhere.
Proposed fix. Return value_bytes on kv_set and kv_get; add a bytes field to kv_size. Optionally emit a soft warning above a threshold pointing the caller at load_data / a real table instead.
4. No batch operations (API layer already supports it)
Problem. Every write is one call. Seeding an N-item work queue is N calls; reading a whole store's values (not just keys) is kv_list + one kv_get per key. Chatty for the "session state" use case the store is built for.
Proposed fix. Expose kv_set_many (array of {key, value}) and a kv_get_all / values: true flag on kv_list that returns values inline. Notably the API layer already has pub async fn set_batch(&self, entries: &[(&str, &str)]) — the batch primitive exists and just isn't surfaced as an MCP tool.
Cite. hyperdb-api/src/async_kv_store.rs:339 (set_batch).
5. The SQL-over-JSON bridge is the killer feature and is nearly undocumented (cheapest fix)
Problem. The most powerful capability — that a KV value holding JSON is directly queryable — had to be rediscovered empirically. JSON_VALUE(...) returns not implemented; -> on raw TEXT errors with requires a structured data type. The working pattern is value::json then -> / ->> / JSON_EACH(...). The hyper://schema/kv resource documents the LEFT-JOIN enrichment template but says nothing about querying JSON stored in a value, which is likely the highest-value pattern for an LLM memory store.
Also worth a one-line dialect warning discovered in the same session: a bare ::numeric cast defaults to scale 0, so 41.54178215::numeric truncates to 42 before any ROUND(..., 4) runs — silently corrupting money/decimal aggregates. Cast via ::numeric(20,10) (or round the double directly).
Proposed fix (docs only). In the KV section of the README/hyper://schema/kv, add:
A value holding JSON can be queried with value::json + -> / ->> / JSON_EACH(...). JSON_VALUE(...) and -> on raw TEXT are not supported. When casting numeric JSON fields, use ::numeric(p,s) — a bare ::numeric defaults to scale 0 and truncates.
Cite. README KV section at hyperdb-mcp/src/readme.rs:187; hyper://schema/kv resource text.
Recommended sequencing
Summary
Five improvements to the MCP KV tools, surfaced by dogfooding the new API in a real Claude Code session (storing and querying a 70 KB
~/.claude.jsonblob). Each item below is backed by concrete friction hit during that session, with a proposed fix and a code citation. Ranked by impact.Context: this is follow-up feedback from the same collaboration that designed and implemented the KV API + MCP tools (Opus 4.8 + operator). The tools work well; these are refinements, not defects.
1.
kv_setcannot load a value from a file (highest impact)Problem.
valueis inline-only. Storing a 70 KB file as a single KV entry required a 5-step workaround:jq-wrap the file as a JSON string →load_fileinto a staging table → SQLUPDATEto copy the value into thekv_set-created row →DROP TABLE→ hash-verify round-trip. Every step is a corruption opportunity; the only reason I trusted the result was an explicit MD5 check against the original.Proposed fix. Add an optional
value_pathparameter tokv_set(mutually exclusive withvalue), read server-side. The server already has filesystem access —load_file/load_filestake absolute paths — so this reuses existing plumbing and collapses the entire workaround into one call.Cite.
KvSetParamsathyperdb-mcp/src/server.rs:845; handlerfn kv_setathyperdb-mcp/src/server.rs:3123.2.
kv_setgives no insert-vs-overwrite signalProblem. Writing
firstthensecondto the same key both return the identical{"stored": true, ...}. The README calls it an "upsert," but the caller cannot tell whether they created a key or silently clobbered an existing value. For a memory/config store, a typo'd store or key name overwrites data with zero signal — an invisible footgun.Proposed fix. Return
{"stored": true, "created": true|false}(optionallyprevious_valueoroverwritten: true). The upsert path already performs an existence check internally, so the information is on hand — it's just discarded. Also, perhaps have parameter tokv_setto fail if the key already exists.Cite. Response is hard-coded at
hyperdb-mcp/src/server.rs:3136:Ok(()) => Self::ok_content(json!({ "stored": true, "store": p.store, "key": p.key })). The librarysetathyperdb-api/src/async_kv_store.rs:105returnsResult<()>, so the created/overwritten distinction would need to be threaded up from there (or derived via a pre-check in the handler).3. No size reporting or guardrail
Problem.
kv_setaccepted 70 KB silently. Nothing reports a value's byte size, andkv_sizecounts keys, not bytes — so there's no way to see storage weight without dropping to SQL (octet_length(value)on_hyperdb_kv_store). The README's own "a few scraps" framing implies a soft ceiling that is surfaced nowhere.Proposed fix. Return
value_bytesonkv_setandkv_get; add abytesfield tokv_size. Optionally emit a soft warning above a threshold pointing the caller atload_data/ a real table instead.4. No batch operations (API layer already supports it)
Problem. Every write is one call. Seeding an N-item work queue is N calls; reading a whole store's values (not just keys) is
kv_list+ onekv_getper key. Chatty for the "session state" use case the store is built for.Proposed fix. Expose
kv_set_many(array of{key, value}) and akv_get_all/values: trueflag onkv_listthat returns values inline. Notably the API layer already haspub async fn set_batch(&self, entries: &[(&str, &str)])— the batch primitive exists and just isn't surfaced as an MCP tool.Cite.
hyperdb-api/src/async_kv_store.rs:339(set_batch).5. The SQL-over-JSON bridge is the killer feature and is nearly undocumented (cheapest fix)
Problem. The most powerful capability — that a KV value holding JSON is directly queryable — had to be rediscovered empirically.
JSON_VALUE(...)returnsnot implemented;->on raw TEXT errors withrequires a structured data type. The working pattern isvalue::jsonthen->/->>/JSON_EACH(...). Thehyper://schema/kvresource documents the LEFT-JOIN enrichment template but says nothing about querying JSON stored in a value, which is likely the highest-value pattern for an LLM memory store.Also worth a one-line dialect warning discovered in the same session: a bare
::numericcast defaults to scale 0, so41.54178215::numerictruncates to42before anyROUND(..., 4)runs — silently corrupting money/decimal aggregates. Cast via::numeric(20,10)(or round the double directly).Proposed fix (docs only). In the KV section of the README/
hyper://schema/kv, add:Cite. README KV section at
hyperdb-mcp/src/readme.rs:187;hyper://schema/kvresource text.Recommended sequencing
value_path) removes the worst workflow gap; doc: Ssteiner/update docs #5 (docs) is essentially free and unlocks the feature that makes this more than a dict.