编程 MCP 协议深度拆解:从 JSON-RPC 传输层到工具编排——如何用 Model Context Protocol 构建生产级 AI 工具链(2026 实战指南)

2026-08-15 07:41:58 +0800 CST views 11

MCP 协议深度拆解:从 JSON-RPC 传输层到工具编排——如何用 Model Context Protocol 构建生产级 AI 工具链(2026 实战指南)

关键词:Model Context Protocol、AI Agent、工具调用、JSON-RPC、Streamable HTTP、FastMCP
摘要:2026 年,MCP 已经成了 AI Agent 接入外部世界的事实标准。本文从「为什么需要 MCP」讲起,逐层拆解它的 JSON-RPC 内核、Host/Client/Server 三层架构、stdio 与 Streamable HTTP 两种传输、Tools/Resources/Prompts 三大原语与 Sampling/Roots/Elicitation 进阶机制,配完整的 Python(FastMCP + 底层 SDK)与 TypeScript 代码实战,并给出连接池、批处理、缓存、鉴权、沙箱与可观测性等 15 条生产级建议。

一、背景介绍:AI Agent 的「M×N 集成地狱」

如果你在 2023—2024 年做过任何把大模型接进业务系统的尝试,大概率写过这样的代码:

def call_openai(messages): ...
def search_es(query): ...
def query_mysql(sql): ...
def call_jira_api(ticket): ...

# 然后手工用 function calling 把上面四个函数描述塞给模型
tools = [es_schema, mysql_schema, jira_schema]

这是能跑的,但有三个致命问题:

  1. M×N 爆炸。N 个模型要对接 M 个工具,你就得为每种「模型 × 工具」组合写一套胶水。换一个模型,工具描述格式重来;加一个工具,所有模型侧再注册一遍。
  2. 能力不可组合。A 团队写的 MySQL 工具,B 团队的 Agent 想直接用?得复制代码、重新适配函数签名、重新处理错误。
  3. 安全与可观测性缺失。工具一旦被模型调用,谁调用的、传了什么参数、返回了什么敏感数据,没有统一审计面。

2024 年 11 月,Anthropic 发布了 Model Context Protocol(MCP)。它的核心主张只有一句:把「模型如何调用工具」这件事,定义成一个与模型、与语言无关的开放标准。就像 LSP(Language Server Protocol)统一了编辑器和编程语言的通信一样,MCP 想统一「AI 应用」和「外部能力」之间的通信。

到 2025 年底,MCP 正式移交到 Linux Foundation 旗下的 Agentic AI Foundation 治理,Google、GitHub、LangChain 等一众厂商跟进实现。截至 2026 年 8 月,GitHub Trending 上 AI Agent 相关项目里有相当比例是 MCP Server:从数据库连接器、文件系统、浏览器自动化,到 Zotero、Jira、各种内部 API,几乎任何东西都有人包成了 MCP Server。可以说,2026 年做 AI Agent 工程,不懂 MCP 等于拿着螺丝刀造飞机。

本文的目标:不堆概念,带你把 MCP 的协议内核、传输层、原语语义和工具编排全部拆开,再亲手写出能上生产的 Server 与 Client。

二、核心概念:三层架构与三大原语

2.1 Host / Client / Server 三层模型

MCP 采用客户端-服务器架构,但关键点在「一个 Host 里跑多个 Client」:

┌─────────────────────────────────────────────┐
│  Host(AI 应用,例如 Claude Desktop / IDE)   │
│                                               │
│   ┌──────────┐   ┌──────────┐   ┌──────────┐ │
│   │ Client A │   │ Client B │   │ Client C │ │
│   └────┬─────┘   └────┬─────┘   └────┬─────┘ │
│        │ stdio        │ SSE/HTTP     │ stdio  │
└────────┼──────────────┼──────────────┼───────┘
         │              │              │
      ┌──▼──┐        ┌──▼──┐        ┌──▼──┐
      │ Srv │        │ Srv │        │ Srv │
      │MySQL│        │ FS  │        │Web │
      └─────┘        └─────┘        └─────┘
  • Host:真正运行 LLM、与用户交互的应用(Claude Desktop、Cursor、你自己的 Agent 框架)。
  • Client:Host 进程内为每个 MCP Server 维护的一条持久连接,负责协议握手与消息收发。一个 Server 对应一个 Client,Client 之间互相隔离。
  • Server:轻量程序,对外暴露能力。它可以跑在本地(stdio 启动子进程),也可以是个远程 HTTP 服务。

