MCP 集成
Trove 可以把问数能力作为 MCP 服务暴露给 AI 客户端 (Claude Desktop / Cursor / Claude Code 等)。这一页讲三种传输方式、 鉴权边界,以及对外暴露的 3 个 tool、3 个 resource、2 个 prompt。
启动
trove mcp # stdio(默认)
trove mcp --transport sse --port 8001
trove mcp --transport streamable-http --port 8001 --token <trove-token>
main.py:963 main_mcp main.py:848 --transport server.py:82 FastMCP
| transport | 典型用途 |
|---|---|
| stdio | 本地挂载——Claude Desktop / Cursor / Claude Code 直接拉起进程 |
| sse | HTTP 传输,Server-Sent Events |
| streamable-http | HTTP 传输(http 是它的别名) |
鉴权:网络绑定必须带 token
这里有一条 fail-closed 的硬规则:绑定到非回环地址时,必须提供
--token,否则直接拒绝启动。
这个 token 不是随便一个字符串,而是必须能被解析成真实的 Trove 用户—— 解析失败也拒绝启动。也就是说 MCP 通道的鉴权复用同一套用户体系, 没有第二套凭证。
main.py:911 _build_mcp_auth_middleware
HTTP 传输下带 Bearer 中间件,连 health 端点也不豁免—— 未授权的客户端应当快速失败,而不是从一个开放的探活端点推断出服务存在。
身份语义
| 传输 | 身份 | 数据源可见性 |
|---|---|---|
| stdio | 视作本机可信(等价 admin) | 全部 |
| HTTP + token | token 对应的用户 | 按 grants 校验,与 HTTP API 同语义 |
server.py:141 _authorize_datasource policy.py:146 Principal
授权判定不在这里实现:本模块只解析目标数据源、把主体挂上,结论问
services/authz/policy.py。MCP 与 HTTP 两侧共用同一份策略——
此前各有一份手抄并且已经漂移过。
「stdio 视作本机可信」是合理的:能拉起这个进程的人本来就有本机权限。 但这也意味着不要把 stdio 形态包装成一个别人能远程触发的服务。
flowchart TB
CLI["trove mcp"]:::entry --> T{"--transport"}:::entry
T -- "stdio(默认)" --> L["身份 = 本机可信(等价 admin)
数据源:全部"]:::entry
T -- "sse / streamable-http" --> B{"绑定非回环地址?"}:::gate
B -- "否 · 回环" --> OK["不强制 token"]:::entry
B -- "是" --> TOK{"--token 能解析成真实用户?"}:::gate
TOK -- "否" --> NO["拒绝启动"]:::err
TOK -- "是" --> U["身份 = token 对应的用户
数据源按 grants 校验,与 HTTP API 同语义"]:::entry
classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px;
classDef err fill:#dc262614,stroke:#dc262699,stroke-width:1.5px;
linkStyle 5 stroke:#f59e0b,stroke-width:1.5px
三个 tool
| tool | 作用 |
|---|---|
| ask_data | 问一个问题,走完整链路 |
| list_datasources | 列出当前身份可见的数据源 |
| kb_status | 看某个数据源的 KB 状态 |
server.py:349 ask_data server.py:398 list_datasources server.py:405 kb_status
ask_data 的返回里除了答案还带 sql、row_count、
verdict,以及 refusal / no_model 两个状态位。
后两者很重要——拒绝回答与「数据源没有语义模型」不是同一种失败,
客户端应当能区分并给出不同的提示。
多轮对话在进程内按 session 缓存(上限 200),数据源名做了路径穿越防护。
server.py:61 _SESSION_CACHE_MAX server.py:197 _ds_name_safe
三个 resource
| URI | 内容 |
|---|---|
| trove://datasources | 可用数据源清单 |
| trove://{datasource}/schema | 该数据源的表结构 |
| trove://{datasource}/semantics | 该数据源的语义模型 |
server.py:219 datasources server.py:239 schema server.py:245 semantics
resource 与 tool 的区别在这里体现得很清楚:ask_data 是动作,
而语义模型是可以读的上下文。把语义模型作为 resource 暴露,
意味着客户端可以把「这个库有哪些指标」直接放进模型上下文,
从而在提问之前就知道边界在哪。
两个 prompt
| prompt | 作用 |
|---|---|
| datasource_guide | 介绍某个数据源该怎么问 |
| ask_data | 把一个自然语言意图转成 ask_data 调用 |
server.py:323 datasource_guide server.py:338 ask_data
语义优先也在这里生效
可见数据源清单和 HTTP 侧遵循同一条规则:没有语义模型的数据源不出现在清单里。
server.py:152 _datasources_visible
客户端因此不会看到一个「问什么都会失败」的数据源。 这是语义优先在接口层的体现: 可答边界不只影响生成,也影响暴露什么。
客户端配置示例
{
"mcpServers": {
"trove": {
"command": "uv",
"args": ["run", "trove", "mcp"],
"cwd": "/path/to/trove"
}
}
}
stdio 形态下客户端会自动拉起进程,不需要预先启动服务。
记得 cwd 要指向仓库根——配置与 .trove/ 都是相对它解析的。