Blog
/
AI & ML
Monty Lennie
Software Engineer @ Credible

Tutorial: Query any data with an agent, through Malloy

This is the hands-on follow-up to our post on open-sourcing the agent skills and tools. Here you will stand up an agent that answers data questions against a Malloy model, see the loop it runs, and get the reasoning behind how it retrieves context. By the end you will have it working against real data in a few minutes, no model of your own required.

If the terms "package," "source," or "model" are new, the anchor post has a short primer (and Malloy's docs go much deeper). The one-line version: a Malloy model defines your data's meaning (sources, dimensions, measures, views), and Malloy Publisher serves it over an API and over MCP, the protocol agents use to call tools.

Set it up

Publisher runs from a single command, and it loads example packages on startup, so you have real data to query immediately:

npx @malloy-publisher/server

That gives you an ecommerce store, an IMDB movie dataset, and FAA flight data to ask questions of. The server brings up two MCP endpoints on its own, nothing to wire by hand: the agent endpoint on port 4041 for the retrieval tools (malloy_getContext, malloy_searchDocs), and Publisher's core endpoint on port 4040 for running queries and reading models (malloy_executeQuery, malloy_modelGetText). The agent uses both together.

What you configure is your own agent: point its MCP client at both endpoints. In most clients that means adding this to the MCP config file:

{
  "mcpServers": {
    "malloy-publisher": {
      "url": "http://localhost:4040/mcp"
    },
    "malloy-publisher-agent": {
      "url": "http://localhost:4041/mcp"
    }
  }
}

(Today the retrieval tools and the query tool live on separate endpoints. We may fold them onto one standalone endpoint later; for now, configure both.)

Worth saying plainly: this is not a local-only toy. The same Publisher server you just ran is what serves models in production. Two things change on that path, and both are out of scope for this tutorial. First, the localhost config above only works for agents that run on your machine and can reach it, like Claude Code, Claude Desktop, and Cursor; cloud-hosted assistants such as claude.ai and chatgpt.com run in the provider's cloud and cannot see your localhost, so they need Publisher reachable at a real URL. Second, open-source Publisher does not ship an authentication layer, so exposing it beyond your own machine means putting your own auth or gateway in front. This post stays local, where neither applies.

One scoping note on tools: here you point an agent at a model that already exists and ask questions of it, which needs no VS Code extension or CLI. Those come in when you start authoring your own models, the subject of a later post.

The loop

Ask the agent a data question and it runs this loop:

  1. It loads the relevant skill (analysis, charts) so it knows the workflow.
  2. It calls malloy_getContext with your question to find the right source, views, and fields.
  3. It reads the model text to confirm the exact names.
  4. It writes Malloy using only those names, calling malloy_searchDocs if it is unsure of syntax.
  5. It runs the query with malloy_executeQuery, then sanity-checks the result before answering: is the scope what you would expect, are the magnitudes plausible, do the parts reconcile with the whole. (Malloy's own semantics prevent a lot of classic errors, like fan-out from a join, so the agent checks less than it would against raw SQL.)

malloy_getContext and malloy_searchDocs are the two tools we added; malloy_executeQuery and the model-reading tool are already part of Publisher. The agent uses all of them together.

Two examples

The clean case. Ask for top product categories by revenue against the ecommerce package. The agent finds the category dimension and the total_sales measure on the order_items source, runs the prebuilt top_categories view, and reports Jeans at roughly $1.95M, about 64% ahead of the runner-up. No invented fields, and a quick check that the totals reconcile.

The case that shows why retrieval matters. Ask "top carriers by departure delay" against the FAA flight data. The field is named dep_delay, which shares no words with "departure delay," so a naive keyword match against the question misses it. The two-phase retrieval handles it: the agent discovers the flights source, drills into it, sees dep_delay in the field list, and uses it. This is the difference between handing the agent a source and asking it to guess, and giving it a way to actually find what the model defines.

Why lexical retrieval, on purpose

What malloy_getContext searches is small and curated: a model's sources, fields, and their short documentation, where the words in a good question tend to overlap the words a modeler chose for the names and the docs. So it uses classic keyword search (BM25 via lunr) rather than vector embeddings. Keyword search matches that overlap directly, runs in-process with no extra service to stand up, and adds no dependency or cold-start.

The choice of BM25 matters less than the shape around it. Retrieval runs in two phases: phase one finds the right source, phase two reads that source's fields, so a field like dep_delay gets found through the source it lives on even when its name does not echo the question. A single literal match against the whole model would miss it. How well that holds as a single package grows to hundreds of sources and fields is exactly what we want to measure, since phase one, landing the right source before the fields come into play, is where the pressure shows up first. If you have pushed this at scale, we would love to compare notes.

Embeddings earn their keep when the vocabulary gap is wide or the corpus is huge, neither of which is the common case for one package's worth of curated sources and fields. When that changes, an embedding backend can slot in behind the same tool, as something you opt into rather than a cost everyone pays by default.

Go build something

Spin it up, point your agent at it, and throw a messy, plain-English question at the example data. Watch it find the right fields, write the Malloy, and check its own work.

  • Try your own model next: connect a warehouse and serve it with Publisher (the subject of the next post in the series).
  • Write a skill for your team's patterns and send a PR via the contributor guide.
  • Tell us how it goes: join the Malloy community Slack, open an issue, and tell us where it shines and where it falls short. (New to Publisher? Start at docs.malloydata.dev.)

That loop, discover, confirm, run, sanity-check, is what turns an agent from a confident guesser into something you can trust with a real question.