大模型 MCP 本质原理:从协议到代码实现

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 本身不规定传输方式,通常使用 HTTPWebSocketTCP SocketStdio(如 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
  • 响应 :包含 idresulterror
  • 通知 :没有 id 字段,表示单向消息,接收方无需回复。

并且 MCP 基于 JSON‑RPC 2.0 定义了生命周期管理、核心原语(工具、资源、提示词)的交互序列。

其中三种 MCP 服务器可暴露的核心原语如下:

  1. 工具(Tools)
    AI 可调用的函数,执行操作。
    发现:tools/list → 执行:tools/call
  2. 资源(Resources)
    只读的数据源,如文件内容、数据库记录。
    发现:resources/list → 读取:resources/read
  3. 提示词(Prompts)
    预定义的模板,帮助结构化与 LLM 的交互。
    发现:prompts/list → 获取:prompts/get

简单来说就是 MCP 服务器可以提供工具(Tools)资源(Resources)提示词(Prompts) 三种能力。然后 MCP 客户端可以通过 tools/list 方法查看具体有哪些工具可以提供给大模型使用,然后大模型真的要使用的时候则通过 tools/call 方法执行具体的工具。具体如下:

  1. 发现 :客户端发送 tools/list,服务器返回工具数组。
  2. 执行 :客户端发送 tools/call,包含工具名和参数。服务器执行后返回 content 数组(可包含文本、图像等)。

虽然 MCP 服务器可以提供这么多能力,但不是每个 MCP 服务器都会提供全部的能力,所以需要设计一个生命周期来管理这些能力。

5.2 为什么需要生命周期管理?

因为 MCP 不预设固定的功能集,所以不同的 MCP 服务器可能支持不同的原语(工具、资源、提示词)、不同的通知机制、不同的扩展。客户端也未必支持所有特性。因此,在正式开始交换上下文之前,双方必须:

  • 协商协议版本 :确保使用双方都理解的版本(例如 2025-06-18)。
  • 交换能力声明 :客户端告知服务器自己能处理什么(如 tools.listChanged 通知、elicitation 引导),服务器告知客户端它提供了什么(如 toolsresourcesPrompts)。
  • 建立一致的初始状态:比如客户端知道服务器有哪些工具可用,服务器知道客户端可以接收哪些通知。

如果缺少这个握手阶段,后续的 tools/listtools/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/listtools/call 完成了工具的发现与执行。现在,我们将把 MCP 客户端真正接入到大模型应用中,构建一个能够智能调用外部工具的 AI Agent。

6.1 集成思路

典型的 AI Agent 工作流程如下:

  1. 用户输入问题 → 添加到对话历史。
  2. 调用大模型(附带工具定义),模型判断是否需要调用工具。
  3. 如果需要,模型返回 tool_calls → Agent 解析工具名称和参数 → 通过 MCP 客户端执行外部工具(或回退到本地实现)→ 将工具结果以 tool 角色消息追加到历史中。
  4. 重复步骤 2‑3,直到模型不再需要调用工具,返回最终自然语言回答。
  5. 将回答展示给用户。

上述描述体现了 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 服务器返回的工具定义(namedescriptioninputSchema)转换为 OpenAI 兼容的函数描述格式,并合并到 tools 列表中。因此大模型能够感知到这些外部工具的存在。
  • 工具执行 :优先通过 mcp_client.call_tool 调用 MCP 服务器中的实际实现。如果调用失败(例如子进程崩溃、超时等),可以根据业务需求回退到本地实现或友好报错。示例中简单返回错误信息,实际生产环境可做得更完善(如重试、降级等)。
  • 消息历史 :无论是模型的普通回复还是工具调用结果,都以标准角色(assistanttool)保存到历史中,确保下一次大模型调用时拥有完整的上下文。

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 为绿色)。
  • 退出条件 :输入 qexit退出 即可结束会话,或者按 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 应用开发。

相关推荐
cong_1 小时前
狐蒂云🦊跑路我的摸鱼岛没了!
前端·后端·github
kyriewen111 小时前
我开发的 Chrome 扒图浏览器插件又更新了❗
前端·javascript·chrome·科技·ai
Data_Journal1 小时前
Puppeteer指纹识别指南:循序渐进,简单易学!
服务器·前端·人工智能·物联网·媒体
晓得迷路了1 小时前
栗子前端技术周刊第 128 期 - Rolldown 1.0、Vitest、Node.js 26.0.0...
前端·javascript·css
金玉满堂@bj1 小时前
Gin 框架零基础全套入门教程(Go 企业级 Web 开发)
前端·golang·gin
qingy_20461 小时前
浏览器页面出现竖向滚动条的解决方案
前端·javascript·vue.js
之歆2 小时前
DAY_17深度博客:CSS 响应式布局 · BFC · JavaScript 完全指南(下)
前端·javascript·css
光影少年2 小时前
React18 函数组件执行顺序、严格模式下重复执行问题
前端·javascript·react.js
之歆2 小时前
DAY_20JavaScript 条件语句与循环结构深度学习(一)
前端·javascript