深入理解 MCP 协议:让 AI Agent 工具调用突破边界

深入理解 MCP 协议:让 AI Agent 工具调用突破边界

引言

在构建 AI Agent 应用时,Tool(工具)是一个核心概念。Tool 让 LLM 拥有了操作外部世界的能力------查询数据库、调用 API、操作文件系统。但传统的 Tool 实现方式存在一些根本性的局限。本文将从一个实际项目(mcp-demo)的思考出发,带你深入理解 MCP(Model Context Protocol)协议是什么、解决了什么问题、以及它是如何工作的。

传统 Tool 的痛点

回顾传统 Agent 开发中 Tool 的实现方式,会发现两个明显的问题:

1. Tool 与项目强耦合

在大多数框架中,Tool 以函数或类的形式直接写在 Agent 项目的代码里。这意味着:

  • 你在项目 A 中写的 Tool,无法直接在项目 B 中使用
  • 每次开始一个新项目,你都需要重新编写或手动迁移 Tool 代码
  • Tool 的复用几乎为零,团队之间无法共享工具能力

2. 语言绑定的限制

假设你用 Node.js 写好了一套完美的 Tool 集合,但你的同事或另一个团队使用 Python / Java / Rust 开发 Agent------这些 Tool 就无法直接复用。每种编程语言生态都有各自的优势库和工具,语言壁垒让跨团队协作变得困难重重。

核心矛盾:Tool 应当独立于 LLM,实现本地/远程、跨进程、跨语言的调用。

MCP 协议是什么

MCP 全称 Model Context Protocol(模型上下文协议),由 Anthropic 提出并开源。它的核心定位是:

标准化 LLM 与 Tool、Resource 之间的通信协议,实现 LLM 与 Tool 的解耦。

通俗地说,MCP 是一种"通用语言"。它让 AI Agent(充当 MCP 客户端 / Host)可以通过统一的方式,调用各种不同语言、不同位置、不同实现方式的 MCP 服务器所提供的工具和资源。

MCP 的通信方式

MCP 设计了两种通信方式,分别覆盖本地和远程两种场景:

stdio(标准输入输出流)------ 本地跨进程通信

这是 MCP 最轻量的通信方式。当一个 Agent 进程需要调用另一个子进程中的工具时,它通过 标准输入输出流(stdin/stdout) 来收发 JSON-RPC 格式的消息。

scss 复制代码
┌─────────────┐     stdin (JSON-RPC)     ┌──────────────┐
│  AI Agent   │ ◄──────────────────────► │  MCP Server   │
│  (Host)     │     stdout (JSON-RPC)    │  (子进程)      │
└─────────────┘                          └──────────────┘
  • Agent 通过 child_process 等方式启动子进程
  • 通信就像键盘输入和控制台输出一样简单直接
  • 子进程可以是任何语言实现------Node.js、Python、Go、Rust 等
  • 无需网络端口,无需额外配置,天然安全

HTTP ------ 远程跨进程通信

对于远程工具调用场景,MCP 支持通过 HTTP 协议进行通信。这让你可以:

  • 部署远程 MCP Server,供多个 Agent 实例共享
  • 调用部署在云端或内网其他机器上的工具
  • 利用 HTTP 生态的成熟基础设施(负载均衡、鉴权、监控等)
scss 复制代码
┌─────────────┐       HTTP (SSE/JSON-RPC)       ┌──────────────┐
│  AI Agent   │ ◄─────────────────────────────► │  MCP Server   │
│  (Host)     │                                 │  (远程服务)    │
└─────────────┘                                 └──────────────┘

实战:手写一个 MCP Server

理论讲完了,我们来看一个完整的 MCP Server 实例。以下代码来自 src/my-mcp-server.mjs

javascript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// 假数据,未来可以走数据库
const database = {
  users: {
    '001': { id: '001', name: '张三', email: 'zs@qq.com', role: 'admin' },
    '002': { id: '002', name: 'nike', email: 'nike@qq.com', role: 'user' },
    '003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
  }
};

const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0'
});

