Skip to content

MCP KV tools: 5 improvements from dogfooding (value_path, overwrite signal, size reporting, batch ops, JSON-query docs) #192

Description

@StefanSteiner

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions