hq-demo
OfficialAllows AI agents to query and analyze data stored in ClickHouse through a semantic layer of datasets, metrics, and derived metrics.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hq-demowhat was total revenue and order count by country last quarter?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hq-demo
A minimal Next.js (App Router) app showing hypequery end to end: a typed query builder, a semantic layer (datasets + metrics), a validated HTTP API, and fully typed React hooks calling it.
There is deliberately no dashboard UI — every result is logged (server console, browser console) and dumped as JSON on the page.
What's in here
File | What it shows |
Generated table → column → ClickHouse type map. Regenerate with | |
| |
Two datasets ( | |
Base metrics and a derived metric ( | |
The agent-safe catalog — what an LLM reads | |
| |
Mounts the API on the App Router | |
| |
Server component: builder, dataset, metric and in-process query results | |
Client component: the same data via hooks | |
Creates + fills the two demo tables |
Related MCP server: ClickHouse MCP Agent
Setup
npm install1. ClickHouse credentials
cp .env.example .env.localFill in:
CLICKHOUSE_URL=https://your-instance.clickhouse.cloud:8443
CLICKHOUSE_DATABASE=default
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=...No ClickHouse to hand? A throwaway local one works:
docker run -d --rm --name hq-demo-ch -p 8123:8123 clickhouse/clickhouse-server:latest…then use CLICKHOUSE_URL=http://localhost:8123, user default, empty password.
2. Seed the demo tables
npm run seedCreates orders (2,000 rows) and events (5,000 rows).
3. Run it
npm run devOpen http://localhost:3000. Server results are printed in the terminal running the dev server; hook results are printed in the browser console. Both are also rendered as JSON on the page.
Pointing it at your own tables
Regenerate schema types from your live database (writes
analytics/schema.ts):npm run generate:typesScaffold datasets from the real tables, then curate the names:
npm run generate:datasetsUpdate
analytics/metrics.tsandanalytics/api.tsto match.Regenerate the React route manifest (the hooks read it to resolve URLs):
npm run generate:manifest
Step 4 matters: lib/analytics.ts imports analytics/hypequery-manifest.json
rather than the serve API itself, so no server code — and no ClickHouse driver —
ends up in the browser bundle. Wire it into prebuild if you don't want to
remember it.
Where the types come from
Nothing in this repo hand-writes a result interface.
db.table('orders').select(['country'])— columns checked againstIntrospectedSchema, row type inferred.analytics.execute(Orders, { measures: ['revenue'] })— checked against the dataset definition.useDataset('events', { measures: ['revenue'] })— compile error:eventshaseventCountandusers, notrevenue.useMetric('recentOrders')— compile error: that's a query, not a metric.
Run npm run typecheck after breaking one of them on purpose.
Scripts
Script | Does |
| Next dev server |
| Production build |
|
|
| Create + fill the demo tables |
| Schema types from live ClickHouse |
| Scaffold dataset definitions from live ClickHouse |
| Rebuild the React route manifest |
| hypequery dev server with docs + OpenAPI at |
Filters: three different things
Metric definitions take no filters — Orders.metric() accepts only measure,
label, description and metadata. Filtering happens in three other places, and
they do different jobs:
Where | Example | Who it binds |
On a measure |
| Everyone. Baked into the definition, so "completed revenue" means one thing everywhere. Compiles to |
On a query |
| This call only. Compiles to |
On the dataset |
| Governs what a query filter is allowed to ask for. |
The dataset filters block is an allow-list, not a set of presets. It gives
you no named shorthand — you still pass { field, operator, value } at the call
site. What it does is restrict the vocabulary.
It also flips the default. Omit it and every dimension with
filterable !== false is filterable with any operator. Declare it and only the
listed fields and operators are accepted. In analytics/datasets.ts
customerId is deliberately excluded: an agent can group by customer, but cannot
probe for a specific one.
Verified against the running API:
Request | Result |
| 200 |
| 200 |
| 400 |
| 400 |
Query filters work on derived metrics too, pushed into the CTE rather than applied after aggregation:
WITH base AS (
SELECT country, SUM(if((status='refunded'), amount, 0)) AS refundedRevenue, …
FROM orders WHERE channel = ? GROUP BY country
)
SELECT country, (refundedRevenue) / (NULLIF(completedRevenue, 0)) AS refundRate FROM baseCaveat — the allow-list is runtime-only. Dimensions and measures are narrowed in the hook types, but filter fields and operators are not:
DatasetInstancehas noTFilterstype parameter, so the declaration is erased before the types are built.useMetric('revenue', { filters: [{ field: 'customerId', … }] })compiles cleanly and fails with a 400 on the wire.
Metadata for AI
Every dataset, dimension, measure and metric carries description plus
examples, synonyms, unit, currency, sensitivity, timezone and
freshness. This is what an agent reads to pick the right field, so the text is
written for someone who has never seen the table — including the traps
(revenue is gross, customers is non-additive across time buckets).
Validation is enforced at definition time: currency must be a three-letter
uppercase code, sensitivity is one of public / internal / confidential /
restricted, examples and synonyms must be duplicate-free, and each string
is capped at 4 KiB. A bad value throws when the dataset is defined, not on first
query. Note that examples and synonyms are deduplicated and sorted, so
don't encode meaning in their order.
Where it surfaces, measured on this project:
Surface | Carries descriptions |
| Yes, including per-metric metadata |
MCP | Yes |
| No — field names only |
| No |
See analytics/catalog.ts to print exactly what an agent
would see. It also documents the one sharp edge: metrics must be attached to the
catalog source explicitly ({ ...Orders, metrics: { ... } }) or they are
dropped silently. serve() handles this for you; MCP does not.
Explore the API without the app
npm run hq:devServes the whole API on port 4000 with docs and an OpenAPI document, no Next.js involved:
docs UI — http://localhost:4000/api/analytics/docs
full semantic contract — http://localhost:4000/api/analytics/contract
It registers 9 routes: the 4 metrics, 2 dataset query endpoints, recentOrders
(both its auto-registered /queries/recentOrders path and the explicit
/recent-orders one), and /contract. It watches for changes, so editing a
dataset re-registers immediately.
MCP — exposing this to an AI agent
The same analytics/api.ts becomes an MCP server with no extra code:
npm install @hypequery/mcp
npm run mcp:checkmcp:check runs --self-test: it loads the entrypoint, reports the datasets,
and exits without speaking MCP.
There is no localhost port to open. @hypequery/mcp speaks stdio only —
the MCP client spawns npm run mcp as a child process and talks to it over the
pipe. Running it in a terminal yourself just leaves a process waiting for
JSON-RPC on stdin. (createMCPProtocolServer is transport-neutral if you want to
wire an HTTP transport from the MCP SDK yourself, but nothing ships one.)
Claude Code
Already wired up — .mcp.json is committed, so all you need is:
cd /path/to/hq-demo && claudeClaude Code asks once whether to trust the project's MCP server; approve it, then
/mcp shows hq-demo connected. (claude mcp list will report
⏸ Pending approval until you have done this — project-scoped servers are never
auto-trusted, since a .mcp.json can arrive with a clone.)
If you would rather register it yourself, or want it outside this directory:
claude mcp add hq-demo --scope local -- npx hypequery mcp analytics/api.tsThen ask: "what datasets do you have, and what was net revenue by country?" It
will call list_datasets, then get_dataset_schema, then query_metric.
No credentials in the MCP config. The hypequery CLI loads .env.local
itself, so .mcp.json carries an empty env block and stays committable.
Verified by running the server with every CLICKHOUSE_* variable stripped from
its environment — query_metric still returned rows.
Claude Desktop, or any client reading JSON config
Claude Desktop does not run from this directory, so point it here explicitly:
{
"mcpServers": {
"hq-demo": {
"command": "npm",
"args": ["--prefix", "/absolute/path/to/hq-demo", "run", "--silent", "mcp"]
}
}
}--silent is not optional. Without it npm run prints > hq-demo@0.1.0 mcp
to stdout, which is the JSON-RPC channel. Measured — first stdout line:
Command | First stdout line |
|
|
|
|
Lenient clients skip the junk line; a strict one fails the handshake. This will bite any npm-script-wrapped MCP server, not just this one.
Seeing it in a browser
If you want a clickable UI rather than a chat client, the official MCP Inspector spawns the same stdio command and gives you a local web page to drive it:
npx @modelcontextprotocol/inspector npx hypequery mcp analytics/api.tsIt prints a http://localhost:6274 URL. That port belongs to the Inspector, not
to hypequery — the hypequery process underneath is still stdio.
Tools
Four tools are exposed: list_datasets, get_dataset_schema, query_metric,
query_dataset. No raw SQL tool — an agent can only ask for dimensions and
measures the datasets declare, so a prompt-injected "drop table" has nothing to
call. get_dataset_schema returns the descriptions and synonyms from
analytics/datasets.ts, which is how the agent picks the
right field.
For a tenant-scoped deployment, pin the tenant at the process boundary so the agent cannot choose it:
hypequery mcp analytics/api.ts --tenant acmeNot covered here
Multi-tenancy. Add
tenantKey: 'tenant_id'to a dataset and it fails closed until you supplyruntime.tenant. Over HTTP that means configuring tenant extraction oninitServe. The seeded tables already carry atenant_idcolumn if you want to try it.Relationships / joins between datasets (
belongsTo,hasMany).MCP.
@hypequery/mcpexposes these same datasets to agents as bounded tools rather than raw SQL.
This server cannot be deployed
Maintenance
Related MCP Connectors
Governed access to production AI-agent traces in an existing ClickHouse store.
The grounded data layer for any LLM: governed SQL, metrics, lineage and catalog over your data.
Agent-Native Amplitude/Mixpanel - connect data sources, prompt for charts
Ask data questions in natural language. Get SQL, insights, and charts from your databases.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to query and manage ClickHouse databases, supporting SELECT queries, DDL/DML statements, and metadata listing.55 npmMIT
- AlicenseAqualityCmaintenanceEnables querying ClickHouse databases using natural language with AI models, supporting multiple providers and access restrictions via per-call allow-lists.32MIT
- AlicenseNot gradedqualityDmaintenanceEnables executing SQL queries, listing databases, and listing tables on a ClickHouse cluster through natural language.Apache 2.0
- AlicenseAqualityCmaintenanceProvides AI assistants with semantic layer visibility and multi-dimensional querying capabilities over Cube.js data.25 npm1MIT