我想蹭一波 Trae 的流量,所以我写了关于 MCP 的这篇文章并起了这个标题...

309 阅读19分钟

大家好,我继续来进行一个落后于热点资讯的老生常谈。

用一句话来概括本文内容:本文介绍了关于 MCP 的前世今生,通过以某个 AI IDE 举例来叙述了 MCP 的背景、配置方案、如何实现一个简单的 MCP,同时,在结尾处简单叙述了 Agent 的背景故事与基础原理。

1、什么是 MCP?

MCP 是一种开放协议,它标准化了应用向 AI 应用提供上下文的方式。您可以将 MCP 视为 AI 应用的 USB-C 端口。正如 USB-C 提供了一种将设备连接到各种外围设备和配件的标准化方式一样,MCP 提供了一种将 AI 模型连接到不同数据源和工具的标准化方式。

简单的说,MCP 就像是一个万能接口,可以让 AI Client 调用外部本地的命令文件,或是服务器上的 API 服务,来拓展并增强 Client 的能力。

2、我该如何使用 MCP?

  1. 找到开源的 MCP 服务 / 自己编写一个
https://github.com/ferrislucas/iterm-mcp  
https://github.com/GLips/Figma-Context-MCP
  1. 配置网络 API 服务 / 本地脚本服务
pnpx figma-developer-mcp --figma-api-key=\<\your-figma-api-key\>\
  1. 在 Cursor 的设置中,找到 MCP 的配置并进行配置
"iterm-mcp": {  
    "command": "npx",  
    "args": ["-y", "iterm-mcp"]  
},  
"figma-context-mcp": {  
    "url": "http://localhost:3333/sse"  
}

万事俱备!

我们现在可以使用 MCP 来调用本地应用程序 / 网络服务中间件了

我们只需要在对话框中显式 / 隐式的调用 MCP 服务即可

例如:

image.png

image.png

我们也可以通过 github.com/modelcontex… 查看更多的 MCP 服务

3、我该如何写一个属于自己的 MCP?

无论营销号们将 MCP 吹的多么的上天,MCP 终究只是类似于 Schema、HTTPS、Type-C 等的一种协议。

也就是说,无论我们用任何语言,用任何方式,只要遵循 MCP 的规范,我们的应用就可以被叫做:(符合)MCP(规范的应用)

此章节会以一个简单的获取实时天气的 MCP 服务为例来实现一个 MCP 服务。

前置准备:彩云天气API高德地图API

前置知识储备

· MCP 使用 JSON-RPC 2.0 作为其传输格式。传输层负责将 MCP 协议消息转换为 JSON-RPC 格式进行传输,并将收到的 JSON-RPC 消息转换回 MCP 协议消息

· MCP 使用 Zod 进行严格的类型校验

· MCP 有两种调用方式:stdio | sse,stdio 使用标准输入 / 输出进行通信;sse 使用 Server-Sent Events 进行通信

stdio(standard input/output)

stdio(standard input/output)模式指的是: 你的程序作为子进程运行,通过标准输入(stdin)读取请求,通过标准输出(stdout)返回响应

Cursor(主进程)  
│  
├── 启动你的 CLI 工具 → [node index.js]  
│  
├── 写入 stdin(请求)  
│  
└── 读取 stdout(响应)

1· 完全本地运行(无需开启端口、没有防火墙/SSL问题)  
2· 安全、简单(不暴露服务,不占端口)  
3· 支持自动退出(工具执行完毕可自动关闭进程)  
SSE(Server-Sent Events)

SSE(Server-Sent Events)模式指的是: 你的程序作为本地 HTTP 服务运行,提供一个支持事件流(event-stream)的 `/sse` 路径,供 Cursor 建立长连接并通过 `/messages` 接收请求。

Cursor(主进程)  
│  
├── 发起 GET 请求连接你的服务:`http://localhost:9614/sse`  
│  
├── 发起 POST 请求传递消息:`/messages`  
│  
└── 等待服务端通过 SSE 推送响应

