当前位置: 首页 > news >正文

032、自定义 MCP 插件:从开发到发布的全流程

032、自定义 MCP 插件:从开发到发布的全流程

上周五凌晨两点,我盯着终端里那行血红色的报错发呆:

Error: MCP tool 'fetch_github_issue' returned non-serializable result

Claude Code 调用我写的 MCP 插件时,返回了一个包含datetime对象的字典——JSON 序列化直接炸了。这个坑让我意识到,写一个能用的 MCP 插件和写一个能上生产环境的 MCP 插件,中间隔着一条河。

为什么需要自定义 MCP 插件

Claude Code 内置的工具集已经很强了,但总有边界。比如我需要它直接操作公司内部的 Jira 工单系统、查询自建的 CI/CD 流水线状态、或者调用某个内部 API 做数据脱敏——这些场景下,自定义 MCP 插件是唯一解。

MCP(Model Context Protocol)本质上是一个轻量级的 RPC 协议,Claude Code 通过它来发现和调用外部工具。每个插件暴露一组工具(tools),每个工具有自己的输入参数和输出格式。Claude 会像人类阅读 API 文档一样,根据你的 prompt 自动选择合适的工具来调用。

脚手架搭建:别从零开始

我见过太多人从mkdir my-mcp-plugin开始,然后手写整个项目结构。别这样写,直接用官方脚手架:

npx @anthropic/create-mcp-server my-plugincdmy-pluginnpminstall

这个脚手架会生成一个 TypeScript 项目,包含完整的类型定义和开发服务器。你只需要关注业务逻辑。

项目结构长这样:

my-plugin/ ├── src/ │ ├── index.ts # 入口,注册工具 │ ├── tools/ # 每个工具一个文件 │ │ ├── hello.ts │ │ └── fetch_data.ts │ └── utils/ # 工具函数 │ └── api_client.ts ├── package.json └── tsconfig.json

写第一个工具:从踩坑开始

假设我们要写一个查询 GitHub Issue 的工具。先定义工具 schema:

// src/tools/fetch_issue.tsimport{z}from'zod'// 这里踩过坑:参数名一定要用下划线命名法,Claude 对驼峰的支持不稳定exportconstFetchIssueSchema=z.object({owner:z.string().describe('仓库所有者,比如 "anthropics"'),repo:z.string().describe('仓库名,比如 "claude-code"'),issue_number:z.number().int().positive().describe('Issue 编号'),})exporttypeFetchIssueParams=z.infer<typeofFetchIssueSchema>exportasyncfunctionfetchIssue(params:FetchIssueParams){const{owner,repo,issue_number}=paramsconsturl=`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`constresponse=awaitfetch(url,{headers:{'Accept':'application/vnd.github.v3+json',// 别这样写:把 token 硬编码在这里// 'Authorization': 'Bearer ghp_xxx'}})if(!response.ok){thrownewError(`GitHub API 返回${response.status}:${response.statusText}`)}constdata=awaitresponse.json()// 这里踩过坑:直接返回 data 会包含 Date 对象,导致序列化失败// 必须手动序列化return{title:data.title,state:data.state,body:data.body?.substring(0,500),// 限制长度,Claude 上下文有限labels:data.labels.map((l:any)=>l.name),created_at:data.created_at,// 已经是字符串,安全html_url:data.html_url,}}

关键点:返回的数据必须是纯 JSON 可序列化的。任何DateMapSet或者循环引用的对象都会让 Claude Code 崩溃。我那次凌晨的报错就是因为忘了把datetime转成字符串。

注册工具:别漏了这一步

写好了工具函数,需要在入口文件注册:

// src/index.tsimport{Server}from'@anthropic/mcp-server'import{fetchIssue,FetchIssueSchema}from'./tools/fetch_issue'constserver=newServer({name:'github-helper',version:'1.0.0',})// 注册工具:name 要简短,description 要详细// Claude 会根据 description 来决定是否调用这个工具server.tool('fetch_github_issue','获取 GitHub 仓库中指定 Issue 的详细信息,包括标题、状态、标签和内容摘要',FetchIssueSchema,async(params)=>{constresult=awaitfetchIssue(params)return{content:[{type:'text',text:JSON.stringify(result,null,2)}]}})server.start()

