前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9321
  • 实测:选对模型把我AI API账单削减90%
  • Agentic AI延迟问题:算力堆叠不是答案
  • NoWreck:验证AI代码修改是否属实的工具
  • AI Agent成为新型攻击面:自我复制威胁研究
  • 多模型API退出测试:成本账本才是选型关键
  • 为什么不能只用Claude做所有事
  • Axonius多租户AI Agent隔离方案实践
  • 用LiteLLM将Claude Code路由到DeepSeek节省成本
  • Claude Code支持AGENTS.md跨工具标准配置
  • Coding Agent 账单省 50-70%:利用 sticky routing 保住 Prompt Cache 命中
  • AI Agent 为何还在用 while(true) 循环——工程陷阱深度剖析
  • SEO Agent 选 MCP 还是 REST?一份实用决策框架
  • SGLang深度解析:如何高效服务DeepSeek-V4-Pro
  • FlakeFixer: 用Agent自动分析Flaky Test
  • AI编码Agent记忆系统设计的四个教训:删除不是过期
  • 盲人开发者为视障群体打造AI描述应用ScribeMe
  • AI代码审查员的验证悖论:声称完成≠真正完成
  • LLM API多租户安全清单:tenants-safety essential
  • AI Agent不应持有你的钥匙:权限最小化原则
  • Claude 多智能体系统上演自复制恶意软件攻防战
  • MCP Server 开发避坑指南:工具描述比 TypeScript 更难
  • 生产级 Solana Agent 交易生命周期深度解析
  • 5分钟让AI助手读懂你的代码库
  • 用Python构建AI简历筛选器
  • Agent上下文满了该丢什么:长对话记忆管理实战
  • Warp推出Factories:一站式AI软件开发工厂基础设施
  • AI Agent试点到生产:成本暴涨700倍的教训
  • Cursor发布Origin功能:AI编程上下文管理
  • TryHackMe 提示词注入 CTF 实战攻略
  • 面向 Agent 的运维队列:失败自动转Ticket
  • 四个静默失败的 CI 检查:它们都是绿的,但什么都没做
  • AI 编码工具会读取 .env:本地 DLP 代理 Anonmyz 在prompt边界截流
  • Google 开源 SAM:零配置的 AI Agent P2P 发现与调用网络
  • Cursor Skills完全指南:格式规范与跨Agent迁移实测
  • OpenAI Codex Skills规范详解:目录结构与官方文档未记载的细节
  • 用Gitea自建Claude Code内部插件市场,团队Skill统一分发
  • Claude Skills规范深度解读:从格式到团队协作
  • 2026年LLM应用架构实战:摆脱if/else链式判断
  • Anthropic CEO:AI天然趋向集中,开源只是转移权力
  • 微软 Copilot 隐藏参数漏洞可被利用窃取密码
  • LangChain 揭示:Agent 效果不佳时换模型是误区,换 Harness 才是关键
  • 三阶段工作流让 AI Agent 保持精准:Research-Plan-Implement
  • 小米MiMo桌面应用即将上线,AI编程助手6月已开源
  • Agent成熟度记分卡:追踪AI Agent可靠性的五个核心维度
  • 模型趋同时代:系统架构比选模型更重要
  • 代码审核功能默认关闭的教训
  • 程序员用 Claude 为 Windows 专用 HP 打印机编写 macOS 驱动
  • NIST AI风险管理框架生产级RAG实战
  • Codex自动探索优化技能:研究-测试-评判闭环
  • AI协作UML编辑器:代码臭味一目了然
  • AI API成本降低95%的实战经验
  • 已加载 51 / 9321
9.0
重磅
AI SCORE
编程提效2026-08-18 22:32

MCP Server 开发避坑指南:工具描述比 TypeScript 更难

dev.to · AI#MCP#Claude Code#Agent
Editor brief · 编辑速览

作者周末搭建了第一个 MCP Server 让 Claude Code 查询内部服务目录,发现给智能体提供工具本质是 UX 问题而非工程问题:工具命名、描述、返回值和错误提示的设计占了 14 小时中的 12 小时。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

我花了整个周末构建了第一个 MCP server,让 Claude Code 能查询我们的内部服务目录,而不用我再把 JSON 复制粘贴到聊天窗口里。代码只用了两个小时。让 agent 真正用好它却花了剩下的十四个小时——而且几乎全花在了工具描述、输出大小和错误信息上,而不是 TypeScript。下面是这个可用的 server 和我会告诉过去自己的五条经验。

我一直卡在同一个蠢循环里。

Claude Code 每次执行到一半时会问类似这样的问题:"billing-events 服务归哪个团队所有,它的当前部署目标是什么?"这些数据在我们的内部服务目录里——一个部署在 VPN 后的只读 HTTP API。于是我会切换窗口,调用接口,用 jq 处理一下,把输出粘贴回去,然后看着 agent 继续。

每天十次。每次。

