大模型 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 本身不规定传输方式,通常使用 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 服务器可暴露的核心原语如下:

  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 引导),服务器告知客户端它提供了什么(如 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 工作流程如下:

  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 服务器返回的工具定义(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 应用开发。

相关推荐
怕浪猫1 小时前
2026 年 AI Agent 面试到底考什么?这套题库覆盖了 90% 的高频考点
面试·aigc·ai编程
奕鼎竜瑆1 小时前
Solid 前端响应式开发从零到精通
前端·人工智能
动恰客流统计2 小时前
景区客流统计怎么做?兼顾管控与运营的实施方案解析
大数据·前端·人工智能
派小心.3 小时前
A/B测试工具免费版够用吗?
前端·数据分析
前端snow3 小时前
ai agent --- redis 缓存
前端
duanshu_4 小时前
官网表单与下载异常怎么排查?五个环节定位故障
前端·自动化·flyweight
Zhou1411364 小时前
SpringBoot_03_Web开发
前端·spring boot·python
辻弋2014 小时前
【无标题】
大数据·服务器·前端·搜索引擎·开源软件
ii_best4 小时前
按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)
android·ios·智能手机·自动化·ai编程·按键精灵