「JS全栈AI Agent学习」四、MCP:给AI工具世界造一个USB接口

0 阅读11分钟

📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。 ⏱️ 预计阅读时间:15 分钟 📖 原书地址adp.xindoo.xyz 前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~


🗺️ 系列导航

主题状态
第一篇提示链 · 路由 · 并行化
第二篇反思 · 工具使用 · 规划
第三篇多智能体 · 记忆管理 · 学习适应
本篇MCP 协议

前言

我有一个 my-resume 项目——最初只是一个静态展示页面,放了简历、项目经历、技术栈。 后来想把它改造成真正的全栈项目:NestJS 后端 + 数据库 + 前端交互 + AI 能力 + 部署上线,三端齐备,一条龙。

在这个改造过程中,我开始认真思考 AI 能力怎么集成进来。

第一步,我给项目写了一个读 PDF 的 Tool——用户上传简历 PDF,AI 解析内容,然后做各种分析。 写完挺好用的,但写完之后我意识到一个问题:

这个 Tool,只能在我自己的项目里用。

换个框架要重写,换个 AI 平台要重写 …… 这个问题,就是这篇文章的起点。


一、从一个 PDF Tool 说起

我在 LangChain.js 里注册了一个 PDF 阅读工具,大概长这样:

const pdfTool = new DynamicTool({
  name: "read_pdf",
  description: "读取PDF文件并转换为文本,保留原始布局和排版结构。当任务涉及阅读、解析PDF文件内容时触发,仅上传文件不触发。",
  func: async (filePath: string) => {
    return await extractPdfText(filePath);
  }
});

Agent 靠 description 判断要不要调用这个工具——语义匹配,不是关键词匹配。

这里有个细节值得单独说:description 写得好不好,直接决定 Agent 调用的精准度

写不好 description 的时候,不妨问自己:如果这是一份需求文档,自己刚拿到知道什么时候该用这个接口吗?

工具本身没问题。但问题来了——

假设不只是我自己用,而是 100 个开发者都需要这个 PDF Tool。 假设不只是 LangChain.js,还要支持 Claude、GPT-4、自己搭的 Agent…… 每个人都要复制代码、手动注册、适配不同框架——

这个成本,随着工具数量和使用方数量的增加,会指数级爆炸。


二、VSCode 插件的类比——以及它哪里不够用

第一反应是:这不就像 VSCode Extensions 吗?

VSCode 插件市场解决了类似的问题:

  • 开发者按规范写插件,发布到市场
  • 用户订阅安装,开箱即用
  • 统一管理,不用到处复制代码

这个思路是对的。规范 + 发布 + 订阅 + 协作,MCP 都有。

但我多想了一秒,感觉哪里不太对——

VSCode 插件是这样工作的:

插件开发者 → 发布到 Marketplace → 用户安装 → VSCode 加载插件

插件运行在哪里?运行在 VSCode 里。VSCode 是唯一的宿主。

但 AI Tool 的场景完全不同:

你的 PDF Tool,可能被 Claude 调用,可能被 GPT-4 调用,可能被你自己用 LangChain.js 写的 Agent 调用。 这三个"宿主",是三个完全不同的系统。

所以需要的不是"插件市场",而是一个跨系统的通信协议

VSCode ExtensionsMCP
解决什么如何扩展 VSCode 的功能如何让任何 Agent 调用任何工具
宿主只有 VSCodeClaude / GPT / LangChain / 任何框架
本质插件规范通信协议
类比乐高积木的形状规范USB 接口标准

USB 出现之前:鼠标有鼠标接口,键盘有键盘接口,打印机有打印机接口。 USB 出现之后:一个接口,接任何设备。

MCP 就是 AI 工具世界的 USB。


三、MCP 是什么

MCP(Model Context Protocol,模型上下文协议),是 Anthropic 在 2024 年底提出并开源的一个开放标准。

它要解决的问题,用一张图说清楚:

MCP 出现之前:
─────────────────────────────────────────
PDF Tool (Claude版)    ──→ Claude
PDF Tool (OpenAI版)    ──→ GPT-4
PDF Tool (LangChain版) ──→ 你的 Agent
同一个功能,写三遍

MCP 出现之后:
─────────────────────────────────────────
                       ──→ Claude
PDF Tool (MCP版) ────────→ GPT-4
                       ──→ 你的 Agent
写一次,到处用

一句话定义:MCP 是 AI 工具世界的 USB 接口标准——工具只写一次,任何兼容 MCP 的 Agent 都能调用。


四、MCP 协议定义了什么

既然是"通信协议",那它需要定义哪些东西,两端才能"说上话"?

类比 HTTP 协议:它定义了请求方法(GET/POST)、状态码(200/404)、Header 格式……