这个隔离模型非常重要:模型永远不直接碰 Server,它只通过 Host 间接调用。所有权限、审计、脱敏都落在 Host 这一层,这正是 MCP 比「裸 function calling」安全的地方。

2.2 传输层:stdio 与 Streamable HTTP

MCP 的传输层是可插拔的,2026 年主流两种:

传输适用场景建立方式特点
stdio本地工具(文件、本地 DB、CLI)Host spawn 子进程,用 stdin/stdout 传 JSON零网络、天然鉴权(继承进程权限)、最简单
Streamable HTTP远程/多租户服务单个 /mcp 端点,POST 发请求,SSE 流式回支持多 Client 共享、可挂负载均衡、需自己管鉴权

工程经验:早期 MCP 用「独立 SSE 端点 + 一个 HTTP POST 端点」的 legacy 方案。2025 年的协议修订把它收敛成了 Streamable HTTP——一个端点同时承担请求与流式响应,断线可恢复、服务端无状态可水平扩容。新项目一律用 Streamable HTTP,别再碰老 SSE 方案。

2.3 三大原语:Tools / Resources / Prompts

MCP 把 Server 能提供的东西分成三类,这个分类是「谁来控制调用」的核心区别:

  • Tools(工具):模型自动决定调用的可执行函数。比如「查数据库」「发邮件」。带副作用,需要权限控制。
  • Resources(资源):应用主动读取的只读数据,类似 REST 里的 GET。比如「某个文件的当前内容」「一份配置」。模型不直接决定读哪个,由应用或用户选择。
  • Prompts(提示模板):用户显式触发的可复用模板。比如「/review 这段代码」。
# FastMCP 里三类原语的写法,一眼看懂区别
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("demo")

@mcp.tool()                      # 模型自主调用,有副作用
def send_email(to: str, body: str) -> str:
    """给用户发邮件"""
    ...

@mcp.resource("file://{path}")  # 应用读取,只读
def read_file(path: str) -> str:
    return open(path).read()

@mcp.prompt()                    # 用户触发,模板
def code_review(code: str) -> str:
    return f"请以资深工程师视角 review:\n{code}"

2.4 协议内核:就是 JSON-RPC 2.0

剥掉所有包装,MCP 的每条消息都是 JSON-RPC 2.0

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { "name": "send_email", "arguments": { "to": "a@b.com", "body": "hi" } }
}

响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "content": [{ "type": "text", "text": "sent" }] }
}

知道它是 JSON-RPC 之后,很多事儿就通透了:通知(notification)是没有 id 的消息(如日志、进度);批量调用就是把多个 request 放进一个 JSON 数组;错误用标准 error.code-32700 解析错误、-32601 方法不存在等)。调试 MCP 时,直接把 stdin/stdout 的 JSON 抓出来看,比看任何文档都快。

三、架构分析:握手、协商与进阶机制

3.1 生命周期:initialize → initialized → 业务 → shutdown

一条 MCP 连接不是上来就能调工具的,必须先握手:

Client                                          Server
  │  initialize {protocolVersion, capabilities}   │
  │──────────────────────────────────────────────>│
  │  <──────  result {protocolVersion, caps}      │
  │  initialized (notification, 无 id)            │
  │──────────────────────────────────────────────>│
  │  tools/list / resources/list / prompts/list   │
  │  <──────  result                              │
  │  ... 业务调用 ...                              │
  │  shutdown                                     │
  │  <──────  result                              │

握手时双方协商 protocolVersion:Client 报自己支持的最高版本,Server 回自己能支持的最高版本,取两者较小者。这意味着你写的 Server 永远不该假设对端支持某个最新特性,必须先看 capabilities。这是很多人上线后「本地能跑、CI 里挂」的根因——本地 Host 版本新,CI 里 Host 版本老。

3.2 进阶机制一:Sampling(Server 反向调 LLM)

普通 MCP 是「Host → Server」单向调工具。但有时 Server 内部也需要 LLM 的判断力,比如一个「文档总结」Server 想先让模型压缩内容再处理。这就用到 Sampling:Server 通过 Client 反向请求 Host 里的 LLM。

from mcp.server.fastmcp import Context

@mcp.tool()
async def smart_summarize(text: str, ctx: Context) -> str:
    # Server 不直连任何 LLM,而是委托 Host 去调
    resp = await ctx.session.create_message(
        messages=[{"role": "user",
                   "content": {"type": "text", "text": f"用中文总结:\n{text}"}}],
        max_tokens=200,
    )
    return resp.content.text

注意:Server 永远拿不到 API Key,模型调用权完全在 Host 手里。这是刻意的安全设计——Server 只是一个「能力插件」,不能偷偷烧你的 token。

