Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions hyperdb-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- **Upgraded the `rmcp` SDK dependency from 1.x to 3.4.** Resolves the
outstanding `rmcp` security advisories. Purely an internal dependency bump —
the MCP wire protocol and tool surface exposed by this server are unchanged.
- **`query_file` now exposes the file's rows as `data` by default**, matching
`query_data`, instead of the file's stem. Reference them as `SELECT ... FROM
data` (or pass `table_name`). The previous stem default was unpredictable and
contradicted the documented example.
- **`attach_database` and `copy_query`'s `temp_attach` no longer require
`kind`** — it defaults to `"local_file"`, the only supported kind.
- **Trimmed the always-in-context tool descriptions (~26% smaller)** — moved
the format-selection and edge-case detail into `get_readme` while keeping the
actionable rules inline, cutting the tokens the tool catalog costs per load.

### Fixed

- **`query_data` / `query_file` no longer leak their scratch table on a failed
query** — the temp table is now dropped whether the query succeeds or fails,
so a bad query no longer leaves a `_tmp_*` table behind in `describe`.
- **Table listings (`describe`) now hide transient scratch tables** (`_tmp_*`
and `__hyperdb_merge_*`), not just `_hyperdb_*` internals.
- **`query_data` / `query_file` table-name substitution is whole-word** — an
alias like `data` no longer corrupts a column named `metadata` or `data_url`.
- **Unknown-table SQL errors now suggest running `describe`** and name the
default `data` alias, instead of the generic "check SQL syntax" hint.

## [1.0.0-rc.4] - 2026-09-08

Expand Down
33 changes: 29 additions & 4 deletions hyperdb-mcp/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2072,17 +2072,29 @@ pub const CLIENT_LOG_FILE_NAME: &str = "hyperdb-mcp.log";
/// automatically — no per-table filter list to keep in sync.
pub const HYPERDB_INTERNAL_PREFIX: &str = "_hyperdb_";

/// Returns true when `name` is one of `HyperDB`'s own internal tables
/// (matches [`HYPERDB_INTERNAL_PREFIX`]). Factored into a helper so
/// every filter site calls the same predicate and a future move to a
/// more nuanced scheme (e.g. per-table allowlist) is a single edit.
/// Reserved prefixes for short-lived scratch tables the server creates and
/// drops within a single tool call: `_tmp_` (`query_data` / `query_file`
/// staging) and `__hyperdb_merge_` (`load_file` merge staging). Filtered
/// from user-facing listings so an in-flight — or, after a mid-call
/// failure, leaked — scratch table never surfaces as if it were user data.
const INTERNAL_SCRATCH_PREFIXES: [&str; 2] = ["_tmp_", "__hyperdb_merge_"];

/// Returns true when `name` is one of `HyperDB`'s own internal tables:
/// a persistent internal table ([`HYPERDB_INTERNAL_PREFIX`]) or a
/// transient scratch table (`INTERNAL_SCRATCH_PREFIXES`). Factored into
/// a helper so every filter site calls the same predicate and a future
/// move to a more nuanced scheme (e.g. per-table allowlist) is a single
/// edit.
///
/// Note: `_table_catalog` lives in the persistent attachment, not the
/// ephemeral primary, so it doesn't show up in `describe_tables` even
/// without the filter — `describe_tables` only enumerates the primary.
#[must_use]
pub fn is_internal_table(name: &str) -> bool {
name.starts_with(HYPERDB_INTERNAL_PREFIX)
|| INTERNAL_SCRATCH_PREFIXES
.iter()
.any(|prefix| name.starts_with(prefix))
}

/// Compute the log directory for both `hyperd` output and the client-side
Expand Down Expand Up @@ -2724,6 +2736,19 @@ mod endpoint_description_tests {
mod tests {
use super::*;

#[test]
fn is_internal_table_covers_persistent_and_scratch_prefixes() {
// Persistent internals and transient scratch tables are hidden;
// ordinary user tables are not.
assert!(is_internal_table("_hyperdb_kv_store"));
assert!(is_internal_table("_tmp_data_123456789"));
assert!(is_internal_table("__hyperdb_merge_events_1_2_3"));
assert!(!is_internal_table("sales"));
assert!(!is_internal_table("data"));
// A user table merely containing "tmp" is not filtered.
assert!(!is_internal_table("tmp_sales"));
}

/// A lock conflict on a user-facing `attach_database` call must surface
/// as `RESOURCE_BUSY` (with doctor-oriented guidance), mirroring the
/// reserved persistent-attach path — the generic `From` conversion leaves
Expand Down
7 changes: 7 additions & 0 deletions hyperdb-mcp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ impl From<hyperdb_api::Error> for McpError {
}
return McpError::new(ErrorCode::SqlError, err.to_string());
}
"42P01" => {
// undefined_table — the query referenced a table that
// does not exist. Steer toward listing what's there
// rather than the generic "check your SQL" hint.
return McpError::new(ErrorCode::SqlError, err.to_string()).with_suggestion(
"Referenced table does not exist. Run `describe` (with the same `database` you queried) to list available tables; unquoted names fold to lowercase. In query_data / query_file the loaded rows are exposed as `table_name` (default `data`), e.g. `SELECT * FROM data`.");
}
_ => {} // fall through to message-based classification
}
}
Expand Down
32 changes: 18 additions & 14 deletions hyperdb-mcp/src/readme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ alias. `copy_query` also retains `target_database`.
fastest path when the user asks \"what's in this file?\".

### Load

**Format preference (ingest and export):** Parquet (fastest, server-side,
preserves every type incl. NUMERIC precision / DATE / TIMESTAMP — best for
large data) > Arrow IPC (very fast, no compression; schema overrides
rejected since its schema is authoritative) > CSV (portable but types are
inferred on load / lost on export) > JSON / JSONL (small or irregular data
only — parsed row-by-row). Iceberg is a directory of Parquet for data-lake
interop; `.hyper` export snapshots every table for Tableau.

- `load_file` — load one CSV / JSON / JSONL / Parquet / Arrow IPC file
into a named database table. `mode`: `replace` (default) /
`append` / `merge`. Use `merge` to upsert by `merge_key` (column
Expand Down Expand Up @@ -182,20 +191,15 @@ alias. `copy_query` also retains `target_database`.

### Export
- `export` — write a table or query result to a file (Parquet, Iceberg,
Arrow IPC, CSV, .hyper). Hyper export leaves the source database
unchanged, but creates or replaces the destination `.hyper` file and
materializes all user tables into it. Column constraints are carried
across: NOT NULL, DEFAULT, COLLATE, ASSUMED PRIMARY KEY, and ASSUMED
UNIQUE all survive the copy, so a `.hyper` export is a faithful backup
rather than a data-only dump. The response carries a `schema_fidelity`
Arrow IPC, CSV, .hyper). A `.hyper` export leaves the source unchanged
but creates/replaces the destination file and materializes every user
table into it — a faithful backup: NOT NULL, DEFAULT, COLLATE, ASSUMED
PRIMARY KEY, and ASSUMED UNIQUE all survive (Hyper never accepts
enforced PRIMARY KEY / UNIQUE / FOREIGN KEY / CHECK at CREATE TABLE, so
no source table carries those). The response carries a `schema_fidelity`
object (`fully_preserved` plus per-class counts and an `unpreserved`
list, each entry naming its `table` and `column`) — check it before
treating an export as a backup. Note that Hyper rejects
PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK at CREATE TABLE
(`Index support is disabled` / `check constraints not implemented
yet`), so no source table can carry those to begin with; ASSUMED
PRIMARY KEY and ASSUMED UNIQUE are the forms Hyper accepts, and it
records them without enforcing them.
list naming each `table` + `column`) — check it before trusting an
export as a backup.
- `chart` — render a bar / line / scatter / histogram PNG or SVG from a
SQL query as a quick diagnostic. Use long-format data (numeric y;
optional `series` grouping). See `Chart delivery and presentation`.
Expand Down Expand Up @@ -415,7 +419,7 @@ sample({ \"table\": \"sales\" })
query({ \"sql\": \"SELECT region, SUM(amount) FROM sales GROUP BY region\" })

// Cross-database join via attachment
attach_database({ \"alias\": \"lookup\", \"kind\": \"local_file\", \"path\": \"/data/dim.hyper\" })
attach_database({ \"alias\": \"lookup\", \"path\": \"/data/dim.hyper\" })
query({
\"sql\": \"SELECT s.region, d.country_name, SUM(s.amount) \
FROM sales s JOIN lookup.public.dim_region d ON s.region = d.code \
Expand Down
Loading
Loading