server.registerTool('query_user', {
  description: `查询数据库中的用户信息。输入用户ID, 
  返回该用户的详细信息(姓名、邮箱、角色)`,
  inputSchema: {
    userId: z.string().describe('用户ID, 例如:001, 002, 003')
  }
}, async ({ userId }) => {
  const user = database.users[userId];
  if (!user) {
    return {
      content: [
        { type: 'text', text: `用户 ID ${userId} 不存在。
        可用的ID: 001, 002, 003` }
      ]
    };
  }
  return {
    content: [
      { 
        type: 'text', 
        text: `用户 ${user.id} 的信息是:
        姓名: ${user.name}, 邮箱: ${user.email}, 角色: ${user.role}`
      }
    ]
  };
});

// 跨进程通信方式 stdio
const transport = new StdioServerTransport();
await server.connect(transport);

下面我们逐段解析这段代码,看看一个 MCP Server 到底由哪些部分组成。


第一步:引入依赖

javascript 复制代码
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

这里引入了三个核心模块:

模块 作用
McpServer MCP 服务器的核心类,负责创建 Server 实例、注册 Tool 和 Resource,是整座房子的"骨架"
StdioServerTransport stdio 传输层实现,负责通过标准输入输出流与 Agent Host 通信,是数据进出 MCP Server 的"管道"
z(Zod) 一个 TypeScript 优先的 schema 声明与验证库,用来定义 Tool 的输入参数格式。Agent 在调用 Tool 前会先拿到这个 schema,从而知道该传什么参数、参数是什么类型

💡 为什么需要 Zod? MCP 协议本身并不规定你必须用 Zod。但 SDK 选择了 Zod 作为 inputSchema 的描述方式------它既是运行时的参数校验,又是 LLM 的参数理解依据。Agent 拿到你的 Tool 时,会读到 inputSchema 中的 z.string().describe(...),从而推理出"调用这个工具需要传一个字符串类型的 userId"。


第二步:准备数据------Tool 独立于 Agent 项目

javascript 复制代码
const database = {
  users: {
    '001': { id: '001', name: '张三', email: 'zs@qq.com', role: 'admin' },
    '002': { id: '002', name: 'nike', email: 'nike@qq.com', role: 'user' },
    '003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
  }
};

这里用一个内存对象模拟了数据库。注意一个关键点:这段数据存在于 MCP Server 进程内部,而不是 Agent 项目里。

这就是 MCP 的"解耦"思想在代码层面的体现:

  • Agent(Host)不需要知道数据怎么存的、存了什么
  • MCP Server 独立持有数据、独立运行、独立部署
  • 未来把这个内存对象替换成真实的 MySQL / PostgreSQL / Redis,Agent 端一行代码都不需要改

第三步:创建 MCP Server 实例

javascript 复制代码
const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0'
});

两行代码,创建了一个 MCP Server。nameversion 是必须提供的元信息------当 Agent(Host)连接到这个 Server 时,首先拿到的就是这些基本信息。

类比理解:这就像是启动了一个"微服务",只不过它不是监听某个端口,而是等待父进程通过 stdio 发来的 JSON-RPC 消息。


第四步:注册 Tool------核心步骤

javascript 复制代码
server.registerTool('query_user', {
  description: `查询数据库中的用户信息。输入用户ID, 
  返回该用户的详细信息(姓名、邮箱、角色)`,
  inputSchema: {
    userId: z.string().describe('用户ID, 例如:001, 002, 003')
  }
}, async ({ userId }) => {
  // ... 处理逻辑
});

registerTool 是 MCP Server 最重要的 API。它接收三个核心参数:

4.1 Tool 名称 ------ 'query_user'

这是 Tool 的唯一标识符。Agent 在调用时会说"我要调用 query_user 工具"。命名应该简洁、动词开头、语义清晰。

4.2 Tool 元数据 ------ description + inputSchema