这里有个容易被忽略的点:description 字段是 Claude 理解工具用途的唯一途径。写得太简略,Claude 可能不会调用你的工具;写得太啰嗦,Claude 可能误解。我一般控制在 50-100 字,包含:工具做什么、输入是什么、输出是什么。

本地调试:模拟 Claude 的调用

开发阶段最痛苦的是每次都要启动 Claude Code 来测试。我后来发现可以直接用 MCP 的调试工具:

# 启动开发服务器npmrun dev# 在另一个终端,用 mcp-cli 测试npx @anthropic/mcp-cli call fetch_github_issue\--params'{"owner": "anthropics", "repo": "claude-code", "issue_number": 42}'

这样能快速验证工具是否正常工作,而不需要经过 Claude 的 prompt 解析层。等工具逻辑稳定了,再集成到 Claude Code 里做端到端测试。

配置管理:环境变量的正确姿势

插件里免不了要配置 API Key、数据库连接串之类的敏感信息。别写死在代码里,也别用.env文件——Claude Code 的插件运行环境不一定能读到你的.env

正确做法是使用 MCP 的配置机制:

// 在工具函数里读取环境变量constGITHUB_TOKEN=process.env.GITHUB_TOKENif(!GITHUB_TOKEN){thrownewError('请设置 GITHUB_TOKEN 环境变量')}

然后在 Claude Code 的配置文件~/.claude/settings.json里注入:

{"mcpServers":{"github-helper":{"command":"node","args":["path/to/your/plugin/dist/index.js"],"env":{"GITHUB_TOKEN":"ghp_your_token_here"}}}}

这样配置的好处是:token 只存在于 Claude Code 的配置中,不会泄露到代码仓库里。

错误处理:让 Claude 知道发生了什么

工具调用失败时,返回的错误信息要足够清晰,因为 Claude 会根据错误信息决定下一步操作。别返回Error: something went wrong这种废话。

