Claude Code-MCP调用原理详解

本文基于 Claude Code 实际使用经验,深入拆解 MCP 协议的调用原理。如果你正在用 Claude Code 或准备接入 MCP,这篇文章会帮你真正理解「LLM 是怎么调用外部工具的」。

前言:LLM 的能力边界问题

大语言模型很强,但有一个根本限制:它只能生成文本,不能直接操作外部世界。

它不能帮你查数据库,不能帮你调 GitLab API,不能帮你读写文件 ------ 除非有人把这些能力「接」给它。

过去的做法是各家各写各的插件系统(ChatGPT Plugins、LangChain Tools......),互不兼容。

MCP 就是为了解决这个问题而生的。

一、MCP 是什么

MCP(Model Context Protocol) 是 Anthropic 在 2024 年底提出的开放协议,一句话概括:

LLM 的 USB-C 接口 ------ 定义了一套标准协议,让任何 LLM 都能以统一方式调用任何外部工具。

就像 USB-C 统一了充电线,MCP 统一了 LLM 与外部工具的通信方式:

java 复制代码
传统方式:每个工具写一套适配代码
LLM ←→ 自定义适配器A ←→ GitLab
LLM ←→ 自定义适配器B ←→ MySQL
LLM ←→ 自定义适配器C ←→ Slack

MCP 方式:一套协议,即插即用
LLM ←→ MCP Client ←→ MCP Server (GitLab)
                   ←→ MCP Server (MySQL)
                   ←→ MCP Server (Slack)

二、架构:三个角色

MCP 的架构非常清晰,只有三个角色:

scss 复制代码
┌─────────────────────────────────────────────────┐
│              MCP Host (Claude Code)              │
│                                                   │
│  用户提问 → LLM 推理 → 决定调用哪个工具 → 返回结果  │
│                   ↕                               │
│            MCP Client (内置)                       │
└────────┬──────────────┬───────────────────────────┘
         │ stdio        │ stdio
         ↓              ↓
┌────────────────┐ ┌─────────────────┐
│ MCP Server A   │ │  MCP Server B   │
│ (GitLab)       │ │  (MySQL)        │
│                │ │                 │
│ 50+ 个工具     │ │  8 个工具        │
└────┬───────────┘ └────┬────────────┘
     ↓                   ↓
  GitLab API          MySQL DB
角色 职责 实际例子
Host 宿主应用,面向用户,内嵌 LLM Claude Code、Cursor、Windsurf
Client 内置于 Host,管理所有 Server 连接 Claude Code 启动时自动创建
Server 独立进程,封装具体能力,暴露工具 @zereight/mcp-gitlab、自建 Server

核心设计思想:Host 和 Client 是 1:1 的,Client 和 Server 是 1:N 的。一个 Claude Code 实例可以同时连接多个 MCP Server。

三、完整调用流程:5 步拆解

下面用一个真实场景走一遍完整流程:用 Claude Code 查看 GitLab 仓库的 Merge Request。

Step 1:启动时注册 ------ "握手"

Claude Code 启动时,读取配置文件 ~/.claude.json

json 复制代码
{
  "mcpServers": {
    "gitlab": {
      "command": "npx",
      "args": ["-y", "@zereight/mcp-gitlab"],
      "env": {
        "GITLAB_API_URL": "https://gitlab.example.com/api/v4",
        "GITLAB_TOKEN": "glpat-xxxxxxxx"
      }
    }
  }
}

Claude Code 做了两件事:

1)启动子进程

bash 复制代码
# Claude Code 内部执行
npx -y @zereight/mcp-gitlab
# 这个进程通过 stdin/stdout 与 Claude Code 通信

2)握手协商:获取工具列表

Client 向 Server 发送初始化请求,Server 返回它所提供的全部工具定义:

json 复制代码
{
  "tools": [
    {
      "name": "list_merge_requests",
      "description": "List merge requests for a project",
      "inputSchema": {
        "type": "object",
        "properties": {
          "project_id": { "type": "string" },
          "state": { "type": "string", "enum": ["opened", "closed", "merged"] }
        },
        "required": ["project_id"]
      }
    }
    // ... 还有 50+ 个工具
  ]
}

这些工具被注册到 Claude 的可用工具列表中,统一加上命名空间前缀:

markdown 复制代码
list_merge_requests → mcp__gitlab__list_merge_requests
create_issue        → mcp__gitlab__create_issue

前缀规则mcp__{server名}__{工具名},这就是你在 Claude Code 中看到的工具全名。

Step 2:用户提问,LLM 决策 ------ "选工具"

用户输入:

复制代码
帮我看看项目 ID 为 2 的仓库最近有哪些打开的 MR

此时,LLM 收到的 prompt 中包含了所有可用工具的描述和参数定义(类似 OpenAI 的 Function Calling)。LLM 推理后,输出一段结构化的工具调用意图:

json 复制代码
{
  "tool": "mcp__gitlab__list_merge_requests",
  "arguments": {
    "project_id": "2",
    "state": "opened"
  }
}

这里有一个关键认知:

LLM 本身不执行任何调用。它只是输出了一段 JSON,表达"我认为应该调用这个工具,参数是这些"。真正执行调用的是 Claude Code。

这就像一个指挥官下达命令,但不亲自上阵。

Step 3:Claude Code 路由 ------ "找到对的 Server"

Claude Code 拿到 LLM 输出的工具调用意图后,解析前缀进行路由:

arduino 复制代码
mcp__gitlab__list_merge_requests
 │      │            │
 │      │            └── 实际方法名:list_merge_requests
 │      └── Server 名:gitlab → 定位到 gitlab 子进程
 └── 标识:这是 MCP 工具(区别于内置工具)

然后通过 stdio 向 gitlab 子进程发送标准的 JSON-RPC 2.0 请求:

json 复制代码
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "list_merge_requests",
    "arguments": {
      "project_id": "2",
      "state": "opened"
    }
  },
  "id": 1
}

Step 4:MCP Server 执行 ------ "干活"

GitLab MCP Server 收到请求后,在内部将其转化为对 GitLab REST API 的调用:

ini 复制代码
JSON-RPC 请求进来
  ↓
解析 name = "list_merge_requests", arguments = {project_id: "2", state: "opened"}
  ↓
构造 HTTP 请求:
  GET https://gitlab.example.com/api/v4/projects/2/merge_requests?state=opened
  Headers: PRIVATE-TOKEN: glpat-xxxxxxxx
  ↓
发送请求,接收 GitLab 响应
  ↓
封装为 JSON-RPC 响应格式返回

返回给 Claude Code:

json 复制代码
{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"iid\": 5, \"title\": \"fix: 修复并发上传文件名冲突\", \"author\": {\"name\": \"zhangsan\"}, \"created_at\": \"2026-02-28\"}]"
      }
    ]
  },
  "id": 1
}

Step 5:LLM 整合回答 ------ "翻译成人话"

Claude Code 把工具返回的结果注入到新一轮 prompt 中,LLM 基于结果生成自然语言回复:

项目 tongue_back 目前有 1 个打开的 MR:

MR 标题 作者 创建时间
!5 fix: 修复并发上传文件名冲突 zhangsan 2026-02-28

完整时序图

vbscript 复制代码
用户            Claude Code          LLM             MCP Server        GitLab
 │                  │                  │                  │                │
 │── "查看 MR" ───→│                  │                  │                │
 │                  │── 组装 prompt ──→│                  │                │
 │                  │  (含工具列表)     │                  │                │
 │                  │                  │── 推理决策        │                │
 │                  │←─ 工具调用意图 ──│                  │                │
 │                  │                                     │                │
 │                  │── JSON-RPC request ───────────────→│                │
 │                  │                                     │── HTTP GET ──→│
 │                  │                                     │←── JSON ──────│
 │                  │←── JSON-RPC response ──────────────│                │
 │                  │                                     │                │
 │                  │── 注入结果到 prompt →                │                │
 │                  │                  │── 生成自然语言     │                │
 │←── 格式化回复 ──│←────────────────│                  │                │

四、通信协议详解

传输层:stdio vs SSE

MCP 支持两种传输方式:

传输方式 原理 优点 缺点 适用场景
stdio 子进程,stdin/stdout 交换数据 简单可靠,无需网络 只能本地,1:1 Claude Code 默认方式
SSE HTTP Server-Sent Events 可远程,可共享 需要部署 HTTP 服务 团队共享 Server

stdio 的完整生命周期:

c 复制代码
Claude Code 进程启动
  │
  ├── 读取 ~/.claude.json
  ├── 对每个 mcpServers 配置:
  │     ├── fork 子进程(如 npx @zereight/mcp-gitlab)
  │     ├── 建立 stdin/stdout 管道
  │     └── 发送 initialize 请求,完成握手
  │
  ├── 会话期间:按需通过 stdin 发请求,stdout 收响应
  │
  └── Claude Code 退出 → 所有 MCP Server 子进程随之终止

消息层:JSON-RPC 2.0

所有通信使用 JSON-RPC 2.0 协议,有三种消息类型:

json 复制代码
// 1. 请求(Request)------ Client → Server
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": { "name": "list_merge_requests", "arguments": {...} },
  "id": 1
}

// 2. 响应(Response)------ Server → Client
{
  "jsonrpc": "2.0",
  "result": { "content": [...] },
  "id": 1
}

// 3. 通知(Notification)------ 单向,无需响应
{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}

五、延迟加载:如何应对 50+ 个工具

一个 GitLab MCP Server 就暴露了 50+ 个工具。如果每个工具的完整 Schema 都塞进 LLM 的 prompt,会严重浪费 token。

Claude Code 的解决方案是 Deferred Tools(延迟加载)

scss 复制代码
启动时:
  只注册工具名列表(几十个名字,很省 token)
  ↓
需要时:
  LLM 判断需要某类工具 → 调用 ToolSearch
  ↓
  ToolSearch("gitlab merge request")
  → 返回匹配的工具 + 完整 Schema
  → 这些工具现在可用了
  ↓
