1篇文章全面读懂MCP

8k words

1篇文章全面读懂MCP

MCP核心概念

简单来说,模型上下文协议 (Model Context Protocol, MCP) 是一个开放标准,它解决了 AI 模型的一个核心痛点:“手够不着数据”

在没有 MCP 之前,AI 就像是一个博学但被关在密室里的学者。如果你想让它看你的 GitHub 代码或 Notion 笔记,开发者必须为每个应用手动编写“翻译代码”。MCP 的出现,就像是为 AI 领域统一了 USB 接口 🔌。

为了彻底理解 MCP,我们可以把它的基本概念拆解为两个核心维度:角色能力

1. 参与的角色 (Roles) 👥

在 MCP 的对话中,通常有三个角色在协作:

  • 宿主 (Host) 🏠:AI 运行的载体(例如:Claude Desktop、IDE 插件)。它是发起请求的“大脑”。

  • 服务器 (Server) 🛠️:专门负责连接特定数据源的小程序(例如:一个专门连接 Google Drive 的 Server)。它负责把复杂的数据翻译成 AI 能懂的语言。

  • 客户端 (Client) 🤝:存在于 Host 内部,负责与 Server 维持连接并交换信息。

2. 交互的能力 (Capabilities) ⚡

Server 告诉 Host 它能提供什么,主要分为三类:

  • 资源 (Resources) 📚:AI 可以读取的“参考资料”(如:本地文件、数据库记录)。

  • 工具 (Tools) 🛠️:AI 可以执行的“动作”(如:发送一封邮件、运行一段 Python 代码)。

  • 提示词 (Prompts) 📝:预设好的“工作流模板”(如:一个专门用于“代码审查”的对话开头)。

MCP的价值

我们来详细对比一下 “N × M”“N + M” 两种模式的差异,看看 MCP 是如何重塑开发成本的。

1. 传统集成的“蜘蛛网”困局 (N × M) 🕸️

在 MCP 出现之前,如果你有 $N$ 个 AI 应用(比如 Claude, ChatGPT, Cursor)和 $M$ 个数据源(比如 GitHub, Slack, Google Drive),由于每个应用和每个数据源的接口(API)都不一样,你必须为每一对组合编写特定的集成代码。

  • 连接数量:总共需要进行 $N \times M$ 次集成。

  • 维护负担:如果 GitHub 的 API 更新了,这 $N$ 个应用的开发者都必须各自修改代码。

  • 开发壁垒:小型开发者很难让自己的工具被所有主流 AI 助手支持。

2. MCP 的“万能接口”模式 (N + M) 🔌

MCP 引入了一个中间标准。应用开发者只需实现一个 MCP Client,而工具开发者只需实现一个 MCP Server

  • 连接数量:总共只需要 $N + M$ 次实现。

  • 成本骤降:每增加一个工具,所有支持 MCP 的应用都能瞬间使用它,无需额外开发。

  • 解耦:应用不再关心数据是怎么来的,Server 也不再关心是谁在调用它。

3. 成本差异对比表 📊

维度 传统集成方式 (API-Specific) MCP 集成方式 (Standardized)
开发工作量 🏗️ 指数级增长 ($N \times M$) 线性增长 ($N + M$)
更新响应 🔄 慢。每个集成点都需要手动更新。 快。只需更新一次 Server 即可同步。
生态互通性 🌐 封闭。每个应用都是信息孤岛。 开放。任何 Server 都能即插即用。
准入门槛 🚪 高。需要深厚的 API 调用经验。 低。遵循协议即可快速接入。

核心组成部分

该协议主要涉及三个角色:

  • Host (宿主) 🏠:用户使用的 AI 界面或应用程序(例如 Claude Desktop 或开发工具)。

  • Server (服务器) 🛠️:小型程序,专门负责连接特定的数据源(如 Google Drive)或提供特定功能(如搜索网络)。

  • Client (客户端) 🤝:在 Host 内部运行,负责与 Server 建立连接并遵循协议进行通信。

架构与通信流程

MCP 的核心在于它如何让不同的软件系统“听懂”彼此。它采用了经典的 客户端-服务器 (Client-Server) 架构,但针对 AI 场景做了优化。

1. 通信的“语言”:JSON-RPC 🤖

MCP 并不发明复杂的底层协议,而是使用了一种轻量级的远程过程调用协议 —— JSON-RPC 2.0

  • 请求 (Request):Host 发出指令(例如:“请列出这个文件夹里的文件”)。

  • 响应 (Response):Server 返回结果(例如:“文件列表如下:…”)。

  • 通知 (Notification):单向信息传递,不需要对方回信。

2. 连接的“管道”:传输层 (Transport) 🛣️