1· 适合流式推送(如对话工具、代码生成、实时结果)  
2· 支持并发连接,可服务多个客户端  
3· 更像真实服务,可由 Nginx 代理、部署在常驻进程中

如果我们想实现一个例如 PDF 解析、天气查询、RSS 抓取等服务的即用即走的插件,我们可以使用 stdio。

如果我们有可以常驻,或是启动过于缓慢,并且需要缓存的服务,例如图像处理、向量检索等需求,我们可以使用 sse。

我们本次以 stdio 的方式来实现一个简单的 MCP 服务。


我们通过大量的 mcp 官方库的方法来搭建我们的 MCP 应用。

  1. 设置应用参数
// index.ts

const argv = yargs(hideBin(process.argv))
  .option('amapToken', {
    type: 'string',
    describe: 'Amap token',
  })
  .option('caiyunToken', {
    type: 'string',
    describe: 'Caiyun token',
  })
  .help()
  .parseSync();
  1. 设置 MCP 应用的入口方法
// server.ts

this.server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_real_time_weather',
        description: '获取实时天气信息',
        inputSchema: {
          type: 'object',
          properties: {
            location: {
              type: 'string',
              description: '地理位置, 格式为 "天安门广场"',
            },
            region: {
              type: 'string',
              description: '地区, 格式为 "北京市"',
            },
          },
          required: ['location', 'region'],
        },
      },
    ],
  };
});
  1. MCP 相应方法的具体实现函数
// server.ts

const getLocation = () => {
  // 根据地区、关键词通过高德地图 API 获取经纬度
}

const getWeather = () => {
  // 根据经纬度通过彩云天气 API 获取实时天气
}

this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    if (request.params.name !== 'get_real_time_weather') {
      throw new Error(`Unsupported tool: ${request.params.name}`);
    }

    const args = request.params.arguments as {
      location: string;
      region: string;
    };

    if (!args.location) {
      throw new Error('Location is required');
    }

    if (!args.region) {
      throw new Error('Region is required');
    }
    const location = await this.getLocation(args.location, args.region);

    if (!location) {
      throw new Error('Location not found');
    }

    const weather = await this.getWeather(location);

    if (!weather || weather.error || weather.status !== 'ok') {
      throw new Error('Caiyun API error: ' + weather.error);
    }

    const { realtime } = weather.result;

    // 一些天气的常量枚举
    const weatherSkyConDesc = ...

    // 生成天气报告的模版字符串
    const weatherTemplate = ``;
    
    return {
      content: [
        {
          type: 'text',
          text: weatherTemplate,
        },
      ],
    };
  } catch (error) {
    throw new McpError(
      ErrorCode.InternalError,
      '[Error] failed to call tool: ' +
        (error instanceof Error ? error.message : String(error))
    );
  }
});
  1. 别的没了

你可能不敢相信,营销号口中的颠覆 AI Client 使用体验的全新 AI 能力竟然只需要这么点代码就可以了???

不 要 相 信 营 销 号

如你所见,如我上文所述。

MCP 只是一个协议,不管我的服务是什么样的(我可以去获取天气、我可以查询互联网内容、我可以通过 AI 优化我的内容的文采、我甚至可以通过 API 去操控我米家中的智能家居...),只要我遵循其结构,我就可以让 AI Client 来通过 MCP 协议来调用我的服务。

4、一些关于调试和发布的后话

调试

有很多种调试 MCP 服务的方式,大家可以自行搜索,此处以 @wong2/mcp-cli 为例

我们可以通过运行 npx trc 打包之后,通过以下代码来进行调试。

npx @wong2/mcp-cli node ./build/index.js --amapToken=xxx --caiyunToken=xxx

发布

我们可以在 AI Client 的设置中看到类似于

"iterm-mcp": {  
    "command": "npx",  
    "args": ["-y", "iterm-mcp"]  
}

