1pub const README: &str = "\
12# HyperDB MCP
13
14## What this is
15
16HyperDB MCP is an in-process SQL analytics service powered by the Tableau
17Hyper database engine. Load data from CSV / JSON / JSONL / Parquet /
18Arrow IPC / Apache Iceberg, query with PostgreSQL-compatible SQL
19(Salesforce Data Cloud SQL dialect), and export results to Parquet,
20Iceberg, Arrow IPC, CSV, or .hyper.
21
22## When to use this MCP
23
24Whenever the user asks to analyze tabular data, run SQL, transform a
25file, or build a chart from a query. Prefer this MCP over ad-hoc Python
26or shell pipelines: it parses files faster, runs SQL natively, and keeps
27intermediate state in a workspace database the LLM can re-query without
28re-loading.
29
30## Workspace model — queryable memory
31
32Every session has TWO databases, plus optional user-attached ones:
33
34- **Ephemeral primary** (default destination). Created fresh per
35 session, deleted on exit. Unqualified SQL routes here. Use as
36 scratch space for exploratory work, intermediate transformations,
37 and one-off analysis the user doesn't need to keep.
38- **Persistent database** (alias `\"persistent\"`). Survives across
39 sessions — this is your **long-term structured memory**. Store
40 reference tables, accumulated results, user preferences, learned
41 facts, or any data you want to recall in future conversations.
42 Unlike flat-text memory, persistent data is **queryable**: you can
43 JOIN, filter, aggregate, and reason over it with SQL. Disabled
44 when the server runs with `--ephemeral-only`.
45- **User-attached writable databases** via `attach_database` with
46 `writable: true`. Each lives in its own `.hyper` file under a
47 user-chosen alias.
48
49### Persistent as memory — when and how to use it
50
51Store data in persistent whenever:
52- The user says \"remember this\", \"save this\", \"keep this\"
53- You produce a useful reference table (lookups, configs, mappings)
54- You accumulate results across multiple conversations
55- You want to recall context in future sessions
56
57Retrieve from persistent whenever:
58- You need context from a prior session
59- The user asks \"what do we have?\" or \"show me what's saved\"
60- You want to JOIN current scratch work against historical data
61
62```
63// Save something for later
64load_data({ table: \"project_decisions\", data: \"[...]\", persist: true })
65
66// Recall it next session
67query({ sql: \"SELECT * FROM project_decisions\", database: \"persistent\" })
68
69// Cross-reference: join session scratch with persistent memory
70query({ sql: \"SELECT s.*, p.decision FROM scratch_analysis s \
71 JOIN \\\"persistent\\\".\\\"public\\\".\\\"project_decisions\\\" p \
72 ON s.topic = p.topic\" })
73```
74
75### Routing data to a destination
76
77- **`database` parameter** (preferred for tools that build their own
78 SQL): `query`, `execute`, `load_data`, `load_file`, `load_files`,
79 `watch_directory`, `describe`, `sample`, `chart`, `export`, and
80 `set_table_metadata` accept `database: \"persistent\"`,
81 `database: \"local\"` (= primary), or any user-attached writable
82 alias. Case-insensitive. Defaults to primary.
83- **`persist: true` shorthand** on `load_data`, `load_file`,
84 `load_files`, `watch_directory` — equivalent to
85 `database: \"persistent\"`.
86- **Fully-qualified SQL** for power users:
87 `INSERT INTO \"persistent\".\"public\".\"customers\" SELECT ...`
88
89Each writable database carries its own `_table_catalog` table that
90tracks load tool, params, timestamps, and any prose metadata set via
91`set_table_metadata` — lazily seeded on first ingest into that DB.
92`detach_database` rejects with `InvalidArgument` if any active
93watcher targets the alias; call `unwatch_directory` first.
94
95## Tool index
96
97### Query
98- `query` — run a read-only SELECT / WITH / EXPLAIN / SHOW / VALUES.
99- `execute` — run DDL / DML (CREATE, INSERT, UPDATE, DELETE, DROP,
100 ALTER, COPY). Disabled in read-only mode.
101- `query_data` — ingest inline JSON or CSV and run one SQL query in a
102 single call (table is temporary).
103- `query_file` — same as `query_data` but reads from a file path. The
104 fastest path when the user asks \"what's in this file?\".
105
106### Load
107- `load_file` — load one CSV / JSON / JSONL / Parquet / Arrow IPC file
108 into a named workspace table. `mode`: `replace` (default) /
109 `append` / `merge`. Use `merge` to upsert by `merge_key` (column
110 name or list); new columns in the incoming file are auto-added via
111 `ALTER TABLE`.
112- `load_files` — load many files in parallel. Files must share a
113 schema (or be unioned). `merge` mode is not supported here — call
114 `load_file` per-file if you need merge.
115- `load_data` — load inline JSON / CSV into a named workspace table.
116- `load_iceberg` — load an Apache Iceberg table by absolute path to its
117 root directory; supports snapshot pinning via `metadata_filename` or
118 `version_as_of`.
119
120### Inspect
121- `describe` — list workspace tables (no args) or describe one table
122 (`table` arg) with columns, types, row count, and prose metadata.
123- `sample` — return schema + first N rows of a table. Use this before
124 writing a non-trivial query.
125- `inspect_file` — dry-run schema inference on a CSV / Parquet / Arrow
126 IPC file without loading it.
127- `status` — plugin health, workspace path, table count, total rows,
128 disk usage, watchers, attached databases, read-only flag.
129
130### Export
131- `export` — write a table or query result to a file (Parquet, Iceberg,
132 Arrow IPC, CSV, .hyper).
133- `chart` — render a bar / line / scatter / histogram PNG from a SQL
134 query. Data must be long-format (one numeric y column; use a `series`
135 column for grouping). On line/scatter charts, DATE / TIMESTAMP /
136 TIMESTAMPTZ x columns auto-detect to a **proportional time axis**
137 (real-world gaps reflected in spacing); TEXT x falls back to evenly
138 spaced categorical mode. Pass `x_as_category: true` to force
139 categorical even on temporal data. Wide-format data must be reshaped
140 with UNION ALL.
141- `copy_query` — run a SELECT across local + attached databases and
142 insert the result into a target table (`mode`: `create`, `append`,
143 `replace`). Cross-database analytics in one tool call.
144
145### Saved queries & metadata
146- `save_query` — save a named read-only SQL query for later reuse.
147- `delete_query` — delete a named saved query.
148- `set_table_metadata` — update prose metadata (source_url, purpose,
149 notes, license, source_description) on a table catalog entry.
150
151### Multi-database
152- `attach_database` — attach an additional .hyper database under an
153 alias. Pass `writable: true` to allow writes through it.
154- `detach_database` — detach a previously attached database.
155- `list_attached_databases` — list current attachments.
156
157### Directory watching
158- `watch_directory` — watch a directory and auto-ingest matching files
159 as they appear.
160- `unwatch_directory` — stop watching a previously registered
161 directory.
162
163### Introspection
164- `get_readme` — this document. Call once at the start of a session.
165
166## Parameter rules
167
168- **File paths must be absolute.** Relative paths are rejected.
169- **Identifiers fold to lowercase** unless double-quoted. `SELECT * FROM
170 Sales` reads `sales`. Use `\"Sales\"` to preserve case.
171- **`query` is read-only.** SELECT / WITH / EXPLAIN / SHOW / VALUES
172 only. For DDL / DML use `execute`.
173- **Read-only mode** (`--read-only` flag on the server) disables:
174 `execute`, all `load_*`, writable `attach_database`, `save_query`,
175 `delete_query`, `set_table_metadata`, `copy_query`, `watch_directory`,
176 `unwatch_directory`. `query`, `describe`, `sample`, `inspect_file`,
177 `export`, `chart`, `status`, `list_attached_databases`, and
178 `get_readme` always work.
179- **Table names** in `load_*` and `query_data` / `query_file` accept
180 unquoted identifiers; the server lowercases them.
181- **`copy_query` modes:** `create` requires the target not exist;
182 `append` requires it does; `replace` drops and recreates atomically.
183- **`load_file` merge mode:** `mode = \"merge\"` requires `merge_key`
184 (column name or list of column names). Rows whose key matches an
185 existing row UPDATE; non-matching rows INSERT. Columns present in
186 the incoming file but not the target are auto-added via
187 `ALTER TABLE ADD COLUMN` (nullable; existing rows fill with NULL).
188 **Type changes on existing columns are rejected** — use `replace`
189 or apply a `schema` override. The DELETE+INSERT pair is not
190 transactional (Hyper auto-commits DDL); a mid-run failure leaves
191 partial state, same as `replace`.
192
193## SQL dialect quick-reference
194
195PostgreSQL-compatible with Salesforce Data Cloud SQL extensions. Key
196differences from standard PostgreSQL:
197
198- **No `information_schema` / `pg_catalog`.** Use `describe` / `sample`
199 instead.
200- **No JSON / JSONB / UUID / SERIAL / BIGSERIAL / geometry types.**
201 Atomic types only: SMALLINT, INTEGER, BIGINT, REAL, DOUBLE PRECISION,
202 NUMERIC(p,s), BOOLEAN, TEXT, CHAR(n), VARCHAR(n), BYTES, DATE, TIME,
203 TIMESTAMP, TIMESTAMPTZ, INTERVAL, plus arrays of any atomic type.
204- **`external(path, format => '...')`** — read Parquet / CSV / Iceberg
205 directly from disk inside a query without first loading it as a
206 table. Usable in the FROM clause.
207- **`APPROX_COUNT_DISTINCT(expr)`** — fast approximate cardinality.
208- **Window functions:** all standard ones plus `modified_rank()` (like
209 `rank()` but assigns the LOWEST rank on ties). `IGNORE NULLS` /
210 `RESPECT NULLS` only on `last_value`.
211- **`DISTINCT ON (expr, ...)`, `GROUPING SETS`, `ROLLUP`, `CUBE`,
212 `FILTER (WHERE ...)`, ordered-set aggregates (`MODE()`,
213 `PERCENTILE_CONT()`, `PERCENTILE_DISC()` with `WITHIN GROUP`)** all
214 supported.
215- **CTEs:** `WITH` and `WITH RECURSIVE`. CTEs evaluate once per query.
216- **`TOP N`** is accepted alongside `LIMIT`.
217- **No AI scalar functions** (`AI_CLASSIFY`, `AI_SENTIMENT`, etc. — those
218 are Data Cloud federation features, not Hyper).
219
220Full reference: https://developer.salesforce.com/docs/data/data-cloud-query-guide/references/dc-sql-reference
221
222## Examples
223
224```
225// Quickest path: query a file without loading it as a table
226query_file({
227 \"path\": \"/tmp/sales.csv\",
228 \"sql\": \"SELECT region, SUM(amount) FROM data GROUP BY region\"
229})
230
231// Inspect a file before committing to a load
232inspect_file({ \"path\": \"/tmp/sales.csv\" })
233
234// Loaded-table workflow (best when you'll run multiple queries)
235load_file({ \"path\": \"/tmp/sales.csv\", \"table\": \"sales\" })
236sample({ \"table\": \"sales\" })
237query({ \"sql\": \"SELECT region, SUM(amount) FROM sales GROUP BY region\" })
238
239// Cross-database join via attachment
240attach_database({ \"alias\": \"lookup\", \"path\": \"/data/dim.hyper\" })
241query({
242 \"sql\": \"SELECT s.region, d.country_name, SUM(s.amount) \
243 FROM sales s JOIN lookup.dim_region d ON s.region = d.code \
244 GROUP BY s.region, d.country_name\"
245})
246
247// Read Parquet directly inside a query — no load step
248query({
249 \"sql\": \"SELECT COUNT(*) FROM external('/tmp/events.parquet', format => 'parquet')\"
250})
251
252// Export a query result
253export({
254 \"sql\": \"SELECT * FROM sales WHERE amount > 1000\",
255 \"path\": \"/tmp/big_sales.parquet\",
256 \"format\": \"parquet\"
257})
258
259// Refresh existing rows + auto-add new columns (upsert by job_id).
260// Use this when you re-parsed source data with extra fields and want
261// to update the table in place without dropping it.
262load_file({
263 \"path\": \"/tmp/extract_failures-with-host.json\",
264 \"table\": \"extract_timing_failures\",
265 \"mode\": \"merge\",
266 \"merge_key\": \"job_id\"
267})
268
269// Chart
270chart({
271 \"sql\": \"SELECT region, SUM(amount) AS total FROM sales GROUP BY region\",
272 \"path\": \"/tmp/sales_by_region.png\",
273 \"chart_type\": \"bar\"
274})
275```
276
277## Tips for picking the right tool
278
279- One-shot \"what's in this file?\" → `query_file` or `inspect_file`.
280- Repeated analysis on the same data → `load_file` once, then `query`.
281- File too large to fit in memory comfortably → `external()` inside
282 `query` (streams from disk).
283- Joining datasets across .hyper files → `attach_database` + `query`.
284- Materializing a query result as a new table → `copy_query`.
285- Need a picture for the user → `chart`.
286- Re-parsed source data with new columns and want to update existing
287 table in place → `load_file` with `mode: \"merge\"`.
288";