MCP 对应地定义了四件事:

你的直觉推导MCP 里的概念说明
支持哪些平台/连接方式Transport 层定义通信方式,解决"怎么连上"
名称和描述Tool Definitionname + description,Agent 靠这个决定要不要调用
怎么使用(说明书)Input Schema用 JSON Schema 定义入参,Agent 知道要传什么
备注/版本/返回格式Output Schema / Metadata返回值格式、版本号、错误码定义

真实的 MCP Tool 定义长这样:

{
  name: "read_pdf",
  description: "读取PDF文件并转换为文本,保留原始布局和排版结构",
  inputSchema: {
    type: "object",
    properties: {
      filePath: {
        type: "string",
        description: "PDF文件的路径"
      },
      pageRange: {
        type: "string",
        description: "可选,指定页码范围,如 '1-5'"
      }
    },
    required: ["filePath"]
  }
}

看到这个结构,有没有觉得很眼熟?

这和写 TypeScript 函数签名,本质上是同一件事:

// TypeScript 函数签名
function readPdf(filePath: string, pageRange?: string): string { ... }

// MCP inputSchema = 把函数签名用 JSON 描述出来,让 Agent 能"读懂"

MCP 的 inputSchema,就是把函数类型定义翻译成 Agent 可以理解的格式。


五、MCP 架构:三个角色

MCP 基于客户端-服务器架构,有三个核心角色:

你的 Agent(MCP Client)
      │
      │  ① 发现:这里有哪些工具?
      ▼
 MCP Server(工具提供方)
      │
      │  ② 返回:工具列表 + 每个工具的 Schema
      ▼
你的 Agent 决策:
  "这个任务需要用 read_pdf"
      │
      │  ③ 调用:传入参数
      ▼
 MCP Server 执行工具
      │
      │  ④ 返回结果
      ▼
Agent 继续处理
角色是什么类比
MCP Client你的 Agent,发起调用方浏览器
MCP Server工具提供方,暴露工具能力Web 服务器
Transport 层两者之间的通信方式HTTP / WebSocket

这里有一个值得单独说的设计细节:

第 ① 步是"发现",不是"被告知"。

传统的 Tool Use,是你在代码里明确告诉 Agent:"你有这些工具"——静态注册,写死在代码里。

MCP 的方式是 Agent 主动去问 Server:"你有什么工具?"——动态发现,运行时查询。

这个差异的实际意义是:MCP Server 可以独立部署、独立更新,Agent 不需要改一行代码,就能感知到工具的变化。工具加了新功能、下线了旧接口,Agent 侧完全无感。


六、Transport 层:同步等待,还是流式返回?

三个角色清楚了,还有一个问题没解决:Agent 调用工具,该怎么等结果?

读 PDF 可能要几秒,查数据库可能要几百毫秒,调用外部 API 可能更慢。 如果 Agent 傻等,整个系统就卡住了。

最理想的设计是:既能根据情况异步请求,也能支持同步读取,最后统一输出。

这在计算机里有个专门的名字:流式响应(Streaming)

MCP 定义了两种标准传输方式:

方式一:stdio(标准输入输出)
─────────────────────────────────────────
Agent ──写入 stdin──▶ MCP Server
Agent ◀──读取 stdout── MCP Server

适合:本地工具,同一台机器上运行
类比:命令行管道  ls | grep pdf


方式二:HTTP + SSE(Server-Sent Events)
─────────────────────────────────────────
Agent ──HTTP POST──▶ MCP Server
Agent ◀──SSE 流式──── MCP Server

适合:远程工具,跨网络调用
类比:流式 AI 回复

SSE 的工作方式,画出来是这样的:

Agent 发出请求 ──────────────────────────▶ MCP Server
                                               │
                                          开始执行工具
                                               │
                ◀── 流式返回(边处理边推送)──── │
                ◀── 流式返回 ────────────────── │
                ◀── 流式返回 ────────────────── │
                ◀── [DONE] ──────────────────── │

Agent 不需要傻等,数据来一点处理一点,最后统一完成

做过前端 AI 应用的同学,这个原理一眼就熟——

你在页面上做的流式渲染 AI 回复,用的就是同一套机制: ReadableStream → 一块一块读 → 渲染到页面

MCP 的 SSE 传输,和你前端写的流式 AI 回复,本质上是同一件事。


七、动手:把 PDF Tool 升级成 MCP Server

理论讲完,直接上代码。

my-resume 项目里的 PDF Tool,改造成一个标准的 MCP Server:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 创建 MCP Server 实例
const server = new Server({
  name: "resume-tools",   // 工具集名称
  version: "1.0.0",       // 版本号
});