的写法。

MCP 并没有一个类似于 Chrome Extension 或 Vscode Plugin 这样的公共的平台,以以上内容为例,我可以直接将我的 MCP 应用发布到 NPM、或是 Python 类似的其他的任何平台。我们只需要保证可以通过命令行调用即可。

5、总结

没啥可总结的。


正文内容到此为止,以下是关于 Agent 的介绍,有兴趣的可以看一看。

6、Agent 基本原理

起源

Agent 一词源自拉丁语 "Agere",意为“行动”或“执行(to do)”。在大语言模型(LLM)的语境中,Agent 可以被看作是一种智能体,它具备自主理解指令、规划决策并执行复杂任务的能力。

对于我们日常的使用场景而言,Agent 类似于私人秘书或助理。当我们设定了一个最终目标,例如:“请帮我查询一下我在 Github 上 Star 的仓库,并将其整理为一个表格,同时编写一个脚本,在这些仓库更新时通知我”,或“帮我创建一个使用 Next.js 和 Shadcn/UI 框架开发的个人网站,并部署在 Vercel 上”,Agent 能够自主地将这些复杂任务自动拆解成具体的子步骤,并逐步执行,直至实现我们的最终目标。


如何明确地定义 Agent?

人们对 Agent 的理解存在着一定差异:

有些观点认为 Agent 是完全自主运行的系统,比如《生化危机》中的“红皇后”,或者《钢铁侠》中的“贾维斯”,它们能够独立执行决策,主动解决问题。

另一些观点则认为 Agent 是一种严格遵循预定义流程的系统,更类似于以预先编写好的代码和流程来执行规范性任务的 Workflow。

然而,在 Anthropic 的定义中,Workflow 与 Agent 是有严格区分的两个概念:

  • Workflow:是通过 LLM 以及其他(第三方)工具,根据明确定义的代码或流程,执行特定任务的系统。Workflow 的执行路径通常是预先设计并严格限定的。

  • Agent:则具备自主规划、信息检索、工具调用及记忆能力的智能体,能够在无需人为干预的情况下独立完成任务。

因此,我们可以更清晰地界定:

Agent 是一种经过增强的大语言模型,它不仅能处理语言,还能自主地进行规划、信息检索、工具使用和任务记忆,从而自动、高效地完成复杂的任务目标。


Agent 概述

Agent 通常由以下几个关键部分组成:

LLM

Agent 都是由 LLM 来驱动的,LLM 作为 Agent 的大脑,加上一些辅助的关键组件组成了 Agent。

Planning System

Memory System

Tool Use


Planning
  • Planning System 会将大的任务分解成一些较小的、易于管理的小任务,从而更加高效的处理复杂任务。
  • Planning System 会进行自我反省,在错误中吸取教训来改正未来的工作。

Planning 是 Agent 系统的核心能力之一,它允许 Agent 将复杂任务分解为可管理的子任务,并制定执行策略。有效的规划系统使 Agent 能够处理超出简单指令响应范围的复杂、多步骤任务。

Planning 的核心原理是目标驱动与任务分解。

  1. 一切的出发点是一个明确的目标,例如用户输入:“帮我制定一个去上海玩的规划”。

「制定一个去上海玩的规划」就是一个明确的目标

  1. 有了目标之后 Planning 会将一个大目标拆分成多个子任务。

查询车票 -> 预定酒店 -> 安排每日节点 -> 准备物品清单

这个过程通常用到

ReAct 模式(Reasoning + Acting)

ReAct 的核心理念是在每一步中先 Reasoning,再 Acting,然后再继续 Reason。

思维链推理(Chain of Thought | CoT)

CoT 允许 Agent 将复杂推理过程一步步的进行推理,而不是直接给出答案。

树状规划(Tree of Thoughts)

树状规划扩展了 CoT,允许 Agent 同时展开多个思路,然后再沿着最优的路径继续探索。

举例说明

