README.md
August 6, 2026 · View on GitHub
Malloy Publisher
The open-source semantic model server for Malloy
Serve governed data models to applications, BI tools, and AI agents — over REST and MCP.
A 60-second walkthrough — model in your IDE with the Malloy skills, serve with Publisher, build a data app, materialize on a schedule, and analyze. Watch the video for playback controls.
When an AI queries your database directly, it writes its own SQL — and gets it subtly wrong: the wrong join, an invented column, a fan-out that double-counts but still looks plausible. Publisher puts a Malloy semantic layer in front of your data, where measures, dimensions, and joins are defined once, correctly. Applications, BI tools, and AI agents compose queries against that model instead of writing raw SQL, so the numbers come back right by construction. Agents work through the sources the model defines — not your raw tables — and you decide exactly what each caller can see.
Point Publisher at your Malloy models and it serves them over a REST API and a single MCP endpoint.
Requirements
Node.js 20 or newer. Publisher refuses to start on anything older, printing both the version it
needs and the version it found. Every @malloydata/* dependency requires the same floor, as does
this repository, so older runtimes are untested and have failed in ways that never mention Node.
Building from a clone also needs Bun 1.3.13 or newer. The Docker image carries its own runtime and needs neither.
Quick start
npx @malloy-publisher/server --port 4000
Open http://localhost:4000 and explore the bundled example packages —
storefront (a complete ecommerce model),
governed-analytics (access control), and
html-data-app (a no-build dashboard) —
all DuckDB-backed, no credentials required. Give the server a moment to report serving:
curl -s http://localhost:4000/api/v0/status | jq .operationalState # → "serving"
A first npx run fetches the example packages from GitHub and reports its download progress on
stderr. When the server is ready it prints a single line, also to stderr, that scripts can wait for
instead of polling:
PUBLISHER_READY url=http://localhost:4000 mcp=http://localhost:4040 environments=1 packages=3 load_errors=0
load_errors counts configured packages and environments that failed to load. When it is not 0,
/api/v0/status names each one under .loadErrors. The url= host reads localhost when the
server binds every interface (the default); a configured --host shows as itself. If initialization
fails, a PUBLISHER_INIT_FAILED line is printed in its place; a startup failure outside
initialization, like a port already in use, crashes without either token. On a Node older than 20
the server prints PUBLISHER_UNSUPPORTED_NODE required=>=20 detected=<version> and exits non-zero
before it binds anything, so a script waiting on PUBLISHER_READY fails fast instead of hanging.
Start from your own data
The command above serves the bundled examples. To build a package around your own data instead, scaffold one:
mkdir my-data && cd my-data
npm create @malloy-publisher/malloy-package@latest sales
npm start
Keep the @latest. npm create resolves the scaffolder through npm's npx cache, and on a machine
that has run the command before, an unversioned name is satisfied by whatever copy is already
cached, so npm never asks the registry. Drop it and you can quietly scaffold from a months-old
scaffolder that pins an older server than the one you meant to run. The scaffolder checks its own
version against the registry once it has finished writing and tells you when it is behind; that
check is bounded and fails open, it is skipped where CI or NO_UPDATE_NOTIFIER is set,
and CREATE_MALLOY_PACKAGE_NO_UPDATE_CHECK=1 turns it off anywhere else.
Make the directory first. The package lands in ./sales, but the workspace around it is written to
the current directory, so running this somewhere you did not mean to scatters config files through
it. That workspace is start and reset scripts, an MCP config, agent instructions, and the Malloy
agent skills as files your agent can read. npm start runs the server version the scaffolder pinned,
against your package, in watch mode, so edits to the model take effect as you save them.
Run bare like that, the package comes with a small sample dataset, so there is something to query
before you have wired up anything of your own. To start from one of your own files instead, pass
--data (CSV, Parquet, JSON, newline-delimited JSON, or Excel .xlsx - DuckDB reads all of them
in place, so nothing needs converting first):
npm create @malloy-publisher/malloy-package@latest sales -- --data ./orders.csv
That path is relative to the directory you run the command in, so either move your file there first or point at wherever it already lives. Either way the scaffolder copies it into the package, so the original stays where it is.
The -- is required. Without it, npm create reads --data as one of its own options and only the
filename reaches the scaffolder, as a stray argument, so it stops.
A seeded package starts smaller than the sample one, which is worth knowing before you go looking for what is missing: the scaffolder does not read your columns, so you get a row count and an overview over your file, and the modelling starts there. That is the point at which pointing an agent at the workspace pays off.
A package is just Malloy, so it is not limited to a local file: point its model at a database connection your config defines and the same workspace serves a warehouse. The directory it creates is ordinary, so you can commit it, move it, or hand it to someone else.
Either way the server serves your package rather than the bundled examples: npm start points it at
the publisher.config.json the scaffolder wrote, and a bare npx @malloy-publisher/server run from
this directory picks up the same file. The walkthrough in the next section is written against the
examples, so run that one from a directory without this config.
To run the scaffolder without npm create, call the package by its full name:
npx @malloy-publisher/create-malloy-package@latest sales --data ./orders.csv. Note that the name is
create-malloy-package here, where npm create takes the malloy-package shorthand, that the same
caching applies so @latest is worth keeping, and that npx needs no separator: it forwards flags as
they are, so a -- there leaves the flags after it to arrive as stray arguments.
Point your agent at it
This is the fast path to the "wow." In a directory you just made for the purpose there is nothing
to configure; in a directory inside a git repo, which most real projects are, the server tells you
the one command to run instead. On
startup the server writes a .mcp.json into the directory you ran it in, pointing at the MCP port it
actually bound. In one terminal:
npx @malloy-publisher/server --port 4000 --host 127.0.0.1 # writes ./.mcp.json unless one of the cases below applies
and in a second terminal, in that same directory:
claude
Your agent may ask you to trust the folder, to use the server it found, and to approve the first tool call. Say yes, then ask it a question about the data.
The file is only read by a session that started in that directory, so launch your agent there.
It also stays on disk after you stop the server, and is never corrected. That matters more than a
broken link: if something else later holds that port, perhaps a second Publisher serving different
data, an agent started there connects to it and answers confidently from the wrong model. Nothing marks
the file as the server's, so do not go hunting for ones to delete: a .mcp.json may be your own, and it
may hold other servers and their credentials. If you made a scratch directory for this, deleting the
directory is enough. If you are ever unsure what an agent is connected to, ask it to run
malloy_getContext, which names the environment and packages it is actually talking to.
When the server does not write one. It skips a directory that already has a .mcp.json, anything
inside a git working tree (so, usually, your own project), your home directory, and a few other cases.
You do not have to memorise them: whenever it skips, it says so in the startup log and prints the one
command that connects an agent anyway. The full list is in
docs/configuration.md.
--no-mcp-config turns the whole thing off, as does PUBLISHER_NO_MCP_CONFIG=1.
To register the server for yourself rather than for one directory, so the tools are there whichever directory you launch from:
claude mcp add --transport http malloy http://127.0.0.1:4040/mcp -s user
That is also the fix when an agent reports no malloy_* tools. Two things cause it: the session
started somewhere other than the directory holding the config, or there is no config there because
the server skipped one of the cases above. Check the server's startup log, which says which,
and ls -a to see whether the file is there at all. Other MCP clients take the same endpoint through
their own config file; see docs/ai-agents.md.
Then just ask, in plain English:
"Use Malloy to explore the storefront sales data and chart revenue by category."
The agent discovers what data exists (malloy_getContext), grounds itself in the real source, view,
and field names, runs the query (malloy_executeQuery), and returns an answer backed by your
semantic model. No schema spelunking, no hallucinated column names.
- Trust the directory first. This is a second gate, separate from connecting the server: in a
workspace nobody has trusted yet, Claude Code lists the
malloy_*tools and then refuses every call, and a.claude/settings.jsonallowlist is discarded rather than merged. Start Claude Code interactively there once and answer the trust prompt, asked once per directory. A headless run is never asked, so it cannot clear the gate either. You will know it cleared when a query returns data. - Agents: this repo ships an AGENTS.md and a bundled skill library
(
skills/) that most AI coding hosts auto-discover. Start there. - Any MCP client (Cursor, VS Code, Codex, Claude Desktop): see docs/ai-agents.md for per-client config and the stdio bridge.
The server, MCP and REST alike, is stateless and unauthenticated, and it can read any data your models connect to. Bind it to loopback (
--host 127.0.0.1) for local use, and put an authenticating gateway in front before exposing it more widely.
No MCP client, or an agent running unattended that started the server itself? The same loop is available over plain REST:
curl -s -X POST \
http://localhost:4000/api/v0/environments/examples/packages/storefront/models/storefront.malloy/query \
-H 'content-type: application/json' \
-d '{"query":"run: order_items -> by_category","compactJson":true}' | jq -r .result
A package is just a directory with a publisher.json and a .malloy model;
docs/packages.md is the format reference. The running server serves its full
OpenAPI spec at http://localhost:4000/api-doc.yaml, and docs/ai-agents.md
covers agents in both modes, MCP and REST.
What you can do
- Explore, no code. Build and drill into queries visually with Malloy Explorer — every action generates valid Malloy, so metrics stay correct even across joins.
- Answer questions with AI. Connect an agent over MCP and ask in plain English — see above and docs/ai-agents.md.
- Surface analytics your way. Explore and share with zero code in the Publisher App, or ship a no-build HTML data app that Publisher hosts inside a package.
- Build & validate models. Author Malloy models guided by the bundled skills, then
publish them for serving. Agents get the same loop over MCP:
malloy_compilechecks an edit and returns diagnostics without running it, andmalloy_reloadPackagerecompiles a package from disk so a new source or view is queryable by name, no restart. - Govern access. Givens are one runtime-parameter mechanism that powers filter
widgets, row-level access (which rows a caller sees), and
#(authorize)source gates (who can query). Separately, curate what is discoverable and queryable. - Materialize for cost & speed. Persist an expensive source into a table with
#@ persist, then rebuild it on demand or on a cron with the opt-in standalone scheduler — see docs/materialization.md and themalloy-pub schedule/list materializationCLI.
Documentation
The docs/ folder is the reference hub — see its index. Highlights:
| Topic | Doc |
|---|---|
| Runnable example packages | examples/ (storefront · governed-analytics · html-data-app · data-app) |
| Architecture & how it fits together | docs/architecture.md |
| REST & MCP API overview | docs/api-overview.md |
The package format (publisher.json, models, data) | docs/packages.md |
| The Publisher App (navigation & features) | docs/publisher-app.md |
| No-code visual query builder | docs/explorer.md |
| Connect an AI agent (MCP, or REST when unattended) | docs/ai-agents.md |
| Build a custom UI (no build step) | docs/html-data-apps.md |
| Runtime parameters & access control | givens (base) · row-level · authorize · discovery |
| Deploy (npx / Docker / Compose) | docs/deployment.md |
| Database connections | docs/connections.md |
| Materialization & scheduling | docs/materialization.md |
| Docker runtime deep-dive (layout, env, tuning) | packages/server/README.docker.md |
| Theming (light/dark, palette) | docs/theming.md |
| Configuration & tuning reference | docs/configuration.md |
| Build & develop from a clone | docs/development.md |
The complete user guide also lives at docs.malloydata.dev.
Contributing
Build and hack on Publisher from a clone with docs/development.md; contribution process and sign-off are in CONTRIBUTING.md.
Community
- Join the Malloy Slack
- Report issues on GitHub
- Report a security vulnerability privately — see SECURITY.md for the reporting form and what's in scope