调用时:
  LLM 正常调用已加载的工具

好处:上下文窗口只加载当前需要的工具,而不是一股脑全塞进去。

六、MCP vs 直接调 API:该用哪个?

同样是"列出 GitLab 的 MR",对比两种方式:

方式 A:Bash + curl

bash 复制代码
curl -s --header "PRIVATE-TOKEN: glpat-xxx" \
  "https://gitlab.example.com/api/v4/projects/2/merge_requests?state=opened" \
  | jq '.[] | {iid, title, author: .author.name}'

特点

  • LLM 需要知道 API 的 URL、参数格式、认证方式
  • 认证 token 暴露在命令行中
  • 返回原始 JSON,LLM 自己解析
  • 灵活,但 LLM 容易拼错 URL 或参数

方式 B:MCP 工具

ini 复制代码
mcp__gitlab__list_merge_requests(project_id="2", state="opened")

特点

  • LLM 只需填参数名和值,不用关心 API 细节
  • 认证信息封装在 Server 环境变量中
  • Server 内部做了错误处理和格式化
  • 类型安全,参数有 Schema 校验

最佳实践:各取所长

操作 推荐方式 原因
git add / commit / push Bash 本地 Git 操作,MCP 管不到
查看 / 创建 MR、Issue MCP 平台 API 操作,MCP 封装更好
数据库查询 MCP 连接管理、参数化查询更安全
系统命令、脚本 Bash MCP 不涉及本地系统操作

一句话:本地操作用 Bash,平台交互用 MCP。

七、动手写一个 MCP Server

理解了原理,写一个自己的 MCP Server 其实很简单。以 Python FastMCP 为例:

python 复制代码
# my_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-tools")

@mcp.tool()
def query_patient(patient_id: str) -> dict:
    """根据患者 ID 查询诊断记录"""
    import requests
    resp = requests.get(f"http://localhost:8080/api/patient/{patient_id}")
    return resp.json()

@mcp.tool()
def analyze_image(image_path: str) -> dict:
    """分析医学图像,返回 AI 诊断结果"""
    import requests
    with open(image_path, "rb") as f:
        resp = requests.post("http://localhost:5000/predict", files={"image": f})
    return resp.json()

if __name__ == "__main__":
    mcp.run(transport="stdio")

配置到 ~/.claude.json

json 复制代码
{
  "mcpServers": {
    "my-tools": {
      "command": "python",
      "args": ["my_server.py"]
    }
  }
}

然后在 Claude Code 中就能直接说:

复制代码
帮我查一下患者 P001 的诊断记录

Claude 会自动调用 mcp__my_tools__query_patient(patient_id="P001")

八、核心设计要点总结

设计点 说明
LLM 不直接调用 LLM 只输出调用意图(JSON),Host 负责实际执行
权限控制在用户侧 Claude Code 根据权限设置决定是否需要用户确认
工具延迟加载 避免几十个工具 Schema 撑爆上下文窗口
一个 Server 多个工具 一次连接,提供一组相关能力
协议标准化 JSON-RPC 2.0 + MCP 规范,任何语言都能实现 Server
传输可插拔 stdio(本地)和 SSE(远程)可按需选择

写在最后

MCP 的本质是给 LLM 提供了一个标准化的工具调用协议。它不复杂,但解决了一个很实际的问题:让 AI 从"只能聊天"变成"能干活"。

如果你正在用 Claude Code,MCP 已经在替你工作了 ------ 每次你让它查 GitLab、读数据库,背后走的都是这套流程。

理解了原理,你就能:

  1. 更好地配置和调试 MCP Server
  2. 在合适的场景选择 MCP 还是 Bash
  3. 为自己的业务系统写 MCP Server,让 AI 真正成为你的工具

如果这篇文章对你有帮助,欢迎点赞收藏。关于 Claude Code 的更多实践,可以看我的其他文章。

相关推荐
Bingo6662 小时前
我让AI帮我写了个AI工具,来帮我整理我的书签 (已开源)
vibecoding
_哆啦A梦2 天前
Vibe Coding 全栈专业名词清单|设计模式·基础篇(创建型+结构型核心名词)
前端·设计模式·vibecoding
甲维斯2 天前
GLM,Kimi,MiniMax怎么选?Win+C#开发横向对比!
ai编程·vibecoding
阴明3 天前
Vibe Usage - 一个记录你各种 Vibe Coding 使用量的工具
claude·vibecoding
甲维斯4 天前
开发实战:MiniMax开发Windows C#应用!是骡子是马?
ai编程·vibecoding
仿生狮子5 天前
AI 写代码总是半途而废?试试这个免费的工作流工具
ai编程·前端工程化·vibecoding
芦半山7 天前
在这些地方,我选择拒绝AI
vibecoding
dtsola7 天前
AI独立开发的道法术器:一个解决方案架构师的实践与思考
人工智能·ai编程·ai创业·独立开发者·vibecoding·个人开发者·一人公司
前端Fusion7 天前
一文讲透 MCP 和 Skills 的分工与协作
人工智能·vibecoding