我们通过「写一份周报」这个任务来对比一下这三种方法的流程对比

用户说帮我写一份本周的周报,内容包括

  1. 本周完成的工作
  2. 下周的计划
  3. 可能存在的问题和风险

方法一:ReAct | 边想边做,边做边想

Thought: 我要写一份周报,先问用户要“本周完成的内容” Action: 向用户提问「请列出你本周完成的工作」 Observation: 用户回复了三项工作内容

Thought: 然后收集“下周计划” Action: 向用户提问「请列出你下周的工作计划」 Observation: 用户回复了四项计划

Thought: 然后收集“风险与问题” Action: 向用户提问「请列出目前存在的问题和风险」 Observation: 用户回复了两项风险

Thought: 收集完毕,生成周报正文 Action: 编写并格式化完整周报

方法二:CoT | 一步步逻辑推理 -> 输出结果

  1. 思考:用户需要一份周报。周报通常包括:完成内容、计划内容、问题与风险。
  2. 思考:我需要先收集用户这周做了什么。
  3. 思考:再问用户下周准备做什么。
  4. 思考:最后获取用户遇到的困难或风险。
  5. 整理所有信息后,拼接成标准格式周报。

方法三:ToT | 展开多个写法/结构方案 -> 选最优路径生成周报

第一阶段:思考写法风格 Thought A1: 我可以用正式企业风格 Thought A2: 我可以用轻松口语风格 Thought A3: 我可以用结构化 Markdown 风格

第二阶段:对每种风格写出“完成内容”段落 Thought A1 → 本周完成了以下三项核心任务... Thought A2 → 这周我干了三件事,还挺忙的哈哈... Thought A3 → ## 本周完成事项\n - 任务 1\n - 任务 2...

评分阶段:根据“格式整洁度 + 语气适配度”选择 A3 为最优

第三阶段:基于 A3 模板,继续展开“下周计划”、“问题风险”

最终输出:完整的 Markdown 风格周报

什么时候使用这三种方式?

先说结论:LLM 会根据用户的提问来动态的选择推理的方式

例如:以 Claude 为例

当我们输入「帮我写一篇关于 LLM 安全的论文摘要」

Claude 可能会

好的,我们可以先梳理思路:

  1. 明确什么是 LLM 安全
  2. 分析其风险来源(幻觉、有害内容、Prompt Injection 等)
  3. 概述现有的防护机制
  4. 给出未来研究方向 ... (最后生成摘要)

这是典型的 Chain of Thought。

当我们输入「帮我生成一个合适的商业计划书」

Claude 会:

  1. 首先提问:你打算做什么类型的业务?
  2. 根据你的回答,继续提问你要融资多少、主要竞争者是谁……
  3. 最后再输出完整商业计划书

这是一种「模拟 ReAct」的行为(虽然不显式“Action + Observation”,但过程上是 Reason → 用户反馈 → 再 Reason)。

Claude 会根据上下文自动生成最合适的推理路径,Claude 先进行任务识别,然后再根据以往的经验案例,最后在执行中随时修正推理路径。

例如: 假如我想用 Claude 完成一份周报,我输入的信息十分完整:

本周完成了 A/B/C,下周计划做 D/E/F,遇到的问题是 G/H。

这时 Claude 认为你已经给了所有的先决条件,所以通过纯粹的 CoT 来执行你的操作

假如我想用 Claude 完成一份项目总结,我直接说「给我写一份项目总结」

这时 Claude 判断你什么信息都没有给出,所以开始切换为 ReAct 风格的反问:「你的项目是什么样的?做了那些内容?」然后等你补充完成之后,再进行下一步的推理。

• ReAct 模式尤其适用于需要动态信息检索或实时反馈交互的任务; • CoT 更适合已明确信息且逻辑顺序较为简单的推理任务; • ToT 更适合多方案探索和优化、创造性的场景。


Memory

记忆系统分为两个部分:

短期记忆

