Skip to content

MCP server

dosi-server speaks the Model Context Protocol natively, so AI agents (Claude Code, datus-agent, any MCP client) can discover metrics and dimensions, compile metric queries to dialect SQL, and execute them — without an HTTP shim in between. The MCP tools call the same engine seam as the REST API and return the same JSON shapes and structured errors as dosi --format json.

Protocol: targets MCP spec 2026-07-28 and serves it statelessly end to end — no initialize handshake, no Mcp-Session-Id header, client metadata rides per-request _meta. Legacy (pre-2026-07-28) clients are also served without sessions, and simple request/response tool calls get plain application/json replies. Any replica behind a load balancer can serve any request; nothing about a conversation lives on the server.

Transports

Streamable HTTP (primary)

Every running dosi-server mounts MCP at POST /mcp, beside the REST API:

cargo run -p dosi-server -- --model fixtures/orders/model.yaml
curl -s -X POST localhost:8081/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

When --auth-token (env DOSI_SERVER_TOKEN) is set, /mcp requires Authorization: Bearer <token> like the rest of the API — evaluated on every request, which is exactly what a stateless deployment wants. Claude Code remote config:

claude mcp add --transport http dosi http://localhost:8081/mcp \
  --header "Authorization: Bearer $DOSI_SERVER_TOKEN"

stdio (local single-client)

--mcp-stdio serves MCP over stdin/stdout instead of binding HTTP — the standard way for a local agent to spawn the server as a child process. Logs go to stderr; stdout carries only the protocol.

claude mcp add dosi -- \
  cargo run -p dosi-server -- --model /path/to/model.yaml --mcp-stdio

All the usual flags apply (--connections, --db, --disable-execute, --osi-basic, ...); both transports serve the identical tool set.

Tools

Tool Input Returns
list_datasets datasets: name, source, primary key, fields, time dimensions
list_metrics metrics: name, kind, datasets, measures, time dimension, description
list_dimensions dataset.field names with time flags and granularities
describe_metric name one metric's row; unknown names return candidates
list_connections named warehouse profiles: name, dialect, availability
get_capabilities engine mode, supported dialects, execute enabled, model stats
compile_sql metric query + dialect, pretty {dialect, sql} without executing
explain_query metric query the logical plan as text
run_query metric query + dialect, connection, row_limit {dialect, sql, columns, rows, row_count, row_limit_applied}
validate_model model_yaml {valid, issues, compile_errors, warnings}

The metric-query inputs are the exact MetricQuery wire shape the REST API and CLI use (metrics, group_by as [{field, grain}], where_sql, time_range, order_by, limit) — the tool schemas are generated from the same types, so they cannot drift.

Execution safeguards

run_query shares the server's execute machinery: the concurrency semaphore (--max-concurrent-executions), the execute timeout, and --disable-execute (which turns run_query into a tool-level forbidden error). Because results land in an LLM context, rows are capped: a query with no limit gets LIMIT 500, and row_limit clamps at 5000; the applied cap is reported as row_limit_applied. Without a connection, queries run on the server's local DuckDB (--db file or in-memory); name a profile from list_connections to run on a warehouse.

Errors

Engine failures come back as tool-level errors carrying the engine's structured JSON — the same stable code, candidates, and suggested_retry fields as the REST API and CLI:

{"error": {"code": "unknown_metric", "message": "unknown metric \"revenu\"",
           "candidates": ["revenue"], "suggested_retry": {"metrics": ["revenue"]}}}

Agents should read the error and correct the query rather than retrying unchanged; the server's MCP instructions say so too.

Trying it interactively

npx @modelcontextprotocol/inspector \
  cargo run -p dosi-server -- --model fixtures/orders/model.yaml --mcp-stdio