# MCP 与托管工具

> 让你的 agent 用 SciRouter 搜网页、读网页、查文献、核对标识符、找先例：MCP 服务端怎么接（Claude Code / Cursor / Codex，以及 claude.ai / ChatGPT 的 OAuth 连接器），/v1 里怎么声明托管工具让代理替你跑工具循环，限额、错误码与数据流向。

来源：https://www.scirouter.cn/docs/mcp · 更新于 2026-09-25

SciRouter 把「联网」做成了两个出口，用的是同一份工具、同一套闸：

- **MCP 服务端** `https://api.scirouter.cn/mcp`——你自己的 agent（Claude Code、Cursor、Codex CLI，或任何 MCP 客户端）拿 API Key 接上，就能搜索网页、读网页、查文献、核对序列 / 结构 / 化合物的标识符、找先例；claude.ai 与 ChatGPT 的连接器走 OAuth 登录（见下文「在 claude.ai / ChatGPT 里连接」）。
- **`/v1/chat/completions` 的托管工具**——在请求的 `tools` 里声明一次，代理自己执行工具、多轮调模型，把最终答案交回给你。适合「直接对接模型」的场景：你不用写工具循环。

两边的安全闸完全一样：只读公网的 http / https；私网地址、平台自己的站点、拒绝名单里的站点会被拒绝；robots.txt 只认写给 `SciRouterBot` 的规则；
每个站点有并发与频率的闸；网页正文有字数上限。

**工具免费，不扣配额**；只有模型调用计费。

## 五个工具

| 工具 | 入参 | 出参 |
|---|---|---|
| `web_search` | `{ query（1–300 字）, lang?: "zh" \| "en", kind?: "web" \| "scholar" \| "news" }` | `{ kind, items: [{ n, title, url, site, date?, snippet }], related: string[], cached }` |
| `fetch_page` | `{ url }` | `{ url, finalUrl, title, site, date?, text, chars, truncated, cached }` |
| `search_literature` | `{ query（1–300 字）, limit?: 1–10（默认 5） }` | `{ items: [{ n, title, authors[], year?, venue?, doi?, arxiv?, url, abstract?, source? }] }` |
| `resolve_identifier` | `{ kind: "sequence" \| "structure" \| "compound", identifier }` | 按 `kind` 不同，见下表 |
| `find_precedents` | `{ kind: "accession" \| "gene" \| "compound", accession?, gene?, organism?, cid?, inchiKey? }` | `{ basis, basisKey, source, total, papers: [{ pmid, doi, title, journal, year, firstAuthor, chapter, url }], ambiguous, fetchedAt, cached }` |

- `web_search` 的 `scholar` / `news` 要平台开了对应的搜索种类才有；没开时得到的是一条 `isError` 结果「这种搜索没有开」，不是协议错误。
- `fetch_page` 也能读 PDF（平台配了文档解析时）；`truncated` 为 true 说明正文超过了字数上限、被截掉了后面的部分。
- `search_literature` 合并 arXiv / CrossRef / Semantic Scholar 的结果并去重，`source` 说明这一条来自哪里。
- `resolve_identifier` 与 `find_precedents` 是站内证据层的两个能力（消息里的核对徽章、找先例）开给 agent 用，出参与站内同形。

每个工具的结果都有两份：**给人或模型读的文字**（MCP 的 `content[0].text`）与**结构化 JSON**（MCP 的 `structuredContent`），两份同源。
工具「跑了但没成」——网站打不开、被策略拒绝、种类没开——是 `isError: true` 的**正常结果**，附一句原因；JSON-RPC 的错误只用于协议层面的问题（见下文错误码表）。

### 证据层的两个工具

`resolve_identifier` 拿一个标识符去公共库取回记录，出参按 `kind`：

| `kind` | 标识符长什么样 | 出参 |
|---|---|---|
| `sequence` | NCBI 登录号，可带区间：`NC_012920.1:648-1601` | `accession, source, molecule, start, stop（整条为 null）, header, length, sequence（最多 10000 位，超出则截断、truncated=true）, release, sourceUrl, fetchedAt, cached` |
| `structure` | `pdb:1HEL`、`pubchem:2519` | `ref, source, id, format（pdb / sdf）, bytes, sourceUrl, pageUrl, fetchedAt, cached`——**不含文件本身**，要文件按 `sourceUrl` 自己取 |
| `compound` | PubChem CID 或化合物名 | `cid, title, iupacName, formula, molecularWeight, inchiKey, smiles, ambiguous（按名称查到多条时取第一条）, sourceUrl, fetchedAt, cached` |