短期记忆指的是 Agent 在执行单个任务或对话过程中临时存储的信息,类似人类大脑在短暂思考或进行简单推理时所用的记忆。

短期记忆通常通过 Sliding Window / 有限窗口 机制来实现,这个机制可以保证只保留最近的几个对话轮次中的内容,有时也用其他更灵活的方式来实现,例如 Stateful Cache / 状态缓存 等等

短期记忆用来确保 Agent 能够在单一对话中保持连贯性,也就是说,可以理解为短期记忆只用来储存较少的轮次的对话中的信息。例如智能客服中可以在短暂的时间内来保证对话连贯,或是在用户进行对话交互时记住用户刚刚提到的要求或者关键词。

长期记忆

长期记忆类似于知识库、经验库,可以让 Agent 根据以往的经验来做出更加合理的决策。

长期记忆通常通过 RAG(Retrieval-Augmented Generation)、向量数据库或知识图谱等技术实现,将非结构化数据(如文档、网页)或结构化数据(如用户偏好、历史交互记录)转化为可检索的知识形式,帮助 Agent 在未来的决策中参考以往的经验。

长期记忆可以让 Agent 根据以往的经验来做出更加合理的决策。

长期记忆和短期记忆在运行中的协作关系

以一个简单的对话为例:

用户对 Agent 提问“今天上海天气如何?”

(短期记忆记录):用户关注上海今天的天气 (读取长期记忆):用户的语言偏好、天气预报来源偏好等等

用户追问“适合穿什么衣服?”

(读取短期记忆):用户关心的是今天在上海适合穿什么衣服 (读取长期记忆):用户以往的穿衣风格

当用户持续使用一段时间后,Agent 的长期记忆的内容会越来越丰富,使得每一次交互都更加高效、智能。


Tool Use

Agent 的 Tool Use 是它实现“动手能力”的关键模块。它让语言模型不只是“在脑中想”,还能“用手去做”。比如调用搜索引擎、数据库、函数、插件、浏览器、API 等等,把语言思维落地为真实操作。

它的工作流程通常是

  1. 用户发出一个复杂请求
  2. Agent 判断是否需要调用工具
  3. Agent 构造一个调用指令(工具名称 + 参数)
  4. 中间系统(如插件机制、Tool Router)接收指令并执行
  5. 把执行结果返回给 LLM
  6. LLM 根据结果继续思考 / 执行下一步

人们通常通过 专门训练 / 微调 让 LLM 看例子,然后来让 LLM 知道对于什么样的场景要调用什么样的工具

类似于

用户:请告诉我今天北京的天气

助手: Thought: 我不知道当前天气,需要使用 weather API 查询 Action: get_weather("Beijing") Observation: 北京今天多云,温度 21°C Thought: 得到了结果,可以回答用户了 Answer: 北京今天是多云,气温在 21°C 左右。

Agent 要怎样调用各种工具?

Agent 会通过外部的 Tool Router、Function Handler、Plugin Runtime 等模块调用工具。

Tool Router

Tool Router 是一个判断语言调用意图,并分发任务的中间组件。

它负责

  1. 看懂 LLM 的输出
  2. 判断 LLM 要使用哪个工具
  3. 把指令交给服务去执行

比如说,我们举一个「请帮我在项目中初始化 tailwind」的例子,LLM 可能发出了这样的一些调用命令:

{
  "tool_call": {
    "name": "search",
    "arguments": {
      "query": "tailwind 初始化命令"
    }
  }
}

{
  "tool_call": {
    "name": "execute_terminal_command",
    "arguments": {
      "query": "npx tailwind init"
    }
  }
}

{
  "tool_call": {
    "name": "read_terminal",
  }
}

Tool Router 会判断 LLM 想要调用的是哪个工具,里面带有哪些参数,然后分发给相应的被绑定的 Search Handler

Function Handler

如果把 LLM 类比为司令,把 Tool Router 类比成参谋,Function Handler 就是下面的大头兵。

