Skip to main content

Claude Code MCP 与 CLI 集成指南——我每天如何把自定义工具接进来

作者: Jim Liu12 分钟阅读

Claude Code 如何与 MCP 服务器对话、什么时候把 CLI 封装成 MCP 而不是直接走 Bash,以及我调试中踩过的传输错误。

Claude Code MCP 与 CLI 集成:我怎么把自定义工具接进来

Claude Code 从 ~/.claude.json(以及项目的 .claude/settings.json)读取 MCP 服务器配置,把每一个都当作子进程拉起来,然后把它暴露的工具交给模型调用。如果你已经有一套信得过的 CLI,有两种接进来的方式:包装成 MCP 服务器,或者让 Claude 通过 Bash 调用它。这篇文章是这件事的实战版本,写自我每天用 Claude Code 跑十三个游戏站和四个内容站的工作流里。

TL;DR

  • MCP 服务器是 Claude Code 通过 JSON-RPC 对话的本地子进程(或 HTTP 端点),它对外暴露带类型的工具供模型直接调用。
  • 一次性脚本就让 Claude 走 Bash 调你的 CLI。跨项目、跨会话反复用的,就花二十分钟封装成 MCP——schema 和结构化错误绝对值这个时间。
  • 最难受的失败模式是静默的:MCP 服务器启动崩了,Claude Code 的聊天 UI 里完全没消息。解法是 claude --debug + 直接读 ~/.claude/logs/
  • 我自己的决策线:如果你会写 Bash 一行命令,那就留在 Bash;如果你会写一个有参数、输出 JSON 的 Python 脚本,那就上 MCP。

Claude Code 的 MCP 架构(从外部看)

Claude Code 是一个 TypeScript 写的 CLI 程序。启动时它会扫三处 MCP 配置:项目的 .claude/settings.json、用户级的 ~/.claude.json,以及 --mcp-config 这个命令行参数。每条配置就是一个服务器定义——拉起的命令、传输方式(stdio 或 HTTP)、参数和环境变量。

对 stdio 服务器,Claude Code 把进程拉起来,往它 stdin 写 JSON-RPC 帧,服务器在 stdout 回复。stderr 留给 --debug 模式下查看的日志,正常聊天里看不到。这一条最坑过我——后面调试章节细讲。

对 HTTP 服务器,Claude Code 维持一个连接,在类 WebSocket 通道里交换同样的 JSON-RPC 消息。取舍很清晰:stdio 部署最省事(只要个可执行文件),HTTP 允许多个 Claude 会话共享同一个跑着的服务器。

握手完成后,Claude Code 向服务器要工具列表。每个工具带一份 JSON schema 描述参数和返回结构。模型看到这些工具就跟看到原生的 ReadEditBash 一样——亲和度完全一致。

多数人忽略的点:MCP 服务器就是一个遵守协议的程序。没有 SDK 强制,没有语言限制。我见过 Python、Go、Rust 写的服务器,甚至一个 90 行的 bash + jq 也能跑。如果你 CLI 已经在了,封装一下就行,不必重写逻辑。

~/.claude.json 里配置一个服务器

下面是我用户级配置里实际的 MCP 块,稍作脱敏:

{
  "mcpServers": {
    "obsidian-wiki": {
      "command": "node",
      "args": ["D:/projects/personal/knowledge/obsidian-mcp/dist/index.js"],
      "env": {
        "WIKI_ROOT": "D:/projects/personal/knowledge/obsidian"
      }
    },
    "site-publisher": {
      "command": "python",
      "args": ["-m", "scripts.mcp_publish_blog"],
      "cwd": "D:/projects/personal/agents/ai-distribution-agent"
    }
  }
}

这里有三件小事很关键。command 跑在 Claude Code 继承到的 PATH 上——Windows 上经常不是你终端看到的 PATH,所以遇到服务器死活拉不起来,我就直接锁绝对路径的 python.exenode.execwd 字段对依赖兄弟模块的 Python 服务器是救命稻草,不写就 ModuleNotFoundError,聊天里也看不到栈。env 是每个服务器独立的,不会自动继承你 shell 里 export 的变量——你终端能看到的 API key,服务器未必看得到。