// ① 声明工具列表
// Agent 来问"你有什么工具"时,返回这个
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "read_pdf",
      description:
        "读取PDF文件并转换为文本,保留原始布局和排版结构。" +
        "当任务涉及阅读、解析PDF内容时触发,仅上传文件不触发。",
      inputSchema: {
        type: "object",
        properties: {
          filePath: {
            type: "string",
            description: "PDF文件路径",
          },
          pageRange: {
            type: "string",
            description: "可选,页码范围,如 '1-5'",
          },
        },
        required: ["filePath"],
      },
    },
  ],
}));

// ② 处理工具调用
// Agent 决定调用某个工具时,走这里
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "read_pdf") {
    const { filePath, pageRange } = request.params.arguments as {
      filePath: string;
      pageRange?: string;
    };

    const result = await extractPdfText(filePath, pageRange);

    return {
      content: [{ type: "text", text: result }],
    };
  }

  throw new Error(`未知工具: ${request.params.name}`);
});

// ③ 启动(stdio 模式,本地调用)
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("MCP Server 已启动,等待 Agent 连接...");

看这个结构,有没有觉得很像你写 NestJS Controller

NestJS:  @Controller@Get/@Post         → 处理请求 → 返回响应
MCP:      Server     → ListTools/CallTool → 处理请求 → 返回结果

这个类比不是巧合——两者解决的是同一类问题:定义接口、处理请求、返回结果。只是服务的对象从"HTTP 客户端"变成了"AI Agent"。

关于 my-resume 项目的思考

my-resume 的全栈改造里,MCP 的三个角色可以这样对应:

MCP Server = 我写的工具层(NestJS 后端)
  → read_pdf:解析用户上传的简历 PDF
  → get_resume_data:从数据库读取结构化简历数据
  → search_projects:搜索项目经历

MCP Client = 调用工具的 Agent(前端发起,后端编排)
  → 接收用户指令:"帮我优化这段工作经历"
  → 决策:先调用 get_resume_data 拿到原始数据
  → 再交给 LLM 处理,返回优化建议

Transport = stdio(本地)或 HTTP+SSE(部署后远程调用)

Server 提供能力,Client 使用能力,Transport 是中间的管道。

这个分层思路,和 NestJS 的 Controller / Service / Repository 分层,逻辑上是一脉相承的。


八、MCP vs 工具函数调用:别混淆

学到这里,有一个容易混淆的地方值得单独说清楚。

MCP 和 LangChain 里的 Tool Use(工具函数调用)看起来很像,但有本质区别:

特性工具函数调用(Tool Use)MCP
标准化各平台专有,格式不统一开放标准,跨平台互通
工具发现你明确告诉 Agent 有哪些工具Agent 主动查询,动态发现
可重用性与特定应用/框架耦合独立部署,任何兼容方都能用
架构一对一(LLM ↔ 工具)客户端-服务器(多对多)

一句话区分:

工具函数调用 = 给 AI 一套专用工具箱,工具是定制的,只能在这个项目里用。

MCP = 造一个标准插座,任何符合规格的工具都能插进来,任何兼容的 Agent 都能用。


九、核心洞察

洞察一句话
MCP 是什么AI 工具世界的 USB 接口标准
解决什么问题工具只写一次,任何 Agent 都能用
三个角色Client(Agent)· Server(工具)· Transport(通道)
两种传输stdio(本地)· HTTP+SSE(远程+流式)
和 VSCode 插件的本质区别插件规范 vs 跨系统通信协议
和 NestJS 的类比Controller/Service 分层 ≈ Server/Handler 分层
你已经在用的类似概念流式 AI 回复的 ReadableStream
动态发现 vs 静态注册MCP 让 Agent 主动问"你有什么",而不是被动被告知

结语

MCP 这章,我觉得是目前为止最"工程感"的一章。

它不是一个新的 AI 能力,而是一个工程规范——解决的是"怎么让 AI 能力可复用、可组合、可跨平台"这个问题。

对于 my-resume 全栈改造来说,这章给了我一个很清晰的架构思路:

不要把 AI 工具写死在业务代码里。把它们抽成 MCP Server,独立部署,独立维护。 今天接 Claude,明天换 GPT-4,后天自己搭 Agent——工具层一行代码不用改。

这个思路,和后端开发里"接口与实现分离"是同一个道理。只不过现在,接口的调用方从"前端页面"变成了"AI Agent"。

学到这里,越来越觉得:AI 工程和软件工程,底层是同一套思维。 分层、解耦、标准化——这些事工程师早就在做了,只不过现在的场景换了。


💬 系列地址:持续更新中

📖 原书地址adp.xindoo.xyz

🛠️ 实战项目:my-resume(静态页面 → NestJS + 数据库 + AI + 部署上线,进行中)

如果这篇对你有帮助,欢迎点赞收藏,我们下篇见 👋