这是 MCP 协议的精髓所在------Tool 会"自我介绍"

  • description:用自然语言告诉 LLM 这个工具是干什么的。LLM 会根据这段描述,自主判断"当前用户的问题是否需要调用这个工具"。描述越清晰精准,LLM 调用工具的时机就越准确。

  • inputSchema:定义这个工具需要什么参数。这里用了 Zod:

    javascript 复制代码
    { userId: z.string().describe('用户ID, 例如:001, 002, 003') }

    它告诉 LLM:"你调用我时需要传一个 userId 参数,它是字符串类型,取值类似 001002003"。LLM 看到这段 schema 后,就能从用户的话里提取出 userId,正确地构造调用参数。

这就是 MCP 的"扩展 Context":Tool 把自己的能力描述(description)和调用规范(inputSchema)注入到 LLM 的上下文中。不是 LLM 主动去查,而是 Tool 主动报到。

4.3 Tool 执行逻辑 ------ Handler 函数
javascript 复制代码
async ({ userId }) => {
  const user = database.users[userId];
  if (!user) {
    return {
      content: [
        { type: 'text', text: `用户 ID ${userId} 不存在。可用的ID: 001, 002, 003` }
      ]
    };
  }
  return {
    content: [
      { type: 'text', text: `用户 ${user.id} 的信息是:
        姓名: ${user.name}, 邮箱: ${user.email}, 角色: ${user.role}` }
    ]
  };
}

Handler 函数的执行流程非常清晰:

  1. 接收参数 ------ { userId } 由 MCP 框架自动从 Agent 的调用请求中解析并注入,类型已在 inputSchema 中校验
  2. 执行业务逻辑 ------ 查数据库(这里是内存对象),找到对应 user
  3. 返回标准化结果 ------ 返回一个 content 数组,每个元素包含 typetext

返回结构的标准化同样重要:

javascript 复制代码
{ content: [{ type: 'text', text: '...' }] }

无论成功还是失败,MCP Server 都返回同样结构的结果。Agent 拿到后,会把它注入到 LLM 上下文中------LLM 看到这段文本,就能把查询结果用自然语言转述给用户。

  • 找到用户时:返回详细信息,LLM 可以向用户直接播报
  • 未找到用户时:返回友好提示 + 可用 ID 列表,LLM 会告诉用户"没有这个用户哦,试试 001、002、003 吧"

这就是 Tool 的"闭环":描述(告诉 LLM 怎么用) → 参数 Schema(约束调用格式) → Handler 执行业务 → 结构化返回(注入 LLM 上下文) → LLM 用自然语言回答用户。


第五步:连接传输层------启动服务

javascript 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

这是 MCP Server 的"最后一公里"------把 Server 实例挂载到一个传输层上,让它真正运转起来。

  • StdioServerTransport 创建了一个 stdio 传输层实例。它做的事情很简单:监听 stdin,把收到的 JSON-RPC 消息转给 server 处理,然后把 server 的返回结果写到 stdout。
  • server.connect(transport) 是一个异步操作,把 server 和传输管道连接起来。连接成功后,server 就处于"就绪"状态------等待 Agent Host 发来的指令。

整个数据流可以这样理解:

c 复制代码
Agent (Host)                          MCP Server (这个文件)
    │                                        │
    │  ① 启动子进程: node my-mcp-server.mjs   │
    │───────────────────────────────────────►│
    │                                        │
    │  ② 通过 stdin 发送 JSON-RPC:            │
    │  {"method":"tools/call",               │
    │   "params":{"name":"query_user",        │
    │   "arguments":{"userId":"001"}}}        │
    │───────────────────────────────────────►│ ③ transport 接收 → server 处理
    │                                        │    → handler 查数据库
    │  ④ 通过 stdout 返回 JSON-RPC:           │
    │  {"content":[{"type":"text",           │
    │   "text":"用户 001 的信息是:..."}]}      │
    │◄───────────────────────────────────────│
    │                                        │
    │  ⑤ LLM 将结果转述给用户                   │

这段代码揭示的 MCP 设计哲学

