Serve any AI a Malloy semantic model over MCP — accurate, consistent answers from your data.
32
stars
412
commits
TypeScript
primary language
Sep 14, 2026
updated
A natural-language interface to any corpus of data, however complex — accurate and consistent, served to any AI over MCP.
Problem: AI + document context + your analytical database = inconsistent results. Pointed at a raw database, an AI writes SQL from scratch — so the same question tomorrow yields a different query and different numbers, with wrong joins, invented columns, or fan-out double-counts that still look right.
Solution: AI + a Malloy semantic layer + your analytical database = consistent results. Measures, dimensions, and joins are defined once, correctly; the AI composes queries against the model instead of writing SQL, so numbers come back right by construction.
Malloyyo is the thin layer that serves that model:
malloyyo publish it (or point Malloyyo at a GitHub repo).Try the demo server and "Explore further with Claude" — sign in with any Google account.
Questions, or built something cool? We'd love to hear from you. Come say hi on Slack, and learn more about Malloy at malloydata.dev and in the documentation.
MCP client (claude.ai) Browser (you)
│ OAuth 2.1 │ Google sign-in
┌───────▼──────────┐ ┌─────────▼─────────┐
│ MCP server /mcp │ │ Web UI │
│ analytical tools │ │ datasets · ltool │
└───────┬──────────┘ └─────────┬─────────┘
└──────────────┬────────────────┘
│ compile · run ┌────────────────────┐
│ │ Authoring Models │
│ │ • Malloyyo CLI │
│ │ • Claude │
│ └─────┬───────────┬──┘
│ push │ │ develop
┌──────────────────────▼───────────────────────┐ (deploy)| │
│ Malloyyo │◀────────┘ │
│ load · compile · store · serve │ │
└──────┬─────────────────────────────┬─────────┘ │
│ │ │
┌──────▼──────┐ ┌────────▼────────┐ │
│ Neon │ │ Analytical DB │◀─────────────────────┘
│ Postgres │ │ • BigQuery │ direct (dev)
│ metadata │ │ • MotherDuck │
│ │ │ • Snowflake │
│ datasets │ │ • Databricks │
│ malloy_ │ │ (or S3/GCS) │
│ models │ │ │
│ users │ │ your data │
└─────────────┘ └─────────────────┘
Develop your model in a repo with an index.malloy at its root, then publish it with the malloyyo CLI.
First tell the repo where it publishes to. Nothing writes this for you — malloy-config.json is yours, and the CLI only ever reads it — so add a malloyyo block beside your connections:
{
"connections": { "warehouse": { "is": "postgres", "host": "…" } },
"malloyyo": {
"targets": { "prod": { "url": "https://your-instance.example.com", "dataset": "orders" } }
}
}
Then:
malloyyo login # one-time browser sign-in
malloyyo publish # bundle *.malloy + malloy-config.json and push
Both take a target name (malloyyo publish prod) when the repo defines more than one.
The dataset must exist first — create it in the UI, or add --create-dataset to the first
publish (it creates a private dataset, and only once the model compiles).
In CI there is no browser to sign in with, so mint an API token instead — in the UI at
/settings/tokens (any member can, for themselves) — and hand it over in the environment:
export MALLOYYO_TOKEN=myo_… # instead of `malloyyo login`
malloyyo publish
A token is shown once, carries the scopes you tick (publish, mcp), expires when you say
so or never, and is revocable from the same page — effective on the very next request.
See API tokens for the CI recipe and the rest.
The CLI records the git commit it published from; Malloyyo compiles and introspects the model and stores a new version. If it doesn't compile, the push is rejected and the live model is left unchanged.
Alternatively, point Malloyyo at a GitHub repo and it pulls index.malloy (and any
imports) plus dashboard artifacts under dashboards/ directly — a webhook endpoint
(/api/datasets/<id>/webhook/github) refreshes them on every push.
| Database | What lives there |
|---|---|
| Cloud database (BigQuery, Snowflake, MotherDuck, MySQL, Postgres, Presto, Trino) or S3/GCS | Your analytical data |
| Neon Postgres | Metadata — datasets, malloy_models, malloy_model_files, users, accounts, sessions, OAuth clients and tokens |
/mcp| Tool | What it does |
|---|---|
list_sources | List the Malloy sources you can query on this endpoint |
describe_source | A source's semantic model — measures, dimensions, views, joins |
query | Run a Malloy query; returns rows + a shareable link (execute: false for SQL only) |
open_share_link | Resolve a shared link back to its source, question, and Malloy |
The MCP endpoint speaks OAuth 2.1, so claude.ai's remote MCP integration can connect after a one-time authorization flow.
You don't have to know Malloy — or wire up the database by hand. Install the malloyyo CLI, register it with Claude, then ask Claude to do the rest: connect to your data, turn your existing SQL / dbt / Looker definitions into a Malloy model, and test it against real data before you publish. One tool for the whole loop.
Want none of this on your laptop? Open your model repo in a GitHub Codespace on the prebuilt Malloyyo dev container — Claude Code, the Malloy and Claude VS Code extensions, the
malloyyoCLI, Node, Playwright andgcloud/bqare already in the image.malloyyo initwrites the one-line.devcontainer/devcontainer.jsonthat opts a repo in, and its codespace starts in seconds.
1. Install and set up your model repo.
npm install -g @malloydata/malloyyo # one tool for the whole loop
cd my-model-repo
malloyyo init # write .mcp.json + scaffold index.malloy
malloyyo init sets the repo up so that cd my-model-repo && claude opens in author mode — connected to malloyyo mcp --develop, whose tools (compile, compile_file, prettify, query, yo_help) let Claude build the model and ask data questions of it. The server is named malloyyo_author, so the mode shows in every tool call (mcp__malloyyo_author__…) — no confusion with the core malloy-cli. init also scaffolds an index.malloy (the entry model the dashboard/publish tooling needs) if you don't have one.
Two modes, chosen at launch, kept separate:
| command | surface | for |
|---|---|---|
cd repo && claude (after init) | author (--develop) | build the model — compile/edit/query, any .malloy file |
malloyyo test | explore (--explore) | preview exactly what claude.ai web will see (list_sources/describe_source/query, index.malloy only) |
malloyyo test launches Claude wired to only the explore surface, so it's a faithful web dress rehearsal. (malloyyo author is the explicit, single-surface counterpart to the cd && claude default.) Full authoring guidance — dashboards, givens/controls, grid layout, Vega charts — is reachable from either mode via yo_help (topics under dashboards/*, develop/*). For Claude Desktop or another client, add malloyyo mcp --develop (author) or malloyyo mcp --explore (test) to its MCP config.
2. Build your model with Claude. Start claude in your model directory and just describe what you have:
claude
> connect to my Postgres warehouse and build a Malloy model from these dbt sources
> add a "net revenue" measure and verify it against last quarter's numbers
Claude sets up the connection and writes the .malloy for you. It follows a compiler-in-the-loop discipline — validate against the compiler, run real queries through the test window, confirm the numbers — and you steer from the results. You rarely write Malloy by hand.
The connection lives in malloy-config.json at the model root, and Claude knows how to write it (yo_help("connection-setup"), or the connection config docs). Two things worth knowing:
malloy-config.json, an in-memory duckdb connection just works, so Claude can read your local CSV / Parquet files immediately. Other backends (BigQuery, Postgres, MotherDuck, Snowflake, Databricks, MySQL, Trino, Presto) get a small malloy-config.json entry.{ "env": "VAR_NAME" } (e.g. ANALYTICAL_DATABASE_SECRET), resolved from the environment when the connection opens. The same malloy-config.json ships to production on publish, so put anything environment-specific behind { "env": … } and set that var locally and on the Malloyyo server.3. Publish. Once the model queries cleanly, sign in once and push:
malloyyo login <your-instance> # one-time browser sign-in
malloyyo publish <your-instance> # bundle *.malloy + malloy-config.json and push
The server compiles and introspects the model and stores a new version; a compile failure rejects the push and leaves the live model unchanged. (Or push to GitHub and point Malloyyo at the repo.) See packages/cli/README.md for the CLI.
@malloydata/malloy + @malloydata/db-duckdb) — semantic layermalloyyo CLI (packages/cli) — publish models from a repo over an OAuth-authenticated push endpointThe button forks the repo into your GitHub and creates a Vercel project. The schema
creates and upgrades itself at boot (migrations are on by default in production;
set RUN_MIGRATIONS_ON_BOOT=0 only if you want to manage the schema yourself), so
you never run a migration. The import screen prompts for these env vars:
DATABASE_URL — a Postgres connection string (you can get a free instance from
neon.tech). The build needs it, so paste one here.
Prefer Vercel-managed storage? Finish the import with a temporary value, then add
Postgres under the project's Storage tab — it overwrites DATABASE_URL — and
redeploy.AUTH_SECRET — openssl rand -base64 32.APP_ADMIN_EMAILS — your email (admins add datasets / publish).INSTANCE_NAME / INSTANCE_CODE — a display name + a short, unique slug (e.g. gld).AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET — for sign-in; you can leave these blank now
and fill them after the first deploy (next step).APP_BASE_URL — your deployment's URL, e.g. https://<yourproject>.vercel.app.Then enable Google sign-in: create a Google OAuth app (Google Cloud Console →
Credentials → Web application), set its authorized redirect URI to
https://<your-domain>/api/auth/callback/google, put the client ID/secret + your
APP_BASE_URL into the project's env vars, and redeploy.
Prefer Okta or Microsoft Entra ID (Azure AD) sign-in, or want the full details? See Authentication.
After that, sign in with the admin email and add a dataset.
Malloyyo sends a small allowlist of anonymized, server-side product-usage events by
default. It never sends query text, SQL, results, URLs, email addresses, or raw user IDs.
Self-hosters can stop all Malloyyo product telemetry by setting
MALLOYYO_TELEMETRY_DISABLED=1, or inspect the sanitized events without sending them by
setting MALLOYYO_TELEMETRY_DEBUG=1. See Product telemetry for the
exact event/property allowlist, privacy boundaries, and pseudonymous identities.
Your model's
malloy-config.jsonreferences your analytical database's secret from an env var (e.g.ANALYTICAL_DATABASE_SECRET— see Developing Malloy models). Set that var on the project so the server can connect.GITHUB_TOKENis optional (private-repo model pulls).
Prefer containers to Vercel? The repo ships a production Dockerfile. See
Self-hosting with Docker for build and run instructions.
Copy .env.local.example to local/<instance> and fill in the blanks:
DATABASE_URL=postgresql://... # Neon (or any Postgres)
APP_BASE_URL=http://localhost:3000
APP_ADMIN_EMAILS=you@example.com
AUTH_SECRET=... # openssl rand -base64 32
AUTH_GOOGLE_ID=... # Google OAuth client ID
AUTH_GOOGLE_SECRET=... # Google OAuth client secret
# GITHUB_TOKEN=github_pat_... # Optional; needed for private repos
# MALLOYYO_TELEMETRY_DISABLED=1 # Optional; self-hosted product telemetry opt-out
Google sign-in needs a Google OAuth app — Google Cloud Console → APIs & Services →
Credentials → Create OAuth client ID → type Web application. Put its client
ID/secret into AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET, and add this Authorized redirect
URI:
http://localhost:3000/api/auth/callback/google
(Miss this and Google rejects sign-in with redirect_uri_mismatch.)
Okta and Microsoft Entra ID (Azure AD) sign-in are also supported — see Authentication for all three.
npm install
npx dotenv-cli -e local/main -- npx tsx scripts/run-boot-migrations.ts # first run only
npx dotenv-cli -e local/main -- npm run dev
Open http://localhost:3000.
packages/cli + src/app/api/datasets/[id]/model/push — the malloyyo publish path: the CLI bundles and uploads model files; the route compiles, introspects, and stores a versioned model (git provenance in malloy_models.git_*).src/lib/github.ts + src/lib/github-refresh.ts — the GitHub pull path: model loading and webhook-triggered refresh.src/lib/malloy.ts — single-file and multi-file Malloy compilation and execution via InMemoryURLReader.src/lib/mcp-tools.ts + src/app/mcp/route.ts — the MCP server. Tools are pure functions; the route is a JSON-RPC dispatcher.src/db/schema.ts — Drizzle schema for all Postgres tables.Hacker News (1)
TypeScript
95.2%
Shell
1.9%
PLpgSQL
1.0%
Serve any AI a Malloy semantic model over MCP — accurate, consistent answers from your data.
32
stars
412
commits
TypeScript
primary language
Sep 14, 2026
updated
A natural-language interface to any corpus of data, however complex — accurate and consistent, served to any AI over MCP.
Problem: AI + document context + your analytical database = inconsistent results. Pointed at a raw database, an AI writes SQL from scratch — so the same question tomorrow yields a different query and different numbers, with wrong joins, invented columns, or fan-out double-counts that still look right.
Solution: AI + a Malloy semantic layer + your analytical database = consistent results. Measures, dimensions, and joins are defined once, correctly; the AI composes queries against the model instead of writing SQL, so numbers come back right by construction.
Malloyyo is the thin layer that serves that model:
malloyyo publish it (or point Malloyyo at a GitHub repo).Try the demo server and "Explore further with Claude" — sign in with any Google account.
Questions, or built something cool? We'd love to hear from you. Come say hi on Slack, and learn more about Malloy at malloydata.dev and in the documentation.
MCP client (claude.ai) Browser (you)
│ OAuth 2.1 │ Google sign-in
┌───────▼──────────┐ ┌─────────▼─────────┐
│ MCP server /mcp │ │ Web UI │
│ analytical tools │ │ datasets · ltool │
└───────┬──────────┘ └─────────┬─────────┘
└──────────────┬────────────────┘
│ compile · run ┌────────────────────┐
│ │ Authoring Models │
│ │ • Malloyyo CLI │
│ │ • Claude │
│ └─────┬───────────┬──┘
│ push │ │ develop
┌──────────────────────▼───────────────────────┐ (deploy)| │
│ Malloyyo │◀────────┘ │
│ load · compile · store · serve │ │
└──────┬─────────────────────────────┬─────────┘ │
│ │ │
┌──────▼──────┐ ┌────────▼────────┐ │
│ Neon │ │ Analytical DB │◀─────────────────────┘
│ Postgres │ │ • BigQuery │ direct (dev)
│ metadata │ │ • MotherDuck │
│ │ │ • Snowflake │
│ datasets │ │ • Databricks │
│ malloy_ │ │ (or S3/GCS) │
│ models │ │ │
│ users │ │ your data │
└─────────────┘ └─────────────────┘
Develop your model in a repo with an index.malloy at its root, then publish it with the malloyyo CLI.
First tell the repo where it publishes to. Nothing writes this for you — malloy-config.json is yours, and the CLI only ever reads it — so add a malloyyo block beside your connections:
{
"connections": { "warehouse": { "is": "postgres", "host": "…" } },
"malloyyo": {
"targets": { "prod": { "url": "https://your-instance.example.com", "dataset": "orders" } }
}
}
Then:
malloyyo login # one-time browser sign-in
malloyyo publish # bundle *.malloy + malloy-config.json and push
Both take a target name (malloyyo publish prod) when the repo defines more than one.
The dataset must exist first — create it in the UI, or add --create-dataset to the first
publish (it creates a private dataset, and only once the model compiles).
In CI there is no browser to sign in with, so mint an API token instead — in the UI at
/settings/tokens (any member can, for themselves) — and hand it over in the environment:
export MALLOYYO_TOKEN=myo_… # instead of `malloyyo login`
malloyyo publish
A token is shown once, carries the scopes you tick (publish, mcp), expires when you say
so or never, and is revocable from the same page — effective on the very next request.
See API tokens for the CI recipe and the rest.
The CLI records the git commit it published from; Malloyyo compiles and introspects the model and stores a new version. If it doesn't compile, the push is rejected and the live model is left unchanged.
Alternatively, point Malloyyo at a GitHub repo and it pulls index.malloy (and any
imports) plus dashboard artifacts under dashboards/ directly — a webhook endpoint
(/api/datasets/<id>/webhook/github) refreshes them on every push.
| Database | What lives there |
|---|---|
| Cloud database (BigQuery, Snowflake, MotherDuck, MySQL, Postgres, Presto, Trino) or S3/GCS | Your analytical data |
| Neon Postgres | Metadata — datasets, malloy_models, malloy_model_files, users, accounts, sessions, OAuth clients and tokens |
/mcp| Tool | What it does |
|---|---|
list_sources | List the Malloy sources you can query on this endpoint |
describe_source | A source's semantic model — measures, dimensions, views, joins |
query | Run a Malloy query; returns rows + a shareable link (execute: false for SQL only) |
open_share_link | Resolve a shared link back to its source, question, and Malloy |
The MCP endpoint speaks OAuth 2.1, so claude.ai's remote MCP integration can connect after a one-time authorization flow.
You don't have to know Malloy — or wire up the database by hand. Install the malloyyo CLI, register it with Claude, then ask Claude to do the rest: connect to your data, turn your existing SQL / dbt / Looker definitions into a Malloy model, and test it against real data before you publish. One tool for the whole loop.
Want none of this on your laptop? Open your model repo in a GitHub Codespace on the prebuilt Malloyyo dev container — Claude Code, the Malloy and Claude VS Code extensions, the
malloyyoCLI, Node, Playwright andgcloud/bqare already in the image.malloyyo initwrites the one-line.devcontainer/devcontainer.jsonthat opts a repo in, and its codespace starts in seconds.
1. Install and set up your model repo.
npm install -g @malloydata/malloyyo # one tool for the whole loop
cd my-model-repo
malloyyo init # write .mcp.json + scaffold index.malloy
malloyyo init sets the repo up so that cd my-model-repo && claude opens in author mode — connected to malloyyo mcp --develop, whose tools (compile, compile_file, prettify, query, yo_help) let Claude build the model and ask data questions of it. The server is named malloyyo_author, so the mode shows in every tool call (mcp__malloyyo_author__…) — no confusion with the core malloy-cli. init also scaffolds an index.malloy (the entry model the dashboard/publish tooling needs) if you don't have one.
Two modes, chosen at launch, kept separate:
| command | surface | for |
|---|---|---|
cd repo && claude (after init) | author (--develop) | build the model — compile/edit/query, any .malloy file |
malloyyo test | explore (--explore) | preview exactly what claude.ai web will see (list_sources/describe_source/query, index.malloy only) |
malloyyo test launches Claude wired to only the explore surface, so it's a faithful web dress rehearsal. (malloyyo author is the explicit, single-surface counterpart to the cd && claude default.) Full authoring guidance — dashboards, givens/controls, grid layout, Vega charts — is reachable from either mode via yo_help (topics under dashboards/*, develop/*). For Claude Desktop or another client, add malloyyo mcp --develop (author) or malloyyo mcp --explore (test) to its MCP config.
2. Build your model with Claude. Start claude in your model directory and just describe what you have:
claude
> connect to my Postgres warehouse and build a Malloy model from these dbt sources
> add a "net revenue" measure and verify it against last quarter's numbers
Claude sets up the connection and writes the .malloy for you. It follows a compiler-in-the-loop discipline — validate against the compiler, run real queries through the test window, confirm the numbers — and you steer from the results. You rarely write Malloy by hand.
The connection lives in malloy-config.json at the model root, and Claude knows how to write it (yo_help("connection-setup"), or the connection config docs). Two things worth knowing:
malloy-config.json, an in-memory duckdb connection just works, so Claude can read your local CSV / Parquet files immediately. Other backends (BigQuery, Postgres, MotherDuck, Snowflake, Databricks, MySQL, Trino, Presto) get a small malloy-config.json entry.{ "env": "VAR_NAME" } (e.g. ANALYTICAL_DATABASE_SECRET), resolved from the environment when the connection opens. The same malloy-config.json ships to production on publish, so put anything environment-specific behind { "env": … } and set that var locally and on the Malloyyo server.3. Publish. Once the model queries cleanly, sign in once and push:
malloyyo login <your-instance> # one-time browser sign-in
malloyyo publish <your-instance> # bundle *.malloy + malloy-config.json and push
The server compiles and introspects the model and stores a new version; a compile failure rejects the push and leaves the live model unchanged. (Or push to GitHub and point Malloyyo at the repo.) See packages/cli/README.md for the CLI.
@malloydata/malloy + @malloydata/db-duckdb) — semantic layermalloyyo CLI (packages/cli) — publish models from a repo over an OAuth-authenticated push endpointThe button forks the repo into your GitHub and creates a Vercel project. The schema
creates and upgrades itself at boot (migrations are on by default in production;
set RUN_MIGRATIONS_ON_BOOT=0 only if you want to manage the schema yourself), so
you never run a migration. The import screen prompts for these env vars:
DATABASE_URL — a Postgres connection string (you can get a free instance from
neon.tech). The build needs it, so paste one here.
Prefer Vercel-managed storage? Finish the import with a temporary value, then add
Postgres under the project's Storage tab — it overwrites DATABASE_URL — and
redeploy.AUTH_SECRET — openssl rand -base64 32.APP_ADMIN_EMAILS — your email (admins add datasets / publish).INSTANCE_NAME / INSTANCE_CODE — a display name + a short, unique slug (e.g. gld).AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET — for sign-in; you can leave these blank now
and fill them after the first deploy (next step).APP_BASE_URL — your deployment's URL, e.g. https://<yourproject>.vercel.app.Then enable Google sign-in: create a Google OAuth app (Google Cloud Console →
Credentials → Web application), set its authorized redirect URI to
https://<your-domain>/api/auth/callback/google, put the client ID/secret + your
APP_BASE_URL into the project's env vars, and redeploy.
Prefer Okta or Microsoft Entra ID (Azure AD) sign-in, or want the full details? See Authentication.
After that, sign in with the admin email and add a dataset.
Malloyyo sends a small allowlist of anonymized, server-side product-usage events by
default. It never sends query text, SQL, results, URLs, email addresses, or raw user IDs.
Self-hosters can stop all Malloyyo product telemetry by setting
MALLOYYO_TELEMETRY_DISABLED=1, or inspect the sanitized events without sending them by
setting MALLOYYO_TELEMETRY_DEBUG=1. See Product telemetry for the
exact event/property allowlist, privacy boundaries, and pseudonymous identities.
Your model's
malloy-config.jsonreferences your analytical database's secret from an env var (e.g.ANALYTICAL_DATABASE_SECRET— see Developing Malloy models). Set that var on the project so the server can connect.GITHUB_TOKENis optional (private-repo model pulls).
Prefer containers to Vercel? The repo ships a production Dockerfile. See
Self-hosting with Docker for build and run instructions.
Copy .env.local.example to local/<instance> and fill in the blanks:
DATABASE_URL=postgresql://... # Neon (or any Postgres)
APP_BASE_URL=http://localhost:3000
APP_ADMIN_EMAILS=you@example.com
AUTH_SECRET=... # openssl rand -base64 32
AUTH_GOOGLE_ID=... # Google OAuth client ID
AUTH_GOOGLE_SECRET=... # Google OAuth client secret
# GITHUB_TOKEN=github_pat_... # Optional; needed for private repos
# MALLOYYO_TELEMETRY_DISABLED=1 # Optional; self-hosted product telemetry opt-out
Google sign-in needs a Google OAuth app — Google Cloud Console → APIs & Services →
Credentials → Create OAuth client ID → type Web application. Put its client
ID/secret into AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET, and add this Authorized redirect
URI:
http://localhost:3000/api/auth/callback/google
(Miss this and Google rejects sign-in with redirect_uri_mismatch.)
Okta and Microsoft Entra ID (Azure AD) sign-in are also supported — see Authentication for all three.
npm install
npx dotenv-cli -e local/main -- npx tsx scripts/run-boot-migrations.ts # first run only
npx dotenv-cli -e local/main -- npm run dev
Open http://localhost:3000.
packages/cli + src/app/api/datasets/[id]/model/push — the malloyyo publish path: the CLI bundles and uploads model files; the route compiles, introspects, and stores a versioned model (git provenance in malloy_models.git_*).src/lib/github.ts + src/lib/github-refresh.ts — the GitHub pull path: model loading and webhook-triggered refresh.src/lib/malloy.ts — single-file and multi-file Malloy compilation and execution via InMemoryURLReader.src/lib/mcp-tools.ts + src/app/mcp/route.ts — the MCP server. Tools are pure functions; the route is a JSON-RPC dispatcher.src/db/schema.ts — Drizzle schema for all Postgres tables.Hacker News (1)
TypeScript
95.2%
Shell
1.9%
PLpgSQL
1.0%