hyperdb-mcp 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +141 -46
  2. package/bin.js +105 -19
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -23,7 +23,7 @@ This means an LLM can:
23
23
  - **Build on prior work** — load yesterday's cleaned dataset and extend it without re-processing from scratch
24
24
  - **Maintain structured context** — store relationship graphs, timelines, or decision logs as proper tables with typed columns
25
25
 
26
- 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.
26
+ The local database is ephemeral 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.
27
27
 
28
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
29
 
@@ -48,9 +48,9 @@ The ephemeral database is scratch space (think: a whiteboard). The persistent da
48
48
  - **Smart schema inference** — exact (Arrow/Parquet), structural (JSON), heuristic (CSV) with full-file numeric widening
49
49
  - **Pre-ingest file inspection** — `inspect_file` dry-runs the same inference without touching Hyper so LLMs can build safe schema overrides in one shot
50
50
  - **Partial schema overrides** — supply just the columns you want to correct (e.g. `{"population":"BIGINT"}`) — the rest keep their inferred type
51
- - **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
51
+ - **Rich resource surface** — database overview, 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
52
52
  - **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
53
+ - **Key-value scratchpad** — lightweight `kv_set` / `kv_set_many` / `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
54
54
  - **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
55
55
 
56
56
  ---
@@ -118,8 +118,11 @@ export HYPERD_PATH="$PWD/.hyperd/current" # or pass via your MCP config
118
118
  `hyperdb-bootstrap` also has a library API if you'd rather wire the
119
119
  download into your own build script — see its
120
120
  [README](../hyperdb-bootstrap/README.md). If you already have `hyperd`
121
- elsewhere (Tableau Hyper API for C++/Python/Java ships one), point
122
- `HYPERD_PATH` at it or add it to your `PATH`.
121
+ elsewhere (Tableau Hyper API for C++/Python/Java ships one), set
122
+ `HYPERD_PATH` to either the executable or its containing directory.
123
+ When that variable is absent or non-UTF-8, the runtime walks upward from its
124
+ current directory for `.hyperd/current/hyperd`; it does not perform a general
125
+ `PATH` lookup.
123
126
 
124
127
  ### MCP Client Configuration
125
128
 
@@ -156,7 +159,12 @@ By default, persistent storage lives at the platform data dir (`~/Library/Applic
156
159
  "args": ["--persistent-db", "/path/to/my-project.hyper"]
157
160
  ```
158
161
 
159
- Multiple MCP clients can point at the **same** persistent file simultaneously — they all connect through the shared `hyperd` daemon and use Hyper's MVCC transaction isolation. See [Operating Modes](#operating-modes) below.
162
+ Multiple MCP clients can point at the **same** persistent file simultaneously
163
+ when they reuse the shared `hyperd` daemon; Hyper's MVCC transaction isolation
164
+ coordinates their connections. A separate private `hyperd`, Tableau, or another
165
+ process trying to attach the same file can instead receive contextual
166
+ `RESOURCE_BUSY`. See [Operating Modes](#operating-modes) and
167
+ [Error Handling](#error-handling).
160
168
 
161
169
  #### Claude Code / AI Suite
162
170
 
@@ -182,7 +190,7 @@ Any tool that supports the MCP stdio transport can use this server. Point it at
182
190
 
183
191
  ## Operating Modes
184
192
 
185
- Each session has **two databases**: an ephemeral primary (scratch space — always created fresh per session, deleted on exit) and a persistent database (queryable long-term memory — stored at the platform-default location or a path you supply, survives indefinitely). Unqualified SQL targets the ephemeral primary; the persistent database is reachable as the `"persistent"` alias.
193
+ Each session has **two databases**: the ephemeral **local** primary (scratch space — always created fresh per session, deleted on exit) and a **persistent** database (queryable long-term memory — stored at the platform-default location or a path you supply, survives indefinitely). Unqualified SQL targets local; the durable database is reachable as the `"persistent"` alias. Additional `.hyper` files are **attached databases** under user-chosen aliases.
186
194
 
187
195
  ### Hyper engine
188
196
 
@@ -197,15 +205,15 @@ The shared daemon is the bigger win for users running multiple AI clients (Claud
197
205
 
198
206
  | Mode | Flag | Behavior |
199
207
  |---|---|---|
200
- | **Default** | *(none)* | Ephemeral primary in `$TMPDIR/hyperdb-mcp-<pid>-<n>/scratch.hyper` + persistent attachment at the platform data dir (e.g. `~/Library/Application Support/hyperdb/workspace.hyper` on macOS). |
208
+ | **Default** | *(none)* | Ephemeral local database in `$TMPDIR/hyperdb-mcp-<pid>-<n>/scratch.hyper` + persistent attachment at the platform data dir (e.g. `~/Library/Application Support/hyperdb/workspace.hyper` on macOS). |
201
209
  | **Custom persistent path** | `--persistent-db <PATH>` | Same as default but the persistent file lives at `<PATH>`. The deprecated `--workspace <PATH>` is accepted as an alias with a stderr warning. |
202
- | **Ephemeral-only** | `--ephemeral-only` | No persistent attachment; the session has only the ephemeral primary plus any user-attached databases via `attach_database`. Saved queries fall back to in-memory storage and disappear when the session ends. |
210
+ | **Ephemeral-only** | `--ephemeral-only` | No persistent attachment; the session has only the local database plus any user-attached databases via `attach_database`. Saved queries fall back to in-memory storage and disappear when the session ends. |
203
211
 
204
212
  `HYPERDB_PERSISTENT_DB` overrides the default persistent path the same way `--persistent-db` does.
205
213
 
206
214
  ### Working with both databases
207
215
 
208
- Tool calls default to the ephemeral primary — that's the LLM's scratch space for exploratory work that doesn't need to outlive the session. To store data in long-term memory (the persistent database), there are two ways to reach it:
216
+ Tool calls default to the local database — that's the LLM's ephemeral scratch space for exploratory work that doesn't need to outlive the session. To store data in long-term memory (the persistent database), there are two ways to reach it:
209
217
 
210
218
  **1. Per-tool `database` parameter** (preferred for ergonomic LLM workflows):
211
219
 
@@ -222,7 +230,12 @@ describe({ database: "persistent" })
222
230
  sample({ table: "customers", database: "persistent" })
223
231
  ```
