自己启动 MCP 服务¶
在 Claude Code 和 Codex 里,Dosi 插件会替你启动 dosi 的 MCP 服务。 用别的 MCP 客户端、不想装插件,或者想让几个客户端共用一个服务时,就自己启动。 所有客户端的注册方式都一样,要么给一条启动命令,要么给一个 URL,所以这一页对所有 客户端都适用。每个工具的入参形状和错误契约见 MCP 参考。
安装 dosi¶
这会把 dosi 和 dosi-server 装到 ~/.local/bin,示例模型装到
~/.local/share/dosi/examples。在中国大陆,在 sh 前面加上 DOSI_SOURCE=oss。
没有数仓也能跟着做:把自带的 orders 示例灌进一个 DuckDB 文件。
$ export DOSI_EXAMPLES=~/.local/share/dosi/examples
$ duckdb orders.duckdb < $DOSI_EXAMPLES/orders/seed.sql
选传输方式¶
stdio(--mcp-stdio) |
Streamable HTTP(POST /mcp) |
|
|---|---|---|
| 谁来启动 | 客户端拉起的子进程,每个会话一个 | 你自己,长驻进程 |
| 适合 | 单个本地客户端,零配置 | 共享服务、多客户端、CI、容器 |
| 模型 | 跟随文件:改动会自动加载,启动时模型可以还不存在 | 启动时编译一次 |
| 鉴权 | 进程边界 | --auth-token,每个请求都校验 |
| 日志 | 走 stderr,stdout 只有协议 | 照常 stdout/stderr |
两者提供相同的工具。除非需要共享服务,用 stdio 就行。
用 stdio¶
由客户端自己启动服务。把它注册到客户端的 MCP 配置里,多数客户端接受这种写法:
{
"mcpServers": {
"dosi": {
"command": "dosi-server",
"args": ["--mcp-stdio", "--model", "dosi/model.yaml"]
}
}
}
这份配置放在哪里、键名具体怎么写,因客户端而异,在它的文档里查“MCP servers”即可。
相对路径按客户端启动服务时所在的目录解析,通常就是项目根目录。若 dosi-server
不在客户端的 PATH 上,command 要写绝对路径;也绝不要指向 cargo run,在 MCP
客户端里,编译看起来就像卡死了。
常用参数:
| 参数 | 作用 |
|---|---|
--model <文件或目录> |
模型。在 --mcp-stdio 下服务会跟随它:文件还不存在时也能启动,每次改动后重新加载,出错时返回 no_model 或 model_invalid,而不是退出 |
--db <文件> |
不指定连接的查询用的 DuckDB 文件,比如上面灌好的 orders.duckdb |
--connections <文件> |
具名的数仓连接配置。不传时依次找 ./dosi-connections.yaml、~/.config/dosi/connections.yaml。见连接数仓 |
--env-file <文件> |
KEY=VALUE 格式,用来填连接配置里的 ${VAR}。文件改动后重新读取,值不会出现在任何输出里 |
--ontology、--mapping |
本体和它的映射,同样跟随文件 |
示例对应的参数是
["--mcp-stdio", "--model", "<examples>/orders/model.yaml", "--db", "orders.duckdb"],
<examples> 要写成绝对路径。
用 HTTP¶
启动一个服务,让所有客户端都连它:
$ 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 挂在顶层的 POST /mcp,不在 /v1 下面。要换监听地址传 --bind;别人能访问到
的服务都要加 --auth-token(环境变量 DOSI_SERVER_TOKEN)。然后注册这个 URL:
{
"mcpServers": {
"dosi": {
"type": "http",
"url": "http://127.0.0.1:8081/mcp",
"headers": { "Authorization": "Bearer ${DOSI_SERVER_TOKEN}" }
}
}
}
模型在启动时编译一次,模型不合法时服务在开始监听之前就退出。改了模型要重启服务,
或者加上 --lazy-model,让它像 stdio 那样跟随文件。
验证一下¶
不经过 Agent,用 MCP Inspector 就能列出工具并调用:
$ npx @modelcontextprotocol/inspector dosi-server --mcp-stdio \
--model $DOSI_EXAMPLES/orders/model.yaml --db orders.duckdb
先调 list_metrics,再用
{"metrics": ["revenue"], "group_by": [{"field": "orders.status"}]} 调 run_query:
在示例数据上,已完成订单是 350,已取消的是 100。在客户端里,这些工具的名字一般是
mcp__dosi__<工具名> 这样的形式。
决定 Agent 能做什么¶
所有工具都标为只读(readOnlyHint),所以会自动放行只读工具的客户端(比如 Codex 的
exec 模式)可以无人值守地调用它们。dosi 只生成 SELECT 语句,但过滤条件和参数值
由数仓计算,dosi 不检查其中调用的函数有没有副作用。run_query、select 和
attribute_metric 会访问数仓:请给连接配一个只读的数据库账号。
另外还有两个开关,可以叠加:
- 服务端:
--disable-execute。会访问数仓的工具都返回forbidden错误,发现和 编译仍然可用。共享的只读部署就该这样。 - 客户端:工具白名单。评审场景只放行
compile_sql和explain_query、不放行run_query;分析场景全放开。“只用 Dosi”的 Agent,就是放行dosi服务的工具, 再禁掉 shell。
让 Agent 优先走指标¶
光有工具,拦不住一个同时还有 shell 权限的 Agent 自己去写 SQL。下面这几条规则可以。
把它放进 AGENTS.md、CLAUDE.md,或你的客户端对应的系统提示词里:
## 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.
这段文字与我们自己评测时用的系统提示词是同一个文件,所以文档写的行为和实测的行为 不会各走各的。
排错¶
| 现象 | 原因 | 处理 |
|---|---|---|
run_query 报表不存在 |
没传 --db 也没指定连接,本地 DuckDB 是空的内存库 |
传一个灌好数据的 --db 文件,或者指定连接 |
每个模型工具都返回 no_model |
--model 指向的文件还不存在(stdio) |
写出模型,下一次调用就能用 |
| 客户端显示服务连接失败 | 路径写成了 /v1/mcp、服务没起、cargo run 还在编译,或者模型不合法(HTTP) |
MCP 在顶层的 POST /mcp;用安装好的二进制;运行 dosi validate |
400 且响应体为空 |
缺 Host 头(DNS 重绑定防护) |
代理或手搓请求时不要剥掉 Host |
每次调用都 401 |
设了 --auth-token,客户端没带 bearer 头 |
补上;鉴权是每请求的,不是每会话 |
config 错误:Conflicting lock is held |
别的进程以读写方式开着这个 DuckDB 文件(dosi 自己只读打开,多个服务可以共用一个文件) | 关掉那个写入的进程,或者让它用自己的副本 |
list_connections 显示 missing_env |
连接配置里有 ${VAR} 没设置 |
在环境变量或 --env-file 里设置 |
run_query 返回 forbidden |
--disable-execute |
改用 compile_sql,或起一个不带该参数的服务 |
busy / timeout |
执行信号量占满 / 触发执行超时 | 调大 --max-concurrent-executions、--execute-timeout-secs,或把查询写轻 |
validate_model 在大模型上失败 |
/mcp 请求体上限 1 MiB |
大模型改用 dosi validate 校验 |
| 结果看起来被截断 | 默认 LIMIT 500,row_limit 上限 5000 |
在查询里做聚合;读 row_limit_applied |
| Agent 编造指标名 | 没有配提示词规则 | 加上前面那段规则 |