显而易见的解决方案是"给 agent 一个工具"。我低估的部分是:给 agent 一个工具是 UX 问题,不是接线问题。接线是已经解决好的、枯燥的、有完善文档的事情。真正的 UX——工具叫什么名字、它说自己是干什么的、它返回什么、失败时说什么——才是你真正花掉周末的地方。

一个有趣的约束:目录 API 返回很啰嗦。单个服务记录有约 40 个字段,其中大部分自 2023 年以来就没人看过。把这些全塞进 agent 的上下文,是用掉 8k token 来回答"谁负责这个"的正确方式。

给还没接触过 MCP 的人一个快速入门。

MCP(Model Context Protocol)是一个开放协议,用于向 LLM 客户端暴露工具、资源和 prompt。你写一个 server;客户端(这里是 Claude Code)启动它,询问它能做什么,然后调用它。重要的架构细节:server 是一个独立进程,传输层通常走 stdio——客户端启动你的进程,通过 stdin/stdout 发送 JSON-RPC。

flowchart LR
    A[Claude Code] -->|spawns process| B[MCP server]
    B -->|tools/list| A
    A -->|tools/call| B
    B -->|HTTPS| C[Internal catalog API]
    C -->|JSON| B
    B -->|text content| A

"通过 stdio 的独立进程"这个细节有一个后果会咬到每个人:你打印到 stdout 的任何东西都是协议流量。一条多余的 console.log 就会破坏数据流,你的 server 会报一个莫名其妙的解析错误然后挂掉。要打到 stderr。这个我后面会再说。

我用的是 TypeScript SDK(@modelcontextprotocol/sdk 1.x,Node.js 22.x)。下面是除了 API 客户端的完整 server,确实就这么小:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { fetchService, searchServices } from "./catalog.js";

const server = new McpServer({
  name: "service-catalog",
  version: "1.0.0",
});

server.registerTool(
  "lookup_service",
  {
    title: "Look up a service",
    description:
      "Get ownership, deploy target, and on-call info for one service " +
      "by its exact catalog name (e.g. 'billing-events'). Use this when " +
      "you know the service name. If you only have a partial name or a " +
      "team name, use search_services first.",
    inputSchema: {
      name: z.string().describe("Exact service name, lowercase-hyphenated"),
    },
  },
  async ({ name }) => {
    const svc = await fetchService(name);
    if (!svc) {
      return {
        content: [
          {
            type: "text",
            text:
              `No service named "${name}". Service names are ` +
              `lowercase-hyphenated. Try search_services with a partial name.`,
          },
        ],
        isError: true,
      };
    }
    return { content: [{ type: "text", text: summarize(svc) }] };
  },
);

await server.connect(new StdioServerTransport());

然后在仓库根目录的 .mcp.json 里,这样它会被提交进版本管理,我的整个团队都能用到:

{
  "mcpServers": {
    "service-catalog": {
      "command": "node",
      "args": ["./tools/catalog-mcp/dist/index.js"],
      "env": { "CATALOG_TOKEN": "${CATALOG_TOKEN}" }
    }
  }
}

运行 claude mcp list 确认已连接,搞定。两小时,大部分时间在回忆 tsconfig 的模块解析是怎么工作的。

真正花掉周末的部分

第一个版本"能用"——工具可以调用,返回正确数据。但仍然没用, transcript 里这个模式告诉我问题在哪:

Me: Who owns billing-events? Claude: (calls get) → 6,200 tokens 的 JSON Claude: The billing-events service is owned by... let me check the owner_team_ref field... it's t_8813.

正确!但完全没用,贵,而且没能把团队 ID 解析成人名,因为我没给它这个能力。以下所有改进都是为了解决这个问题。

1. 工具描述才是真正的产品

我最初写的描述是 description: "Look up a service"。那是 docstring,不是 description。

description 是模型在决定是否调用你的工具时唯一会读到的东西。它不是给会读源码的人类看的文档——它是被注入 agent 决策过程的一段 prompt。我的从 5 个词变成了 4 行,有用的补充全是关于路由的:

它期望什么输入格式(lowercase-hyphenated)

什么时候用这个工具 vs. 隔壁那个("如果你只知道部分名称或团队名,先用 search_services")

在我加上"先用 search_services"的提示之前,agent 会用"Billing Events"调用 lookup_service,什么都查不到,然后就放弃了。之后,它每次都会在第一次查询失败后自我修正。同一份代码,只是换了个句子。

如果你只调一个东西,就调 description 。它是整个 server 里每字符杠杆效应最高的工作。

2. 限制你的输出,并为其阅读体验做塑形

那个 6,200 token 的数据 dump 才是真正的 bug。所以我不再返回 API 响应,而是返回一个摘要:

function summarize(svc: Service): string {
  return [
    `service: ${svc.name}`,
    `owner: ${svc.ownerTeamName} (${svc.ownerSlackChannel})`,
    `on-call: ${svc.oncallRotation ?? "none"}`,
    `deploy target: ${svc.deployTarget}`,
    `tier: ${svc.tier}`,
    `repo: ${svc.repoPath}`,
    `last deploy: ${svc.lastDeployAt}`,
  ].join("\n");
}