为了让数据动起来,MCP 支持多种传输方式,最常见的有两种:

  • Stdio (标准输入输出):最简单的方式,Server 作为一个本地进程运行,通过命令行流进行通信。

  • HTTP/SSE (服务器发送事件):允许 Host 与远程服务器通信,适合云端集成。

3. 通信流程示例 🔄

当你在 AI 界面(Host)输入一个指令时,内部发生了什么?

  1. 初始化 (Initialize):Client 与 Server 握手,交换各自支持的能力和版本信息。

  2. 列表请求 (List):Host 询问 Server:“你有哪些资源或工具可用?”

  3. 执行操作 (Call):当你要求 AI 处理数据时,Host 通过 Client 调用 Server 的特定 工具 (Tool)

核心能力组件:资源 (Resources)和 工具 (Tools)

理解 资源 (Resources)工具 (Tools) 的区别,就像是理解“只读文档”和“可执行脚本”的区别。

为了让你更直观地理解,我们可以把 AI 想象成一个在办公室工作的助理

1. 资源 (Resources) 📚 —— 助理的“参考资料”

资源是只读的信息。Server 把数据“暴露”给 AI,AI 可以读取这些内容作为上下文,但不能通过这个接口修改原始数据

  • 特点:被动、安全、信息导向。

  • 具体场景

    • 本地文档:AI 读取你的 README.md 文件来理解项目。

    • 数据库快照:AI 查看昨天的销售数据图表。

    • 日志文件:AI 读取服务器错误日志进行分析。

  • 类比:助理去档案室查阅卷宗,他只能看,不能在卷宗上涂改。

2. 工具 (Tools) 🛠️ —— 助理的“行动能力”

工具是可执行的。它们允许 AI 触发某些动作,这些动作往往会改变现实世界的状态或产生新的结果。

  • 特点:主动、有副作用(Side Effects)、动作导向。

  • 具体场景

    • 文件操作:AI 不仅看文档,还要“创建一个新的 .txt 文件”。

    • 发送消息:AI 调用 Slack 接口发一条通知给你的同事。

    • 代码执行:AI 运行一段 Python 脚本来计算复杂的数学题。

  • 类比:助理拿起电话拨号,或者操作打印机打出一份新报表。


场景对比:处理一份财务报表

你可以看看下表,对比一下在同一个任务中,两者是如何协作的:

动作类型 它是 Resource 还是 Tool? 为什么?
读取去年的财务 PDF Resource 📚 AI 只是获取信息作为参考。
搜索数据库中的特定交易 Tool 🛠️ 搜索通常需要带参数的函数调用。
生成本月的支出摘要文件 Tool 🛠️ 这是一个“写”的操作,改变了文件系统。

安全性与权限控制

这是一个非常关键的问题!当 AI 获得“行动能力”时,安全性就成了 MCP 协议设计的重中之重。

MCP 并不是给 AI 一把“万能钥匙”,而是通过多层防御机制来确保它在一个受控的安全屋里工作。

1. 最小权限原则 (Least Privilege) 🛡️

在 MCP 架构中,AI 模型本身无法直接访问你的硬盘。它必须通过 Server 才能操作。

  • 物理隔离:如果你启动一个“文档读取 Server”,你可以在配置中指定它只能访问 C:\Users\Documents\ProjectA 这一个文件夹。

  • 功能受限:如果 Server 的代码里没有写“删除文件”的函数,模型无论怎么下指令,Server 都不可能执行删除操作。

2. 人类在回路中 (Human-in-the-loop) 👤

这是最重要的一道防线。大多数集成了 MCP 的 Host(如 Claude Desktop)都会实施“确认机制”:

  • 敏感操作拦截:当 AI 尝试调用一个具有“副作用”的 Tool(比如修改文件、发送邮件、删除数据)时,Host 会弹出一个对话框。

  • 人工审核:你会看到一条提示:“AI 想要运行 delete_file(path='important_data.db'),是否允许?”只有你点击“允许”,指令才会真正发给 Server。

3. 环境隔离 (Sandboxing) 🏗️

对于高风险的任务(比如运行 Python 代码),Server 通常会运行在沙箱环境(如 Docker 容器)中。

  • 即使 AI 运行了一段恶意代码,它也只能破坏沙箱内部的虚拟环境,无法穿透到你的真实操作系统。

我们可以从这三个层面来看安全防护:

防护层级 负责对象 作用
配置层 ⚙️ 用户/开发者 限制 Server 能看到的文件夹和数据库范围。
协议层 📝 MCP 规范 定义了 Resource 是只读的,Tool 需要明确声明。
交互层 👆 Host (宿主) 在 AI 执行危险动作前,请求人类确认。