项目级覆盖放在 .claude/settings.json,会合并到用户级之上。某个 site repo 可能只想加一个本仓库专用的 MCP 服务器(比如某个 Postgres schema 的内省工具)。我把重型服务器放用户级,仓库专用的放项目级。

改完配置要重启 Claude Code。没有热重载。我第一次加服务器的时候花了五分钟纳闷怎么没生效,就是因为没重启。

把一个 CLI 包装成 MCP 服务器

不管什么语言,转化模式都一样。你拿现有的 CLI——就叫它 my-cli——写一层薄薄的适配器:

  1. 用 JSON-RPC over stdio 说话。
  2. 把每个子命令登记成一个工具,附带参数的 schema。
  3. 被调用时 shell out 到真的 CLI。
  4. 抓取 stdout/stderr,作为结构化输出返回。

整套语言无关的转换流程我写在另一篇 CLI 转 MCP 转换指南 里了。这里只讲 Claude Code 特有的考量:

  • **工具名影响路由。**模型挑工具一部分靠名字。publish-blog 一眼明白;tool_3 大概率被忽略。我固定用 kebab-case 动词。
  • **描述就是 prompt。**每个工具的 description 字段每次模型考虑调用都会进上下文。写得越具体越好。
  • **Schema 在约束模型。**严格的 JSON schema 加 required 字段能挡掉一半模型会犯的错。别偷懒。
  • **返回结构化数据,不要拼字符串。**返回 {"slug": "...", "url": "...", "http_status": 200} 让模型能链式调用;返回 "已发布到 https://..." 模型还得自己解析。

一个具体例子。我的 publish_blog.py 在 MCP 出现之前就存在了。包装器大约 70 行 Python,import 同一个 publish_blog 模块,把 publish_oath_blogpublish_lrts_blog 暴露成两个独立工具,返回 slug + URL + indexnow 状态的 JSON。原脚本在终端里照常能用,包装器只是多给 Claude 开了一个嘴。

真实会话:用我的自定义工具发文章

上周一次真实记录。我写完一篇草稿,想发到 OATH 但不想碰终端。

我打了:"把 output/blog-drafts/deepseek-v4-pro-price-cut 这篇草稿发到 OATH"

Claude Code 先调用 keyword_dedup 工具(我另外一个 MCP 服务器)做去重预检。干净。然后它调 publish-blog,参数 site=oathslug=deepseek-v4-pro-price-cut、外加两份 markdown 的路径。MCP 服务器 SSH 到 VPS,跑 INSERT,回了一坨 JSON:两个 URL,还有 indexnow_status: "submitted"。Claude 把这个转述出来,加了一句要验证什么,就停了。

整个过程大概四十秒。没有 MCP 的话,同样的步骤:我自己查去重表、敲发布命令、把 slug 复制到 URL 栏里、再开一个标签页打中文版。我专注时大概三分钟,分心时更久。

更大的赢面是结构化返回能让 Claude 链式工作。发布完模型手里有 slug,下一步就能喂给另一个工具——sitemap ping、Slack 通知、写一条进我 SEO 日志。返回散文的话这些都串不起来。

我不会假装每次都这么顺。这个月早些时候,有个服务器对一个 schema 标了 required 的字段返回了 null,Claude 试了三次才放弃。Schema 撒谎、服务器有 bug——这两个东西都是我自己写的。教训:提交服务器之前,先拿自己的 schema 校验一遍它真实的输出。

MCP vs 直接 Bash——什么时候用哪个

我观察过自己几十次抉择。下面这个启发式比较准:

