# MCP 接入指南 > 写给:要把自己系统的能力做成 MCP 服务、接入 AgentServer 的开发者。 > 依据:MCP 规范 2025-11-25;平台的实现见 [design/16-mcp-support.md](../design/16-mcp-support.md)。 > 示例代码:[`examples/mcp-reference-server/server.mjs`](../../examples/mcp-reference-server/server.mjs)(虚构的订单系统,覆盖本指南的全部写法)。 > 最后更新:2026-10-29 本指南不另立标准,只回答两个问题:MCP 规范里的可选项,在 AgentServer 上**哪些有效、怎么用**;工具怎么设计,**模型才好用**。 --- ## 1. 先读这一页 **五条原则** 1. **工具是写给模型用的。** 名称、说明、参数、错误信息的读者都是模型,要让它不看文档也知道什么时候用、怎么填、出错了下一步做什么。 2. **一个工具做一件事。** 宁可多几个小工具,也不要一个工具带十几个可选参数和模式开关。 3. **长时间的事交给平台等。** 超过 3 分钟的操作用长任务(§5),不要让模型轮询。 4. **如实声明工具提示。** 只读、破坏性、幂等这几个提示决定平台是否审批、能否自动重试(§3.3)。 5. **返回少而准的结果。** 大结果分页、给摘要,文件以资源形式返回,不要把几万字塞进模型的上下文(§3.5、§4.3)。 **平台支持情况速查** | MCP 能力 | 平台的处理 | 章节 | |---|---|---| | 工具(tools) | ✅ 转接给 Claude 与 Codex,工具名为 `服务名__工具名` | §3 | | 工具提示(annotations) | ✅ 决定默认的审批策略和自动重试,管理员可以覆盖 | §3.3 | | 结构化输出(outputSchema / structuredContent) | ✅ 按 schema 校验;结构说明附在工具说明后告诉模型 | §4.2 | | 进度通知(progress) | ✅ 收到进度时延长等待时间 | §5.1 | | 长任务(Tasks,实验性) | ✅ 平台按任务 id 查询,完成后唤醒会话;服务重启后继续 | §5 | | 询问用户(Elicitation,表单 / 网址) | ✅ 变成会话里的卡片 | §6 | | 工具列表变化(listChanged) | ✅ 自动刷新 | §7 | | 资源(Resources)、资源模板 | ✅ 模型按需读取;用户可以附加到消息 | §8.1 | | 提示模板(Prompts) | ✅ 用户在输入框"快捷指令"中使用 | §8.3 | | 认证:请求头令牌、OAuth 2.1 | ✅ 令牌由服务端保存;OAuth 按服务共享一个账号 | §9 | | Sampling(服务借用平台的模型) | ❌ 不支持 | §10 | | 资源订阅、Roots、补全(completion)、日志(logging) | ❌ 暂不支持,声明了也不会使用 | §10 | --- ## 2. 接入方式 ### 2.1 传输 | 方式 | 适合 | 说明 | |---|---|---| | **Streamable HTTP(推荐)** | 独立部署的服务 | 平台按"服务 × 会话"建立连接;旧版 SSE 服务自动兼容 | | 本地进程(stdio) | 命令行工具、只在本机使用 | 在沙箱中运行:系统目录只读、独立的 home、**默认不能联网**、只能访问管理员开放的路径;密钥只通过环境变量传入。所有会话共用一个进程 | 需要询问用户(§6)的服务请用 HTTP:本地进程被多个会话同时使用时,平台无法确定询问属于哪个会话,会拒绝询问。 ### 2.2 登记 管理员在后台"MCP 服务"页面添加服务、测试连接,再到 Profile 中勾选;勾选了这个服务的 Profile,对话中才会出现它的工具。 - **服务名**(工具名前缀):小写字母、数字、`_`、`-`,最多 32 位。模型看到的工具名是 `服务名__工具名`,**整体最多 64 个字符**,超出部分被截掉。所以工具名要短:`get_order` 好于 `get_order_detail_by_order_number`。 - 服务的 `instructions`(初始化时返回的服务说明)目前不会转给模型,要让模型知道的内容写在各个工具的说明里。 ### 2.3 SDK 推荐用官方 SDK:TypeScript `@modelcontextprotocol/sdk`(1.32 及以上,参考服务使用 1.32.1)、Python `mcp`。长任务和询问用户需要支持 2025-11-25 规范的版本。 --- ## 3. 工具设计 ### 3.1 粒度与命名 - 按**用户要完成的事**划分工具,而不是照搬后端接口:`cancel_order` 好于 `update_order(status='cancelled')`。 - 动词开头、`snake_case`:`get_`、`list_`、`search_`、`create_`、`update_`、`delete_`、`export_`。 - 读和写分成不同的工具。只读工具可以免审批、在只读模式下使用;混在一起,整个工具都要按写操作对待。 - 一个服务的工具控制在 **20 个以内**。工具越多,模型选错的概率越高,每一轮也要多花 token 列出它们。 - 设置 `title`(显示名),后台和审批卡片会显示它。 ### 3.2 说明与参数 工具说明(`description`)回答三件事:**做什么、什么时候用、有什么限制**。 ``` 按订单号查询订单的客户、金额、状态和商品。订单号形如 A1001。 不知道订单号时,先用 list_orders 按状态查找。 ``` - 前 2000 字以内说清楚(平台截断到 2000 字)。 - 不要写"你必须……"、"忽略其他工具"之类的指令。这是工具投毒的常见手法,平台审计时会被当作风险。 - 参数用 JSON Schema 写全:每个参数都有 `description`;能枚举的用 `enum`;格式用 `pattern` / `format`;给出 `default`。 - 必填参数尽量少,3 到 5 个为宜。可选参数太多时拆成两个工具。 - 不要让模型传大段内容。要处理文件时,参数里放文件地址或 id。 ### 3.3 工具提示(annotations) | 提示 | 含义 | 平台的用法 | 不写时 | |---|---|---|---| | `readOnlyHint: true` | 不修改任何东西 | 默认**直接调用**;只读模式下只提供这类工具 | 按非只读处理 | | `destructiveHint: true` | 删除、覆盖、不可恢复 | 默认**总是审批**:自动模式下也要用户确认,而且每次都问 | 平台**不**据此强制审批(规范的默认值是 true,平台只认明确写出的 true) | | `idempotentHint: true` | 同样的参数重复调用,结果相同 | 遇到网络错误时自动重新连接并**重试一次** | 不重试,避免重复执行 | | `openWorldHint: false` | 只操作本系统,不与外部交互 | 后台和审计中不标"外部交互" | 视为与外部交互 | - 提示必须准确。写错的后果:只读工具写成非只读,用户被多问;破坏性工具漏写,自动模式下不经确认就执行;非幂等工具写成幂等,网络抖动时可能**重复下单**。 - 管理员可以在后台逐个工具覆盖这三个提示,但不要依赖管理员替你纠正。 ### 3.4 错误 - **业务错误**用工具结果返回:`isError: true`,加一段写给模型的说明,讲清原因和下一步怎么做。 ```json { "isError": true, "content": [{ "type": "text", "text": "订单 A9999 不存在。可以先用 list_orders 查看现有订单号。" }] } ``` - **协议错误**(参数不符合 schema、工具不存在)用 JSON-RPC 错误返回,SDK 会处理。 - 不要把堆栈、SQL、内部地址放进错误信息:模型会原样转述给用户。 - 权限不足、令牌失效时说明清楚("令牌没有导出权限"),平台和用户才知道该找谁。 ### 3.5 分页与结果大小 - 列表类工具要分页:接收 `cursor`,返回 `nextCursor`,每页 20 到 50 条。 - 默认只返回关键字段,详情另用一个工具查。 - 平台的处理:文本超过 **10 万字**时只给模型前一部分,完整内容存成工作区文件;图片、音频超过 **5 MB** 时存成文件。但长结果本身就会消耗模型的上下文,最好在服务端控制在几千字以内。 - 需要大量数据时,生成文件并以资源返回(§4.3),让模型去读文件、做检索。 ### 3.6 幂等与重复提交 - 会产生副作用的操作,尽量支持调用方传入去重键(例如 `requestId`),或者在服务端按业务键去重。 - 提交类操作(创建作业、下单)做成幂等的:同样的任务已经在执行时,返回已有的结果或作业 id,不要再执行一次。这样就不需要模型先查一次状态再决定要不要提交。 --- ## 4. 返回结果 ### 4.1 文本 给模型读的结论放在最前面,例如"共 3 条,其中 1 条已逾期"。表格用 Markdown 或 CSV,不要用 HTML。 ### 4.2 结构化输出 需要模型(或平台)**准确使用字段**时,声明 `outputSchema` 并返回 `structuredContent`,**同时**把同样的 JSON 序列化成一段文本放进 `content`(兼容只读文本的客户端): ```js server.registerTool('get_order', { description: '按订单号查询订单……', inputSchema: { orderId: z.string() }, outputSchema: { id: z.string(), customer: z.string(), amount: z.number(), status: z.enum(['paid', 'shipped', 'cancelled']) }, annotations: { readOnlyHint: true }, }, async ({ orderId }) => { const o = await db.getOrder(orderId) return { content: [{ type: 'text', text: JSON.stringify(o) }], structuredContent: o } }) ``` 平台的处理: - 文本里已经有同样的 JSON 时不重复;没有时把 JSON 附在文本后面交给模型。 - 按 `outputSchema` 校验 `structuredContent`。不符合时结果照常交给模型,审计中记录校验错误,管理员能看到。 - 在工具说明后附上返回结构的简述(最多 1500 字),模型调用前就知道会得到什么。所以 schema 要精简,字段加 `description`。 ### 4.3 文件 报表、导出、图片等文件,以**嵌入资源**返回: ```js return { content: [ { type: 'text', text: '10 月销售报表已生成:共 1024 行' }, { type: 'resource', resource: { uri: 'ref://reports/sales-2026-10.csv', mimeType: 'text/csv', text: csv } }, // 二进制用 blob(base64) ] } ``` - 平台把嵌入资源**一律**保存到会话工作区的 `.agent/mcp/`。文件名取 URI 的最后一段,所以 URI 要以有意义的文件名结尾。 - 模型只看到文件路径、大小和类型,看不到内容。需要时它会自己去读或分析这个文件,用户也可以下载。 - 同时给一句文字摘要(行数、时间范围),模型不用读文件就能回答简单的问题。 --- ## 5. 长任务 ### 5.1 什么时候用 | 执行时间 | 做法 | |---|---| | 几秒到 3 分钟 | 普通工具。超过 30 秒的,建议发进度通知(`notifications/progress`),平台会延长等待时间 | | 3 分钟以上,或者不确定 | **长任务(Tasks)** | 不支持 Tasks 的工具运行超过 3 分钟时,平台也会转到后台,保持连接等待(最长 24 小时)。但服务重启、网络断开都会让这次调用失败,所以超过几分钟的操作请用 Tasks。 **不要再做"提交 + 查询"两个工具让模型轮询。** 模型没有时钟,只能连续查询(浪费 token),或者这一轮结束后就没人再查了。查询进度的工具可以保留,供用户问"进度怎么样"时使用。 ### 5.2 实现 ```js const server = new McpServer({ name: 'orders', version: '1.0.0' }, { capabilities: { tasks: { requests: { tools: { call: {} } }, cancel: {} } }, taskStore, // 见 §5.3 taskMessageQueue: new InMemoryTaskMessageQueue(), }) server.experimental.tasks.registerToolTask('export_report', { description: '导出指定月份的销售报表。很慢,以长任务方式执行,完成后平台会把结果发给你。', inputSchema: { month: z.string().regex(/^\d{4}-\d{2}$/) }, execution: { taskSupport: 'optional' }, // required:只能以任务方式调用 annotations: { readOnlyHint: true }, }, { async createTask({ month }, { taskStore, taskRequestedTtl }) { const task = await taskStore.createTask({ ttl: taskRequestedTtl, pollInterval: 30_000 }) startExport(month, { // 在后台执行,不要等它完成 progress: pct => taskStore.updateTaskStatus(task.taskId, 'working', `已完成 ${pct}%`), done: result => taskStore.storeTaskResult(task.taskId, 'completed', result), fail: err => taskStore.storeTaskResult(task.taskId, 'failed', { isError: true, content: [{ type: 'text', text: err.message }] }), }) return { task, _meta: { 'io.modelcontextprotocol/model-immediate-response': `${month} 报表已开始导出,预计 20 分钟` } } }, getTask: async (_args, { taskId, taskStore }) => taskStore.getTask(taskId), getTaskResult: async (_args, { taskId, taskStore }) => taskStore.getTaskResult(taskId), }) ``` 平台的处理: 1. 以任务方式调用(任务保留时间 `ttl` 为 24 小时),先在前台查询最多 3 分钟。期间完成的,结果直接交给模型,和普通工具一样。 2. 超过时间后转为后台:模型收到"已转为后台执行"和 `model-immediate-response` 中的说明,结束这一轮,告诉用户已提交。 3. 平台按 `pollInterval` 查询状态(最短 5 秒,最长 5 分钟,不提供时为 30 秒),`statusMessage` 显示在后台调用栏和后台页面。 4. 到达终态后取回结果,作为一条新消息发到会话(或工作组中发起调用的机器人),唤醒模型继续处理。结果的处理与普通工具相同(§4)。 5. 用户或管理员取消时,平台调用 `tasks/cancel`。请实现取消,停止后台的执行。 `createTask` 要**立即返回**(几秒以内),实际工作在后台进行。 ### 5.3 任务归属与保留 - **按认证身份(令牌或 OAuth 账号)归属任务,不要绑定在 MCP 会话上。** 平台重连或重启后,用新的 MCP 会话查询原来的任务;绑定在会话上的任务会查不到,平台只能按失败通知用户。SDK 自带的 `InMemoryTaskStore` 默认按会话绑定,参考服务给出了改为共享的写法(`SharedTaskStore`)。 - 任务状态要持久化(数据库或 Redis),否则服务重启后任务就丢了。内存存储只适合示例。 - 至少保留到 `ttl` 到期,不要早于平台的最长等待时间(24 小时)。 - 任务 id 要随机、不可猜测;按身份隔离,A 的令牌查不到 B 的任务。 ### 5.4 任务失败 - 业务失败:`storeTaskResult(taskId, 'failed', { isError: true, content: [...] })`,说明写给模型看。 - 平台查询连续失败超过 30 分钟,或者服务返回任务不存在时,按失败处理。 --- ## 6. 询问用户(Elicitation) ### 6.1 什么时候用 工具执行到一半,缺少信息或需要确认时,可以向用户提问,不必直接报错再让模型来回追问: - 有歧义:"有两个叫张三的客户,选哪一个?" - 需要补充:"请填写收件地址" - 需要授权、付款等在别的页面完成的事:网址模式 不要把**每个**写操作都做成询问确认:审批由平台负责(§3.3 的工具提示)。 ### 6.2 两种模式 **表单模式**: ```js const r = await server.server.elicitInput({ mode: 'form', message: '请填写订单 A1001 的收件信息', requestedSchema: { type: 'object', properties: { name: { type: 'string', title: '收件人', minLength: 1 }, speed: { type: 'string', title: '配送方式', oneOf: [{ const: 'standard', title: '普通' }, { const: 'express', title: '加急' }], default: 'standard' }, insured: { type: 'boolean', title: '保价', default: false }, }, required: ['name'], }, }) if (r.action === 'accept') { /* r.content.name … */ } else if (r.action === 'decline') { /* 用户拒绝 */ } else { /* cancel:用户暂不处理或超时 */ } ``` 平台支持的字段(规范限定为一层对象): | 类型 | 写法 | 卡片 | |---|---|---| | 文本 | `type: 'string'`,可加 `minLength`、`maxLength`、`format`(`email` / `uri` / `date` / `date-time`) | 输入框 | | 数字 | `type: 'number'` 或 `'integer'`,可加 `minimum`、`maximum` | 数字输入框 | | 是否 | `type: 'boolean'` | 勾选框 | | 单选 | `type: 'string'` + `enum`(可加 `enumNames`)或 `oneOf: [{ const, title }]` | 下拉框 | | 多选 | `type: 'array'` + `items.enum` 或 `items.anyOf: [{ const, title }]`,可加 `minItems`、`maxItems` | 多选按钮 | 每个字段都写 `title`,可以加 `description` 和 `default`。不支持嵌套对象。平台在用户提交时按这些约束校验一次,服务端仍要自己校验。 **网址模式**: ```js const r = await server.server.elicitInput({ mode: 'url', message: '请登录 CRM 并授权', url: 'https://crm.example.com/oauth/authorize?…', elicitationId: crypto.randomUUID() }) ``` 平台显示完整网址和域名,用户自己点开,完成后点"我已完成"(`accept`)。平台不会自动打开网址。 ### 6.3 注意 - **不要用表单索要密码、密钥、验证码**(规范禁止)。需要授权的,用网址模式让用户在你的页面完成。 - 三种结果都要处理。用户 10 分钟(后台可调)没有处理时,结果为 `cancel`。 - 定时任务等无人值守的场景,询问会等到超时。需要无人值守执行的工具,要提供不询问的默认行为(例如参数中提供足够的信息时就不询问)。 - 长任务中询问:先把任务状态设为 `input_required`,再发出询问,并带上 `relatedTask`(参考服务的 `purge_cancelled`)。平台会显示卡片,用户回答后任务继续。 --- ## 7. 工具列表变化 工具会随配置或权限变化时,声明 `capabilities.tools.listChanged: true`(官方 SDK 在注册工具时自动声明并发送通知)。工具增减后发送 `notifications/tools/list_changed`,平台会重新获取工具列表: - Claude 在下一轮看到新的工具列表;Codex 的会话在下一轮切换到新的工具列表。 - 正在进行的这一轮看不到新增的工具。工具删除后再被调用,会返回"工具不存在"。 工具列表最好保持稳定。频繁变化会让模型的行为不稳定,Codex 的会话也会频繁切换。 --- ## 8. 资源与提示模板 ### 8.1 资源(Resources) 适合做成资源的:**参考资料**,例如产品手册、接口说明、数据字典、表结构、配置、运行状态。它们由模型按需读取,或者由用户附加到对话,而不是"去做一件事"。 ```js server.registerResource('order-api', 'ref://docs/order-api.md', { title: '订单系统接口说明', description: '订单状态的含义、退款规则', mimeType: 'text/markdown' }, async uri => ({ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: markdown }] })) // 资源模板:按参数读取 server.registerResource('order', new ResourceTemplate('ref://orders/{orderId}', { list: listOrders }), { title: '订单详情', mimeType: 'application/json' }, async (uri, { orderId }) => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(await getOrder(orderId)) }] })) ``` 平台怎么用: | 方式 | 说明 | token 消耗 | |---|---|---| | 模型按需读取 | 网关提供 `mcp_resources`(列出目录)和 `mcp_resource_read`(读取)两个只读工具 | 列目录时只有标题和说明;只有读取时才消耗内容的篇幅 | | 用户附加 | 输入框的"资料"按钮:选中的资源保存为会话文件,作为附件发出 | 这一轮附件的篇幅 | 写法建议: - `title`、`description` 写清楚内容和用途:模型和用户都是**只看目录**来挑选的。 - 提供 `mimeType` 和 `size`。文本超过 2 万字时,平台只给模型开头,完整内容存成文件。所以大文档按章节拆成多个资源。 - URI 用稳定的、可读的路径,以有意义的文件名结尾(用户附加时会保存成这个文件名)。 - **规范中没有搜索资源的方法。** 资料多(几十篇以上)的服务,另外提供一个 `search_docs` 工具,返回匹配的资源 URI 和片段。 - 资源模板的参数尽量是简单值(id、日期),并在 `description` 中说明取值。 ### 8.2 资源与工具怎么选 | 内容 | 做成 | |---|---| | 不需要参数、偶尔变化的参考资料 | 资源 | | 按 id 读取一条记录 | 资源模板,或只读工具(两者都可以;需要筛选、分页时用工具) | | 查询、搜索、统计 | 只读工具 | | 有副作用的操作 | 工具 | ### 8.3 提示模板(Prompts) 适合固定的工作流程,例如"订单复盘"、"生成周报"。用户在输入框的"快捷指令"中选择、填写参数后,展开的内容放进输入框,用户可以修改后发送。 ```js server.registerPrompt('order_review', { title: '订单复盘', description: '针对一个订单写复盘:时间线、问题、改进建议', argsSchema: { orderId: z.string().describe('订单号'), focus: z.string().optional().describe('关注点') }, }, async ({ orderId, focus }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `请对订单 ${orderId} 做一次复盘……` } }, { role: 'user', content: { type: 'resource', resource: { uri: `ref://orders/${orderId}`, mimeType: 'application/json', text: json } } }, ], })) ``` - 参数都写 `description`,它会显示为输入框的提示。参数值都是字符串。 - 文本部分合并后放进输入框;嵌入的资源、图片保存为附件,一起发送。 - 不需要附带资料时,只返回文本消息即可。 --- ## 9. 认证与安全 ### 9.1 请求头令牌 最简单的方式:管理员在后台为服务填写请求头(如 `Authorization: Bearer …`)。令牌只保存在服务端,后台和接口不会回显。 ### 9.2 OAuth 2.1 需要登录授权的服务,按 MCP 的授权规范实现,平台作为 OAuth 客户端: 1. MCP 端点在未授权时返回 `401`,带 `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource…"`。 2. 提供受保护资源元数据(`/.well-known/oauth-protected-resource`),其中的 `authorization_servers` 指向授权服务器。 3. 授权服务器提供元数据(`/.well-known/oauth-authorization-server`),必须支持 **PKCE(S256)**,并在 `code_challenge_methods_supported` 中声明。 4. 客户端注册,两种方式任选: - 支持**动态注册**(`registration_endpoint`),平台会自动注册; - 或者预先为平台登记一个客户端,回调地址为 `https://<平台地址>/admin/v1/mcp/oauth/callback`,把 Client ID / Secret 交给管理员填写。 5. 签发**刷新令牌**:平台在访问令牌过期后自动续期。没有刷新令牌时,令牌一过期,管理员就要重新授权。 6. 支持 `resource` 参数(RFC 8707),令牌只对这个 MCP 服务有效。 平台的授权**按服务共享**:管理员授权一次,所有使用这个服务的会话都用这个账号。所以请为平台单独建一个权限合适的账号(最小权限),不要用个人管理员账号授权。 ### 9.3 安全要求 - 每个请求都校验令牌,包括 `tasks/get`、`tasks/result`、资源读取。 - 只信任令牌,不信任参数里的身份字段。 - 参数一律在服务端校验,不要依赖模型"会填对"。 - 工具说明、资源内容、返回结果都会进入模型的上下文:不要在其中放入指令性的文字,也不要回显用户可控的内容而不加标注(提示词注入)。 - 本地进程服务在沙箱中运行,默认不能联网、只能写自己的 home 目录。需要联网或访问额外目录时,在登记时说明,由管理员开放。 --- ## 10. 不支持或不建议 | 能力 | 原因 | |---|---| | Sampling(服务请平台调用模型) | 花平台的 token,外部服务可以借此驱动模型。需要模型能力的服务,请自己接入模型 | | 资源订阅(`resources/subscribe`) | 暂不支持。资源变化后,下次读取时就是新的内容 | | Roots | 平台不向服务暴露本地文件系统,文件通过资源或参数传递 | | 补全(`completion/complete`) | 暂不支持。参数的取值写在说明或 `enum` 中 | | 服务端日志(`notifications/message`) | 不会显示给用户。排查问题请看服务自己的日志 | | 服务的 `instructions` | 不转给模型,内容写进工具说明 | --- ## 11. 上线前自查 **工具** - [ ] 工具名短(`服务名__工具名` 不超过 64 个字符),动词开头,一个工具一件事 - [ ] 说明写清做什么、什么时候用、限制;没有指令性文字 - [ ] 参数都有说明;能枚举的用 `enum`;必填参数少 - [ ] 只读 / 破坏性 / 幂等提示准确 - [ ] 错误用 `isError` 返回,说明原因和下一步 - [ ] 列表分页,结果控制在几千字以内 **结果** - [ ] 需要准确字段的工具有 `outputSchema` 和 `structuredContent`,并同时返回 JSON 文本 - [ ] 文件以嵌入资源返回,URI 以文件名结尾,并附一句摘要 **长任务与询问** - [ ] 超过 3 分钟的操作用 Tasks;`createTask` 立即返回;提供 `pollInterval` 和进度说明;实现了取消 - [ ] 任务按认证身份归属、持久化保存、至少保留 24 小时 - [ ] 询问处理了 accept / decline / cancel 三种结果;不索要密码和密钥 - [ ] 无人值守时工具有不询问的执行方式 **资源、提示模板与安全** - [ ] 资源有清楚的标题、说明、`mimeType`;大文档已拆分;资料多时提供搜索工具 - [ ] 提示模板的参数都有说明 - [ ] 每个请求都校验令牌;OAuth 支持 PKCE(S256)和刷新令牌;平台使用最小权限的账号 --- ## 12. 参考服务与验证 参考服务 [`examples/mcp-reference-server`](../../examples/mcp-reference-server/server.mjs) 用一个虚构的订单系统演示了本指南的写法: | 工具 / 资源 | 演示内容 | |---|---| | `get_order`、`list_orders` | 只读、结构化输出、分页、写给模型的错误信息 | | `add_note`、`cancel_order` | 不幂等的写操作;破坏性 + 幂等(自动模式下也要确认) | | `ship_order`、`connect_crm` | 表单询问;网址询问 | | `export_report` | 长任务:进度、即时说明、结果中返回 CSV 文件 | | `purge_cancelled` | 长任务中询问用户确认 | | `enable_beta` | 工具列表变化 | | `ref://docs/order-api.md`、`ref://orders/{orderId}` | 资源、资源模板 | | `order_review` | 提示模板(附带订单资源) | ```bash cd examples/mcp-reference-server pnpm install # 或 npm install PORT=8790 REF_TOKEN=换成随机字符串 node server.mjs ``` 在后台"MCP 服务"中添加:远程 HTTP,地址 `http://<主机>:8790/mcp`,请求头 `Authorization: Bearer `。点"刷新工具"后,工具列表中应当显示只读、破坏性、幂等、结构化输出、长任务等标记,服务旁显示"资源"、"提示模板"。在 Profile 中勾选这个服务,到 Playground 中试一试: - "查一下订单 A1001":直接返回,不审批 - "取消订单 A1002":弹出审批,且没有"本会话不再询问"的选项 - "给 A1001 安排发货":弹出收件信息表单 - "导出 2026-10 的销售报表,执行 300 秒":转为后台,5 分钟后结果作为新消息出现,附带 CSV 文件 - 输入框的"资料"中附加"订单系统接口说明","快捷指令"中使用"订单复盘" 自己的服务可以按同样的步骤验证。平台的自动化测试 `apps/server/test/mcp-reference.test.ts` 会启动参考服务并逐项检查,保证本指南、示例与平台的实际行为一致。