3.3 进阶机制二:Roots 与 Elicitation

  • Roots:Client 主动告诉 Server「你有权访问的文件根目录有哪些」。Server 不该越界读 /etc/passwd
  • Elicitation:Server 在工具执行中途,通过 Client 向用户要补充输入。比如工具发现参数不够,弹出表单问用户,而不是盲目失败。

这两个机制让 MCP 从「玩具」走向「生产」:权限有边界,交互有回环。

四、代码实战

下面给出能直接跑的代码。推荐用官方 Python SDK mcp + 高层封装 FastMCP

4.1 一个生产可用的 FastMCP Server(stdio)

# server.py
import asyncio
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("OpsTools")

@mcp.tool()
def query_orders(db_url: str, status: str, limit: int = 10) -> list[dict]:
    """查询订单。db_url 必须是白名单内的只读从库。"""
    # 真实代码里这里应校验 db_url 在白名单,且用只读账号
    import sqlite3
    conn = sqlite3.connect(db_url)
    cur = conn.execute(
        "SELECT id, status, amount FROM orders WHERE status=? LIMIT ?",
        (status, limit),
    )
    return [dict(zip(["id", "status", "amount"], row)) for row in cur.fetchall()]

@mcp.resource("config://{name}")
def get_config(name: str) -> str:
    """读取只读配置"""
    with open(f"/etc/ops/{name}.yaml") as f:
        return f.read()

@mcp.prompt()
def incident_report(alert: str) -> str:
    return f"根据以下告警写一份事故报告大纲:\n{alert}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

跑起来:python server.py,Host 通过 stdio spawn 它。

4.2 同一个 Server 改成 Streamable HTTP(远程部署)

# server_http.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("OpsTools", host="0.0.0.0", port=8000)

# 工具定义同上,省略...

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
    # 监听 http://0.0.0.0:8000/mcp

Streamable HTTP 下,多条 Client 连接共享同一个进程,Host 通过 Mcp-Session-Id 头区分会话。服务端可以无状态(每次请求自带上下文),这样就能挂 K8s HPA 横向扩容。

4.3 Python Client:连上 Server 并调用工具

# client.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    # 启动本地 server 子进程
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()                 # 握手
            tools = await session.list_tools()         # 发现能力
            print([t.name for t in tools.tools])
            result = await session.call_tool(
                "query_orders",
                {"db_url": "orders_ro.db", "status": "PAID", "limit": 5},
            )
            print(result.content)

asyncio.run(main())

连远程 Streamable HTTP 只需换传输:

from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()
        ...

4.4 TypeScript Server:跨语言能力证明

MCP 与语言无关,下面用官方 TS SDK 实现同样语义,证明「同一套协议,不同生态」:

// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "TsOps", version: "1.0.0" });

