三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

深入解析MCP协议:Claude Code的AI扩展接口与实战开发指南

深入解析MCP协议:Claude Code的AI扩展接口与实战开发指南

1. 项目概述:为什么我们要深入MCP?

如果你最近在关注AI编程助手,尤其是Claude Code,那么“MCP”这个词一定高频出现在你的视野里。它可能出现在某个高级教程里,或者在你尝试连接某个外部工具时,配置项里赫然写着“MCP Server”。很多开发者第一次接触时,会下意识地把它和“模型上下文协议”或者某个网络协议混淆。实际上,MCP是Claude Code乃至整个Anthropic AI生态中一个极为关键,却又被官方文档轻描淡写的核心组件——模型上下文协议

简单来说,MCP是Claude Code的“手”和“眼”。没有它,Claude Code只是一个聪明的、但被关在笼子里的“大脑”。它知道如何写代码,但它无法直接读取你的项目文件结构,无法调用本地的构建工具,更无法与Figma、数据库、浏览器调试工具进行交互。MCP定义了一套标准化的通信协议,让Claude Code这个“大脑”能够安全、可控地指挥无数个“MCP服务器”去执行具体的任务,比如读取文件、执行命令、查询数据库、操作UI设计稿。这彻底打破了传统AI助手只能基于你粘贴进去的上下文进行聊天的局限,使其真正成为一个能融入你工作流的“副驾驶”。

理解MCP,是理解Claude Code强大能力来源的钥匙。这也是本系列源码解析选择它作为一章的原因——我们将不再停留在表面的API调用,而是深入到协议设计、通信机制和安全模型,看看Anthropic是如何为AI构建一套可扩展的“操作系统级”接口的。这对于任何想要深度定制AI工作流,甚至构建自己的AI原生工具的开发者来说,都是必不可少的一课。

2. MCP核心架构与设计哲学拆解

2.1 MCP不是什么:澄清常见误解

在深入细节之前,我们先划清边界,这能帮你更快抓住本质。首先,MCP不是一个通用的RPC框架(如gRPC),也不是一个消息队列。它被设计得非常“专一”,其核心目标只有一个:在AI模型(特别是Claude)和外部资源/工具之间,建立一种结构化、声明式、且安全边界清晰的通信通道。

其次,MCP不直接处理模型推理。它不参与生成token,不涉及提示词工程。它的工作发生在模型“思考”之前和之后:在模型需要信息时,提供信息;在模型决定执行动作时,转发指令并返回结果。你可以把它想象成模型与真实世界之间的一个“协议转换器”和“权限守门人”。

一个常见的混淆点是MCP与“Skill”或“Plugin”的区别。在一些其他AI平台(如某些早期的助手框架)中,“Skill”或“Plugin”往往是硬编码的、与核心紧耦合的功能模块。而MCP采用了一种更优雅的“服务器-客户端”模型。Claude Code作为客户端,只负责发起请求和解析响应;具体的功能由独立的、可插拔的MCP服务器实现。这种设计带来了巨大的灵活性:任何开发者都可以用任何语言(Python、Node.js、Go等)编写一个MCP服务器,只要它遵循协议规范,就能立刻被Claude Code识别和使用。你在热词里看到的tavily-mcp(搜索)、playwright-mcp(浏览器自动化)、figma-mcp(设计工具)都是这种思想的产物。

2.2 三层架构:客户端、服务器与资源抽象

MCP的架构可以清晰地分为三层,理解这三层的关系是读懂其源码的关键。

