Model Context Protocol (MCP) 深度实战:构建类型安全的 AI 工具调用协议
引言
2024 年底,Anthropic 开源了 Model Context Protocol(MCP)——一套用于连接大语言模型应用与外部数据源、工具的标准化协议。在此之前,每一个 Agent 框架都要为每一款数据源重写一套胶水代码:Notion 一套、GitHub 一套、PostgreSQL 一套,模型与工具之间呈现出典型的 "M×N" 集成爆炸。
MCP 的核心主张是 "一次编写,处处连接":它用一套统一的 JSON-RPC 2.0 语义,把 Tools、Resources、Prompts 三类能力抽象成标准原语,让模型侧(Host/Client)与工具侧(Server)彻底解耦。本文将深入剖析 MCP 的架构与生命周期,并分别用 TypeScript 官方 SDK 与 Rust 裸 JSON-RPC 从零构建一个可用的 MCP Server。
一、为什么需要 MCP:从 Function Calling 碎片化到统一协议
传统 Function Calling 的工作流是:模型输出一段 JSON,应用解析后调用本地函数,再把结果拼回上下文。问题不在于 "能不能调",而在于 "每接一个系统都要重新约定 Schema、传输方式与安全边界"。MCP 把这套约定标准化为三层收益:
- 协议层统一:工具描述、参数 Schema、返回结构全部走 JSON Schema,模型无需关心后端是 Python 服务还是本地二进制。
- 传输层可插拔:同一份 Server 逻辑既能通过 stdio 作为本地子进程运行,也能通过 Streamable HTTP 作为远程服务暴露,Host 侧零改动。
- 能力可发现:模型在运行时通过
tools/list、resources/list动态发现可用能力,无需硬编码提示词。
二、MCP 架构全景
2.1 三大角色:Host / Client / Server
MCP 采用客户端—服务器模型,但引入了 "Host" 作为协调者:
- Host:真正运行 LLM 的应用(如 Claude Desktop、IDE 插件、自建 Agent)。一个 Host 可同时连接多个 MCP Server。
- Client:Host 内部为每个 Server 维护的一个隔离会话连接,负责协议握手与消息路由。
- Server:提供能力的进程或服务,向 Client 暴露 Tools / Resources / Prompts。
2.2 传输层:stdio 与 Streamable HTTP
早期规范提供 stdio 与 SSE 两种传输。2025 年的规范演进将 SSE 收敛为更简洁的 Streamable HTTP:客户端用普通 HTTP POST 发送请求,服务端可选择以 SSE 流回推通知与进度。对本地工具,stdio 仍是零依赖首选;对远端多租户场景,Streamable HTTP 更合适。
2.3 生命周期:初始化握手
每条连接都必须先完成 initialize 握手,协商协议版本与能力集,随后发送 initialized 通知才算就绪。握手阶段任何一方不支持的 capability 都会被静默降级,保证前向兼容。
三、四大核心原语
3.1 Tools:模型可主动调用的函数
Tools 是最常用原语,对应传统 Function Calling。每个 Tool 必须声明 name、description 与基于 JSON Schema 的 inputSchema。模型在合适时机决定调用,并由 Host 执行。
3.2 Resources:被动注入的上下文数据源
Resources 代表 "可被读取但模型不主动调用" 的内容,如文件、数据库行、日志片段。它们通过 URI 寻址(如 file:///app/config.json),由应用或用户在合适时机注入上下文。
3.3 Prompts:可复用的提示词模板
Prompts 是服务端预定义的交互模板,把常用工作流(如 "总结这段日志")固化为可参数化的提示,降低 Host 侧提示工程成本。
3.4 Sampling:服务端反向调用模型
Sampling 允许 Server 在处理请求时,反向请求 Host 侧的 LLM 完成一段推理(如让模型先把自然语言转成结构化查询)。这是 MCP 区别于普通工具调用最微妙的能力,也最需要安全护栏。
四、实战:用 TypeScript 构建文件系统 MCP Server
4.1 项目骨架
使用官方 SDK @modelcontextprotocol/sdk 与 zod 进行入参校验,几行代码即可启动一个 stdio Server:
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: "fs-mcp", version: "1.0.0" });
server.tool(
"read_file",
"读取指定路径的文本文件内容",
{ path: z.string().describe("文件绝对路径") },
async ({ path }) => {
const content = await Bun.file(path).text();
return { content: [{ type: "text", text: content }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);4.2 注册 Resources 与 Prompts
除 Tool 外,可顺手把配置文件暴露为 Resource,并提供一个总结模板 Prompt:
server.resource(
"app-config",
"file:///app/config.json",
async (uri) => ({
contents: [{ uri: uri.href, text: await Bun.file("/app/config.json").text() }],
})
);
server.prompt(
"summarize",
"将给定文本总结为三句话",
{ text: z.string() },
({ text }) => ({
messages: [{ role: "user", content: { type: "text", text: `总结:\n${text}` } }],
})
);4.3 在 Host 中连接并调用
Host 侧通过 Client 拉起子进程、握手、列工具并调用,全程类型安全:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({ command: "node", args: ["server.js"] });
const client = new Client({ name: "host", version: "1.0.0" });
await client.connect(transport);
const tools = await client.listTools();
const result = await client.callTool({
name: "read_file",
arguments: { path: "/etc/hostname" },
});五、实战:用 Rust 实现最小 MCP Server(裸 JSON-RPC)
当运行环境要求零依赖或极致性能时,可直接在 stdio 上实现 JSON-RPC 2.0,无需任何 SDK。核心是把每行标准输入当作一条请求,路由后回写结果:
use std::io::{self, BufRead, Write};
fn main() -> io::Result<()> {
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() { continue; }
let resp = handle(&line);
writeln!(stdout, "{}", resp)?;
stdout.flush()?;
}
Ok(())
}路由函数负责解析方法名并返回标准 JSON-RPC 响应;initialize 声明能力,tools/list 返回工具清单:
use serde_json::Value;
fn handle(req: &str) -> String {
let v: Value = serde_json::from_str(req).unwrap_or(Value::Null);
let id = v.get("id").cloned().unwrap_or(Value::Null);
let method = v.get("method").and_then(|m| m.as_str()).unwrap_or("");
let result = match method {
"initialize" => serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": { "name": "rust-mcp", "version": "0.1.0" }
}),
"tools/list" => serde_json::json!({
"tools": [{
"name": "echo",
"description": "回显输入字符串",
"inputSchema": {
"type": "object",
"properties": { "msg": { "type": "string" } },
"required": ["msg"]
}
}]
}),
_ => Value::Null,
};
serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": result }).to_string()
}六、传输与安全最佳实践
MCP Server 直接执行代码,安全是第一位。落地时务必遵循以下护栏:
- 最小权限:stdio Server 继承 Host 进程权限,应在沙箱或受限用户下运行,避免读取敏感路径。
- 输入校验:所有 Tool 参数用 Schema 严格校验,Rust 侧用类型系统、TypeScript 侧用 zod,杜绝命令注入与路径穿越。
- Sampling 闸门:若启用 Sampling,Host 必须限制可调用次数与 token 上限,防止 Server 滥用模型。
- 能力白名单:Host 在
initialize阶段只开启所需 capability,未声明的原语一律拒绝。 - 远程传输鉴权:Streamable HTTP 模式必须叠加 mTLS 或 OAuth,禁止匿名暴露工具端点。
七、结语
MCP 并非要取代 Function Calling,而是把它从 "每项目重写" 提升为 "协议级标准"。当你的 Agent 需要连接越来越多外部系统时,与其继续堆叠胶水代码,不如把能力封装成标准 MCP Server——它让模型侧获得可发现、类型安全、可插拔的工具生态,也让你的一次实现能在任意兼容 Host 中复用。下一篇我们将深入 MCP 的 OAuth 与远程多租户部署实战。

发表评论 取消回复