MCP 工具设计:让模型选对并用对
文章指出,将 REST 端点逐个包装成 MCP 工具,容易导致模型误选工具或填错参数;关键在于工具职责划分及运行时描述。材料还介绍了 TypeScript SDK 的包拆分、Schema 支持和部署流水线工具示例。
AI 深度解读
MCP Server 能连通,不代表模型会用。原文讨论的是一个常见落差:把 REST API 的每个端点包装成工具后,模型依然选错工具、填错参数。作者的核心判断是,工具设计要围绕任务展开,描述、参数说明和错误消息都属于接口的一部分。
对准备给编辑器或 coding agent 接内部服务的程序员,这篇的价值在于划清边界:SDK 负责协议和校验,开发者负责让模型理解任务;涉及生产操作时,理解之外还必须有真正的授权。
模型需要的是任务入口,不是端点目录
传统 API 面向开发者,开发者可以读文档、看示例、反复调试。MCP 工具面对的则是运行时读取 JSON Schema 的模型。工具名字相近、参数语义含糊,协议再标准,也无法替你解决选择问题。
作者建议从两三个对应实际任务的工具开始。比如部署场景,先提供「查最近部署」,再提供「回滚部署」,并在描述里交代它们的先后关系。
原文称,TypeScript SDK 的 v2 已是稳定版,对应 2026-07-28 规范,拆成 @modelcontextprotocol/server 和 @modelcontextprotocol/client。Schema 使用 Standard Schema,可搭配 Zod v4、Valibot、ArkType。旧包 @modelcontextprotocol/sdk 属于 v1,仍有修复,并提供迁移指南和 codemod。这些是原文给出的版本信息,本文未独立核验。
下面这段完整示例,把查询范围、参数来源和后续用途一起写进了工具定义:
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
import { listDeploys } from "./deploys.js";
function createServer(): McpServer {
const server = new McpServer({ name: "deploys", version: "1.0.0" });
server.registerTool(
"list-recent-deploys",
{
title: "List recent deploys",
description:
"List the most recent production deploys for one service, newest first. " +
"Use this before rolling back to find the deploy ID to roll back to.",
inputSchema: z.object({
service: z
.string()
.describe(
'Service slug as shown in the dashboard, e.g. "billing-api"',
),
limit: z
.number()
.int()
.min(1)
.max(20)
.optional()
.describe("Defaults to 5"),
}),
annotations: { readOnlyHint: true },
},
async ({ service, limit }) => {
const deploys = await listDeploys(service, limit ?? 5);
if (deploys.length === 0) {
return {
content: [
{
type: "text",
text: `No deploys found for "${service}". Check the service slug.`,
},
],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(deploys, null, 2) }],
};
},
);
return server;
}
void serveStdio(createServer);
console.error("deploys MCP server running on stdio");
重点看 .describe():它会进入模型看到的 JSON Schema。service 不只是字符串,还是控制台里的服务标识;limit 不只是数字,还必须是 1–20 的整数,省略时由 limit ?? 5 执行默认值。说明默认值和落实默认值,是两件事。
按照原文,SDK 从 inputSchema 派生 JSON Schema,在 handler 执行前校验参数,同时推导 handler 类型。另一个容易踩坑的细节是最后的 console.error:stdio 模式下,stdout 承载 JSON-RPC,随手添加 console.log 可能破坏协议流。
错误消息要帮助重试,描述不能代替授权
模型调用失败后,还会继续读取返回结果。因此,错误消息同样影响下一步行为。
原文举例,limit 超过上限时,SDK 会返回带 isError: true 的工具结果,提示 limit: Too big: expected number to be <=20。模型据此有机会修正参数。业务 handler 也应遵循这个思路:告诉调用方哪里出了问题,以及接下来能检查什么。
示例中的「没有找到部署,请检查服务标识」就比一段 TypeError 更有用。不过,修复提示必须符合真实故障,不能把权限不足、服务不可用都归结为参数错误。
结果若还要交给程序处理,作者建议增加 outputSchema,同时返回 structuredContent 和便于阅读的 content。按原文,SDK 会校验结构化结果,并通过 tools/list 公布输出结构,供客户端校验。
回滚工具则多了一层风险。作者为 rollback-deploy 设置:
readOnlyHint: false,表明它会修改状态。destructiveHint: true,表明它具有破坏性。idempotentHint: true,表明它被声明为幂等操作。
其 deployId 参数说明要求从 list-recent-deploys 获取,例如 dpl_8f2c;工具描述还要求用户确认精确的部署 ID。
这些文字和 annotations 都是提示,真正的授权必须在 rollback() 内执行。
宿主可能据此展示确认界面,也可能忽略这些提示。声明幂等也不会自动让业务实现变得幂等,开发者仍需保证重复调用符合声明。
显式传递状态,也先判断是否需要 MCP
原文称,2026-07-28 规范移除了 Streamable HTTP 中的协议级会话、Mcp-Session-Id 请求头以及 initialize 握手,每次请求自行携带协议版本和客户端能力。
跨调用需要保留的状态,应通过显式句柄传递。这里的句柄可以理解为一个后续操作能引用的标识:查询返回 deployId,回滚再接收它,依赖关系就摆在参数里。
原文还提到,规范要求服务端 SHOULD 以确定顺序返回 tools/list,有助于客户端缓存和 LLM 提示缓存命中。因此不要让工具列表的顺序取决于无序 map。
但作者并不主张所有项目都接 MCP。如果只有自己的应用调用模型,直接使用供应商 API 的 function calling,代码和进程都更少。需要让编辑器、桌面助手、coding agent 共用一套工具时,MCP 的复用价值才更明确。
我的判断
我认同把描述当作接口设计来审查。参数能通过类型校验,只能说明形状合法,不能证明模型选对了服务或部署目标。任务边界、参数来源和失败提示,确实值得与 handler 一起评审。
但「描述就是整个 API」更像强调重点的标题。实际接口还包含权限、输出契约、幂等性和宿主行为。原文给了示例,没有提供工具选择准确率、重试成功率,也没有与「每个端点一个工具」做对照实验,因此效果判断属于经验之谈。
版本相关内容尤其需要谨慎。原文涉及 SDK 包拆分和协议握手变化,落地前应确认项目实际使用的 SDK 与宿主支持情况,不能仅凭文章替换现有初始化流程。
能拿来做什么
- 给 Node 服务封装 MCP 时,先选两三个完整任务,为每个工具写清使用时机、参数来源和执行影响,再接业务函数。
- 检查现有工具的报错,优先补上可执行的修复提示;涉及程序消费的结果,再增加明确的输出契约。
- 做部署或回滚工具时,在业务函数里落实授权,并核对幂等声明是否符合实现。
- 用
npx @modelcontextprotocol/inspector npx tsx src/index.ts检查工具注册和调用结果;如果只有自家应用使用模型,也先评估 function calling 是否已经够用。