写给:要把自己系统的能力做成 MCP 服务、接入 AgentServer 的开发者。 依据:MCP 规范 2025-11-25;平台的实现见 design/16-mcp-support.md。 示例代码:
examples/mcp-reference-server/server.mjs(虚构的订单系统,覆盖本指南的全部写法)。 最后更新:2026-10-29
本指南不另立标准,只回答两个问题:MCP 规范里的可选项,在 AgentServer 上哪些有效、怎么用;工具怎么设计,模型才好用。
五条原则
平台支持情况速查
| 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 |
| 方式 | 适合 | 说明 |
|---|---|---|
| Streamable HTTP(推荐) | 独立部署的服务 | 平台按"服务 × 会话"建立连接;旧版 SSE 服务自动兼容 |
| 本地进程(stdio) | 命令行工具、只在本机使用 | 在沙箱中运行:系统目录只读、独立的 home、默认不能联网、只能访问管理员开放的路径;密钥只通过环境变量传入。所有会话共用一个进程 |
需要询问用户(§6)的服务请用 HTTP:本地进程被多个会话同时使用时,平台无法确定询问属于哪个会话,会拒绝询问。
管理员在后台"MCP 服务"页面添加服务、测试连接,再到 Profile 中勾选;勾选了这个服务的 Profile,对话中才会出现它的工具。
_、-,最多 32 位。模型看到的工具名是 服务名__工具名,整体最多 64 个字符,超出部分被截掉。所以工具名要短:get_order 好于 get_order_detail_by_order_number。instructions(初始化时返回的服务说明)目前不会转给模型,要让模型知道的内容写在各个工具的说明里。推荐用官方 SDK:TypeScript @modelcontextprotocol/sdk(1.32 及以上,参考服务使用 1.32.1)、Python mcp。长任务和询问用户需要支持 2025-11-25 规范的版本。
cancel_order 好于 update_order(status='cancelled')。snake_case:get_、list_、search_、create_、update_、delete_、export_。title(显示名),后台和审批卡片会显示它。工具说明(description)回答三件事:做什么、什么时候用、有什么限制。
按订单号查询订单的客户、金额、状态和商品。订单号形如 A1001。
不知道订单号时,先用 list_orders 按状态查找。
description;能枚举的用 enum;格式用 pattern / format;给出 default。| 提示 | 含义 | 平台的用法 | 不写时 |
|---|---|---|---|
readOnlyHint: true |
不修改任何东西 | 默认直接调用;只读模式下只提供这类工具 | 按非只读处理 |
destructiveHint: true |
删除、覆盖、不可恢复 | 默认总是审批:自动模式下也要用户确认,而且每次都问 | 平台不据此强制审批(规范的默认值是 true,平台只认明确写出的 true) |
idempotentHint: true |
同样的参数重复调用,结果相同 | 遇到网络错误时自动重新连接并重试一次 | 不重试,避免重复执行 |
openWorldHint: false |
只操作本系统,不与外部交互 | 后台和审计中不标"外部交互" | 视为与外部交互 |
业务错误用工具结果返回:isError: true,加一段写给模型的说明,讲清原因和下一步怎么做。
{ "isError": true, "content": [{ "type": "text", "text": "订单 A9999 不存在。可以先用 list_orders 查看现有订单号。" }] }
协议错误(参数不符合 schema、工具不存在)用 JSON-RPC 错误返回,SDK 会处理。
不要把堆栈、SQL、内部地址放进错误信息:模型会原样转述给用户。
权限不足、令牌失效时说明清楚("令牌没有导出权限"),平台和用户才知道该找谁。
cursor,返回 nextCursor,每页 20 到 50 条。requestId),或者在服务端按业务键去重。给模型读的结论放在最前面,例如"共 3 条,其中 1 条已逾期"。表格用 Markdown 或 CSV,不要用 HTML。
需要模型(或平台)准确使用字段时,声明 outputSchema 并返回 structuredContent,同时把同样的 JSON 序列化成一段文本放进 content(兼容只读文本的客户端):
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 }
})
平台的处理:
outputSchema 校验 structuredContent。不符合时结果照常交给模型,审计中记录校验错误,管理员能看到。description。报表、导出、图片等文件,以嵌入资源返回:
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 要以有意义的文件名结尾。| 执行时间 | 做法 |
|---|---|
| 几秒到 3 分钟 | 普通工具。超过 30 秒的,建议发进度通知(notifications/progress),平台会延长等待时间 |
| 3 分钟以上,或者不确定 | 长任务(Tasks) |
不支持 Tasks 的工具运行超过 3 分钟时,平台也会转到后台,保持连接等待(最长 24 小时)。但服务重启、网络断开都会让这次调用失败,所以超过几分钟的操作请用 Tasks。
不要再做"提交 + 查询"两个工具让模型轮询。 模型没有时钟,只能连续查询(浪费 token),或者这一轮结束后就没人再查了。查询进度的工具可以保留,供用户问"进度怎么样"时使用。
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),
})
平台的处理:
ttl 为 24 小时),先在前台查询最多 3 分钟。期间完成的,结果直接交给模型,和普通工具一样。model-immediate-response 中的说明,结束这一轮,告诉用户已提交。pollInterval 查询状态(最短 5 秒,最长 5 分钟,不提供时为 30 秒),statusMessage 显示在后台调用栏和后台页面。tasks/cancel。请实现取消,停止后台的执行。createTask 要立即返回(几秒以内),实际工作在后台进行。
InMemoryTaskStore 默认按会话绑定,参考服务给出了改为共享的写法(SharedTaskStore)。ttl 到期,不要早于平台的最长等待时间(24 小时)。storeTaskResult(taskId, 'failed', { isError: true, content: [...] }),说明写给模型看。工具执行到一半,缺少信息或需要确认时,可以向用户提问,不必直接报错再让模型来回追问:
不要把每个写操作都做成询问确认:审批由平台负责(§3.3 的工具提示)。
表单模式:
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。不支持嵌套对象。平台在用户提交时按这些约束校验一次,服务端仍要自己校验。
网址模式:
const r = await server.server.elicitInput({ mode: 'url', message: '请登录 CRM 并授权', url: 'https://crm.example.com/oauth/authorize?…', elicitationId: crypto.randomUUID() })
平台显示完整网址和域名,用户自己点开,完成后点"我已完成"(accept)。平台不会自动打开网址。
cancel。input_required,再发出询问,并带上 relatedTask(参考服务的 purge_cancelled)。平台会显示卡片,用户回答后任务继续。工具会随配置或权限变化时,声明 capabilities.tools.listChanged: true(官方 SDK 在注册工具时自动声明并发送通知)。工具增减后发送 notifications/tools/list_changed,平台会重新获取工具列表:
工具列表最好保持稳定。频繁变化会让模型的行为不稳定,Codex 的会话也会频繁切换。
适合做成资源的:参考资料,例如产品手册、接口说明、数据字典、表结构、配置、运行状态。它们由模型按需读取,或者由用户附加到对话,而不是"去做一件事"。
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 万字时,平台只给模型开头,完整内容存成文件。所以大文档按章节拆成多个资源。search_docs 工具,返回匹配的资源 URI 和片段。description 中说明取值。| 内容 | 做成 |
|---|---|
| 不需要参数、偶尔变化的参考资料 | 资源 |
| 按 id 读取一条记录 | 资源模板,或只读工具(两者都可以;需要筛选、分页时用工具) |
| 查询、搜索、统计 | 只读工具 |
| 有副作用的操作 | 工具 |
适合固定的工作流程,例如"订单复盘"、"生成周报"。用户在输入框的"快捷指令"中选择、填写参数后,展开的内容放进输入框,用户可以修改后发送。
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,它会显示为输入框的提示。参数值都是字符串。最简单的方式:管理员在后台为服务填写请求头(如 Authorization: Bearer …)。令牌只保存在服务端,后台和接口不会回显。
需要登录授权的服务,按 MCP 的授权规范实现,平台作为 OAuth 客户端:
401,带 WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource…"。/.well-known/oauth-protected-resource),其中的 authorization_servers 指向授权服务器。/.well-known/oauth-authorization-server),必须支持 PKCE(S256),并在 code_challenge_methods_supported 中声明。registration_endpoint),平台会自动注册;https://<平台地址>/admin/v1/mcp/oauth/callback,把 Client ID / Secret 交给管理员填写。resource 参数(RFC 8707),令牌只对这个 MCP 服务有效。平台的授权按服务共享:管理员授权一次,所有使用这个服务的会话都用这个账号。所以请为平台单独建一个权限合适的账号(最小权限),不要用个人管理员账号授权。
tasks/get、tasks/result、资源读取。| 能力 | 原因 |
|---|---|
| Sampling(服务请平台调用模型) | 花平台的 token,外部服务可以借此驱动模型。需要模型能力的服务,请自己接入模型 |
资源订阅(resources/subscribe) |
暂不支持。资源变化后,下次读取时就是新的内容 |
| Roots | 平台不向服务暴露本地文件系统,文件通过资源或参数传递 |
补全(completion/complete) |
暂不支持。参数的取值写在说明或 enum 中 |
服务端日志(notifications/message) |
不会显示给用户。排查问题请看服务自己的日志 |
服务的 instructions |
不转给模型,内容写进工具说明 |
工具
服务名__工具名 不超过 64 个字符),动词开头,一个工具一件事enum;必填参数少isError 返回,说明原因和下一步结果
outputSchema 和 structuredContent,并同时返回 JSON 文本长任务与询问
createTask 立即返回;提供 pollInterval 和进度说明;实现了取消资源、提示模板与安全
mimeType;大文档已拆分;资料多时提供搜索工具参考服务 examples/mcp-reference-server 用一个虚构的订单系统演示了本指南的写法:
| 工具 / 资源 | 演示内容 |
|---|---|
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 |
提示模板(附带订单资源) |
cd examples/mcp-reference-server
pnpm install # 或 npm install
PORT=8790 REF_TOKEN=换成随机字符串 node server.mjs
在后台"MCP 服务"中添加:远程 HTTP,地址 http://<主机>:8790/mcp,请求头 Authorization: Bearer <REF_TOKEN>。点"刷新工具"后,工具列表中应当显示只读、破坏性、幂等、结构化输出、长任务等标记,服务旁显示"资源"、"提示模板"。在 Profile 中勾选这个服务,到 Playground 中试一试:
自己的服务可以按同样的步骤验证。平台的自动化测试 apps/server/test/mcp-reference.test.ts 会启动参考服务并逐项检查,保证本指南、示例与平台的实际行为一致。