第一层:MCP客户端。这通常就是Claude Code编辑器插件本身。它内嵌了一个MCP客户端库。这个客户端的核心职责包括:

  1. 服务器发现与管理:读取用户的配置文件(如claude_desktop_config.json),加载其中声明的MCP服务器。配置文件里不仅指定了服务器可执行文件的路径或命令,还定义了传递给服务器的参数和环境变量。
  2. 协议会话管理:与每个MCP服务器建立独立的通信会话(通常通过标准输入输出stdiostdio)。它负责初始化握手、维护连接状态、处理重连。
  3. 请求路由与调度:当用户在Claude Code中提出需求(如“请分析当前目录下的src文件夹结构”),Claude模型会判断需要调用哪个“工具”。这个“工具调用”的请求会被转换为标准的MCP协议消息,由客户端路由到对应的MCP服务器。
  4. 响应处理与上下文注入:将MCP服务器返回的结构化数据(如文件列表、命令输出、数据库查询结果)进行格式化,然后注入到模型的上下文中,供模型在后续的思考中使用。

第二层:MCP协议。这是连接客户端和服务器的“语言”。它基于JSON-RPC 2.0规范,这是一种轻量级的远程过程调用协议。选择JSON-RPC是因为其简单、通用、且与语言无关。协议定义了几类核心的“能力”和对应的消息类型:

  • resources:声明服务器可以提供哪些“资源”。资源是只读的数据源,比如文件系统目录、数据库表结构、API文档。服务器在初始化时会向客户端“广告”自己有哪些资源(如file:///path/to/project)。客户端可以“订阅”这些资源,当资源变化时(如文件被修改),服务器会主动通知客户端。
  • tools:声明服务器可以执行哪些“工具”。工具是可执行的操作,比如运行命令、调用API、写入文件。每个工具都有严格的输入参数Schema定义。
  • prompts:声明服务器提供哪些“提示词模板”。这是一种更高级的抽象,允许服务器预定义一些复杂的、参数化的提示词片段,供模型直接调用和组合。

第三层:MCP服务器。这是具体功能的实现者。一个MCP服务器在启动时,会向客户端发送一个initialize请求,宣告自己支持哪些capabilities(资源、工具、提示词)。之后,它便进入事件循环,等待客户端的请求。例如,一个“文件系统MCP服务器”会宣告自己支持file://资源;当客户端请求list某个目录时,服务器调用本地的fs.readdir,将结果封装成MCP协议格式返回。

注意:MCP服务器与Claude Code客户端的通信默认通过stdio进行,这是一种进程间通信方式。这意味着服务器通常作为一个独立的子进程启动。这种设计将潜在的安全风险隔离在了独立的进程中。即使某个MCP服务器发生崩溃或恶意行为,也很难直接影响主编辑器或客户端核心逻辑。

2.3 安全与权限模型:为什么可以放心使用?

“让AI直接操作我的文件系统和运行命令?”这听起来非常危险。MCP的设计哲学中,安全是首要考量,其安全模型是“显式声明加用户授权”。

  1. 无默认权限:一个MCP服务器在安装后,默认没有任何权限。它必须在配置文件中被用户显式地启用和配置。例如,你想要一个能访问/Users/yourname/projects的服务器,你必须在配置中明确写出这个路径。

    // claude_desktop_config.json 示例片段 { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname/projects"] } } }

    上面这行配置,就是用户亲手赋予该服务器访问指定目录的权限。服务器无法访问配置路径之外的文件。

  2. 最小权限原则:服务器声明的toolsresources必须具体。一个“命令执行”服务器可能需要声明它允许运行哪些特定命令或命令模式,而不是获得一个通用的shell。用户在配置时,可以进一步限制这些参数。

  3. 协议层面的隔离:如前所述,服务器运行在独立进程。客户端与服务器的所有通信都经过严格的序列化和反序列化,服务器无法直接访问客户端的内存或状态。

  4. 审计与可见性:在Claude Code的交互界面中,当模型决定调用一个MCP工具时,通常会有一个明显的提示或确认步骤(取决于设置),告知用户即将执行什么操作。所有通过MCP获取的资源内容,在模型的上下文里也会有明确的来源标记。

这种设计把控制权完全交给了用户。你作为开发者,在编写自己的MCP服务器时,也必须遵循这种“声明式”的范式,清晰地定义你的服务器能做什么,不能做什么。这不仅是协议要求,更是一种最佳实践。

3. 协议深度解析:从消息流看交互本质

要真正理解MCP,我们需要化身为一个数据包,亲历一次完整的交互过程。我们以Claude Code请求列出项目目录为例,拆解背后的每一步。

3.1 初始化握手:建立通信基础

当Claude Code启动并加载了文件系统MCP服务器的配置后,它会启动一个新的子进程来运行服务器命令。通信通道建立(通常是stdio)后,第一件事就是握手。

  1. 客户端 -> 服务器:发送initialize请求。

    { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { // 客户端告知服务器自己支持哪些特性,如哪些通知类型 }, "clientInfo": { "name": "claude-code", "version": "1.0.0" } } }

    关键字段是protocolVersion,这确保了客户端和服务器使用相同版本的协议进行对话,避免兼容性问题。

  2. 服务器 -> 客户端:回复initialize结果,并宣告自己的能力。

    { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "resources": {}, // 表明支持资源能力 "tools": {} // 表明支持工具能力 }, "serverInfo": { "name": "filesystem-server", "version": "0.1.0" } } }

    握手完成,双方就协议版本和基本能力达成一致。

3.2 资源广告与订阅:只读数据的供给模式

初始化后,服务器会主动告知客户端自己有哪些“资源”可用。对于文件系统服务器,它可能会广告根目录作为一个资源。

  1. 服务器 -> 客户端:发送notifications/resources/list通知。

    { "jsonrpc": "2.0", "method": "notifications/resources/list", "params": { "resources": [ { "uri": "file:///Users/yourname/projects", "name": "My Projects Root", "description": "Root directory of my projects", "mimeType": "application/vnd.mcp.resource" } ] } }

    uri是资源的唯一标识符,遵循URI格式。mimeType帮助客户端理解资源类型。

  2. 客户端 -> 服务器:发送requests/resources/subscribe请求,订阅该资源。

    { "jsonrpc": "2.0", "id": 2, "method": "requests/resources/subscribe", "params": { "uri": "file:///Users/yourname/projects" } }
  3. 服务器 -> 客户端:发送notifications/resources/updated通知,提供资源的初始内容。

    { "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { "uri": "file:///Users/yourname/projects", "resource": { "uri": "file:///Users/yourname/projects", "contents": [ { "uri": "file:///Users/yourname/projects/README.md", "name": "README.md" }, { "uri": "file:///Users/yourname/projects/src/", "name": "src/", "type": "directory" // 标明这是一个目录 } // ... 其他文件和目录 ] } } }

    现在,Claude Code客户端就知道了/projects目录下有一个src子目录。这个信息被存储在客户端的上下文中。

资源模式的核心价值:它是一种“推送”模型。对于变化不频繁或需要监控的数据(如文件列表、数据库表结构),一旦订阅,服务器可以在数据变化时主动推送更新,让AI助手始终拥有最新的上下文,而无需反复轮询。

3.3 工具调用:执行动作的标准化流程

当用户在聊天框输入“请列出src目录下的所有TypeScript文件”时,Claude模型会计划调用一个工具。假设我们有一个更强大的“文件查找”工具。

  1. 客户端 -> 服务器:发送requests/tools/call请求。

    { "jsonrpc": "2.0", "id": 10, "method": "requests/tools/call", "params": { "name": "find_files", "arguments": { "directory": "file:///Users/yourname/projects/src", "pattern": "*.ts" } } }
  2. 服务器:接收到请求后,解析参数。它需要将file://URI转换为本地文件系统路径,然后执行类似globfind的操作。

  3. 服务器 -> 客户端:返回工具调用结果。

    { "jsonrpc": "2.0", "id": 10, "result": { "content": [ { "type": "text", "text": "Found 3 TypeScript files:", "data": { "files": [ "file:///Users/yourname/projects/src/index.ts", "file:///Users/yourname/projects/src/utils/helper.ts", "file:///Users/yourname/projects/src/components/Button.tsx" ] } } ] } }

    结果被格式化为结构化的contenttype可以是textimage等。data字段可以包含任意的结构化数据,供客户端和模型进一步处理。

  4. 客户端:将这个结果注入到与Claude模型对话的上下文中。模型现在“看到”了这些文件,并可以基于此进行下一步操作,比如“请打开Button.tsx并分析其内容”。

工具模式的核心价值:它是一种“拉取”或“执行”模型。将复杂的、需要权限的操作封装成一个个定义良好的工具,通过严格的参数Schema进行输入验证,通过结构化的content返回结果。这使得AI可以安全、可靠地执行范围广泛的任务。

3.4 错误处理与生命周期

协议也定义了完善的错误处理。如果服务器在处理请求时出错,它会返回一个标准的JSON-RPC错误响应。

{ "jsonrpc": "2.0", "id": 10, "error": { "code": -32603, "message": "Internal error", "data": "Directory not found: /invalid/path" } }

客户端需要妥善处理这些错误,可能将其转换为用户友好的提示。

生命周期管理包括initialized通知、shutdown请求和exit通知,确保连接能优雅地建立和关闭,避免资源泄漏。

4. 实战:从零编写一个自定义MCP服务器

理解了协议,最好的巩固方式就是动手实现一个。我们以Node.js环境为例,创建一个最简单的“时间服务器”,它提供一个工具来获取当前时间,并提供一个资源来展示时区信息。

4.1 项目初始化与依赖安装

首先,创建一个新目录并初始化项目。

mkdir mcp-server-time cd mcp-server-time npm init -y

安装官方提供的Node.js SDK@modelcontextprotocol/sdk,它封装了协议通信的细节,让我们专注于业务逻辑。

npm install @modelcontextprotocol/sdk

4.2 服务器核心代码实现

创建主文件server.js

// server.js const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); // 1. 创建Server实例,声明名称和版本 const server = new Server( { name: 'time-server', version: '0.1.0', }, { capabilities: { // 声明本服务器支持资源和工具 resources: {}, tools: {}, }, } ); // 2. 定义一个“获取当前时间”的工具 server.setRequestHandler('tools/call', async (request) => { if (request.params.name !== 'get_current_time') { throw new Error(`Unknown tool: ${request.params.name}`); } // 获取参数(本例无参数,但展示了如何获取) // const { timezone } = request.params.arguments || {}; const now = new Date(); const timeString = now.toISOString(); const localTimeString = now.toLocaleString(); return { content: [ { type: 'text', text: `当前时间(UTC):${timeString}\n本地时间:${localTimeString}`, // 可以附加结构化数据供模型使用 data: { isoString: timeString, localeString: localTimeString, timestamp: now.getTime() } }, ], }; }); // 3. 定义一个“时区信息”资源 // 首先,在初始化后告知客户端有这个资源 server.setRequestHandler('resources/list', async () => { return { resources: [ { uri: 'time://info/timezones', name: 'Supported Timezones Info', description: 'A list of common timezone names', mimeType: 'application/json', // 声明资源内容类型为JSON }, ], }; }); // 然后,处理对该资源内容的请求 server.setRequestHandler('resources/read', async (request) => { if (request.params.uri !== 'time://info/timezones') { throw new Error(`Unknown resource: ${request.params.uri}`); } const timezones = [ 'UTC', 'America/New_York', 'Europe/London', 'Asia/Shanghai', 'Asia/Tokyo' ]; return { contents: [ { uri: request.params.uri, // 资源内容可以是文本或JSON mimeType: 'application/json', text: JSON.stringify({ description: 'Commonly used IANA timezone identifiers.', timezones: timezones }, null, 2), // 美化输出 }, ], }; }); // 4. 启动服务器,使用stdio传输层 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Time MCP server running on stdio...'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

4.3 配置Claude Code以使用自定义服务器

要让Claude Code识别我们的服务器,需要编辑其配置文件。配置文件的路径因操作系统而异:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json
  • Linux:~/.config/Claude/claude_desktop_config.json

如果文件不存在,就创建它。添加我们的时间服务器配置:

{ "mcpServers": { "time-server": { "command": "node", "args": ["/绝对路径/到/你的/mcp-server-time/server.js"], "env": { "NODE_ENV": "production" } } } }

重要提示:必须使用绝对路径。相对路径在Claude Code的启动环境中可能无法解析。这是新手配置时最常见的坑。

保存配置文件后,必须完全重启Claude Code桌面应用。配置是在启动时加载的。

4.4 测试与验证

重启Claude Code后,打开聊天界面。你可以尝试以下提示:

  • “调用一下时间服务器,看看现在几点。”
  • “时间服务器能提供哪些时区信息?”

Claude Code应该能识别出你新配置的服务器提供的get_current_time工具和time://info/timezones资源。当你询问时间时,模型会调用该工具,并将返回的结果展示给你。

如果遇到问题,首先检查Claude Code的日志。在macOS上,你可以通过运行Console.app,在左侧选择你的设备,然后搜索“Claude”来查看应用日志。日志中通常会包含加载MCP服务器失败的具体原因,如“命令未找到”、“权限错误”或“协议初始化失败”。

5. 高级主题与生态现状

5.1 传输层与部署模式

我们例子中使用的是StdioServerTransport,这是最简单直接的进程间通信方式。但MCP协议是传输层无关的。官方SDK也支持SSEServerTransport,这允许服务器通过HTTP Server-Sent Events运行,从而实现远程连接。这意味着你可以将MCP服务器部署在一台远程机器或容器中,让本地的Claude Code通过网络调用它。这为构建企业级、中心化的AI工具网关打开了大门。

例如,你可以构建一个连接公司内部数据库的MCP服务器,部署在内网服务器上。开发者在自己的Claude Code中配置该服务器的HTTP端点,即可安全地查询开发数据库,而无需在每台电脑上安装数据库客户端或暴露连接凭证。

5.2 与“Skill”的对比及未来演进

在Claude Code的语境中,“Skill”有时被用来指代一些更复杂、集成度更高的功能模块。从源码角度看,一些内置的“Skill”可能内部也使用了MCP与外部服务通信,但“Skill”本身可能包含了更复杂的UI集成、状态管理和提示词链。可以粗略地理解为:MCP是底层协议和基础设施,而Skill是建立在MCP之上的、面向最终用户的功能产品

随着生态发展,MCP协议本身也在迭代。关注Anthropic官方在Github上的modelcontextprotocol仓库是了解最新动态的最佳方式。社区也在积极贡献各种服务器的实现,从git操作、docker管理到kubernetes集群查询,几乎覆盖了开发者日常工作的方方面面。

5.3 性能优化与调试技巧

编写生产可用的MCP服务器时,需要考虑以下几点:

  1. 资源占用:服务器是常驻进程。避免在工具实现中进行阻塞式或消耗大量内存的操作。对于耗时操作,考虑异步处理或实现进度通知。
  2. 错误恢复:实现健壮的错误处理。如果服务器崩溃,客户端应能检测到并尝试重启(如果配置允许)。在服务器代码中,使用try-catch包裹核心逻辑,返回友好的错误信息。
  3. 日志记录:由于服务器运行在后台,其console.log输出可能会重定向到Claude Code的日志系统。建议使用结构化的日志库(如pinowinston),并输出到文件,便于排查问题。
  4. 协议兼容性:严格遵循你声明的protocolVersion。在升级服务器时,如果协议有破坏性变更,需要同步考虑客户端的兼容性。

调试时,一个有用的技巧是暂时将服务器改为独立运行模式。修改你的server.js,暂时不使用StdioServerTransport,而是创建一个简单的HTTP服务器来接收和打印原始JSON-RPC消息,这能帮你直观地检查协议数据是否正确。

6. 常见问题与排查实录

在实际使用和开发MCP服务器时,你会遇到一些典型问题。这里记录了我踩过的坑和解决方案。

6.1 服务器加载失败:配置与路径问题

问题现象:Claude Code启动后,MCP服务器没有出现,或者在聊天中调用工具时提示“服务器不可用”。

排查步骤

  1. 检查配置文件路径和格式:确保claude_desktop_config.json文件在正确的目录,并且是合法的JSON格式。一个多余的逗号就会导致整个配置被忽略。可以使用JSONLint在线工具验证。
  2. 验证命令路径:这是最常见的问题。args中的路径必须是绝对路径。在终端中使用pwdwhich node命令来获取准确的路径。
    // 错误示例(相对路径) "args": ["./server.js"] // 正确示例(绝对路径) "args": ["/Users/username/projects/mcp-server-time/server.js"]
  3. 检查文件权限:确保服务器脚本具有可执行权限(在Unix系统上可能需要chmod +x server.js),并且Node.js命令在系统PATH中可用。
  4. 查看应用日志:如前所述,Claude Code的桌面应用日志是查找加载失败原因的金矿。错误信息通常会明确指出是“命令未找到”、“文件不存在”还是“协议初始化失败”。

6.2 协议通信错误:版本与消息格式

问题现象:服务器进程启动了,但Claude Code无法与其正常通信,或者调用工具时返回模糊的错误。

排查步骤

  1. 确认协议版本:确保服务器代码中new Server()时传入的protocolVersion与SDK版本兼容。最好使用SDK默认导出的版本,不要硬编码一个过时的版本号。
  2. 检查消息结构:严格按照JSON-RPC 2.0规范构建请求和响应。常见的错误包括:缺少jsonrpc: "2.0"字段、id不匹配、methodparams字段名拼写错误。使用官方的SDK可以避免大部分低级错误。
  3. 模拟客户端测试:编写一个简单的测试客户端脚本,使用StdioClientTransport连接到你的服务器,手动发送请求,观察服务器的响应。这能帮你隔离问题,确定是服务器逻辑错误还是Claude Code集成问题。

6.3 工具调用无响应或超时

问题现象:在Claude Code中调用工具后,长时间没有反应,最后可能超时。

排查步骤

  1. 检查工具处理函数:确保server.setRequestHandler('tools/call', ...)中的逻辑正确,并且一定记得return结果。如果函数内部有未捕获的异常,或者是一个异步函数但没有返回Promise,请求就会挂起。
  2. 避免同步阻塞:如果你的工具执行一个长时间运行的任务(如网络请求、大文件处理),确保处理函数是异步的(使用async),并且没有进行同步阻塞操作。
  3. 超时设置:目前MCP协议本身没有定义客户端超时,但Claude Code客户端可能有内置超时。对于长时间任务,考虑将其拆分为多个步骤,或实现一个带有进度反馈的机制(如果协议未来支持)。

6.4 生态服务器使用问题

问题现象:使用社区开发的MCP服务器(如tavily-mcp,playwright-mcp)时遇到问题。

排查步骤

  1. 阅读文档:社区服务器的README文件通常包含关键的配置说明和依赖要求。例如,playwright-mcp可能需要你先安装浏览器驱动。
  2. 检查环境变量:很多服务器需要通过环境变量配置API密钥或连接参数。确保在Claude Code的配置文件中正确设置了env字段。
    "mcpServers": { "tavily-search": { "command": "npx", "args": ["-y", "tavily-mcp"], "env": { "TAVILY_API_KEY": "your_api_key_here" // 关键配置 } } }
  3. 尝试独立运行:先尝试在终端中直接运行服务器的启动命令,看是否能独立工作。这能排除Claude Code环境带来的干扰。
  4. 关注项目Issues:在Github仓库的Issues中搜索是否有类似问题。开源项目的常见问题通常已有讨论和解决方案。

理解MCP,不仅仅是学会配置几个服务器,更是理解未来AI原生应用如何与复杂环境交互的一种范式。它将AI从纯粹的文本生成器,转变为可以协调和利用整个数字世界资源的智能体。当你掌握了MCP的原理和开发方法,你也就获得了为Claude Code乃至其他兼容此协议的AI平台,打造专属“武器库”的能力。

← 返回列表