224
232
 
225
- The `database` parameter is available on `query`, `execute`, `load_data`, `load_file`, `load_files`, `watch_directory`, `describe`, `sample`, `chart`, `export`, and `set_table_metadata`. The shorthand `persist: true` (sugar for `database: "persistent"`) is available on `load_data`, `load_file`, `load_files`, and `watch_directory`. Pass any user-attached writable alias (created via `attach_database`) to target a custom database.
233
+ The `database` parameter is available on `query`, `execute`, `load_data`, `load_file`, `load_files`, `watch_directory`, `describe`, `sample`, `chart`, `export`, and `set_table_metadata`. The shorthand `persist: true` (sugar for `database: "persistent"`) is available on `load_data`, `load_file`, `load_files`, and `watch_directory`. Read tools generally accept a read-only user attachment; write tools require a writable one. The exception is the KV family: every `kv_*` call to a user attachment requires it to be writable because the backing table may need initialization.
234
+
235
+ Every successful database-routed response includes the canonical
236
+ `resolved_database`: `"local"`, `"persistent"`, or the lowercase attached
237
+ alias after precedence is applied. An explicit `database` wins over
238
+ `persist: true`; `copy_query` additionally retains `target_database`.
226
239
 
227
240
  (`query_data` and `query_file` are one-shot tools that materialize the inline data into their own temp table and query it — they do not accept a `database` parameter because the data isn't in a persisted database to begin with.)
228
241
 
@@ -237,7 +250,12 @@ CREATE TABLE "persistent"."public"."revenue_2026" AS
237
250
  SELECT region, SUM(amount) FROM scratch_orders GROUP BY region;
238
251
  ```
239
252
 
240
- **Per-database `_table_catalog`:** every writable database — persistent and any user-attached writable file — gets its own `_table_catalog` lazily seeded on first ingest. MCP-managed metadata (load tool, params, timestamps, prose fields set via `set_table_metadata`) lives alongside the data file, so opening a `.hyper` file later as a primary workspace finds the catalog ready. If you want a pristine `.hyper` file for export with no MCP bookkeeping, run `DROP TABLE "<alias>"."public"."_table_catalog"` once and subsequent sessions opening that file will leave it dropped.
253
+ **Metadata catalogs:** local and persistent tables share the persistent
254
+ `_table_catalog`, keyed by table name across their union. `set_table_metadata`
255
+ therefore targets an existing catalog entry rather than re-checking that the
256
+ table exists in a selected local/persistent database. Each writable
257
+ user-attached database has its own per-database catalog; read-only attachments
258
+ cannot be metadata targets.
241
259
 
242
260
  **Detach safety:** `detach_database` rejects with `InvalidArgument` if any active watcher targets the alias — call `unwatch_directory` first. This prevents the watcher's pool from silently writing into a now-detached file (or worse, the wrong file if the alias is later re-attached to a different path).
243
261
 
@@ -255,7 +273,29 @@ hyperdb-mcp daemon # Run as a daemon explicitly (rarely needed)
255
273
 
256
274
  State files live at `~/.hyperdb/` by default (override with `HYPERDB_STATE_DIR`).
257
275
 
258
- **Port discovery.** The daemon binds a TCP health/lock port — by default it scans upward from **7485** (16 ports) and uses the first free one; set `HYPERDB_DAEMON_PORT` to pin an exact port (no scan). The health port doubles as a single-instance lock and an identity check: clients send `PING` and require a `PONG hyperdb-mcp <version>` reply before trusting a daemon, so an unrelated process occupying the port is skipped rather than mistaken for the daemon.
276
+ For installation and configuration diagnostics that also work before MCP can start, use the native doctor command:
277
+
278
+ ```bash
279
+ hyperdb-mcp doctor
280
+ hyperdb-mcp doctor --json
281
+ ```
282
+
283
+ Doctor is side-effect-free: it creates no directories, does not start a daemon
284
+ or `hyperd`, and does not open or create a database. Its native executable, MCP build,
285
+ and compiled Rust API identities are authoritative; npm wrapper/platform
286
+ details are bounded, optional launcher-reported provenance. A live daemon is
287
+ attributed only after a fresh `STATUS` response is verified. Reports contain
288
+ local paths; review them before sharing.
289
+
290
+ **Port discovery.** MCP auto-spawn discovers a live daemon first, then scans
291
+ upward from **7485** across 16 candidates before starting one at the selected
292
+ exact port. Setting `HYPERDB_DAEMON_PORT` pins auto-spawn to one candidate.
293
+ In contrast, a manually launched foreground `hyperdb-mcp daemon` never scans:
294
+ `--port <PORT>` binds that exact port, while an omitted `--port` binds the
295
+ configured/base port exactly. The health port doubles as a single-instance
296
+ lock and identity check: clients send `PING` and require a
297
+ `PONG hyperdb-mcp <version>` reply before trusting a daemon, so an unrelated
298
+ process is not mistaken for HyperDB.
259
299
 
260
300
  **Staying resident.** By default the daemon never idle-shuts-down — keeping `hyperd` warm means the next tool call connects immediately instead of triggering a "restarting, please retry" round-trip. To opt into auto-shutdown (e.g. on CI), pass `--idle-timeout <SECS>` or set `HYPERDB_DAEMON_IDLE_TIMEOUT`.
261
301
 
@@ -273,7 +313,7 @@ If hyperd repeatedly fails to start (3 attempts within 60 seconds — e.g., misc
273
313
 
274
314
  | Flag | Behavior |
275
315
  |---|---|
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). |
316
+ | `--read-only` | Guards `execute`, all four `load_*` tools, `watch_directory`, saved-query mutations, `set_table_metadata`, `copy_query`, `kv_set`, `kv_set_many`, `kv_delete`, `kv_pop`, `kv_clear`, and writable/create attachment. Read-only attachment, `unwatch_directory`, and export (including `.hyper`) stay available. See [Read-Only Mode](#read-only-mode). |
277
317
 
278
318
  ---
279
319
 
@@ -312,11 +352,11 @@ query_file(path: '/tmp/sales.parquet', sql: 'SELECT TOP 10 * FROM sales ORDER BY
312
352
  | `table_name` | string | no | Table name — defaults to filename stem |
313
353
  | `schema` | object | no | Partial column-name → type map (see [Schema Overrides](#schema-overrides)) |
314
354
 
315
- ### Workspace Tools
355
+ ### Database Tools
316
356
 
317
357
  #### `load_data`
318
358
 
319
- Load inline data into a named workspace table.
359
+ Load inline data into a named local, persistent, or attached-database table.
320
360
 
321
361
  ```
322
362
  load_data(table: 'customers', data: '[{"id":1,"name":"Alice"},...]')
@@ -332,7 +372,7 @@ load_data(table: 'customers', data: '[{"id":1,"name":"Alice"},...]')
332
372
 
333
373
  #### `load_file`
334
374
 
335
- Load a file into a named workspace table.
375
+ Load a file into a named local, persistent, or attached-database table.
336
376
 
337
377
  ```
338
378
  load_file(table: 'orders', path: '/tmp/orders.csv')
@@ -353,7 +393,7 @@ so you can build a minimal, correct override in one shot.
353
393
  #### `load_iceberg`
354
394
 
355
395
  Load an [Apache Iceberg](https://iceberg.apache.org/) table into a named
356
- workspace table. Pass the absolute path to the Iceberg table root (the
396
+ local table. Pass the absolute path to the Iceberg table root (the
357
397
  directory containing `metadata/` and `data/`); hyperd's native Iceberg
358
398
  reader derives the schema and resolves the snapshot.
359
399
 
@@ -374,7 +414,7 @@ Iceberg table metadata.
374
414
 
375
415
  #### `query`
376
416
 
377
- Run a **read-only** SQL query against the workspace. Accepts `SELECT`, `WITH`, `EXPLAIN`, `SHOW`, `VALUES`. For DDL/DML use `execute`.
417
+ Run a **read-only** SQL query against local (default), persistent, or an attached database. Accepts `SELECT`, `WITH`, `EXPLAIN`, `SHOW`, `VALUES`. For DDL/DML use `execute`.
378
418
 
379
419
  ```
380
420
  query(sql: 'SELECT c.name, SUM(o.amount) FROM orders o JOIN customers c ON o.customer_id = c.id GROUP BY c.name')
@@ -405,7 +445,7 @@ Validation rules enforced before any SQL hits the server:
405
445
 
406
446
  #### `describe`
407
447
 
408
- List all workspace tables with their schemas, column types, and row counts.
448
+ List all tables in the selected database with their schemas, column types, and row counts.
409
449
 
410
450
  #### `sample`
411
451
 
@@ -510,23 +550,24 @@ delete_query(name: 'top_5_customers')
510
550
  Returns `{ "deleted": true }` when the query existed, `{ "deleted": false }`
511
551
  when it did not (no error on unknown names). Disabled in read-only mode.
512
552
 
513
- ### Key-Value Store
553
+ ### Key-Value Scratchpad
514
554
 
515
555
  Lightweight named scratchpad for stashing a value under `store` + `key` and
516
556
  recalling it later — remember a variable, a summary, a JSON config, or a
517
557
  work-queue entry without creating a table or running `load_data`.
518
558
 
519
- > **Stores default to the EPHEMERAL database and are LOST on server restart.**
559
+ > **Stores default to the local database and are LOST on server restart.**
520
560
  > Pass `database="persistent"` (or `persist=true`) to make a store durable
521
561
  > across restarts, or an attached alias to target that database. Each database
522
562
  > has its own isolated set of stores; a store in one database is invisible from
523
563
  > another.
524
564
 
525
- Eight tools cover the surface:
565
+ Nine tools cover the surface:
526
566
 
527
567
  | Tool | Purpose | Parameters |
528
568
  |---|---|---|
529
569
  | `kv_set` | Write/overwrite a value (upsert) | `store`, `key`, `value`, `database`, `persist` |
570
+ | `kv_set_many` | Atomically write an `entries` batch, optionally skipping existing keys | `store`, `entries`, `overwrite`, `database`, `persist` |
530
571
  | `kv_get` | Read a value by store + key (`value` is null when absent, not an error) | `store`, `key`, `database`, `persist` |
531
572
  | `kv_delete` | Remove one key (`{deleted: true/false}`, no error on unknown key) | `store`, `key`, `database`, `persist` |
532
573
  | `kv_list` | List all keys in a store, sorted ascending | `store`, `database`, `persist` |
@@ -541,7 +582,12 @@ kv_get(store: 'session', key: 'last_report')
541
582
  ```
542
583
 
543
584
  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.
585
+ - **Read-only mode** — the five mutators (`kv_set`, `kv_set_many`, `kv_delete`, `kv_pop`, `kv_clear`) are disabled and return `READ_ONLY_VIOLATION`; the global guard leaves the four readers (`kv_get`, `kv_list`, `kv_size`, `kv_list_stores`) available.
586
+ - **Attached-database access** — every attached target must have been attached
587
+ with `writable=true`, even for readers, because a KV call may need to
588
+ initialize its backing table. The global `--read-only` guard still blocks
589
+ only the five mutators; an allowed reader can use local/persistent storage but
590
+ cannot use a read-only user attachment.
545
591
  - **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
592
  - **No store registry** — a store that becomes empty simply **drops out** of `kv_list_stores`; there is no separate registry of store names.
547
593
  - **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.
@@ -564,13 +610,16 @@ export(sql: 'SELECT ...', path: '~/Desktop/analysis.hyper', format: 'hyper')
564
610
  | `path` | string | yes | Output file path |
565
611
  | `format` | string | yes | `"csv"`, `"parquet"`, `"iceberg"`, `"arrow_ipc"`, or `"hyper"` |
566
612
 
567
- The `"hyper"` format produces a `.hyper` file that opens directly in **Tableau Desktop**.
613
+ The `"hyper"` format produces a `.hyper` file that opens directly in **Tableau
614
+ Desktop**. It does not mutate the source database; it creates or replaces the
615
+ destination and materializes every user table from the selected source into it.
568
616
 
569
617
  ### Visualization
570
618
 
571
619
  #### `chart`
572
620
 
573
- Render a chart from a SQL query and return it inline as an image.
621
+ Render a bounded quick diagnostic from a SQL query. This convenience tool is
622
+ for inspecting or sharing one chart, not for dashboard/layout composition.
574
623
 
575
624
  ```
576
625
  chart(sql: 'SELECT product, SUM(revenue) as total FROM sales GROUP BY product', chart_type: 'bar', x: 'product', y: 'total', title: 'Revenue by Product')
@@ -579,17 +628,46 @@ chart(sql: 'SELECT product, SUM(revenue) as total FROM sales GROUP BY product',
579
628
  | Parameter | Type | Required | Description |
580
629
  |-----------|------|----------|-------------|
581
630
  | `sql` | string | yes | Read-only SQL query returning the data to plot |
631
+ | `database` | string | no | Route SQL to `local` (default), `persistent`, or an attached alias |
582
632
  | `chart_type` | string | yes | `bar`, `line`, `scatter`, or `histogram` |
583
633
  | `x` | string | yes* | X-axis column (for histogram, the value column) |
584
634
  | `y` | string | yes* | Y-axis column (not required for histogram) |
585
635
  | `series` | string | no | Grouping column for multi-series plots |
636
+ | `color_map` | object | no | Map series names to hex colors such as `{"East":"#e41a1c"}` |
637
+ | `label_points` | bool | no | Label line/scatter points by series and suppress their legend |
586
638
  | `title` | string | no | Chart title |
587
639
  | `format` | string | no | `png` (default) or `svg` |
588
640
  | `width` | int | no | Pixels (default 800, clamped 200..4096) |
589
641
  | `height` | int | no | Pixels (default 480, clamped 150..4096) |
590
642
  | `bins` | int | no | Histogram bins (default 20, clamped 1..500) |
591
-
592
- Returns an `ImageContent` (base64 PNG or SVG) plus a stats JSON block.
643
+ | `output_path` | string | no | Destination file; parent directories are created |
644
+ | `inline` | bool | no | Return image bytes inline (default `true`) |
645
+ | `overwrite` | bool | no | Permit replacing `output_path` (default `true`) |
646
+ | `bar_orientation` | string | no | `vertical` (default) or `horizontal`; bars only |
647
+ | `label_values` | bool | no | Draw each original y scalar beside its bar |
648
+ | `show_legend` | bool | no | Show series legend (default `true`) |
649
+ | `y_scale` | string | no | `linear` (default) or positive `log`; no log histograms |
650
+ | `x_as_category` | bool | no | Force even categorical spacing on line/scatter x values |
651
+ | `x_range` / `y_range` | number pair | no | Explicit finite, strictly increasing bounds |
652
+
653
+ With neither path nor delivery override, the PNG (or requested SVG) is returned
654
+ inline and no file is written. `output_path` means write plus inline; set
655
+ `inline=false` for disk-only output, with an auto-generated temp path when no
656
+ path is supplied. Explicit `format` and the path extension must agree. The
657
+ result ends with a stats JSON block containing `resolved_database` and, when
658
+ written, `output_path`.
659
+
660
+ Line/scatter DATE, TIMESTAMP, and TIMESTAMPTZ x columns use proportional
661
+ temporal spacing automatically; TEXT is categorical. Set `x_as_category=true`
662
+ only when even spacing is deliberate. Bars always treat x as categorical.
663
+ Horizontal rankings preserve SQL row order with the first row at the top.
664
+ Long or Unicode labels are accepted but not auto-sized, so increase width or
665
+ height when needed.
666
+
667
+ All explicit ranges must be finite, strictly increasing, and representable.
668
+ Log y values and bounds must also be positive and the explicit range must
669
+ contain every plotted value. Log bars begin at the effective positive lower
670
+ bound, never zero.
593
671
 
594
672
  ### Incremental Ingest
595
673
 
@@ -620,27 +698,32 @@ Key properties:
620
698
 
621
699
  #### `status`
622
700
 
623
- Returns plugin health, workspace mode, table count, total rows, disk usage, read-only flag, and active directory watchers with per-watcher stats.
701
+ Returns MCP/native/API installation identity, daemon and Hyper connection facts,
702
+ `default_database: "local"`, persistent-path state, table/row/disk statistics,
703
+ read-only state, attachments, and active watchers. A full response has
704
+ `engine_busy: false`. When `engine_busy: true`, the prompt response is partial:
705
+ SQL-dependent statistics are intentionally omitted and `hyperd_running: false`
706
+ is inconclusive. Retry `status` after the in-progress operation completes.
624
707
 
625
708
  ---
626
709
 
627
710
  ## MCP Resources
628
711
 
629
- The server exposes workspace state as MCP **Resources**, discoverable via
712
+ The server exposes local and persistent database state as MCP **Resources**, discoverable via
630
713
  `resources/list`. Each resource advertises its own MIME type so clients
631
714
  can route it appropriately (LLM context vs. file download vs. chart).
632
715
 
633
716
  | URI | MIME | Content |
634
717
  |-----|------|---------|
635
- | `hyper://workspace` | `application/json` | Workspace mode, table count, total rows, disk usage |
718
+ | `hyper://workspace` | `application/json` | Local/persistent state, table count, total rows, disk usage |
636
719
  | `hyper://tables` | `application/json` | Full list of tables with schemas and row counts |
637
- | `hyper://readme` | `text/markdown` | Workspace overview as markdown: table catalog, related resources per table, and tool hints for a cold-started LLM |
720
+ | `hyper://readme` | `text/markdown` | Database overview as markdown: table catalog, related resources per table, and tool hints for a cold-started LLM |
638
721
  | `hyper://tables/{name}/schema` | `application/json` | Columns, types, nullability, and row count for one table |
639
722
  | `hyper://tables/{name}/sample` | `application/json` | First 5 rows of a table as JSON, with schema |
640
723
  | `hyper://tables/{name}/csv-sample` | `text/csv` | First 20 rows of a table as CSV, header-first |
641
724
  | `hyper://queries/{name}/definition` | `application/json` | Stored SQL + metadata for a saved query |
642
725
  | `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 |
726
+ | `hyper://schema/kv` | `text/plain` | KV scratchpad schema: backing table and `LEFT JOIN` pattern, local-vs-persistent durability, global read-only guards, and writable user-attachment requirement (including readers) |
644
727
 
645
728
  Resource templates (discoverable via `resources/templates/list`):
646
729
 
@@ -670,8 +753,8 @@ of mutation:
670
753
  | `load_data` / `load_file` (replace mode) | `hyper://workspace`, `hyper://tables`, `hyper://readme`, per-table schema + sample + csv-sample | Yes |
671
754
  | `load_data` / `load_file` (append mode) | Same per-table + summary URIs | No &sup1; |
672
755
  | `watch_directory` ingest of a `.ready` pair | Same per-table + summary URIs | No &sup1; |
673
- | `execute` (INSERT / UPDATE / DELETE) | Workspace summary URIs | No |
674
- | `execute` (CREATE / DROP / ALTER / TRUNCATE / RENAME) | Workspace summary URIs | Yes |
756
+ | `execute` (INSERT / UPDATE / DELETE) | Database-summary URIs | No |
757
+ | `execute` (CREATE / DROP / ALTER / TRUNCATE / RENAME) | Database-summary URIs | Yes |
675
758
  | `save_query` | (none per-URI) | Yes — two new `hyper://queries/{name}/...` resources |
676
759
  | `delete_query` | `hyper://queries/{name}/definition`, `hyper://queries/{name}/result` | Yes — two resources disappeared |
677
760
 
@@ -706,8 +789,8 @@ Four guided analytical workflows registered as MCP **Prompts**.
706
789
  hyperdb-mcp --persistent-db ~/analytics.hyper --read-only
707
790
  ```
708
791
 
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`
792
+ - **Allowed:** `query`, `query_data`, `query_file`, `describe`, `sample`, `inspect_file`, `status`, `chart`, `export` in every format including Hyper, read-only `attach_database`, `detach_database`, `list_attached_databases`, `unwatch_directory`, `get_readme`, and the KV readers `kv_get`, `kv_list`, `kv_size`, `kv_list_stores`
793
+ - **Blocked:** `execute`, `load_data`, `load_file`, `load_files`, `load_iceberg`, `watch_directory`, `save_query`, `delete_query`, `set_table_metadata`, `copy_query`, `kv_set`, `kv_set_many`, `kv_delete`, `kv_pop`, and `kv_clear` — return `READ_ONLY_VIOLATION`. `attach_database` is also guarded when `writable: true` or `on_missing: "create"`; ordinary read-only attachment remains available.
711
794
  - **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
712
795
 
713
796
  The `query` tool also enforces read-only at the SQL level — only `SELECT`/`WITH`/`EXPLAIN`/`SHOW`/`VALUES` are accepted.
@@ -837,7 +920,8 @@ Full reference: [Data Cloud SQL Reference](https://developer.salesforce.com/docs
837
920
  hyperdb-mcp [OPTIONS] [COMMAND]
838
921
 
839
922
  Commands:
840
- daemon Run as a background daemon managing a shared hyperd process
923
+ daemon Run a foreground daemon managing a shared hyperd process
924
+ doctor Inspect identities/configuration without starting Hyper
841
925
 
842
926
  Options:
843
927
  --persistent-db <PATH> Path to the persistent .hyper file. Defaults to the platform
@@ -847,8 +931,8 @@ Options:
847
931
  the HYPERDB_PERSISTENT_DB env var.
848
932
  --ephemeral-only Skip the persistent attachment entirely. Disables save_query
849
933
  persistence (queries fall back to session storage).
850
- --read-only Disable mutating tools (execute, load_data, load_file,
851
- save_query, delete_query, watch_directory)
934
+ --read-only Guard all load/mutation tools and writable/create attachment;
935
+ read-only attachment, unwatch, and all export formats stay allowed
852
936
  --no-daemon Disable the shared daemon and spawn a private hyperd
853
937
 
854
938
  Deprecated:
@@ -856,19 +940,21 @@ Deprecated:
856
940
  stderr warning, and will be removed in a future release.
857
941
 
858
942
  Daemon subcommand:
859
- hyperdb-mcp daemon Start the daemon (usually auto-spawned)
943
+ hyperdb-mcp daemon Start foreground on the configured/base port exactly
860
944
  hyperdb-mcp daemon stop Gracefully stop the running daemon
861
945
  hyperdb-mcp daemon status Show running daemon info
862
- hyperdb-mcp daemon --port <PORT> Pin the health/lock port. When omitted,
863
- scans upward from 7485 for a free port.
946
+ hyperdb-mcp daemon --port <PORT> Bind this exact health/lock port; foreground
947
+ startup never performs the auto-spawn scan.
864
948
  hyperdb-mcp daemon --idle-timeout <SECS> Opt into idle shutdown after SECS idle.
865
949
  When omitted, the daemon stays resident.
866
950
 
867
951
  Environment:
868
- HYPERD_PATH Path to hyperd binary (auto-detected if on PATH)
952
+ HYPERD_PATH Hyperd executable or containing directory; when absent or
953
+ non-UTF-8, walk upward for .hyperd/current/hyperd (no PATH lookup)
869
954
  HYPERDB_PERSISTENT_DB Override the default persistent-db path
870
955
  HYPERDB_STATE_DIR Override daemon state directory (default ~/.hyperdb/)
871
- HYPERDB_DAEMON_PORT Pin daemon health/lock port (default: scan from 7485)
956
+ HYPERDB_DAEMON_PORT Pin auto-spawn discovery to one health/lock candidate;
957
+ foreground startup binds this configured/base port exactly
872
958
  HYPERDB_DAEMON_IDLE_TIMEOUT Opt into idle shutdown (seconds); default: stay resident
873
959
  ```
874
960
 
@@ -887,6 +973,7 @@ Errors include a machine-readable code and a suggestion:
887
973
  | `SQL_ERROR` | Invalid SQL | Fix the query |
888
974
  | `TABLE_NOT_FOUND` | Table doesn't exist | Use `describe` to list tables |
889
975
  | `READ_ONLY_VIOLATION` | Mutating op in read-only mode | Use `query_*` / `inspect_file`, or restart without `--read-only` |
976
+ | `RESOURCE_BUSY` | The reserved persistent attachment hit file contention (SQLSTATE 55006) | Run `hyperdb-mcp doctor`; compare client/daemon identities; close the possible owner (Hyper, Tableau, or another process), or copy/select another `.hyper` file; retry |
890
977
  | `CONNECTION_LOST` | `hyperd` crashed or wire protocol desynchronized | Retry — the server tears down the engine and reconnects on the next call |
891
978
 
892
979
  Server-returned errors include a machine-readable `code`, a `message`, and a
@@ -895,6 +982,12 @@ an overflow names the workflow directly: "call `inspect_file`, then retry with
895
982
  a partial schema override", so the LLM does not need to infer the recovery
896
983
  steps from the SQLSTATE alone.
897
984
 
985
+ `RESOURCE_BUSY` is contextual: only contention while attaching the configured
986
+ persistent file gets this classification. The error preserves the effective
987
+ path, raw Hyper diagnostic, and SQLSTATE; unrelated `55006` SQL errors remain
988
+ `SQL_ERROR`. Doctor compares evidence but does not claim which possible owner
989
+ holds the file and never kills a process.
990
+
898
991
  ---
899
992
 
900
993
  ## Troubleshooting
@@ -903,7 +996,9 @@ steps from the SQLSTATE alone.
903
996
 
904
997
  **Server registered but tools not callable (Claude Code)** — Add `"mcp__HyperDB__*"` to the `permissions.allow` array in `~/.claude/settings.json`.
905
998
 
906
- **hyperd not found** — Set `HYPERD_PATH` in the MCP server's `env` config, or place `hyperd` on your `PATH`.
999
+ **hyperd not found** — Set `HYPERD_PATH` in the MCP server's `env` config to
1000
+ the executable or its containing directory, or install it under an ancestor's
1001
+ `.hyperd/current/` directory. The runtime does not search the general `PATH`.
907
1002
 
908
1003
  ---
909
1004
 
package/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright (c) 2026, Salesforce, Inc. All rights reserved.
3
3
  // SPDX-License-Identifier: Apache-2.0 OR MIT
4
4
 
5
- const { execFileSync } = require('child_process')
5
+ const { spawnSync } = require('child_process')
6
6
  const { join, dirname } = require('path')
7
7
  const { existsSync } = require('fs')
8
8
 
@@ -39,20 +39,39 @@ function findBinary() {
39
39
 
40
40
  // Try resolving from the installed platform package
41
41
  try {
42
- const pkgDir = dirname(require.resolve(`${pkg}/package.json`))
42
+ const packagePath = require.resolve(`${pkg}/package.json`)
43
+ const pkgDir = dirname(packagePath)
43
44
  const bin = join(pkgDir, getBinaryName())
44
- if (existsSync(bin)) return { bin, dir: pkgDir }
45
+ if (existsSync(bin)) return { bin, dir: pkgDir, pkg, packagePath }
45
46
  } catch (_) {}
46
47
 
47
48
  // Fallback: binary in platform subdirectory (local dev / assemble-npm.sh)
48
49
  const platformDir = pkg.replace('hyperdb-mcp-', '')
49
50
  const subdir = join(__dirname, platformDir)
50
51
  const subdirBin = join(subdir, getBinaryName())
51
- if (existsSync(subdirBin)) return { bin: subdirBin, dir: subdir }
52
+ const sourcePackagePath = join(subdir, 'package.json')
53
+ if (existsSync(subdirBin)) {
54
+ return {
55
+ bin: subdirBin,
56
+ dir: subdir,
57
+ pkg,
58
+ packagePath: sourcePackagePath,
59
+ }
60
+ }
52
61
 
53
62
  // Fallback: binary in same directory
54
63
  const localBin = join(__dirname, getBinaryName())
55
- if (existsSync(localBin)) return { bin: localBin, dir: __dirname }
64
+ if (existsSync(localBin)) {
65
+ return {
66
+ bin: localBin,
67
+ dir: __dirname,
68
+ pkg,
69
+ // The manifest sits next to the binary in this branch — not in the
70
+ // platform subdirectory `sourcePackagePath` points at (that path does
71
+ // not exist here), so recompute it relative to __dirname.
72
+ packagePath: join(__dirname, 'package.json'),
73
+ }
74
+ }
56
75
 
57
76
  throw new Error(
58
77
  `Could not find hyperdb-mcp binary for ${platform}-${arch}. ` +
@@ -60,24 +79,91 @@ function findBinary() {
60
79
  )
61
80
  }
62
81
 
63
- const { bin, dir } = findBinary()
82
+ function packageIdentity(packagePath, fallbackName) {
83
+ let manifest = {}
84
+ try {
85
+ manifest = require(packagePath)
86
+ } catch (_) {}
87
+
88
+ return {
89
+ name: typeof manifest.name === 'string' ? manifest.name : fallbackName,
90
+ version: typeof manifest.version === 'string' ? manifest.version : null,
91
+ package_path: packagePath,
92
+ }
93
+ }
94
+
95
+ function buildLauncherInfo({ wrapper, platform, executable_path }) {
96
+ return {
97
+ wrapper: {
98
+ name: wrapper.name,
99
+ version: wrapper.version ?? null,
100
+ package_path: wrapper.package_path,
101
+ },
102
+ platform: {
103
+ name: platform.name,
104
+ version: platform.version ?? null,
105
+ package_path: platform.package_path,
106
+ },
107
+ executable_path,
108
+ }
109
+ }
110
+
111
+ function prepareLauncherEnvironment({
112
+ inherited_env,
113
+ configured_hyperd,
114
+ bundled_hyperd,
115
+ launcher_info,
116
+ }) {
117
+ const env = { ...inherited_env }
118
+ if (!configured_hyperd && bundled_hyperd !== undefined) {
119
+ env.HYPERD_PATH = bundled_hyperd
120
+ }
121
+ env.HYPERDB_MCP_LAUNCHER_INFO = JSON.stringify(launcher_info)
122
+ return env
123
+ }
124
+
125
+ function launch({ executable_path, args, env, spawnSync }) {
126
+ const result = spawnSync(executable_path, args, {
127
+ stdio: 'inherit',
128
+ env,
129
+ })
64
130
 
65
- // Point hyperdb-mcp at the bundled hyperd if not already set
66
- if (!process.env.HYPERD_PATH) {
67
- const hyperd = join(dir, getHyperdName())
68
- if (existsSync(hyperd)) {
69
- process.env.HYPERD_PATH = hyperd
131
+ if (result.error) {
132
+ throw result.error
70
133
  }
134
+
135
+ return result.status ?? 1
71
136
  }
72
137
 
73
- // Spawn the MCP server, inheriting stdio for MCP protocol communication
74
- const result = require('child_process').spawnSync(bin, process.argv.slice(2), {
75
- stdio: 'inherit',
76
- env: process.env,
77
- })
138
+ function main() {
139
+ const { bin, dir, pkg, packagePath } = findBinary()
140
+ const configuredHyperd = process.env.HYPERD_PATH
141
+
142
+ // Point hyperdb-mcp at the bundled hyperd if not already set
143
+ const bundledHyperd = join(dir, getHyperdName())
144
+ const launcherInfo = buildLauncherInfo({
145
+ wrapper: packageIdentity(join(__dirname, 'package.json'), 'hyperdb-mcp'),
146
+ platform: packageIdentity(packagePath, pkg),
147
+ executable_path: bin,
148
+ })
149
+ const env = prepareLauncherEnvironment({
150
+ inherited_env: process.env,
151
+ configured_hyperd: configuredHyperd,
152
+ bundled_hyperd: existsSync(bundledHyperd) ? bundledHyperd : undefined,
153
+ launcher_info: launcherInfo,
154
+ })
155
+
156
+ // Spawn the MCP server, inheriting stdio for MCP protocol communication
157
+ return launch({
158
+ executable_path: bin,
159
+ args: process.argv.slice(2),
160
+ env,
161
+ spawnSync,
162
+ })
163
+ }
78
164
 
79
- if (result.error) {
80
- throw result.error
165
+ if (require.main === module) {
166
+ process.exit(main())
81
167
  }
82
168
 
83
- process.exit(result.status ?? 1)
169
+ module.exports = { buildLauncherInfo, prepareLauncherEnvironment, launch }
package/package.json CHANGED
@@ -29,10 +29,10 @@
29
29
  "engines": {
30
30
  "node": ">= 21"
31
31
  },
32
- "version": "0.7.2",
32
+ "version": "0.7.3",
33
33
  "optionalDependencies": {
34
- "hyperdb-mcp-darwin-arm64": "0.7.2",
35
- "hyperdb-mcp-linux-x64-gnu": "0.7.2",
36
- "hyperdb-mcp-win32-x64-msvc": "0.7.2"
34
+ "hyperdb-mcp-darwin-arm64": "0.7.3",
35
+ "hyperdb-mcp-linux-x64-gnu": "0.7.3",
36
+ "hyperdb-mcp-win32-x64-msvc": "0.7.3"
37
37
  }
38
38
  }