为了深入理解,我们可以假设一个具体的危险场景。如果你正在使用一个可以执行终端命令的 Tool,你觉得哪种防护方式最能让你安心?

  1. 完全禁止:禁止 AI 使用任何可能删改文件的命令。

  2. 严格审批:每一条命令执行前,你都要亲自看一遍代码。

  3. 虚拟化隔离:让 AI 在一个完全独立的云端虚拟机里折腾,随便它怎么删。

使用MCP tokens消耗快问题

调用 MCP (Model Context Protocol) 比普通对话更消耗 Token。

简单来说,这是因为为了让 AI 能够“聪明地”使用外部工具,Host(宿主程序)在后台做了大量模型感知不到的“脏活累活”。

我们可以从以下三个维度来拆解这些 Token 都花在哪了:

1. 庞大的“说明书” (System Prompt Overheads) 📜

每当你连接一个 MCP Server,Host 都会把该 Server 提供的所有 ToolsResources 的定义注入到上下文(Context)中。

  • 不仅仅是名字:为了让 AI 知道什么时候该调用 search_database,Server 必须提供详细的 JSON Schema 描述(包括参数类型、每个参数的含义、返回值的格式等)。

  • 累加效应:如果你连接了 5 个 Server,每个 Server 有 10 个工具,即使你还没开始提问,系统提示词里可能就已经塞进了几千个 Token 的“说明书”。

2. 多轮交互的“往返票” (The Multi-turn Loop) 🔄

MCP 的工作逻辑通常是多轮对话,每一轮都会产生新的 Token 开销:

  1. 第一轮:用户提问 $\rightarrow$ 模型推理后决定调用工具(输出 call_tool 指令)。

  2. 第二轮:Host 执行工具 $\rightarrow$ 将厚厚的执行结果(可能是几百行日志或数据)塞回上下文。

  3. 第三轮:模型阅读结果 $\rightarrow$ 总结陈词或决定调用下一个工具。

    由于 LLM 的 Context 是累积计费的,第二轮交互时,第一轮的提示词和工具定义会再次被计算一次。

3. 冗余的上下文注入 (Context Stuffing) 📥

为了确保 AI 拥有“长久记忆”,有些 MCP 客户端会将 Resources(如整个文档的内容)直接作为上下文注入。

  • 如果你让 AI 总结一个 50KB 的本地文档,这 50KB 的内容会被完整转化为 Token 发送给模型。

Token 消耗对比表

交互方式 Token 消耗主要来源 消耗程度
普通闲聊 💬 仅用户输入 + 模型回复 最低
单工具调用 🛠️ 工具定义 + 调用指令 + 执行结果 中等
复杂链式调用 🔗 多个工具定义 + 多次往返上下文累积 最高

有以下两个方向的优化方法:

  1. 开发侧优化:作为 Server 开发者,精简工具描述或利用 Prompt Templates 来节省空间

  2. 使用侧技巧:作为用户,通过配置(如控制同时挂载的 Server 数量)来降低成本

减少tokens消耗的解决方案

开发侧优化

我们可以通过一个实际的文件管理场景,来彻底理清这两者的边界。

1. 资源 (Resources) 📚 —— 数据的“只读视图”

资源是 Server 想要分享给 AI 的静态信息。你可以把它想象成 AI 能够翻阅的“参考书”。

  • 场景示例:你有一个包含 100 个笔记的文件夹。

  • 作为资源:Server 会提供一个 URI(统一资源标识符),比如 mcp://notes/travel-plan.md。AI 只能读取这个文件的内容。

  • 核心逻辑:AI 获取了信息,但它对外部世界(你的硬盘)没有产生任何物理上的改观。

2. 工具 (Tools) 🛠️ —— 数据的“操作杆”

工具是可执行的代码函数。它允许 AI “走出”对话框,去真实地修改数据或触发程序。

  • 场景示例:你不仅想让 AI 读笔记,还想让它帮你“删除重复的笔记”。

  • 作为工具:Server 会提供一个名为 delete_note(note_id) 的函数。当 AI 决定调用它时,Server 就会执行一段删除代码。

  • 核心逻辑:这产生了“副作用 (Side Effects)”,外部世界的状态发生了变化。


对比总结表

维度 资源 (Resources) 📚 工具 (Tools) 🛠️
交互性质 被动、只读 主动、可写/可执行
安全性 极高(AI 只能看) 需谨慎(AI 可能会删改)
典型用途 查阅文档、读取配置、查看状态 发送邮件、执行脚本、更新数据库

使用侧优化

作为用户,你虽然不能直接修改 Server 的代码,但你是 上下文窗口 (Context Window) 的主宰。通过合理的配置和习惯,你可以显著降低不必要的 Token 消耗。