从这 45 行代码中,我们可以抽象出 MCP Server 的几个核心设计原则:

  1. 声明式注册 :Tool 通过 registerTool 声明式注册,包含名称、描述、参数 schema、处理逻辑。Agent 端不需要任何代码层面的"引入"或"import"------它在连接时自动发现所有 Tool。

  2. 标准化通信:无论是 Tool 注册的元数据(description + inputSchema)还是 Tool 调用的结果(content\[\]),都有固定的结构。这就是 MCP 作为"协议"的价值------双方都按约定好的格式说话,无需猜测。

  3. 传输层可替换 :这段代码用的是 StdioServerTransport。如果未来要改成 HTTP 模式,只需要把最后两行换成 new StreamableHTTPServerTransport() 并启动 HTTP 服务------Server 本身(注册的 Tool、Handler 逻辑)完全不需要改动

  4. 进程即边界:整个 MCP Server 是一个独立的 Node.js 进程。它和 Agent 之间唯一的耦合就是 stdio(或 HTTP)。这意味着你可以用 Python 写另一个 MCP Server,Agent 同时连接 Node.js Server 和 Python Server------对 Agent 来说,它们都是 MCP 协议下的平等工具提供者。

MCP 的核心特点

跨进程调用工具

这是 MCP 最大的特点。无论是本地进程还是远程服务,Agent 都可以通过统一的 MCP 协议来发现和调用工具。你只需要在 Client 端配置好 MCP Server 的连接信息,工具就会自动出现在 Agent 的可用工具列表中。

不是 Fetch,而是扩展 Context

这里有一个关键认知:MCP 不是简单的接口调用

普通的 HTTP 调用(fetch)是你向某个 API 发起请求并获取数据,这是"拿数据"的逻辑。而 MCP 的目标更高一层------它是扩展 LLM 的 Context(上下文)

MCP 为 LLM 提供两类核心扩展:

扩展类型 说明 举例
Tool 让模型能"做更多" 查数据库、发邮件、操作文件
Resource 让模型能"知道更多" 读取文档、获取实时数据、访问知识库

真实的生态价值

基于 MCP,社区已经构建了丰富的工具生态。一个用 Python 写的 MCP Server(比如文件系统操作工具),可以被任何支持 MCP 的 Agent(无论是 Claude Desktop、Cursor、还是你自己写的 Agent)直接使用------无需重写、无需适配、即插即用。

总结

MCP 协议解决了一个根本性的工程问题:如何让 AI Agent 的工具能力可复用、可共享、跨语言、跨平台。 它通过标准化的通信层(stdio + HTTP),让 Tool 从"项目代码的一部分"变成"独立的、可被任意 Agent 调用的服务"。

其设计哲学可以概括为:

  • 解耦:LLM 和 Tool 独立演进,互不依赖
  • 标准化:统一的协议,消除碎片化
  • 开放:任何语言、任何平台都可以实现 MCP Server
  • 扩展性:本地 stdio 和远程 HTTP 两种模式覆盖所有场景

MCP 正在成为 AI Agent 时代的"通用接口标准"------就如同 USB 之于外设、HTTP 之于 Web 服务。理解并掌握 MCP,是每一个 AI Agent 开发者的必修课。

相关推荐
孟无岐3 小时前
从零到可玩:用 WorkBuddy + TapTap 制造做出一款放置挂机游戏(完整实战记录)
mcp·ai游戏开发·workbuddy·taptap制造·放置挂机游戏·零代码游戏
冬奇Lab15 小时前
MCP 系列(01):MCP 是什么——为什么 Function Calling 不够
人工智能·llm·mcp
Esaka_Forever16 小时前
MCP Prompt(模型上下文协议 提示模板)完整详解
mcp
元Y亨H20 小时前
Model Context Protocol (MCP) 全方位解析
llm·mcp
love530love1 天前
GPT + Codex CLI + CodexPro 三位一体:AI 编程协作工作流
人工智能·windows·gpt·chatgpt·devops·mcp
VIP_CQCRE1 天前
把 Ace Data Cloud 接入 AI 助手:MCP 平台管理与文档检索实战
ai·大模型·api·mcp·acedatacloud
cypking2 天前
TypeScript 开发AI Agents MCP 完整实战指南
mcp
Esaka_Forever2 天前
MCP 协议 Resources(资源)完整解读
mcp
ServBay2 天前
如何实现30秒实现动嘴建站,Claude Code + ServBay MCP 实战指南
aigc·ai编程·mcp