MCP 服务¶
dosi-server 原生支持
Model Context Protocol,
所以 AI Agent(Claude Code、Codex、OpenCode,或任何 MCP 客户端)可以发现指标和维度、
把指标查询编译成方言 SQL 并执行,中间不需要一层 HTTP 垫片。
这些 MCP 工具调用的是与 REST API 相同的引擎接缝,
返回的 JSON 形态和结构化错误也与 dosi --format json 一致。
一方的最佳搭档是 datus-agent —— Datus 自己的 data agents,用来端到端地完成数据工程与数据分析:建模、管道,以及在其之上的分析。 Dosi 为它们提供指标层,因此 Agent 算出来的数字,与整套技术栈其余部分用的是同一份定义。
协议:面向 MCP 规范 2026-07-28,端到端无状态:
没有 initialize 握手,没有 Mcp-Session-Id 头,客户端元数据随每个请求的
_meta 一起携带。旧版(2026-07-28 之前)客户端同样以无会话方式服务,
简单的请求/响应式工具调用会得到普通的 application/json 回复。
负载均衡后的任何副本都能服务任何请求,会话状态一律不驻留在服务端。
想找上手指南?
本页是参考手册。要把 Dosi 接进 Agent,先看 在 Agent 里使用 Dosi,或者挑一篇客户端全流程走一遍: Claude Code、Codex、 OpenCode。
传输方式¶
Streamable HTTP(主要方式)¶
每个运行中的 dosi-server 都会把 MCP 挂在 POST /mcp 上(顶层路径,
不在 /v1 之下),与 REST API 并列:
export DOSI_EXAMPLES=~/.local/share/dosi/examples
dosi-server --model $DOSI_EXAMPLES/orders/model.yaml --db orders.duckdb
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"}'
这个路径就是安装脚本放内置示例模型的地方; 换成任何一份 Apache Ossie(原 OSI)模型文件都一样能跑。
/mcp 上还有两条请求层面的规则:必须带 Host 头(rmcp 的 DNS 重绑定防护,
缺失时返回 400 且没有响应体);请求体上限 1 MiB,实际只会限制
validate_model,更大的模型改用 dosi validate 校验。
设置了 --auth-token(环境变量 DOSI_SERVER_TOKEN)时,/mcp 会像 API 其余部分
一样要求 Authorization: Bearer <token>,每个请求都校验,
这正是无状态部署需要的。Claude Code 的远程配置:
claude mcp add --transport http dosi http://localhost:8081/mcp \
--header "Authorization: Bearer $DOSI_SERVER_TOKEN"
stdio(本地单客户端)¶
--mcp-stdio 通过 stdin/stdout 提供 MCP,不绑定 HTTP,
这是本地 Agent 把服务作为子进程拉起的标准做法。日志走 stderr,
stdout 上只有协议内容。
claude mcp add dosi -- \
dosi-server --model /path/to/model.yaml \
--db /path/to/warehouse.duckdb --mcp-stdio
客户端要指向安装好的二进制(若 dosi-server 不在客户端的 PATH 上就写绝对
路径),绝不要指向 cargo run——MCP 客户端分不清编译和卡死。
常规的那些参数都适用(--connections、--db、--disable-execute、
--osi-basic 等);两种传输方式提供完全相同的工具集。
服务端只暴露工具,没有 MCP resources,也没有 prompts;此外还带一段
instructions,说明"发现 → 预览 → 执行"的工作流,并要求客户端读结构化错误、
不要原样重试。
工具¶
| 工具 | 输入 | 返回 |
|---|---|---|
list_datasets |
— | 数据集:名称、来源、主键、字段、时间维度 |
list_metrics |
— | 指标:名称、种类、数据集、度量、时间维度、描述 |
list_dimensions |
metric(可选) |
dataset.field 名称,带时间标记和粒度,每行附 is_dimension(是否值得分组)与 source(declared,或命中的推断规则)。给了 metric 就只返回该指标能分组的字段,推荐的在前 |
describe_metric |
name |
单个指标的那一行;参数化指标还带 param_schema(params 的 JSON Schema);未知名称会返回候选项 |
list_connections |
— | 具名的数仓配置:名称、方言、可用性 |
get_capabilities |
— | 引擎模式、支持的方言、是否启用执行、模型统计 |
compile_sql |
指标查询 + dialect、pretty |
{dialect, sql},不执行 |
explain_query |
指标查询 | 以文本形式给出的逻辑计划 |
run_query |
指标查询 + dialect、connection、row_limit |
{dialect, sql, outputs, columns, rows, row_count, row_limit_applied} |
attribute_metric |
metric、dimensions、baseline/current 窗口 + where_sql、dialect、connection |
指标变化的按维度分解:strategy、total_change、dimension_ranking、带 drill_down.where_sql 的贡献值(见归因分析) |
compile_select |
明细查询 + dialect、pretty |
{dialect, sql},不执行(明细查询) |
explain_select |
明细查询 | 明细计划的文本形式——看清楚选了哪些连接 |
select |
明细查询 + dialect、connection、row_limit |
{dialect, sql, columns, rows, row_count, row_limit_applied},行级记录 |
validate_model |
model_yaml |
{valid, issues, compile_errors, warnings} |
指标查询类的输入就是 REST API 和 CLI 所用的那个 MetricQuery 传输形态
(metrics、写作 [{field, grain}] 的 group_by、where_sql、
context_filter、time_range、order_by、limit)。工具的 schema 由同一批类型生成,
不可能产生漂移。要看某个市场在全部市场里的名次,把它写在 where_sql;
要只在这个市场内部排名,用 context_filter(见过滤条件)。
明细查询类的输入同样就是 DetailQuery 的传输形态(from、fields、
where_sql、time_range、order_by、limit)。答案是记录而不是聚合值时
用 select——"列出最大的 100 笔订单"、"这个月哪些合同到期"。它有两种拒绝
值得读一读再改,而不是盲目重试:detail_fanout 说明根会被一对多的一跳放大,
并给出应该改用的根;ambiguous_join_path 列出点名各条候选路径的关系前缀。
两者都给出可以直接粘进下一次调用的 suggested_retry
(见明细查询)。
执行方面的保护¶
run_query 复用服务端的执行机制:并发信号量
(--max-concurrent-executions,占满时返回 busy)、执行超时
(--execute-timeout-secs,超时返回 timeout),以及 --disable-execute
(它会让 run_query 变成一个工具级的 forbidden 错误)。注意
--request-timeout-secs 不覆盖 /mcp,约束 run_query 的只有上面两项。
select 走同一套机制,同样受这三个开关约束。
结果会进入 LLM 上下文,所以行数有上限:没写 limit 的指标查询自动加
LIMIT 500,row_limit 最高钳制在 5000;明细查询默认 LIMIT 100,
最高钳制在 10000——调用方自己写的更大的 limit 同样会被钳制。
实际生效的上限由 row_limit_applied 报出,Agent 据此能分辨截断页和完整结果。
不带 connection 时查询跑在服务端的本地 DuckDB 上(--db 指定的文件或内存库);
要在数仓上跑,点名一个 list_connections 里的配置即可。
run_query 需要有数据
不带 --db 时,本地 DuckDB 是内存库且没有灌数据,于是 compile_sql
正常、run_query 报表不存在。--db 要指向真正建好模型所需表的库:
错误¶
引擎的失败以工具级错误返回(isError: true),携带引擎的结构化 JSON,
字段与 REST API 和 CLI 相同:稳定的 code、candidates、suggested_retry。
名字写错时,错误里会给出合法拼写:
{"error": {"code": "unknown_metric", "message": "unknown metric \"revenu\"",
"metrics": ["revenu"],
"candidates": ["revenue", "order_count", "unique_customers",
"avg_order_value", "total_margin"]}}
如果要改的不是名字而是查询形状,suggested_retry 会用散文说清楚——它是一个
字符串,不是查询对象:
{"error": {"code": "grain_on_non_time_dimension",
"message": "orders.status is not a time dimension; a grain cannot be applied",
"suggested_retry": "drop the :grain suffix or mark the field with dimension.is_time: true"}}
工具参数本身不合法时,拦截发生在更早一层——由工具 schema 直接拒绝:
{"grain": "fortnight"} 返回的是纯文本反序列化错误,列出合法取值
(day、week、month、quarter、year),而不是 {"error": …} 结构。
Agent 应当读错误再改查询,而不是原样重试。服务端的 MCP 指引里也是这么写的。
交互式试用¶
npx @modelcontextprotocol/inspector \
dosi-server --model $DOSI_EXAMPLES/orders/model.yaml \
--db orders.duckdb --mcp-stdio
下一步¶
-
传输方式、工具白名单,以及让 Agent 老老实实走指标的提示词规则。
-
注册服务、用自然语言提问、读懂工具调用轨迹—— Claude Code、Codex、 OpenCode。