try{constresult=awaitfetchIssue(params)return{content:[{type:'text',text:JSON.stringify(result)}]}}catch(error){// 这里踩过坑:直接返回 error.message 可能不够// Claude 需要知道:为什么失败?用户能做什么?if(errorinstanceofFetchError){return{isError:true,content:[{type:'text',text:`GitHub API 请求失败:${error.message}。请检查 owner 和 repo 名称是否正确,或者 Issue 是否存在。`}]}}// 兜底错误return{isError:true,content:[{type:'text',text:`未知错误:${error}`}]}}

注意isError: true这个字段——告诉 Claude 这是一个错误响应,而不是正常结果。Claude 会据此调整后续行为,比如向用户解释错误原因,或者尝试其他参数。

发布到 npm:版本号要谨慎

插件开发完成后,发布到 npm 让团队其他人使用:

# 先构建npmrun build# 更新版本号,遵循 semver# 别这样写:npm version patch 直接推# 先确认 changelog 和 README 都更新了npmversion patchnpmpublish

发布前检查package.json里的files字段,确保只包含构建产物:

{"files":["dist/**/*","README.md"],"main":"dist/index.js","types":"dist/index.d.ts"}

别把src/目录和node_modules/也发布上去,浪费空间不说,还可能暴露源码逻辑。

版本兼容性:一个容易被忽视的坑

MCP 协议本身在快速迭代中。我遇到过最坑的情况是:插件在本地调试正常,部署到 CI 环境后 Claude Code 报Tool not found。排查了半天,发现是 CI 环境里的@anthropic/mcp-server版本太旧,不支持我用的某个 API。

解决方案:在package.json里锁定@anthropic/mcp-server的版本范围:

{"peerDependencies":{"@anthropic/mcp-server":">=0.3.0 <0.5.0"}}

同时在 README 里明确标注兼容的 Claude Code 版本。

个人经验:三个让插件更好用的技巧

  1. 工具粒度要适中。别把整个业务逻辑塞进一个工具里,也别拆得太碎。一个工具对应一个原子操作,比如“查询 Issue”、“创建 Issue”、“关闭 Issue”各一个工具。Claude 会组合调用多个工具来完成复杂任务。

  2. 给工具加缓存。如果工具查询的是不常变化的数据(比如项目配置、用户信息),在工具内部加一个简单的内存缓存,TTL 设 30 秒。Claude 有时会在同一个对话里多次调用同一个工具,缓存能显著提升响应速度。

  3. 日志是救命稻草。在工具的关键路径上加console.error日志(别用console.log,会污染 Claude 的响应解析)。当 Claude 调用工具失败时,这些日志会出现在 Claude Code 的调试输出里,帮你快速定位问题。

console.error(`[github-helper] 开始查询 Issue #${issue_number}`)// ... 业务逻辑console.error(`[github-helper] 查询完成,耗时${Date.now()-start}ms`)

最后说一句:MCP 插件开发的门槛不高,但要做好需要理解 Claude 的思维方式——它不是一个普通的 API 调用者,而是一个会“思考”的代理。你的工具设计得越符合直觉,Claude 用起来就越顺手。

http://www.jsqmd.com/news/1047578/

相关文章:

  • Appium自动化测试环境搭建全攻略:从零到一跨过移动测试第一道坎
  • PHP Webshell安全防护:从原理到实战的立体化防御体系
  • 2026年铜川市贵金属旧料回收优质靠谱实体门店精选五家 黄金回收铂金回收白银回收彩金回收真实探店测评清单及联系方式推荐 - 前途无量YY
  • 2026年云浮市贵金属旧料回收优质靠谱实体门店精选五家 黄金回收铂金回收白银回收彩金回收真实探店测评清单及联系方式推荐 - 前途无量YY
  • 山南市奢侈品手表包包回收门店整理,各区均有分店联系方式公布 - 谊识预商贸
  • 番茄病害YOLO检测数据集:千张田间真图+农业专家标注
  • 2026年温州市老百姓优先选择的五家贵金属回收门店 黄金回收白银回收铂金回收彩金回收合规靠谱门店测评合集+联系方式 - 亦辰小黄鸭
  • 2026 北京黄金回收核心门店综合测评|靠谱连锁品牌实力横向对比研判 - 奢侈品回收
  • ai学习第一天 - 小镇
  • SMUDebugTool:解锁AMD Ryzen处理器隐藏性能的终极调试指南
  • MC68HC908RFRK2电气特性深度解析:从参数表到低功耗无线设计实战
  • Java防内鬼审计黑匣子:构建不可篡改的企业级日志架构
  • 丽水云和县全吨位全新地磅定制销售|上门实地勘测地磅安装整机调试|地磅维修处理称重误差与仪表损坏故障 - 天堂海洋
  • 2026年乌海市老百姓优先选择的五家贵金属回收门店 黄金回收白银回收铂金回收彩金回收合规靠谱门店测评合集+联系方式 - 亦辰小黄鸭
  • GDA:Android应用安全分析利器,一键反编译与深度漏洞挖掘
  • 宿迁市爱马仕手表包包奢侈品回收,5家门店最新回收价格整理 - 谊识预商贸
  • 2026年铜陵市贵金属旧料回收优质靠谱实体门店精选五家 黄金回收铂金回收白银回收彩金回收真实探店测评清单及联系方式推荐 - 前途无量YY
  • 高效智能获取百度网盘提取码:技术爱好者的自动化解决方案
  • AI攻防竞速:构建秒级响应的智能安全防御体系
  • 线上投票工具哪个最好用?2026 多平台实测对比分析 - 微信投票小程序
  • 2026年乌兰察布市老百姓优先选择的五家贵金属回收门店 黄金回收白银回收铂金回收彩金回收合规靠谱门店测评合集+联系方式 - 亦辰小黄鸭
  • CefFlashBrowser:Flash内容终极解决方案,让经典游戏和应用重获新生
  • 2026年铜仁市贵金属旧料回收优质靠谱实体门店精选五家 黄金回收铂金回收白银回收彩金回收真实探店测评清单及联系方式推荐 - 前途无量YY
  • 如何3秒破解百度网盘提取码:免费智能工具完整使用指南
  • SCMP学习周期多久?众智商学院APP刷题两周够吗? - 众智商学院课程中心
  • 深入解析MC68HC08AB16A监控ROM与TIMA模块:嵌入式调试与定时控制核心
  • 2026应用安全监测避坑:从POC测试到SLA谈判的完整采购指南
  • Ghidra逆向工程实战:三大核心功能提升分析效率
  • JMeter结合Python实现动态参数化压测:从CSV到实时服务的实战指南
  • 2026 北京黄金奢侈品回收核心门店咨询电话|本地专业靠谱连锁店铺线上预约渠道 - 奢侈品回收