七行。约 60 token 而不是 6,200。注意 ownerTeamName——API 返回的是 owner_team_ref: "t_8813",所以 server 做第二次查询然后解析它。在你的 server 里做 join,不要在 agent 脑子里做。agent 要追的每个字段都是又一轮往返和又一次猜错的机会。

对于 search_services(可能匹配多行),我硬性限制结果为 20 条,并追加一行说明:

const shown = hits.slice(0, 20);
const note = hits.length > 20
  ? `\n\n(showing 20 of ${hits.length} matches — narrow your query)`
  : "";

末尾的提示比限制本身更重要。一个被静默截断的列表对 agent 来说看起来像完整列表,它会自信满满地告诉你某服务不存在——因为它在第 21 条。

3. 错误信息也是 prompt

❌ Error: 404
✅ No service named "Billing Events". Service names are lowercase-hyphenated.
   Try search_services with a partial name.

第一种让 agent 向我道歉。第二种让它自我修复然后重试——通常在同一轮对话里,不需要我介入。

MCP server 里的每个错误路径都应该回答:哪里出问题了,以及你应该怎么做?把 isError: true 的响应当作恢复指令,而不是状态码。这大概是对 server 改了 20 行,但它产生了单次最大幅度的提升——agent 在我没有介入的情况下完成问题的频率。

4. 更少、更粗粒度的工具优于忠实地映射 API

我的本能是映射 REST API:get_service、list_services、get_team、list_team_services、get_deploy_history。五个工具,一个端点一个。干净、对称,但错了。

结果:agent 链式调用三次来回答我本来一次就能回答的问题,而且大约有三分之一的时间它会选错第一个工具——因为从外部看它们之间的边界是模糊的。

我把它压缩成两个工具——lookup_service 和 search_services——把团队/部署查询折叠进去。选对工具的准确率基本达到 100%,因为剩下的有意义的决策只有一个:"我是知道确切名称还是不知道?"

这条规则的一般形态:

围绕用户会问的问题来设计工具,而不是围绕你的 API 暴露的端点。如果两个工具总是被一起调用,它们就是一个工具。

我宁愿有 2 个工具带一些内部分支,也不愿有 5 个工具让模型每轮都要区分。

5. 在接入 agent 之前先独立测试 server

最初几个小时我通过问 Claude Code 问题然后盯着答案看对不对来调试。这是一个糟糕的反馈循环——你和 bug 之间有两层非确定性。

MCP Inspector 解决了这个问题:

npx @modelcontextprotocol/inspector node ./dist/index.js

它给你一个 UI,列出你的工具并让你用手动写的参数调用它们,显示原始响应。现在这是一次正常的 API 调试会话:确定性输入,确定性输出,模型不在循环里。

还有那个 stdout 的事,因为它让我花了 40 分钟:server 里的一条 console.log 就会破坏协议。stdout 是传输层。你得到的是一个 JSON 解析错误,和你加的那行代码没有任何明显联系。在第一天就把它接好:

const log = (...args: unknown[]) => console.error("[catalog-mcp]", ...args);

任何不是协议消息的东西都打到 stderr。我现在在写任何其他代码之前先加上那行。

两个我正在做的东西。

资源,不只是工具。现在一切都是工具调用。但服务目录的 tier 定义和 deploy-target 词汇表是静态参考资料——这些更适合做成 MCP 资源,客户端可以直接附加到上下文而不用通过调用来回往返。

写访问,谨慎地来。目录有一个 PATCH 端点来更新 ownership,有个很诱人的场景是"agent 发现 ownership 过期了然后修掉它"。但同样也有一个明摆着会出岔子的方式。如果我做的话,会把它放在确认 prompt 后面加一个 dry-run 模式——返回 diff 但不实际应用——只读工具是宽容的,写工具不是。

我会给开始前的自己的总结:

描述就是 prompt。说清楚工具做什么、什么时候用、以及什么时候用另一个。

限制和塑形输出。摘要、在 server 端解析引用、截断时要明确说出来。

错误应该教人。"404"是死路;"试试 search_services"是恢复路径。

合并总是被一起调用的工具。建模问题,而不是建模端点。

用 Inspector 调试。把模型从循环里拿出来修管道。

协议本身真的很容易——如果你能写 Express handler,今天下午就能写一个 MCP server。把你的时间预算留给接口设计,因为那才是决定 agent 是用好你的工具还是只是用它的部分。

如果你正在构建自己的 MCP server 并遇到了什么奇怪的东西,评论里说一声——我想收集那些坑。关注我这里如果你想看 MCP 资源和安全写访问的后续;我边做边写。

使用的版本:@modelcontextprotocol/sdk 1.x,Node.js 22.x,TypeScript 5.x,Claude Code(2026-08)。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
Claude 多智能体系统上演自复制恶意软件攻防战
下一篇
生产级 Solana Agent 交易生命周期深度解析