Run the MCP server yourself¶
The Dosi plugin starts dosi's MCP server for you in Claude Code and Codex. Start it yourself when you use another MCP client, when you do not want the plugin, or when several clients should share one server. Every client registers it the same way, with a command to start or a URL to call, so this page covers them all. Every tool, its input shape and the error contract are in the MCP reference.
Install dosi¶
This installs dosi and dosi-server into ~/.local/bin and the example
models into ~/.local/share/dosi/examples. In mainland China, add
DOSI_SOURCE=oss before sh.
To follow along without a warehouse, seed the bundled orders example into
a DuckDB file:
$ export DOSI_EXAMPLES=~/.local/share/dosi/examples
$ duckdb orders.duckdb < $DOSI_EXAMPLES/orders/seed.sql
Choose a transport¶
stdio (--mcp-stdio) |
Streamable HTTP (POST /mcp) |
|
|---|---|---|
| Who starts it | the client, as a child process, one per session | you, as a long-lived process |
| Good for | one local client, zero setup | shared servers, several clients, CI, containers |
| The model | followed: edits are picked up, and it may not exist yet | compiled once at startup |
| Auth | the process boundary | --auth-token, checked per request |
| Logs | stderr; stdout carries only the protocol | stdout and stderr as usual |
Both serve the same tools. Use stdio unless you need a shared server.
Over stdio¶
The client starts the server itself. Register it in the client's MCP configuration; most clients accept this shape:
{
"mcpServers": {
"dosi": {
"command": "dosi-server",
"args": ["--mcp-stdio", "--model", "dosi/model.yaml"]
}
}
}
Where that configuration lives, and its exact key names, depend on the
client: look up "MCP servers" in its documentation. Relative paths resolve
from the folder the client starts the server in, usually the project root.
If dosi-server is not on the client's PATH, give command an absolute
path, and never cargo run: under an MCP client a compile looks like a
hang.
The arguments that matter:
| Argument | What it does |
|---|---|
--model <file or dir> |
The model. Under --mcp-stdio the server follows it: it starts before the file exists, reloads after every edit, and answers no_model or model_invalid instead of exiting |
--db <file> |
A DuckDB file for queries that name no connection, such as the seeded orders.duckdb |
--connections <file> |
Named warehouse profiles. Without it, dosi looks for ./dosi-connections.yaml, then ~/.config/dosi/connections.yaml. See Connect a warehouse |
--env-file <file> |
KEY=VALUE lines that fill the ${VAR}s of the connections file. Re-read when it changes; values never appear in any output |
--ontology, --mapping |
An ontology and its mapping, followed the same way |
For the sample, the arguments are
["--mcp-stdio", "--model", "<examples>/orders/model.yaml", "--db", "orders.duckdb"],
with <examples> written out as an absolute path.
Over HTTP¶
Start one server and point every client at it:
$ dosi-server --model $DOSI_EXAMPLES/orders/model.yaml --db orders.duckdb
INFO dosi_server::bootstrap: model compiled model=.../orders/model.yaml mode="datus" datasets=3 metrics=5 generation=1
INFO dosi_server: listening on http://127.0.0.1:8081
MCP is mounted at POST /mcp, at the top level and not under /v1. Pass
--bind to listen elsewhere and --auth-token (env DOSI_SERVER_TOKEN)
for any server others can reach. Then register the URL:
{
"mcpServers": {
"dosi": {
"type": "http",
"url": "http://127.0.0.1:8081/mcp",
"headers": { "Authorization": "Bearer ${DOSI_SERVER_TOKEN}" }
}
}
}
The model is compiled once at startup, and an invalid one stops the server
before it listens. Restart it after you edit the model, or pass
--lazy-model to have it follow the files as stdio does.
Check it¶
Without an agent in the loop, the MCP Inspector lists the tools and calls them:
$ npx @modelcontextprotocol/inspector dosi-server --mcp-stdio \
--model $DOSI_EXAMPLES/orders/model.yaml --db orders.duckdb
Call list_metrics, then run_query with
{"metrics": ["revenue"], "group_by": [{"field": "orders.status"}]}: on the
sample, completed orders give 350 and cancelled ones 100. In your client,
the server's tools show up as mcp__dosi__<tool> or similar.
Decide what the agent may do¶
Every tool is marked read-only (readOnlyHint), so clients that run
read-only tools without asking, Codex's exec mode among them, can call
them unattended. dosi only generates SELECT statements, but filters and
parameter values are evaluated by the warehouse, and dosi does not check that
the functions they call are free of side effects. run_query, select and
attribute_metric reach the warehouse: give the connection a read-only
database user.
Two further dials, and they compose:
- Server side,
--disable-execute. The tools that reach the warehouse return aforbiddenerror; the agent can still discover and compile. This suits a shared, read-only deployment. - Client side, tool allowlists. Grant
compile_sqlandexplain_querybut notrun_queryfor a review workflow; grant all of them for analysis. A Dosi-only agent is an allowlist of thedosiserver's tools plus a denial of shell access.
Teach the agent to prefer metrics¶
Tools alone do not stop an agent from writing SQL by hand when it also has
shell access. These rules do. Put them in your AGENTS.md, CLAUDE.md, or
your client's equivalent:
## Data questions
Answer data questions through the `dosi` MCP tools, never by writing SQL
against raw tables.
1. Discover: `list_metrics`, then `list_dimensions` for the breakdown fields
(`dataset.field`, plus the reserved `metric_time`). Use `describe_metric`
when a metric's meaning matters.
2. Preview: `compile_sql` (or `explain_query`) before executing, and show me
the SQL you are about to run.
3. Execute: `run_query`. Omit `connection` for the server's local database, or
pick one from `list_connections`.
Rules:
- Never invent a metric or dimension name. If a name is rejected, read the
error's `candidates` / `suggested_retry` and retry with a valid one.
- Never re-derive a metric by hand — no ad-hoc `SUM`/`COUNT DISTINCT`, no
hand-written joins or filters. The metric definition is the governed answer,
and a hand-rolled equivalent will drift.
- If no metric covers the question, say so instead of approximating.
- Report the number the tool returned, plus the metric name and any filter
applied, so the answer is auditable.
- `run_query` caps rows (`row_limit_applied`). Never present a capped page as
a complete result — aggregate in the query instead.
This text is the same file our own evaluation runs use as the system prompt, so the documented behavior and the measured behavior cannot drift apart.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
run_query → table does not exist |
no --db and no connection: the local DuckDB is in memory and empty |
pass --db with a seeded file, or a connection |
no_model from every model tool |
--model names a file that does not exist yet (stdio) |
write the model; the next call picks it up |
| the client shows the server as failed | wrong path (/v1/mcp), server not running, cargo run still compiling, or an invalid model (HTTP) |
MCP is POST /mcp at the top level; use the installed binary; run dosi validate |
400 with an empty body |
missing Host header (DNS-rebinding protection) |
do not strip Host in proxies or hand-built requests |
401 on every call |
--auth-token set, client sends no bearer header |
add the header; auth is per request, not per session |
config error: "Conflicting lock is held" |
another process has the DuckDB file open for writing (dosi itself opens it read-only, so several servers can share one file) | close the writer, or give it its own copy of the file |
missing_env from list_connections |
a ${VAR} in the connections file is not set |
set it in the environment or the --env-file |
forbidden from run_query |
--disable-execute |
use compile_sql, or run a server without the flag |
busy / timeout |
execute semaphore saturated / execute timeout hit | raise --max-concurrent-executions / --execute-timeout-secs, or make the query cheaper |
validate_model fails on a large model |
/mcp bodies are capped at 1 MiB |
validate large models with dosi validate |
| results look truncated | default LIMIT 500, row_limit clamps at 5000 |
aggregate in the query; read row_limit_applied |
| the agent invents a metric name | no prompt rules in place | add the rules above |
Where to next¶
-
Every tool, its input shape, the protocol, and the error contract.
-
Move from the sample DuckDB file to Postgres, Snowflake, StarRocks, and more.
-
The server, the skills and the password guard in one install, for Claude Code, Codex and DeepSeek Harness.