上文我们提到,Tool Router 会将相对应的任务分发给 Function Handler,Function Handler 就类似于一个函数

function search({ query }) {
  return realGoogleSearchAPI(query);
}

这样的一个函数,Function Handler 执行完之后再把结果返回给 Router,然后 Router 再把结果交给 LLM。

Plugin Runtime

类似于我们会将服务运行在 Docker 中,Agent 的 Function Handler 也是被放在类似于 Docker 的 Plugin Runtime 中,Plugin Runtime 为 Agent 提供安全、受控的隔离执行环境(如容器或沙箱),管理插件生命周期,执行 Function Handler,并处理安全权限控制、资源限制、上下文传递等功能。

MCP

MCP(Model Context Protocol)是一种可以让 LLM 与外界系统进行通信的协议。

在实际使用中,人们经常用『MCP 插件』来泛指「遵循 MCP 协议的服务模块」,例如「支付宝 MCP」「高德地图 MCP」,本质上是指这些服务作为 Agent 插件,按照 MCP 协议暴露了可调用的方法。

以 Cursor 为例,我们可以在其 mcp.json 配置文件中添加自定义的 MCP 服务。Cursor 会将这些服务所暴露的方法统一注册到其内部的函数调用表中,作为可供 Agent 调用的工具。这些方法本质上就是 MCP 协议定义的接口,实现了对应的 Function Handler,并由 Tool Router 动态路由和触发调用。

注意!LLM 的调用并不完全遵循 MCP 协议,MCP 协议主要是为了解决不同厂商的 LLM 之间的协议不互通的问题,也就是为了让第三方服务可以通过一套方案来适配不同的 LLM。类似 ChatGPT 和 Claude,在其 LLM 内部都有属于自家厂商的私有协议。拿 search 这个动作来举例,LLM 可能通过私有协议调用 LLM 本身的网络搜索能力,也可能通过 MCP 来调用第三方插件来获取更加强大更具有拓展性的搜索。

使用哪一种方案取决于 Router 调度系统的决策。比如模型能力优先、插件优先、条件分发、混合对比模式。

MCP 的大致结构

  1. 请求(Requests) 请求消息用于从客户端向服务器发起操作,或者从服务器向客户端发起操作。

请求消息的结构如下:

{
  "jsonrpc": "2.0",
  "id": "string | number",
  "method": "string",
  "params": {
    "[key: string]": "unknown"
  }
}

jsonrpc:协议版本,固定为"2.0"。 id:请求的唯一标识符,可以是字符串或数字。 method:要调用的方法名称,是一个字符串。 params:方法的参数,是一个可选的键值对对象,其中键是字符串,值可以是任意类型。

  1. 响应(Responses) 响应消息是对请求的答复,从服务器发送到客户端,或者从客户端发送到服务器。

响应消息的结构如下:

{
  "jsonrpc": "2.0",
  "id": "string | number",
  "result": {
    "[key: string]": "unknown"
  },
  "error": {
    "code": "number",
    "message": "string",
    "data": "unknown"
  }
}

jsonrpc:协议版本,固定为"2.0"。 id:与请求中的 id 相对应,用于标识响应所对应的请求。 result:如果请求成功,result 字段包含操作的结果,是一个键值对对象。 error:如果请求失败,error 字段包含错误信息,其中: code:错误代码,是一个数字。 message:错误描述,是一个字符串。 data:可选的附加错误信息,可以是任意类型。

  1. 通知(Notifications) 通知消息是一种单向消息,不需要接收方回复。

通知消息的结构如下:

{
  "jsonrpc": "2.0",
  "method": "string",
  "params": {
    "[key: string]": "unknown"
  }
}

jsonrpc:协议版本,固定为"2.0"。 method:要调用的方法名称,是一个字符串。 params:方法的参数,是一个可选的键值对对象,其中键是字符串,值可以是任意类型。