1. 前置知识:两个概念 #

1.1 JSON-RPC 是什么? #

JSON-RPC 是一种「用 JSON 格式发远程调用」的约定。你可以把它理解成:一方发一条 JSON 消息说「请执行某方法」;另一方回一条 JSON 消息说「结果是啥」。

MCP 的所有通信都基于 JSON-RPC 2.0。消息只有三类:

类型 有没有 id 要不要回复 生活类比
请求(Request) 有 要 你问问题,等对方回答
响应(Response) 有(和请求相同) — 对方给出答案
通知(Notification) 无 不要 对方广播一声,不用回

下面是一条请求长什么样:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "greet",
    "arguments": { "name": "小明" }
  }
}

下面是对应的响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "Hello, 小明!" }]
  }
}

记住:每条请求有唯一 id,响应必须带回同一个 id,客户端靠它配对。

1.2 stdio(标准输入输出)是什么? #

stdio = 程序从「标准输入」读数据、往「标准输出」写数据。本地 MCP 最常用的连接方式就是 stdio 传输:

┌──────────┐  stdin 写入请求   ┌──────────────┐
│ 客户端    │ ───────────────► │ MCP 服务器    │
│ (Cursor)  │ ◄─────────────── │ (子进程)      │
└──────────┘  stdout 读出响应   └──────────────┘

这也是为什么本地 MCP 配置里常见 "type": "stdio" 和 "command" / "args":本质就是「帮我启动这个程序,并用 stdin/stdout 跟它说话」。

1.2.1 stdio_parent.py #

# 导入 os 模块,用于后面拼接子进程脚本路径
import os
# 导入 subprocess 模块, 用于创建子进程以及操作标准输入输出管道
import subprocess
# 导入 sys 模块, 用于获取当前 Python 解释器路径
import sys

# 获取当前脚本文件的所在目录
base_dir = os.path.dirname(os.path.abspath(__file__))
# 拼接得到子进程要运行的脚本的完整路径
child_path = os.path.join(base_dir, "stdio_child.py")

# 创建子进程对象,启动子进程并设置参数
proc = subprocess.Popen(
    # 指定用当前的 Python 解释器来运行目标子进程脚本
    [sys.executable, child_path],
    # 把子进程的标准输入设置为管道,父子进程之间可通信
    stdin=subprocess.PIPE,
    # 把子进程的标准输出设置为管道
    stdout=subprocess.PIPE,
    # 子进程的标准错误输出继承父进程(直接显示在终端),便于输出日志
    stderr=None,
    # 文本模式,直接用字符串读写,而不是字节
    text=True,
    # 指定使用 utf-8 编码,避免 Windows 下中文乱码
    encoding="utf-8",
)

# 要发送给子进程的消息内容
request = "你好,子进程"
# 在父进程终端打印发送的消息内容
print(f"[父进程] 发送: {request}")
# 把消息写入到子进程的标准输入,需要加换行符以便子进程读取完整一行
proc.stdin.write(request + "\n")
# 刷新标准输入管道,确保消息及时发送到子进程
proc.stdin.flush()
# 从子进程的标准输出读取一行并去除字符串首尾空白字符
response = proc.stdout.readline().strip()

# 在父进程终端输出接收到的子进程回应
print(f"[父进程] 收到: {response}")

# 关闭父进程到子进程标准输入的写入端,让子进程检测到 EOF 并退出
proc.stdin.close()
# 等待子进程退出,最多等待 3 秒
proc.wait(timeout=3)

1.2.2 stdio_child.py #

# 导入 sys 模块,用来访问标准输入输出和错误
import sys

# 使用一个无限循环,持续处理父进程传来的请求
while True:
    # 从标准输入读取一行数据;如果父进程没写会阻塞等待
    line = sys.stdin.readline()
    # 如果读取到空字符串,表示输入流被关闭,子进程需退出
    if not line:
        break
    # 去除收到数据的首尾空白字符,并存入 message 变量
    message = line.strip()
    # 如果处理后字符串为空(如仅输入回车),则跳过本次循环
    if not message:
        continue
    # 向标准错误输出打印调试信息(确保协议数据不会干扰)
    print(f"[子进程] 收到: {message}", file=sys.stderr)
    # 构造一条回复消息,表示已收到父进程发来的内容
    reply = f"子进程回复: 已收到「{message}」"
    # 将回复消息写入标准输出,父进程可以读取
    sys.stdout.write(reply + "\n")
    # 刷新标准输出缓冲区,确保消息及时发送到父进程
    sys.stdout.flush()

1.2.3 期望输出 #

[父进程] 发送: 你好,子进程
[子进程] 收到: 你好,子进程
[父进程] 收到: 子进程回复: 已收到「你好,子进程」

对应到 MCP:父进程写入的 request 就是 JSON-RPC 请求;子进程 stdout 写回的就是 JSON-RPC 响应。Cursor 启动 MCP 服务器时,做的就是这件事——只不过消息格式换成了 JSON。

2. MCP 是什么 #

MCP(Model Context Protocol,模型上下文协议) 是一套开放标准,规定 AI 应用如何连接外部数据和工具。

可以把它想成 「AI 的 USB-C」:不同 AI 应用(Cursor、Claude Desktop、VS Code)可以用同一套规则,连接不同数据源(文件、数据库、API),而不用每个应用、每个工具各写一套对接代码。

2.1 它解决什么问题? #

没有 MCP 时,每接入一个新工具,就要在 AI 应用里单独开发集成代码,维护成本高。MCP 把这件事标准化了:

没有 MCP 有 MCP
每个 AI 应用各自对接 GitHub、数据库 工具写一次 MCP 服务器,多个 AI 都能用
用户手动复制文件内容给 AI AI 通过协议直接读文件、调 API
工具接口五花八门 统一的 tools/list、tools/call 等