`find_precedents` 按登录号 / 基因（可加物种）/ 化合物（CID 或 InChIKey）在 PubMed 找先例：`basis` 与 `basisKey` 说明是按什么找的，`total` 是库里的总数，`papers` 最多几十条；
`chapter` 为 true 的是书籍章节（GeneReviews、StatPearls），那时 `journal` 是书名、`year` 是最近一次修订的年份；`ambiguous` 说明基因名或化合物名对上了不止一条。

- 两个工具的结果只是「库里有这条记录、记录长这样」，不替你判断回答对不对。
- **「库里没有」是 `isError: true` 的结论文本**（「NCBI 里没有这个登录号」），不是协议错误；入参形状不对（不认识的 `kind`、缺对应字段）才是 JSON-RPC `-32602`。
- 次数与站内证据层**共用同一份桶**：核对每用户每分钟 60 次、找先例每用户每小时 30 次；超了是 `RATE_LIMITED`，按错误返回（MCP 里是 JSON-RPC `-32000`，`data.retryAfterSeconds` 说等几秒），不是 `isError` 结果。
  `/v1` 托管工具里限流**不报错**：这一轮的工具结果是失败态（`tool_calls` 里该条 `status: "failed"`，文本「调用太频繁，这次没有执行：稍后再试或换个办法」），循环照常继续，由模型自己决定重试还是换办法。
- 团队项目下的 Key 看团队的「外部检索」档位（见下文）；发出去的只有标识符，不含对话内容。
- 工具免费，与另外三个一样。

## 接 MCP 服务端

### 鉴权

