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 写入 JSON-RPC 消息
- 服务器从 stdin 读消息,处理完后往 stdout 写回响应
- 日志、调试信息应写到 stderr,不能污染 stdout(否则协议会乱)
┌──────────┐ 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 写了啥」。背后可能发生:
- Cursor(主机)通过文件系统 MCP 服务器读取文件(资源)
- 把内容作为上下文交给大模型
- 模型回答你的问题
如果你再说:「把第一段改成中文」,模型可能调用「写文件」工具 完成修改(需你确认)。
3. 三个角色:谁在和谁说话 #
理解这三个角色,是读懂所有 MCP 文档的前提。它们不是三个独立软件,而是一次连接里的分工。
3.1 主机(Host) #
主机就是你直接使用的 AI 应用,负责整体体验:展示对话、让你确认工具调用、连接多个 MCP 服务器。
举例:Cursor、VS Code + Copilot、Claude Desktop。
3.2 客户端(Client) #
客户端是主机内部的「通信模块」,每个 MCP 服务器对应一个客户端。它负责发 JSON-RPC 请求、收响应、处理通知。
3.3 服务器(Server) #
服务器是提供能力的程序,向外暴露 工具、资源、提示。可以是:
- 本地进程(stdio):如读写文件的 server
- 远程服务(HTTP): 如飞书知识库的 API
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 初始化四步 #
| 步骤 | 做什么 | 为什么重要 |
|---|---|---|
| ① | 客户端发 initialize |
对齐协议版本、交换能力 |
| ② | 服务器返回支持的 capability | 例如有没有 tools |
| ③ | 客户端发 initialized 通知 |
告诉服务器「我准备好了」 |
| ④ | 正常业务 | 如 tools/list、tools/call |
规则:在收到 initialized 之前,服务器不应发业务请求(ping、日志除外)。
5.3 工具调用完整链路 #
用户问「杭州天气怎么样」时,典型链路如下:
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]"