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 把这套约定标准化为三层收益:

  1. 协议层统一:工具描述、参数 Schema、返回结构全部走 JSON Schema,模型无需关心后端是 Python 服务还是本地二进制。
  2. 传输层可插拔:同一份 Server 逻辑既能通过 stdio 作为本地子进程运行,也能通过 Streamable HTTP 作为远程服务暴露,Host 侧零改动。
  3. 能力可发现:模型在运行时通过 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 直接执行代码,安全是第一位。落地时务必遵循以下护栏:

  1. 最小权限:stdio Server 继承 Host 进程权限,应在沙箱或受限用户下运行,避免读取敏感路径。
  2. 输入校验:所有 Tool 参数用 Schema 严格校验,Rust 侧用类型系统、TypeScript 侧用 zod,杜绝命令注入与路径穿越。
  3. Sampling 闸门:若启用 Sampling,Host 必须限制可调用次数与 token 上限,防止 Server 滥用模型。
  4. 能力白名单:Host 在 initialize 阶段只开启所需 capability,未声明的原语一律拒绝。
  5. 远程传输鉴权:Streamable HTTP 模式必须叠加 mTLS 或 OAuth,禁止匿名暴露工具端点。

七、结语

MCP 并非要取代 Function Calling,而是把它从 "每项目重写" 提升为 "协议级标准"。当你的 Agent 需要连接越来越多外部系统时,与其继续堆叠胶水代码,不如把能力封装成标准 MCP Server——它让模型侧获得可发现、类型安全、可插拔的工具生态,也让你的一次实现能在任意兼容 Host 中复用。下一篇我们将深入 MCP 的 OAuth 与远程多租户部署实战。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部