我们可以从以下三个层面进行优化:

1. 动态挂载策略 (Selective Mounting) 🔌

不要把所有的 MCP Server 都“一脑儿”全开着。

  • 按需启动:如果你今天只是写代码,就只连接 githubfilesystem Server。关掉 google-mapsslack Server。

  • 多配置文件:你可以为不同的任务场景准备不同的配置文件(例如 claude_desktop_config.json 的备份)。一个用于“深度研究”,一个用于“快速办公”。

  • 核心逻辑:减少挂载数量直接减少了注入到 System Prompt 中的“工具说明书”体积。

2. 限制资源范围 (Scoping Resources) 📂

在配置 Server 时,尽量精确。

  • 路径精简化:如果 filesystem Server 支持配置根目录,不要直接给它整个 C:\ 盘或 ~/ 目录。只给它当前项目的文件夹。

  • 减少深度:有些 Server 会尝试扫描子目录。通过配置限制扫描深度,可以防止它一次性拉取成千上万个文件名作为上下文。

3. 提示词工程 (Query Strategy) 🎯

改变你和 AI 的交互方式:

  • 明确指定工具:在对话中直接告诉 AI:“请使用 search_files 查找,不要读取所有内容。”

  • 分阶段执行:不要问“分析这个项目并写个总结”。先问“列出项目结构”,等它返回后再针对具体文件提问。这样可以避免一次性把所有文件内容都塞进单次请求中。


用户配置优化清单

优化维度 具体操作 节省比例
Server 数量 🛠️ 同时运行的 Server 控制在 3 个以内。 显著降低基础消耗。
目录权限 🔒 仅授权当前工作所需的子文件夹。 防止 AI 索引过多无关信息。
交互逻辑 💬 鼓励 AI 先做摘要或列表,再读具体内容。 避免大段文本重复注入。

开发自己的MCP实战小案例(Python版)

让我们开始动手构建一个 Python 版的 MCP Server。我们将使用 FastMCP 框架,这是目前最快上手的方式。

1. 环境准备 🏗️

首先,我们需要安装官方提供的 SDK:

Bash

pip install mcp

2. 构建 Server 骨架 🐍

在 MCP 中,我们使用装饰器 (Decorators) 来告诉 Server 哪些数据是准备给 AI 使用的。

下面是一个基础的代码结构。我们定义了一个 Resource (资源),它的地址是 file://project-brief,AI 可以通过这个标识符来请求数据。

Python

from mcp.server.fastmcp import FastMCP

初始化 Server

mcp = FastMCP(“MyLocalServer”)

定义一个资源

@mcp.resource(“file://project-brief”)
def get_project_brief() -> str:
  “””提供本地项目简介文件的内容”””
  # 这里的代码负责从硬盘读取数据
  try:
      # TODO: 编写读取文件的逻辑
      return “这是读取到的内容”
  except Exception as e:
      return f”读取失败: {str(e)}”

if name == “main“:
  mcp.run()

3. 实现读取逻辑 📖

为了让 AI 真正读到你的文件,我们需要在函数内部实现读取操作。在 Python 中,我们通常使用 with open(...) 这种安全的方式。

选择了 TypeScript 🚀 是个明智的决定!利用强类型检查,我们可以更安全地定义 AI 能够访问的数据结构,减少运行时错误。

在 Node.js 生态中,开发 MCP Server 通常分为以下几个步骤:

开发自己的MCP实战小案例(TypeScript版)

1. 初始化项目 🏗️

首先,我们需要创建一个新的目录并安装必要的依赖:

1
2
3
4
mkdir my-mcp-server  
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk

2. 构建基础代码结构 🛠️

在 TypeScript 中,我们通过实例化 Server 类并注册 Resource (资源) 来工作。这里是骨架代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Server } from "@modelcontextprotocol/sdk/server/index.js";  
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// 1. 创建 Server 实例
const server = new Server(
{ name: "my-local-server", version: "1.0.0" },
{ capabilities: { resources: {} } }
);

// 2. 注册资源读取逻辑
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "file://project-data") {
  // 这里将编写读取文件的代码
  return {
    contents: [{
      uri: request.params.uri,
      mimeType: "text/plain",
      text: "这是文件的初始内容"
    }]
  };
}
throw new Error("资源未找到");
});

// 3. 启动服务器(使用标准输入输出通信)
const transport = new StdioServerTransport();
await server.connect(transport);


3. 实现文件读取逻辑 📖

在 Node.js 中,我们通常使用 fs 模块来操作文件系统。为了保证非阻塞性能,使用 fs.promises 是更好的实践。