`Authorization: Bearer YOUR_API_KEY`，与 `/v1` 是同一把 Key，在[控制台的 API Keys 页面](https://www.scirouter.cn/console/keys)新建。
鉴权失败是 HTTP 401 / 403（错误体是 OpenAI 形状，同[错误码](https://www.scirouter.cn/docs/errors)那一页），不是 JSON-RPC 错误。

### 协议

- **Streamable HTTP**：单个端点、只收 `POST`；`GET` / `DELETE` 回 405。
- 实现 MCP 规范 **2026-07-28**（无状态、`server/discover`），同时接受 2025-03-26 / 2025-06-18 / 2025-11-25 的客户端：`initialize` 握手照答，不发 session id。
  旧版客户端不需要带 `MCP-Protocol-Version` 头；按 2026-07-28 写的请求要带 `MCP-Protocol-Version: 2026-07-28`、`Mcp-Method`，`tools/call` 还要带 `Mcp-Name`，且与正文一致。
- 只支持 **tools**；不支持 prompts / resources / sampling。
- 响应一律 `application/json`（工具都在几秒到几十秒内完成，不开 SSE 流）。

### 各客户端怎么配

Claude Code：

```bash
claude mcp add --transport http scirouter https://api.scirouter.cn/mcp --header "Authorization: Bearer YOUR_API_KEY"
```

Cursor / Codex CLI（写进它们的 MCP 配置文件）：

```json
{
  "mcpServers": {
    "scirouter": {
      "url": "https://api.scirouter.cn/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

Anthropic Messages API（请求带 beta 头 `anthropic-beta: mcp-client-2025-11-20`）：

```json
{
  "mcp_servers": [
    { "type": "url", "url": "https://api.scirouter.cn/mcp", "name": "scirouter", "authorization_token": "YOUR_API_KEY" }
  ]
}
```

OpenAI Responses API：

```json
{
  "tools": [
    { "type": "mcp", "server_label": "scirouter", "server_url": "https://api.scirouter.cn/mcp", "authorization": "YOUR_API_KEY", "require_approval": "never" }
  ]
}
```

后两种是让**厂商的云**来连我们的 MCP：要求 MCP 端点对厂商的公网可达，并且你把 Key 交给了厂商转发。**能用，但不是我们保证的路径**——出了问题先看厂商那边的日志。

claude.ai / ChatGPT 网页端的「连接器」不填 Key、走 OAuth 登录：见下文「在 claude.ai / ChatGPT 里连接（OAuth）」。

### 手工调一次

```bash
# 列工具（旧版写法：不带协议版本头也行）
curl https://api.scirouter.cn/mcp \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 调工具（2026-07-28 写法：协议版本头 + Mcp-Method / Mcp-Name 与正文一致）
curl https://api.scirouter.cn/mcp \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: web_search" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"web_search","arguments":{"query":"钙钛矿 封装 稳定性","lang":"zh"}}}'
```

成功的响应：

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{ "type": "text", "text": "1. … — example.org（2026-08-01）\n   …" }],
    "structuredContent": { "kind": "web", "items": [ { "n": 1, "title": "…", "url": "https://example.org/…", "site": "example.org", "snippet": "…" } ], "related": ["…"], "cached": false },
    "isError": false
  }
}
```

工具跑了但没成时，`result.isError` 是 true、`content[0].text` 是原因（例如「读网页失败：不允许的地址」）；HTTP 状态仍是 200。

### 限额与错误码

每把 Key **每分钟 120 次**工具调用；超了是 JSON-RPC 错误 `-32000`，`data.retryAfterSeconds` 说等几秒。

| 错误码 | 含义 |
|---|---|
| `-32700` | JSON 坏了 |
| `-32600` | 不是合法的 JSON-RPC 请求；批量（数组）不支持 |
| `-32601` | 方法不支持 |
| `-32602` | 入参不合法；没有这个工具 |
| `-32603` | 内部错误 |
| `-32000` | 被限频，`data.retryAfterSeconds` 说等几秒 |
| `-32020` | 头与正文不一致（`Mcp-Method` / `Mcp-Name` 与 `method` / `params.name` 对不上），HTTP 400 |
| `-32022` | 协议版本不支持，`data.supported` 列出支持的版本，HTTP 400 |

```json
{ "jsonrpc": "2.0", "id": 2, "error": { "code": -32000, "message": "…", "data": { "retryAfterSeconds": 12 } } }
```

### 团队项目下的 Key

Key 归属于团队项目时，工具受团队的「外部检索」档位约束（三档：核对 + 相似性 `all`、只核对 `verify`、全部关闭 `off`）：

- `resolve_identifier` 只发编号，「只核对」及以上就能用（`verify` / `all`）；
- 其余四个（`web_search`、`fetch_page`、`search_literature`、`find_precedents`）要「核对 + 相似性」（`all`）。

档位不够时工具返回 `isError`「团队策略不允许联网检索」。个人 Key 不受这条影响。

## 在 claude.ai / ChatGPT 里连接（OAuth）

claude.ai 与 ChatGPT 的「连接器」不让你填 API Key，只走 OAuth 2.1：你在它们那里填 MCP 地址，它们自动发现 SciRouter 的授权服务器，把你带到 SciRouter 的授权页；
你在页面上**选一把 Key 授给它**，跳回去就连上了。**它拿到的令牌等价于那把 Key**：计费、模型白名单、团队档位都按那把 Key 算。

- **claude.ai**：设置 → 连接器 → 添加自定义连接器，远程 MCP 服务器地址填 `https://api.scirouter.cn/mcp`；client id / secret 留空。
- **ChatGPT**：设置 → 连接器 → 创建（要先开开发者模式），MCP 服务器地址填同一个 `https://api.scirouter.cn/mcp`，鉴权选 OAuth；同样不用填 client id / secret。

两家都会自动读 MCP 地址所在源的 `/.well-known/oauth-protected-resource/mcp` 找到授权服务器，然后：

1. 把你带到 `https://www.scirouter.cn/console/oauth/authorize`——SciRouter 的授权页（没登录先登录，登录完自动回来）；
2. 页面上写着是谁在请求（客户端名与主页）、要什么权限、授权后跳回哪里；你选一把 Key，点「授权」或「拒绝」；
3. 跳回客户端，它换到令牌，之后每次调工具都带着这枚令牌。

**权限（scope）只有两种**：`tools`（调用 MCP 的工具）、`offline_access`（拿刷新令牌，长期保持连接）。

**令牌有效期**：访问令牌 1 小时；刷新令牌 30 天，每次刷新轮换（旧的立刻作废）。

**撤销**：[控制台 API Keys 页](https://www.scirouter.cn/console/keys)的「已连接的应用」，每条一个「撤销」——刷新令牌立刻作废，已签出的访问令牌最多再活到过期（1 小时内）。吊销那把 Key 也会让它的令牌失效。

### 自己写 OAuth 客户端

按 MCP 规范 2026-07-28 的授权章节实现即可。元数据与端点都挂在 MCP 地址所在的源上（把 `https://api.scirouter.cn/mcp` 末尾的 `/mcp` 换成下面的路径就是完整地址）：

| 元数据 | 路径 |
|---|---|
| 受保护资源（RFC 9728） | `/.well-known/oauth-protected-resource/mcp`——列出授权服务器 |
| 授权服务器（RFC 8414） | `/.well-known/oauth-authorization-server`——授权、令牌、注册端点与支持的 PKCE 方法都在里面，**以它为准**，不要写死端点 |
| 客户端注册 | 元数据里的 `registration_endpoint`（RFC 7591 动态注册） |

- **客户端标识两种都支持**：Client ID Metadata Documents（`client_id` 是一个 https 地址，指向你公开的客户端元数据文档；claude.ai 这类）、动态注册（RFC 7591；ChatGPT 这类）。
- **PKCE `S256` 必须**；`response_type=code`；`resource` 参数填 `https://api.scirouter.cn/mcp`（RFC 8707）；`redirect_uri` 要与登记的一致（loopback 地址允许）。
- 拿到的访问令牌当 `Authorization: Bearer <access_token>` 用，与 API Key 的用法一样；刷新令牌每次轮换，用旧的会失败。
- 授权请求本身有问题（不认识的 `client_id`、回跳地址不在名单、缺 PKCE）时授权页**不会跳回你**，只在页面上说明——回跳地址本身可能就是错的。

## `/v1` 里的托管工具

不想自己写工具循环时，让代理替你跑：

```http
POST https://api.scirouter.cn/v1/chat/completions
```

```json
{
  "model": "deepseek/deepseek-v3",
  "messages": [{ "role": "user", "content": "近一年钙钛矿封装稳定性有哪些新进展？给出处。" }],
  "tools": [
    { "type": "mcp", "server_label": "scirouter", "allowed_tools": ["web_search", "fetch_page"], "require_approval": "never" }
  ],
  "scirouter": { "max_tool_rounds": 6 }
}
```

### 请求

- `tools` 里加一项 `type: "mcp"`（借 OpenAI Responses 的形状，官方 SDK 不会拒绝）：
  - `server_label` 只认 `"scirouter"`；
  - `allowed_tools` 可省略，省略 = 五个都给模型；
  - **不接受 `server_url`**（要连外部 MCP 服务器，用上一节的方式让厂商去连）；
  - `require_approval` 只能是 `"never"` 或不填。
- 顶层可选 `"scirouter": { "max_tool_rounds": 6 }`：带工具调模型的轮数上限，缺省 6、最多 30。到了上限，最后再调一次**不带工具**，让模型作答。
- 你自己的 `function` 工具可以同时存在。

### 行为

1. 模型要了托管工具 → 代理执行 → 结果以 `role: "tool"` 消息接进上下文 → 再调模型。
2. 模型要了**你自己的函数** → 循环结束，那一轮原样交回你（你执行后再发下一轮，与今天一样）。
3. 同一轮既要了托管工具又要了你的函数：交回你的函数，托管的那一轮**不执行**（扩展字段里 `status: "skipped"`）；你回复之后模型会重新要。
4. 模型不支持函数调用时上游会报错，照常回 4xx，不退回文本协议。

### 响应

仍是 OpenAI 形状；顶层 `scirouter` 扩展字段（见[可观测性](https://www.scirouter.cn/docs/observability)）多两样：

```json
{
  "choices": [ … ],
  "usage": { … },
  "scirouter": {
    "request_id": "req_…",
    "cost_quota": 23180,
    "tool_rounds": 3,
    "tool_calls": [
      { "round": 0, "id": "call_…", "name": "web_search", "status": "done", "summary": "web 8 条", "ms": 1320 },
      { "round": 1, "id": "call_…", "name": "fetch_page", "status": "failed", "summary": "读网页失败：不允许的地址", "ms": 45 }
    ]
  }
}
```

- `tool_rounds`：调了几次模型；`tool_calls` 每条是一次工具调用（`round` 从 0 起，与请求日志的 `v1_tool_round` 标签同一口径；`status` 是 `done` / `failed` / `skipped`），**不含入参与正文**。
- `cost_quota` 是各轮之和；控制台里每轮一条请求日志，标签 `v1_tool_round`。
- **流式**：中间轮的正文照常流出；托管工具的 `tool_calls` 增量**不下发**；每次工具开始 / 结束各发一个 `choices: []` 的分片，
  `scirouter.tool` 里是那一条工具调用（开始那条 `status` 为空、`ms` 为 0；结束那条带 `status` / `summary` / `ms`）——与可观测性那一页的扩展分片同一机制，官方 SDK 会跳过；
  `[DONE]` 之前的收尾分片带各轮合计。这条流还没有任何分片时（模型第一轮一个字没吐就要了工具）开始那条不发。

```text
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"scirouter":{"tool":{"round":0,"id":"call_…","name":"web_search","status":"","ms":0}}}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"scirouter":{"tool":{"round":0,"id":"call_…","name":"web_search","status":"done","summary":"web 8 条","ms":1320}}}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"近一年…"}}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"scirouter":{"request_id":"req_…","tool_rounds":3,"cost_quota":23180,"tool_calls":[…]}}
data: [DONE]
```

- 带托管工具的请求**不进响应缓存**（结果随网页变），`Idempotency-Key` 照常。

### 计费

每一轮按正常的模型调用计费，工具免费。想控制花费就调小 `max_tool_rounds`，或用 `allowed_tools` 只放需要的工具。

## 数据流向与限制

- 检索词会发给搜索服务商，见[子处理者清单](https://www.scirouter.cn/legal/subprocessors)。
- 读网页以 `SciRouterBot` 的 UA 访问、遵守 robots.txt；网页正文**不落库**。
- MCP 调用日志只记 Key、工具名、耗时与一句摘要，不记正文。
- 只读公网 http / https：私网地址、平台自己的站点、拒绝名单里的站点一律拒绝；每个站点有并发与频率的闸；正文有字数上限。

## 常见问题

**为什么读不了某个链接？** 五种情况：地址是私网 / 本机的；是平台自己的站点；在拒绝名单里；站点的 robots.txt 明确拒绝了 `SciRouterBot`；页面太大超过体积上限。
结果的 `isError` 文字里写着是哪一种。

**为什么 `kind: "scholar"` 没有结果？** 平台没开学术搜索——`web_search` 会以 `isError` 告诉你「这种搜索没有开」。查文献用 `search_literature`，它不依赖这个开关。

**`/v1` 的托管工具用什么模型？** 任何**会函数调用**的模型都行；带 `tools` 的请求只会发往支持工具调用的线路（见[对话补全](https://www.scirouter.cn/docs/chat-completions)）。模型不会函数调用时上游报错、照常回 4xx。

**轮数用完了怎么办？** 到 `max_tool_rounds` 后代理最后再调一次不带工具的模型，让它用已有的资料作答，所以你总能拿到一个回答。想多查几轮就调大它（最多 30）。

**工具调用扣配额吗？** 不扣。MCP 与 `/v1` 托管工具都免费；`/v1` 只按每轮的模型调用计费。

**在 claude.ai 里连上之后能撤掉吗？** 能：[API Keys 页](https://www.scirouter.cn/console/keys)的「已连接的应用」里撤销；或者直接吊销那把 Key。撤销后应用要再用就得重新走一遍授权。

## 相关

- Key 的边界与权限：[认证与 API Key](https://www.scirouter.cn/docs/authentication)
- `scirouter` 扩展字段与流式分片：[可观测性](https://www.scirouter.cn/docs/observability)
- 工具调用只走支持的线路：[对话补全](https://www.scirouter.cn/docs/chat-completions)