维度 直接 Bash 执行 MCP 包装 原生 Claude Code 工具
搭建时间 0 分钟——Claude 自带 Bash 首次封装 20-60 分钟,下一个子命令 5 分钟 内置,无需搭建
对模型的错误可见性 退出码 + stdout/stderr 一团文本,模型自己去 parse 结构化错误对象:code、message、重试提示 原生错误类型,可见性最佳
跨会话/跨 agent 复用 每个会话都要重新试一遍 Schema 持久化,任何 Claude 会话都能看见 到处都默认可用
安全边界 整壳访问——模型能管道、glob、rm 限制在你定义的工具和参数之内 受 Claude Code 权限系统沙箱化
Claude Code / Cursor 跨工具复用 每个工具自己重发明 Bash 调用 任何 MCP 兼容客户端都能用同一个服务器 锁死在 Claude Code

跑了半年下来一条实用准则:同一个逻辑操作每周被你或任何 agent 调超过五次,就值得封装成 MCP。低于这个数,Bash 捷径更划算。高于这个数,schema 和结构化返回省下的"模型读错输出"的错够回本。

安全那一列对个人项目意义不大,对在 CI 里跑或者共享 repo 才重要。一个只暴露 publish-blog 但不暴露 rm -rf 的 MCP 包装器,比直接给个完整 shell 显然安全得多。

调试 MCP 服务器连接

这一节是我四个月前最希望有人塞给我的。失败阶梯按我遇到的频率大致排序如下。

**服务器压根没出现在工具列表里。**要么是配置文件 JSON 语法错(Claude Code 静默丢弃坏条目),要么命令不在 PATH 上。跑 claude --debug 看启动日志。你想看到的错是 failed to spawn server: ENOENT。修法就是 command 写绝对路径。

**服务器拉起来但立刻退出。**这个坑我踩最深。Claude Code 把进程拉起来,等 stdout 上 JSON-RPC 握手,啥也没读到,就静默放弃。进程可能 import 阶段就崩了。修法:手动用配置里同样的命令跑一遍,看 stderr。如果你写的是 Python 服务器,又没在 Claude 继承的 venv 里 pip install,那个 import error 不会经过 Claude Code 到你眼前。

**握手成功但工具调用失败。**模型调了工具,回了一句"tool execution failed"没细节。这通常意味着你的服务器返回了畸形的 JSON-RPC 错误。用 claude --debug 然后 tail -f ~/.claude/logs/mcp-*.log——原始帧都在那里。对着 MCP 规范核——常见 bug 是 jsonrpc 字段填错、id 没回声、resulterror 同时填了。

**Schema 不匹配。**你的工具声明了 slug: string, locale: string,模型发了 slug: string, locale: string, dry_run: boolean。有的服务器拒未知字段,有的悄悄丢弃。这两种我都被咬过。更安全的写法是 additionalProperties: false、显式 fail 在未知字段上——这样错很响,模型自己会调整。

**传输挂死。**服务器从 stdin 读,但永远不回。一般是缓冲 I/O——你 print 到 stdout 但没 flush,帧解析器在那儿等更多字节。Python 里写 sys.stdout.reconfigure(line_buffering=True),或者每帧都 flush。Node 默认就够。

**环境变量服务器看不见。**你在 shell 里 export 了 OPENAI_API_KEY,但 MCP 服务器拿到的是干净 env。在配置块的 env 里显式传。别假设会继承。

关于日志补一句。Claude Code 给每个会话写一个 MCP 日志到 ~/.claude/logs/,文件名形如 mcp-<server>-<session>.log。那是事实。聊天 UI 是谎言。我卡住的时候,必然另开一个终端 tail -f,看真实流出的内容。

本地 CLI vs HTTP MCP 服务器

stdio 是默认,也是九成场景里的正确答案。服务器只是一个 Claude Code 全权管理的子进程。崩了 Claude Code 会重拉。没有端口、没有 auth、没有网络跳跃。

HTTP MCP 适合一个服务器要服务多个客户端的场景——比如 Claude Code 和 Cursor 共用同一个后端,或者团队里所有人连同一个托管 MCP 跑共享内部工具。代价是真的:你得做 auth 层(MCP 对 HTTP 服务器支持 OAuth 流程)、找地方部署、容忍网络故障。

