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 的更多实践,可以看我的其他文章。

相关推荐
甲维斯5 小时前
GPT5.4克隆Claude官网,玩了一把“与众不同”!
ai编程·vibecoding
abo1234565 小时前
Claude Code 核心架构分析与 Agent 公司借鉴路径
agent·vibecoding
KevinZhang135799 小时前
第 15 节:实现数据分析可视化
ai编程·vibecoding
星浩AI9 小时前
Claude Code 项目实战:多 Agent 流程编排,从原型到可运行 ChatBot
后端·claude·vibecoding
SpikeKing1 天前
VibeCoding - Claude Code 的 CLAUDE.md 编写指南
vibecoding·claude code·claude.md
chenxuan5201 天前
还在手打 prompt?我给 OpenCode 做了个语音输入插件,vibe coding 的时候真的爽很多
ai编程·vibecoding
SpikeKing1 天前
VibeCoding - OpenClaw 公网访问配置指南 (自动化)
运维·自动化·vibecoding·openclaw
SpikeKing2 天前
VibeCoding - 配置 Claude Code 接入 Claude 模型
claude·vibecoding·claude code
来一颗砂糖橘2 天前
Vibe Coding:用“氛围感”重塑编程
低代码开发·开发效率·技术趋势·vibecoding