2.2 MCP 管什么、不管什么? #

管 不管
AI 怎么发现外部工具 AI 内部怎么调用大模型
AI 怎么读取外部数据 对话历史怎么存储
客户端和服务器怎么通信 UI 长什么样

2.3 一个真实使用场景 #

你在 Cursor 里问:「帮我看看 README.md 写了啥」。背后可能发生:

  1. Cursor(主机)通过文件系统 MCP 服务器读取文件(资源)
  2. 把内容作为上下文交给大模型
  3. 模型回答你的问题

如果你再说:「把第一段改成中文」,模型可能调用「写文件」工具 完成修改(需你确认)。

3. 三个角色:谁在和谁说话 #

理解这三个角色,是读懂所有 MCP 文档的前提。它们不是三个独立软件,而是一次连接里的分工。

flowchart TD subgraph Host["主机(你正在用的 AI 应用)"] C1["客户端 1"] C2["客户端 2"] end S1["服务器 A:文件"] S2["服务器 B:Git"] C1 --> S1 C2 --> S2

3.1 主机(Host) #

主机就是你直接使用的 AI 应用,负责整体体验:展示对话、让你确认工具调用、连接多个 MCP 服务器。

举例:Cursor、VS Code + Copilot、Claude Desktop。

3.2 客户端(Client) #

客户端是主机内部的「通信模块」,每个 MCP 服务器对应一个客户端。它负责发 JSON-RPC 请求、收响应、处理通知。

3.3 服务器(Server) #

服务器是提供能力的程序,向外暴露 工具、资源、提示。可以是:

3.4 一张表记住关系 #

角色 一句话 谁来实现
主机 总指挥,你用的 App Cursor、VS Code
客户端 和某个服务器对话的联络员 主机内置,或你写的测试脚本
服务器 提供工具/资源/提示 你要写的 MCP 程序

要点:一个主机可以连多个服务器;每个连接是 1 个客户端 ↔ 1 个服务器;服务器之间互相隔离,不直接说话。

4. 三大能力:工具、资源、提示 #

MCP 服务器向 AI 提供三种核心能力。先记住口诀:

工具 = 做事情 | 资源 = 读数据 | 提示 = 套模板

4.1 工具(Tools) #

工具是 AI 可以调用的函数,用来执行操作(查天气、发请求、写文件等)。

项目 说明
谁决定用 大模型根据对话自动选择
协议方法 tools/list 列出工具,tools/call 执行
必备字段 name、description、inputSchema(参数格式)

生活类比:工具像「遥控器上的按钮」——AI 判断该按哪个键,但真正执行的是服务器里的代码。

4.2 资源(Resources) #

资源是 AI 可以读取的数据,只读,不通过资源去修改数据。

项目 说明
谁决定用 应用或用户选哪些资源放进上下文
协议方法 resources/list、resources/read
标识方式 每个资源有唯一 URI,如 file:///path/to/doc

两种常见形式:

类型 URI 示例 含义
静态资源 config://app 地址固定
模板资源 note://{title} 带参数,如 note://周报

4.3 提示(Prompts) #

提示是预先写好的任务模板,用户主动选择(例如输入 /code_review),不是模型自动触发。

项目 说明
谁决定用 用户在界面里选
协议方法 prompts/list、prompts/get
价值 参数结构化,避免每次从零描述需求

4.4 三者对比 #

能力 作用 谁触发 新手优先级
工具 执行操作 模型 ⭐⭐⭐ 必学
资源 提供只读数据 用户/应用 ⭐⭐ 建议学
提示 任务模板 用户 ⭐ 按需学

5. 一次对话背后发生了什么 #

每次 Cursor 连上 MCP 服务器,都会先走一遍 初始化握手,然后才能调工具。

5.1 生命周期三阶段 #

初始化(握手) → 运行(调工具/读资源) → 关闭(断开连接)

5.2 初始化四步 #

sequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: ① initialize(我的版本和能力) S-->>C: ② 响应(协商后的版本和能力) C->>S: ③ notifications/initialized Note over C,S: ④ 之后才能 tools/list、tools/call
步骤 做什么 为什么重要
① 客户端发 initialize 对齐协议版本、交换能力
② 服务器返回支持的 capability 例如有没有 tools
③ 客户端发 initialized 通知 告诉服务器「我准备好了」
④ 正常业务 如 tools/list、tools/call

规则:在收到 initialized 之前,服务器不应发业务请求(ping、日志除外)。

5.3 工具调用完整链路 #

用户问「杭州天气怎么样」时,典型链路如下:

sequenceDiagram participant U as 用户 participant H as 主机 participant L as 大模型 participant S as MCP服务器 H->>S: tools/list S-->>H: [get_weather, ...] U->>H: 杭州天气怎么样? H->>L: 问题 + 工具列表 L-->>H: 调用 get_weather(city=杭州) H->>S: tools/call S-->>H: 22°C,晴 H->>L: 工具结果 L-->>H: 自然语言回答 H-->>U: 显示回复

5.4 协议版本 #

版本号格式为 YYYY-MM-DD(如 2025-11-25),在 initialize 时协商。

6. 环境搭建 #

6.1 MCP SDK #

下表列出官方维护的 MCP SDK,按语言分类:

语言 GitHub 仓库
TypeScript typescript-sdk
Python python-sdk
Java java-sdk
C# csharp-sdk
Go go-sdk
Kotlin kotlin-sdk
Swift swift-sdk
Ruby ruby-sdk
PHP php-sdk

6.2 安装 Python 依赖 #

uv add "mcp[cli]"