Skip to content

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

$ curl -fsSL https://dosi.datus.ai/install.sh | sh

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 a forbidden error; the agent can still discover and compile. This suits a shared, read-only deployment.
  • Client side, tool allowlists. Grant compile_sql and explain_query but not run_query for a review workflow; grant all of them for analysis. A Dosi-only agent is an allowlist of the dosi server'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

  • MCP reference


    Every tool, its input shape, the protocol, and the error contract.

  • Connect a warehouse


    Move from the sample DuckDB file to Postgres, Snowflake, StarRocks, and more.

  • The Dosi plugin


    The server, the skills and the password guard in one install, for Claude Code, Codex and DeepSeek Harness.