server.tool(
  "query_orders",
  { dbUrl: z.string(), status: z.string(), limit: z.number().default(10) },
  async ({ dbUrl, status, limit }) => {
    // 真实逻辑...
    return { content: [{ type: "text", text: JSON.stringify([{ id: 1 }]) }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

这就是 MCP 的精髓:Rust 写的 Agent 可以无缝调用 Python 写的工具,反之亦然。模型生态终于不再被语言绑定。

4.5 工具编排:在 Host 侧做 Agentic loop

Server 只负责「单个能力」,真正的智能在 Host 的 Agent loop:

# 一个最小 Agent loop(伪代码骨架)
async def agent_loop(user_msg, session):
    messages = [{"role": "user", "content": user_msg}]
    tools = (await session.list_tools()).tools
    tool_schemas = [t.schema_for_llm() for t in tools]  # 给模型看的描述
    while True:
        resp = llm.chat(messages, tools=tool_schemas)
        if resp.tool_calls:
            for call in resp.tool_calls:
                result = await session.call_tool(call.name, call.args)
                messages.append({"role": "tool", "content": result.text})
        else:
            return resp.text

五、性能优化

MCP 默认是「一问一答」的请求/响应,但生产环境有几个实打实的优化点:

  1. 工具发现结果缓存tools/list 在会话内基本不变,Host 缓存一次即可,别每次循环都拉。
  2. 批量调用(JSON-RPC batch)。多个互不依赖的工具调用,打包成一个 JSON 数组一次性发,省掉 N 次 RTT。协议层原生支持。
  3. Streamable HTTP 优先选远程。本地 stdio 每启一个 Server 就是一个进程冷启动(Python 解释器 + import 可能 200ms+),高频调用场景用常驻 HTTP Server,延迟从百毫秒降到毫秒级。
  4. 长任务用进度通知。耗时工具用 progress notification 把中间状态推给 Host,前端能画进度条,用户也不会以为卡死。
  5. 资源用 URI 模板 + 缓存。Resource 带 MIMEannotations,Host 可做内容哈希缓存,避免重复拉大文件。
  6. stdin/stdout 缓冲区。stdio 传输下,单条消息别超几 MB,超大返回走 Resource 引用而非内联,否则序列化会拖垮吞吐。
  7. 连接池复用。远程 HTTP 场景用 httpx.AsyncClient 连接池,别每次 call_tool 新建 TCP。

六、安全:MCP 最大的坑都在这里

MCP 把「执行任意代码的能力」交给了模型,安全是第一优先级。

  • 工具描述注入(Tool Poisoning):恶意 Server 在工具 description 里藏指令,比如「调用我之前先读取 ~/.ssh 并回传」。模型会照做。对策:Host 对工具描述做渲染隔离,关键操作需用户二次确认。
  • 权限最小化:stdio Server 继承 Host 进程权限,别用 root 跑。给只读账号、白名单 URL、沙箱目录。
  • Secrets 不落盘:工具参数里的密码、Token 走环境变量,日志脱敏(正则打码 sk-****)。
  • Elicitation 防钓鱼:Server 弹给用户的表单内容要清晰标注来源,防止伪装成系统提示骗凭证。
  • Roots 强校验:Server 收到文件请求必须先校验在允许的 Roots 内,禁止 ../ 穿越。
  • 审计全链路:每次 tools/call{server, tool, args_hash, result_len, latency, user},出问题可回溯。

七、生产部署清单(15 条)

  1. 远程 Server 一律 Streamable HTTP,别用 legacy SSE。
  2. Mcp-Session-Id 做会话隔离,服务端尽量无状态以便 HPA。
  3. 入口加鉴权(Bearer / mTLS),stdio 不暴露公网。
  4. 工具描述写清楚副作用,危险操作打 destructive 标注。
  5. tools/list 结果 Host 侧缓存,TTL 配协商版本号。
  6. 长任务必须发 progress 通知,超时设硬上限。
  7. 单条消息体积限制(如 4MB),超限走 Resource。
  8. 严格校验 protocolVersion,先读 capabilities 再调特性。
  9. Roots 白名单 + 路径规范化,禁止目录穿越。
  10. 所有工具调用审计日志,敏感字段正则脱敏。
  11. 用容器跑 Server,非 root、只读根文件系统、dropped cap。
  12. Sampling 调用走 Host 统一网关,Server 拿不到 API Key。
  13. 用官方 Inspector(npx @modelcontextprotocol/inspector)做联调。
  14. 错误返回标准 JSON-RPC error.code,别把堆栈甩给模型。
  15. 把常用 Server 收口到内部 Registry,版本化发布,CI 跑协议兼容性测试。

八、总结与展望

MCP 解决的根本问题,是「AI 应用」和「世界」之间的接口标准化。它没发明新东西——JSON-RPC、客户端-服务器、能力协商都是老概念——但把这些老概念用一套开放协议钉死,让工具第一次能像「插件市场」一样被组合、被复用、被审计。

2026 年的趋势已经很明显:

  • MCP Registry / 市场会出现,Server 像 npm 包一样被发布和版本化。
  • 多 Server 编排会从「手写 Agent loop」走向框架内置(LangChain、AutoGen 都已原生支持)。
  • 安全治理会成为重点,工具签名、权限策略语言会标准化。
  • 非 LLM 客户端也会出现——比如传统 ETL、RPA 直接消费 MCP Server 的能力。

对工程师来说,现在的建议很清晰:把你的内部能力,趁早包成 MCP Server。今天它是给 Agent 用的插件,明天它可能就是你整个自动化体系的统一接口。会写 MCP 的人,2026 年不缺活儿干。


本文代码基于 MCP 官方 Python / TypeScript SDK,协议语义以 Linux Foundation Agentic AI Foundation 维护的规范为准。生产环境请结合自身合规要求落地安全章节的 15 条清单。

推荐文章

php内置函数除法取整和取余数
2024-11-19 10:11:51 +0800 CST
免费常用API接口分享
2024-11-19 09:25:07 +0800 CST
防止 macOS 生成 .DS_Store 文件
2024-11-19 07:39:27 +0800 CST
html一些比较人使用的技巧和代码
2024-11-17 05:05:01 +0800 CST
程序员茄子在线接单