1. 前言
首先大家或多或少都对大模型 MCP 有所了解,通常的讲法是 MCP 是 "Model Context Protocol(模型上下文协议)" 的缩写,是由 Anthropic 提出并开源的一个标准协议,用于将 AI 应用连接到外部系统,可以把 MCP 理解为 AI 应用的 USB-C 接口。
通过 MCP,像 Claude 或 ChatGPT 这样的 AI 应用可以连接到数据源(例如本地文件、数据库)、工具(例如搜索引擎、计算器)和工作流(例如专用提示词)------从而使它们能够访问关键信息并执行任务。
但作为一名技术开发人员,对 MCP 的理解仅仅限于上述的了解是远远不够的,我们必须深入其底层的实现原理,才能彻底把其搞懂。
本文将结合 MCP 官方规范和不通过 MCP SDK 实现一个可运行的代码示例(Python 客户端 + Node.js 文件读取服务器),深入剖析 MCP 的工作原理。
2. 为什么需要 MCP 协议?
首先我们回顾一下前面文章所学的大模型是怎么调用工具的。大模型调用工具的工作流程如下:
- 定义工具(用 JSON Schema 描述函数名称、功能、参数)。
- 将工具描述附加到模型请求中。
- 模型根据用户输入判断是否需要调用工具,以及调用哪个工具和参数。
- 解析模型的响应(tool_calls)。
- 执行本地函数,获得结果。
- 将结果作为新的消息(tool role)发送给模型。
- 模型结合工具结果生成最终回答。
我们目前的 AI Agent 系统是通过 Python 实现的,本地工具函数也是通过 Python 实现,那么如果现在有另外一个 AI Agent 系统是通过 Node.js 实现的,它的本地工具函数自然也是通过 Node.js 实现的,代码示例如下:
js
const fs = require('fs');
const path = require('path');
// 工具定义
const tools = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文本文件内容。",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "要读取的文件路径"},
"encoding": {"type": "string", "enum": ["utf-8", "gbk"], "description": "文件编码格式"}
},
"required": ["path"]
}
}
}
];
// 具体工具实现
class ReadFileTool {
execute(filePath, encoding = "utf-8") {
try {
const resolvedPath = path.resolve(filePath.replace(/^~/, process.env.HOME || process.env.USERPROFILE));
if (!fs.existsSync(resolvedPath)) {
return `❌ 文件不存在: ${filePath}`;
}
return fs.readFileSync(resolvedPath, encoding);
} catch (e) {
return `❌ 读取失败: ${e.message}`;
}
}
}
const fileTool = new ReadFileTool();
module.exports = {
tools,
ReadFileTool,
fileTool
};
那么现在我们怎么在 Python 写的 Agent 系统里面调用由 Node.js 实现的工具函数呢?最直接的就是重新使用 Python 集成一个,但这种方式显然不妥,因为这种传统的做法会导致重复开发、碎片化严重。
其次我们可以通过 stdio 通信 或 HTTP 通信获取获取外部工具的定义和执行的结果给到本地 AI Agent。但如果要让所开发的工具能够被任意 AI Agent 系统使用,工具系统必须提供统一的方法来获取工具定义并执行工具。
基于此,Anthropic 提出 MCP 协议,采用客户端‑服务器架构 ,通过标准化的 JSON‑RPC 消息实现能力协商、资源发现和工具调用。它不关心 LLM 的内部实现,只规定上下文交换的协议。
也就是如果你实现的工具系统遵循 MCP 协议,那么你所实现的工具函数就可以在任何遵循 MCP 协议的 AI Agent 系统中使用了。具体来说就是必须实现一个获取工具定义的方法:tools/list 和获取执行工具结果的方法:tools/call ,这就是 MCP 协议的核心要义。
官方文档定义:modelcontextprotocol.io/specificati...
3. 什么是 stdio 通信?
在上一小节说到可以通过 stdio 通信 获取外部工具的定义和执行结果给到本地 AI Agent。所以我们先要了解一下什么是 stdio 通信。
首先 stdio 的全称是 Standard Input/Output (标准输入/输出) 是指两个进程之间利用标准输入(stdin) 和标准输出(stdout) 流来交换数据的一种通信方式。
- 进程 A 向进程 B 的 stdin 写入数据 → 进程 B 通过
input()或sys.stdin.read()读取。 - 进程 B 将结果写入自己的 stdout → 进程 A 从子进程的 stdout 读取数据。
这种方式不需要网络端口 ,延迟极低 ,配置简单 ,常用于本地父子进程间的通信。在 MCP(模型上下文协议)中,stdio 是默认的通信方式,客户端将 MCP 服务器作为子进程启动,双方通过 stdin/stdout 交换 JSON-RPC 消息。
下面我们通过一个简单的示例代码来进一步了解 stdio 通信。
子进程代码 server.js:
js
#!/usr/bin/env node
'use strict';
const readline = require('readline');
/**
* 从 stdin 按行读取 JSON 请求,处理后向 stdout 返回 JSON 响应
*/
function main() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
rl.on('line', (line) => {
line = line.trim();
if (!line) return;
let resp;
try {
const req = JSON.parse(line);
// 请求格式: {"text": "hello"}
const text = req.text ?? '';
resp = { result: `Echo: ${text}` };
} catch (e) {
resp = { error: e.message };
}
// 将响应写入 stdout,必须带换行符
process.stdout.write(JSON.stringify(resp) + '\n');
});
}
main();
父进程代码 client.py:
python
#!/usr/bin/env python3
import subprocess
import json
from pathlib import Path
# 路径
server_path = Path(__file__).parent / "server.js"
# 启动子进程,连接其 stdin/stdout
proc = subprocess.Popen(
["node", server_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=None, # 也可以单独重定向 stderr 看日志
text=True # 以文本模式操作,自动处理编解码
)
# 构造请求(JSON 行)
request = {"text": "Hello, stdio!"}
request_line = json.dumps(request) + "\n"
# 发送请求到子进程的 stdin
proc.stdin.write(request_line)
proc.stdin.flush()
# 读取子进程 stdout 的一行响应
response_line = proc.stdout.readline()
response = json.loads(response_line)
print("收到响应:", response)
# 关闭子进程
proc.terminate()
proc.wait()
运行效果:
css
$ python3 client.py
收到响应: {'result': 'Echo: Hello, stdio!'}
上述例子实现了在 Python 运行环境中运行 Node.js 代码,同时展示了 stdio 通信的核心思想:一个进程的 stdout 就是另一个进程的 stdin ,通过简单的流操作即可实现高效的本地进程间通信。MCP 的 stdio 传输层正是在此基础上增加了 JSON-RPC 格式规范和更完善的生命周期管理。
那么什么是 JSON‑RPC 呢?
4. 什么是 JSON‑RPC 架构
MCP 的数据层完全基于 JSON‑RPC 2.0 实现,所以理解 JSON‑RPC 是理解 MCP 通信机制的基础。
JSON‑RPC 是一种轻量级的远程过程调用(RPC)协议 ,使用 JSON(JavaScript Object Notation)作为数据编码格式。RPC 的设计目标极其简单:允许一个进程(客户端)通过网络向另一个进程(服务器)请求执行一个方法,并接收结果------就像调用本地函数一样。例如:你写 add(3, 5),RPC 框架会在背后将函数名和参数打包成网络消息,发送到远程服务器,服务器执行真正的 add 函数,再将结果打包返回。从代码角度看,调用方式几乎和本地一样。而 JSON‑RPC 则是打包的网络消息的一种格式约定。
JSON‑RPC 采用经典的请求‑响应 模型,同时支持单向通知。
vbscript
┌─────────┐ Request (id = 1) ┌─────────┐
│ │ ───────────────────► │ │
│ Client │ │ Server │
│ │ ◄─────────────────── │ │
└─────────┘ Response (id = 1) └─────────┘
┌─────────┐ Notification (no id) ┌─────────┐
│ Client │ ──────────────────────► │ Server │
│ │ │ │
└─────────┘ (no response) └─────────┘
- 客户端:发起请求的一方。
- 服务器:处理请求并返回响应的一方。
- 传输层 :JSON‑RPC 本身不规定传输方式,通常使用 HTTP 、WebSocket 、TCP Socket 或 Stdio(如 MCP 中的例子)。
消息格式如下:
一个请求对象包含以下字段:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
jsonrpc |
string | 是 | 固定为 "2.0" |
method |
string | 是 | 要调用的方法名(如 "initialize", "tools/list") |
params |
object 或 array | 否 | 方法参数。对象格式(按名称)更常用,数组格式(按位置)也可用。 |
id |
string / number | 是* | 请求标识符,用于关联响应。通知消息无此字段。 |
*注意:通知(Notification)不需要 id。
响应数据对象格式如下:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
jsonrpc |
string | 是 | "2.0" |
id |
string / number | 是 | 与请求的 id 相同 |
result |
any | 是 | 方法执行返回的结果 |
error |
- | 否 | 成功时不得包含 error |
简单来说就是请求端发送一个以下的 JSON 对象参数:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "add",
"params": { "a": 3, "b": 5 }
}
服务端根据 method 分发处理并且必须响应以下的 JSON 对象格式数据:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": 8,
"error": '成功时不得包含此字段'
}
根据上述内容我们基于上一小节的 stdio 通信的例子构建一个 JSON‑RPC 的简单客户端架构。代码如下:
python
import json
import subprocess
clientProtocolVersion = '2025-12-25'
# ---------- MCP 客户端 ----------
class MCPClient:
def __init__(self, server_command):
self.server_command = server_command
self.process = None
self.request_id = 0
self.initialized = False
# -------- 启动与握手 --------
def start(self):
print("正在启动 MCP 服务器...")
self.process = subprocess.Popen(
self.server_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
encoding='utf-8',
errors='replace'
)
# 发送消息
response = self.send_request("initialize", {
"protocolVersion": clientProtocolVersion,
})
result = response.get("result", {})
server_version = result.get("protocolVersion")
if server_version != clientProtocolVersion:
raise Exception(
f"协议版本不兼容:客户端版本 {clientProtocolVersion},服务端版本 {server_version}"
)
self.negotiated_version = server_version
# 初始化完成
self.initialized = True
print(f"MCP 服务器已连接(协议版本:{self.negotiated_version})")
# -------- 底层通信 --------
def send_request(self, method, params=None):
if not self.process:
raise Exception("MCP 服务器未启动")
self.request_id += 1
request = {
"jsonrpc": "2.0",
"id": self.request_id,
"method": method,
"params": params or {}
}
try:
self.process.stdin.write(json.dumps(request) + "\n")
self.process.stdin.flush()
import time
start_time = time.time()
response_line = ""
# 延迟 5 毫秒读取
while time.time() - start_time < 5:
# 读取子进程响应
response_line = self.process.stdout.readline()
if response_line and response_line.strip():
break
time.sleep(0.1)
if not response_line or not response_line.strip():
stderr_output = self.process.stderr.read()
if stderr_output:
raise Exception(f"MCP 服务器错误:{stderr_output}")
raise Exception("未收到 MCP 服务器的响应")
# 解析响应
parsed = json.loads(response_line)
return parsed
except Exception as e:
raise Exception(f"MCP 请求失败:{e}")
def stop(self):
if self.process:
try:
self.process.terminate()
self.process.wait(timeout=2)
except:
self.process.kill()
self.process.wait()
# 初始化 MCP 客户端
mcp_client = MCPClient(["node", "server.js"])
mcp_client.start()
简单来说客户端使用 subprocess 启动 Node.js 子进程,然后构造标准的 JSON‑RPC2.0 的请求对象数据格式,写入 stdin 并立即 flush()。随后延迟 5 毫秒读取 stdout 的一行作为响应。
然后初始化握手流程,我们目前只是简单对比客户端和服务端的协议版本是否一致。
接着我们根据上述 JSON‑RPC 协议的要求初步实现的服务端:
js
#!/usr/bin/env node
const readline = require('readline');
// -------- 消息发送工具函数 --------
function sendResponse(id, result) {
const response = {
jsonrpc: '2.0',
id: id,
result: result
};
process.stdout.write(JSON.stringify(response) + '\n');
}
function sendError(id, code, message, data) {
const response = {
jsonrpc: '2.0',
id: id,
error: { code, message, ...(data !== undefined ? { data } : {}) }
};
process.stdout.write(JSON.stringify(response) + '\n');
}
const serverProtocolVersion = '2025-12-26'
// -------- 请求路由 --------
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', (line) => {
try {
const request = JSON.parse(line.trim());
// 根据 method 进行分发处理
if (request.method === 'initialize') {
sendResponse(request.id, {
protocolVersion: serverProtocolVersion
});
// 未知方法
} else {
sendError(request.id, -32601, `方法不存在: ${request.method}`);
}
} catch (e) {
// JSON 解析失败
sendError(request.id, -32700, `JSON 解析失败:${e}`);
}
});
我们可以看到服务端相比上一小节的实现多了对 JSON-RPC 协议的封装,首先监听 stdin 的每一行,因为 JSON-RPC 协议是通过 JSON 格式进行传递数据,所以需要解析 JSON 数据,再根据 method 进行分发处理,这也是 JSON-RPC 协议的核心规定,然后根据 JSON-RPC 协议封装成功消息的响应方法 sendResponse 以及失败消息的响应方法 sendError,同时失败消息的错误码也是遵循 JSON-RPC 协议的错误码规则。
上述代码便是为了展示 JSON-RPC 协议核心概念而简化的实现。并且初步实现了握手流程,只有大家的版本协议一致的情况下才能握手成功。
我们执行代码测试一下:
初始客户端与服务端的版本号都是 2025-12-25。执行结果如下:
ruby
$ python client.py
正在启动 MCP 服务器...
MCP 服务器已连接(协议版本:2025-12-25)
我们将服务端的版本号改成 2025-12-26,再执行结果如下:
arduino
$ python client.py
正在启动 MCP 服务器...
Traceback (most recent call last):
File "client.py", line 95, in <module>
mcp_client.start()
File "client.py", line 35, in start
raise Exception(
Exception: 协议版本不兼容:客户端版本 2025-12-25,服务端版本 2025-12-26
上述具体的功能定义可以根据我们的实际业务进行制订的,而 MCP 则正是通过 JSON-RPC 风格定义了一组标准化的'远程调用',让 AI 应用能够像调用本地函数一样调用 MCP 服务器提供的工具、资源和提示词。
5. MCP 核心架构
5.1 什么是 MCP 协议?
MCP 的数据层完全基于 JSON‑RPC 2.0 实现,也就是说 MCP 的消息格式也都是:
- 请求 :包含
jsonrpc: "2.0",id,method,params。 - 响应 :包含
id和result或error。 - 通知 :没有
id字段,表示单向消息,接收方无需回复。
并且 MCP 基于 JSON‑RPC 2.0 定义了生命周期管理、核心原语(工具、资源、提示词)的交互序列。
其中三种 MCP 服务器可暴露的核心原语如下:
- 工具(Tools)
AI 可调用的函数,执行操作。
发现:tools/list→ 执行:tools/call。 - 资源(Resources)
只读的数据源,如文件内容、数据库记录。
发现:resources/list→ 读取:resources/read。 - 提示词(Prompts)
预定义的模板,帮助结构化与 LLM 的交互。
发现:prompts/list→ 获取:prompts/get。
简单来说就是 MCP 服务器可以提供工具(Tools) 、资源(Resources) 、提示词(Prompts) 三种能力。然后 MCP 客户端可以通过 tools/list 方法查看具体有哪些工具可以提供给大模型使用,然后大模型真的要使用的时候则通过 tools/call 方法执行具体的工具。具体如下:
- 发现 :客户端发送
tools/list,服务器返回工具数组。 - 执行 :客户端发送
tools/call,包含工具名和参数。服务器执行后返回content数组(可包含文本、图像等)。
虽然 MCP 服务器可以提供这么多能力,但不是每个 MCP 服务器都会提供全部的能力,所以需要设计一个生命周期来管理这些能力。
5.2 为什么需要生命周期管理?
因为 MCP 不预设固定的功能集,所以不同的 MCP 服务器可能支持不同的原语(工具、资源、提示词)、不同的通知机制、不同的扩展。客户端也未必支持所有特性。因此,在正式开始交换上下文之前,双方必须:
- 协商协议版本 :确保使用双方都理解的版本(例如
2025-06-18)。 - 交换能力声明 :客户端告知服务器自己能处理什么(如
tools.listChanged通知、elicitation引导),服务器告知客户端它提供了什么(如tools、resources、Prompts)。 - 建立一致的初始状态:比如客户端知道服务器有哪些工具可用,服务器知道客户端可以接收哪些通知。
如果缺少这个握手阶段,后续的 tools/list 或 tools/call 可能因为版本不匹配或能力缺失而失败。生命周期管理通过一次 initialize 交换避免了这些问题。
MCP 生命周期分为三个主要阶段:初始化握手(Initialization) 、运行阶段(Operation) 、关闭/终止(Shutdown) 。官方地址:modelcontextprotocol.io/specificati...
5.3 初始化握手
根据上一小节我们知道在初始化的时候需要协商协议版本 、交换能力声明 、 建立一致的初始状态,协商协议版本其实我们已经做了,所以接下来我们实现交换能力声明。其实也很简单,就是最开始 MCP 客户端向 MCP 服务器发送以下的 JSON 数据:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": {
"listChanged": true
},
},
"clientInfo": {
"name": "mcp-agent-loop",
"version": "1.0.0",
}
}
}
具体实现如下:
diff
# ---------- MCP 客户端 ----------
class MCPClient:
+ # 客户端支持的协议版本
+ clientProtocolVersion = '2025-12-25'
+ # 客户端自身声明的 capabilities(告知服务器本客户端支持哪些特性)
+ CLIENT_CAPABILITIES = {
+ "tools": {
+ "listChanged": True # 客户端可以处理工具列表变更通知
+ },
+ }
def __init__(self, server_command):
# 省略...
# -------- 启动与握手 --------
def start(self):
# 省略...
# 发送消息
response = self.send_request("initialize", {
+ "protocolVersion": self.clientProtocolVersion,
+ "capabilities": self.CLIENT_CAPABILITIES,
+ "clientInfo": {
+ "name": "mcp-agent-loop",
+ "version": "1.0.0",
+ }
})
服务器收到请求后,进行协议版本协商和保存客户端能力,然后响应中包含协商后的 protocolVersion、服务器自身的能力(capabilities)和服务器信息(serverInfo)。
MCP 服务端的初始化实现如下:
diff
+ // 声明服务端支持的能力
+ const SERVER_CAPABILITIES = {
+ tools: {
+ listChanged: true // 工具列表可动态变化,变更时会推通知
+ }
+ };
// 省略...
rl.on('line', (line) => {
try {
const request = JSON.parse(line.trim());
// 根据 method 进行分发处理
if (request.method === 'initialize') {
+ const clientVersions = request.params?.protocolVersion;
+ if (clientVersions !== serverProtocolVersion) {
+ sendError(request.id, -32600, `版本协议不支持`, {
+ clientVersions,
+ serverSupportedVersions: serverProtocolVersion
+ });
+ setTimeout(() => process.exit(1), 100);
+ return;
+ }
+ // 保存客户端声明的 capabilities,供后续调用时做能力检查
+ clientCapabilities = request.params?.capabilities ?? {};
+ console.error(`[MCP 服务器] 协议版本已协商`);
+ console.error(`[MCP 服务器] 客户端能力: ${JSON.stringify(clientCapabilities)}`);
+ sendResponse(request.id, {
+ protocolVersion: serverProtocolVersion,
+ // 告知客户端:本服务器具备哪些 capabilities
+ capabilities: SERVER_CAPABILITIES,
+ serverInfo: { name: 'read-file-server', version: '1.0.0' }
+ });
// 未知方法
} else {
sendError(request.id, -32601, `方法不存在: ${request.method}`);
}
} catch (e) {
// JSON 解析失败
sendError('error', -32700, `JSON 解析失败:${e}`);
}
});
本质上就是 MCP 服务端需要响应如下的一个 JSON 数据给 MCP 客户端:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"logging": {},
"notifications": { "tools": { "listChanged": true } }
},
"serverInfo": { "name": "read-file-server", "version": "1.0.0" }
}
}
接着客户端要检查服务器是否返回了错误,保存协商后的协议版本,保存服务器的 capabilities 信息,验证服务器是否支持客户端的能力。
实现如下:
diff
# ---------- MCP 客户端 ----------
class MCPClient:
# 省略...
def __init__(self, server_command):
self.server_command = server_command
self.process = None
self.request_id = 0
self.initialized = False
+ self.negotiated_version = None # 协商后的协议版本
+ self.server_capabilities = {} # 服务器声明的 capabilities(握手后填入)
+ # -------- 能力查询辅助方法 --------
+ def _has_capability(self, caps: dict, path: str) -> bool:
+ """
+ 检查 capabilities 字典中是否存在给定的能力路径。
+ path 为 '.' 分隔的键序列,例如 'tools.listChanged'。
+ """
+ obj = caps
+ for key in path.split("."):
+ if not isinstance(obj, dict) or key not in obj:
+ return False
+ obj = obj[key]
+ return True
+ def server_has(self, path: str) -> bool:
+ """检查服务器是否声明了某项 capability。"""
+ return self._has_capability(self.server_capabilities, path)
+ def client_has(self, path: str) -> bool:
+ """检查客户端自身是否声明了某项 capability。"""
+ return self._has_capability(self.CLIENT_CAPABILITIES, path)
# -------- 启动与握手 --------
def start(self):
# 省略...
+ # 检查服务器是否返回了错误(版本不兼容)
+ if "error" in response:
+ err = response["error"]
+ self.stop()
+ raise ConnectionError(
+ f"协议版本协商失败 "
+ f"Server error [{err.get('code')}]: {err.get('message')}. "
+ f"Details: {err.get('data', {})}"
+ )
+ result = response.get("result", {})
+ server_version = result.get("protocolVersion")
+ # 保存协商后的协议版本
+ self.negotiated_version = server_version
+ # 能力发现:保存服务器 capabilities
+ self.server_capabilities = result.get("capabilities", {})
+ # 初始化完成
+ self.initialized = True
+ print(f"MCP 服务器已连接(协议版本:{self.negotiated_version})")
+ # 验证服务器是否支持 tools(若不支持则后续调用无意义)
+ if not self.server_has("tools"):
+ print("[WARN] 服务器未声明 'tools' 能力。"
+ "工具调用很可能会失败。")
接着我们再启动 MCP 客户端进行测试:
python client.py
结果如下:
ruby
$ python client.py
正在启动 MCP 服务器...
MCP 服务器已连接(协议版本:2025-12-25)
至此,我们实现了 MCP 客户端和服务器从建立连接到断开连接的完整状态机,其核心是 初始化握手 阶段的协议版本协商和能力发现。通过生命周期中的能力协商,双方可以:在连接建立时就知道对方支持什么,避免调用不存在的功能。以及优雅降级或提前报错,而不是在业务逻辑中间出现奇奇怪怪的错误。
5.4 工具获取流程
连接建立后,客户端可以通过发送 tools/list 请求来发现可用的工具。工具列表请求实现如下:
diff
import json
import subprocess
# ---------- MCP 客户端 ----------
class MCPClient:
# 省略...
# -------- 底层通信 --------
def send_request(self, method, params=None):
# 省略...
# -------- 工具调用--------
+ def list_tools(self):
+ # 调用前检查服务器是否声明了 tools capability
+ if not self.server_has("tools"):
+ raise Exception(
+ "服务器不支持 'tools' 能力。"
+ f"服务器能力列表:{self.server_capabilities}"
+ )
+ result = self.send_request("tools/list")
+ return result.get("result", {}).get("tools", [])
# 省略...
# 初始化 MCP 客户端
mcp_client = MCPClient(["node", "server.js"])
mcp_client.start()
+ # 获取 MCP 工具列表
+ print("获取 MCP 工具列表...")
+ mcp_tools_raw = mcp_client.list_tools()
+ print(f"发现 {len(mcp_tools_raw)} 个 MCP 工具")
本质上就是向 MCP 服务器发送以下请求:
json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
接着 MCP 服务器需要返回如下格式的工具定义:
{ name, title, description, inputSchema }
这跟 OpenAI 规范中的工具定义有点区别,所以我们需要修改 read-file.js 文件中的工具定义,修改如下:
js
const tools = [
{
name: "read_file",
title: "读取文件",
description: "读取文本文件内容。",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "要读取的文件路径"
},
encoding: {
type: "string",
enum: ["utf-8", "gbk"],
description: "文件的字符编码格式"
}
},
required: ["path"]
}
},
];
同时服务端需要实现对 tools/list 的响应:
diff
// 省略...
+ const { tools, fileTool } = require('./read-file.js');
// 省略...
rl.on('line', (line) => {
try {
const request = JSON.parse(line.trim());
// 根据 method 进行分发处理
if (request.method === 'initialize') {
// 省略...
+ } else if (request.method === 'tools/list') {
+ // 返回服务端的工具列表
+ sendResponse(request.id, { tools });
// 未知方法
} else {
sendError(request.id, -32601, `方法不存在: ${request.method}`);
}
} catch (e) {
// 省略...
}
});
接着我们运行 MCP 客户端结果如下:
erlang
$ python client.py
正在启动 MCP 服务器...
MCP 服务器已连接(协议版本:2025-12-25)
获取 MCP 工具列表...
发现 1 个 MCP 工具
从结果可以印证我们的代码功能实现是成功的。
接着我们需要将 MCP 的工具格式转换成 OpenAI 的工具格式:
diff
+ # ---------- 本地工具定义 ----------
+ tools = []
# 省略...
+ def mcp_tool_to_openai(mcp_tool: dict) -> dict:
+ return {
+ "type": "function",
+ "function": {
+ "name": mcp_tool["name"],
+ "description": mcp_tool.get("description", mcp_tool.get("title", "")),
+ "parameters": mcp_tool.get("inputSchema", {"type": "object", "properties": {}})
+ }
+ }
+ mcp_tools = [mcp_tool_to_openai(t) for t in mcp_tools_raw]
+ # 合并原有的工具和 MCP 工具
+ tools = tools + mcp_tools
至此我们 MCP 客户端就可以获取到 MCP 服务器的工具了,获取到工具之后就可以发送给大模型了。
5.5 工具执行流程
工具执行的核心在于大模型返回的 tool_calls 参数包含了具体的调用信息,例如:
json
{ name: "read_file", arguments: { path: "test.txt", encoding: "utf-8" } }
然后,MCP 客户端实例根据这些信息调用对应的工具:
py
txt = mcp_client.call_tool('read_file', { path: "test.txt", encoding: "utf-8" })
print(f"读取内容: {txt}")
因此,我们需要在客户端中实现 call_tool 方法:
diff
# ---------- MCP 客户端 ----------
class MCPClient:
# 省略...
# -------- 工具调用 --------
def list_tools(self):
# 省略...
+ def call_tool(self, name, arguments):
+ # 调用前检查服务器是否声明了 tools capability
+ if not self.server_has("tools"):
+ raise Exception(
+ f"不能调用工具 '{name}': "
+ "服务器不支持调用工具能力."
+ )
+ result = self.send_request("tools/call", {"name": name, "arguments": arguments})
+ tool_result = result.get("result", {})
+ content_parts = tool_result.get("content", [])
+ return "\n".join(part.get("text", "") for part in content_parts)
# 省略...
同时,MCP 服务端需要响应 tools/call 方法,根据 name 路由到具体的工具实现并执行:
diff
rl.on('line', (line) => {
try {
// 省略...
if (request.method === 'initialize') {
// 省略...
} else if (request.method === 'tools/list') {
// 省略...
+ } else if (request.method === 'tools/call') {
+ const { name, arguments: args } = request.params;
+ if (name === 'read_file') {
+ try {
+ const filePath = args.path;
+ const encoding = args.encoding || 'utf-8';
+ const content = fileTool.execute(filePath, encoding);
+ sendResponse(request.id, {
+ content: [{ type: 'text', text: content }]
+ });
+ } catch (e) {
+ sendResponse(request.id, {
+ content: [{ type: 'text', text: '读取失败: ' + e.message }],
+ isError: true
+ });
+ }
} else {
sendError(request.id, -32601, `未知工具: ${name}`);
}
// 未知方法
} else {
sendError(request.id, -32601, `方法不存在: ${request.method}`);
}
} catch (e) {
// 省略...
}
});
接着我们运行 MCP 客户端结果如下:
erlang
$ python client.py
正在启动 MCP 服务器...
MCP 服务器已连接(协议版本:2025-12-25)
获取 MCP 工具列表...
发现 1 个 MCP 工具
读取内容: MCP 测试文件
从执行结果可以看到,我们已经成功读取了文件内容。
通过这一标准化流程,任何遵循 MCP 协议的服务器(无论使用何种编程语言实现)都可以被 MCP 客户端无缝调用,从而彻底解耦 AI Agent 与具体工具的实现。
6. 大模型应用集成
在前面的章节中,我们分别实现了 MCP 客户端(Python)和 MCP 服务器(Node.js),并通过 tools/list 和 tools/call 完成了工具的发现与执行。现在,我们将把 MCP 客户端真正接入到大模型应用中,构建一个能够智能调用外部工具的 AI Agent。
6.1 集成思路
典型的 AI Agent 工作流程如下:
- 用户输入问题 → 添加到对话历史。
- 调用大模型(附带工具定义),模型判断是否需要调用工具。
- 如果需要,模型返回
tool_calls→ Agent 解析工具名称和参数 → 通过 MCP 客户端执行外部工具(或回退到本地实现)→ 将工具结果以tool角色消息追加到历史中。 - 重复步骤 2‑3,直到模型不再需要调用工具,返回最终自然语言回答。
- 将回答展示给用户。
上述描述体现了 Agent 循环 的核心逻辑。下面我们进行具体的代码实现。
6.2 Agent 循环实现
我们定义了一个 agent_loop(messages: list) 函数,它接收一个消息列表(包含系统提示、用户输入及历史对话),不断与大模型交互,直到获得最终答案。
python
def agent_loop(messages: list):
while True:
# 调用大模型(DeepSeek),自动决定是否使用工具
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools, # 包含 MCP 工具转换后的 OpenAI 格式
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg) # 将助手的回复追加到历史
# 若模型没有工具调用请求,说明已生成最终回答
if not msg.tool_calls:
return msg.content
# 遍历所有工具调用,通过 MCP 客户端执行
for tool_call in msg.tool_calls:
tool_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f"Calling tool: {tool_name}, args: {args}")
# 优先通过 MCP 客户端调用工具
if mcp_client:
try:
result = mcp_client.call_tool(tool_name, args)
except Exception as e:
print(f"MCP 调用失败: {e}")
# 可选:回退到本地实现(本地实现我们这里仅给出错误提示)
result = f"工具 {tool_name} 调用失败,MCP 服务不可用。"
else:
# 若 MCP 客户端未初始化,使用本地实现(或报错)
result = f"MCP 客户端未连接,无法调用工具 {tool_name}。"
# 将工具执行结果以 tool 消息格式追加到历史
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": result
})
关键点说明:
tools参数 :我们在第 5 章已经将 MCP 服务器返回的工具定义(name、description、inputSchema)转换为 OpenAI 兼容的函数描述格式,并合并到tools列表中。因此大模型能够感知到这些外部工具的存在。- 工具执行 :优先通过
mcp_client.call_tool调用 MCP 服务器中的实际实现。如果调用失败(例如子进程崩溃、超时等),可以根据业务需求回退到本地实现或友好报错。示例中简单返回错误信息,实际生产环境可做得更完善(如重试、降级等)。 - 消息历史 :无论是模型的普通回复还是工具调用结果,都以标准角色(
assistant、tool)保存到历史中,确保下一次大模型调用时拥有完整的上下文。
6.3 主程序交互
在 __main__ 部分,我们启动一个交互式终端会话,让用户可以反复提问,Agent 循环会调用 MCP 工具(如文件读取)并给出回答。
python
if __name__ == "__main__":
try:
# 初始化历史消息列表,包含系统提示词,系统提示词,用于指导助手的行为
history = [
{"role": "system", "content": "你是一个文件读取助手,必要时可以调用工具帮助用户读取文件内容。"}
]
# 启动交互式终端会话
while True:
try:
# 获取用户输入
query = input("\033[36m用户 >> \033[0m")
except (EOFError, KeyboardInterrupt):
# 处理 Ctrl+D 或 Ctrl+C 退出的情况
break
# 处理正常退出的输入
if query.strip().lower() in ("q", "exit", "退出"):
break
# 将用户输入追加到历史记录中
history.append({"role": "user", "content": query})
# 启动代理循环进行对话
final_answer = agent_loop(history)
# 打印助手给出的最终答案
if final_answer:
print(f"\033[32m助手: {final_answer}\033[0m\n")
finally:
# 清理资源,停止 MCP 服务器
if mcp_client:
mcp_client.stop()
- 用户输入 :支持彩色提示符(
\033[36m为青色,\033[32m为绿色)。 - 退出条件 :输入
q、exit或退出即可结束会话,或者按Ctrl+C/Ctrl+D。 - 资源释放 :在
finally块中调用mcp_client.stop(),确保 Node.js 子进程被正常终止,避免残留进程。
6.4 测试示例运行
假设当前目录下存在一个 test.txt 文件,内容为 "Hello MCP!"。用户与 Agent 的对话可能如下:
ruby
$ python client.py
正在启动 MCP 服务器...
MCP 服务器已连接(协议版本:2025-12-25)
获取 MCP 工具列表...
发现 1 个 MCP 工具
用户 >> 读取 test.txt 文件
Calling tool: read_file, args: {'path': 'test.txt'}
助手: 文件内容如下:
`
MCP 测试文件
`
文件 test.txt 的内容很简单,只有一行文字:"MCP 测试文件"。请问您需要对这个文件进行其他操作吗?
整个过程完全由大模型自动决定何时调用 read_file 工具,而工具的实际执行则由 MCP 服务器(Node.js)完成。Python 客户端仅充当协议桥梁,实现了跨语言的工具调用。
6.5. 小结
通过将 MCP 客户端嵌入到 Agent 循环中,我们成功实现了:
- 标准化工具发现 :通过
tools/list从外部服务器获取工具定义,无需在 Python 代码中硬编码。 - 跨语言工具执行:Node.js 实现的文件读取工具被 Python 大模型应用无缝调用。
- 解耦与复用:同一个 MCP 文件服务器可以被任何支持 MCP 协议的客户端(Claude Desktop、其他语言的 Agent)复用,无需重复开发。
7. 总结
至此,我们完成了从 MCP 原理到实战的完整闭环:理解 stdio + JSON‑RPC 基础 → 实现 MCP 握手与能力协商 → 实现工具发现与执行 → 集成到大模型 Agent 中。后续我们可以基于此模式扩展更多工具(数据库、HTTP API、本地命令等),构建功能强大的 AI Agent 系统。
同时我们上述的实现也遵循了 MCP 的标准核心架构,MCP 定义了如下三种角色:
- MCP 主机(Host) :AI 应用程序,例如 Claude Desktop、VS Code 或我们示例中的
agent_loop。它创建并管理多个 MCP 客户端。 - MCP 客户端(Client) :与单个 MCP 服务器建立专用连接,负责发送请求、接收响应和通知。
- MCP 服务器(Server) :提供上下文数据 和可调用工具的程序。可以运行在本地(stdio 传输)或远程(HTTP 传输)。
对应我们上述的实现:
- 主机 :
agent_loop函数(实际上主机就是整个 Python 脚本)。 - 客户端 :
MCPClient类的实例。 - 服务器 :
mcp-server.js+read-file.js,通过node mcp-server.js启动。
值得注意的是使用 STDIO 传输的本地 MCP 服务器通常只为单个 MCP 客户端服务,而使用可流式 HTTP 传输的远程 MCP 服务器则通常为多个 MCP 客户端服务。
我是程序员Cobyte,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。