对一个包自己 CLI 的独立开发者来说,stdio 就是答案。除了测试外,我没有在生产里部署过 HTTP MCP 服务器。

常见问题

Claude Code 怎么看不到我的 CLI 作为 MCP 服务器?

按概率排三种原因:(1)~/.claude.json.claude/settings.json 里 JSON 语法错——Claude Code 静默丢坏配置。用 jq 校验一下。(2)command 不在 Claude Code 继承的 PATH 上。改成绝对路径。(3)服务器启动时崩了。跑 claude --debug~/.claude/logs/mcp-*.log

MCP 服务器启动就崩,没报错。怎么办?

把配置里的 command + args 在终端手动跑一遍,同样的 cwdenv。crash 就会打到 stderr。Python 服务器最常见原因是 Python 解释器找错(Claude Code 可能拿到一个没装你 venv 包的系统 Python)。修法:把 venv 里 python.exe 的绝对路径写死。

什么时候用本地 CLI MCP,什么时候用 HTTP?

个人工具、单机工作流、不想暴露的东西,全用本地 stdio。多个客户端(Claude Code、Cursor、同事的 IDE)要调同一个后端,或者服务器有重状态不想每次会话重新加载,才用 HTTP。独立开发场景大约九成走 stdio。

怎么把 API key 传给 MCP 服务器但不泄漏到 git?

放在 ~/.claude.json 里那个服务器的 env 块——用户级,在任何 repo 之外。不要放进项目的 .claude/settings.json,因为那个一般会被 commit。如果非得放 repo 里的配置,就让服务器自己读 .env 文件,并把 .env 加进 .gitignore

同一个 MCP 服务器能 Claude Code 和 Cursor 共用吗?

如果是 HTTP MCP,可以——两个客户端连同一个 URL 就行。stdio 服务器的话每个客户端各自拉自己的子进程,对无状态服务器没事,对有状态(缓存、长连接)就会重复。

模型为什么用奇怪的参数调我的工具?

两个原因。第一,你的 schema 太宽——additionalProperties: true 让模型可以塞任何东西。收紧 schema。第二,你的工具描述太含糊。模型挑工具是读描述的,所以描述要写得像 prompt:"用户想把一份写好的草稿发到 OATH 或 LRTS 时用本工具。必须传 en.md 和 zh-cn.md 路径。"——不要写 "发布博客。"

我的 Bash 工作流要不要全改写成 MCP?

只有每周跑超过几次、或者想让别的 agent 也能调的,才值得。一次性的 Bash 一行命令就别折腾了。重复有参数、有错误处理、有返回值的工作流才配得上包装器。

Claude Code 的权限系统对 MCP 工具生效吗?

生效。MCP 工具调用走和 Bash、Edit 一样的审批流。你可以在 .claude/settings.local.json 里预批准某些具体工具来减少弹窗——Claude Code Skills 完整指南 里详细讲过这个文件。

延伸阅读

想往深里看的话,我自己反复翻的几篇姊妹篇是 Claude Code Skills 与 Plugins 对比——因为 plugin 可以打包 MCP 服务器,Claude Code 工作流实例 看这一切怎么塞进 12 个仓库的 monorepo,以及 Claude Code vs Cursor 跨工具 MCP 可移植性的对比。

关于作者

Jim Liu,悉尼开发者,运营十五个内容站和游戏攻略站组成的 portfolio。从 2025 年年中开始每天用 Claude Code,给整个 portfolio 写了博客发布、SEO 数据采集、浏览器自动化三类 MCP 服务器。最后更新:2026-05-26。

每周一封 AI 编程工具邮件

实测好用的 AI 工具 + 独立开发 + 出海,中文,免费。

AI 产品深度评测

SaaS 拆解 · 可复制评分卡

作者: Jim Liu

悉尼全栈开发者。自 2022 年起亲手实测 AI 工具。 联盟披露

Sponsored

Ad served by Adsterra. OpenAIToolsHub is not responsible for advertiser content.