1. 从“AI孤岛”到“无限扩展”:为什么我们需要MCP?
如果你最近在折腾AI应用开发,尤其是和Claude、Cursor这类工具打交道,大概率已经不止一次看到“MCP”这个词了。它可能出现在某个技术博客的角落里,或者在某个开源项目的README中一闪而过,伴随着“Agent”、“工具调用”、“扩展能力”这些听起来很酷但有点模糊的概念。我第一次接触MCP时,感觉它像是一个隐藏在幕后的“连接器”,大家都在谈论它带来的可能性,但具体怎么把它从概念变成手里可用的工具,资料却零散得像拼图。
简单来说,MCP(Model Context Protocol)是一个开放协议,它的核心使命是解决一个大问题:如何让大语言模型(LLM)安全、标准化地使用外部工具和数据。你可以把它想象成AI世界的“USB协议”。在没有USB之前,你的电脑要连接打印机、键盘、U盘,每个设备都需要自己专属的、复杂的驱动和端口,混乱且低效。MCP做的就是类似的事情——它定义了一套标准化的“插口”和“通信规则”,让任何符合这个协议的“工具”(我们称之为MCP Server)都能被任何支持该协议的“AI大脑”(MCP Client,如Claude Desktop、Cursor)即插即用。
为什么这如此重要?回想一下我们使用AI的典型场景:你问Claude“帮我总结一下这个网页”,它做不到,因为它无法访问浏览器;你让Cursor“查询我数据库里最新的用户订单”,它也束手无策,因为它无法直接连接你的MySQL。每个AI应用都像一个能力强大的“孤岛”,但它的视野和手臂却被限制在了本地的文本上下文里。我们过去怎么解决?写复杂的脚本、调用不统一的API、或者等待某个应用自己集成特定功能。过程繁琐,且不可复用。
MCP的出现,正是为了打破这种孤岛状态。它让AI的能力边界从“模型本身的知识和有限的上下文”扩展到了“整个数字世界”。通过搭建或使用一个个专用的MCP Server,AI可以:
- 读取数据:连接你的数据库(SQLite、PostgreSQL)、知识库(Notion、Obsidian)、甚至实时数据源(天气、股价)。
- 执行操作:操作文件系统、发送邮件、调用第三方API(如GitHub、Jira)、控制浏览器(通过Playwright)。
- 处理专业任务:运行代码分析(类似dbg/idapro mcp的想法)、处理设计稿(Figma)、甚至与硬件交互(ESP32)。
而这一切,对于使用AI的用户和开发者来说,体验是统一的:你只需要告诉AI“去做什么”,它自己会找到并调用对应的MCP工具,你无需关心背后的技术细节。这也就是标题所说的“无限扩展能力”的由来——理论上,任何能被程序化的能力,都可以被封装成一个MCP Server,进而成为AI的“技能”。
接下来,我将以一个完全从零开始的视角,带你一步步搭建一个属于自己的、简单但功能完整的MCP服务,并集成到Claude Desktop中。你会看到,从几行代码到一个可用的AI扩展,距离并没有想象中那么遥远。
2. 动手之前:厘清MCP的核心概念与生态组件
在开始写代码之前,我们必须先搞清楚MCP协议中的几个核心角色和它们之间的关系。这能帮助我们在后续搭建过程中,清楚地知道每一步是在构建哪个部分,以及它们如何协同工作。
2.1 MCP的三位主角:协议、服务器与客户端
1. MCP协议本身这不是一个需要你安装的软件,而是一份“标准合同”。它主要规定了两种核心的通信原语(Primitive):
- 工具(Tools):代表AI可以主动调用的“动作”。例如,“搜索网络”、“执行SQL查询”、“创建文件”。每个工具都有名称、描述和参数定义。AI通过调用工具来“做事”。
- 资源(Resources):代表AI可以被动读取的“数据源”。例如,“数据库schema文档”、“项目日志文件”、“系统状态信息”。资源有唯一的URI,AI可以读取它们的内容来获取上下文。
协议还定义了客户端与服务器之间通过JSON-RPC over stdio(标准输入输出)或SSE进行通信的格式。作为初学者,我们不需要深究其二进制细节,只需要知道有成熟的SDK帮我们处理了这些底层通信。
2. MCP Server(服务器/工具提供方)这是我们本次要搭建的核心。它是一个独立的进程,负责两件事:
- 声明能力:启动时告诉客户端:“我提供了哪些工具(Tools)和资源(Resources)”。
- 执行请求:当客户端(AI)调用某个工具或请求某个资源时,服务器执行相应的逻辑(比如查询数据库、调用API),并将结果返回。
一个MCP Server通常只专注于一个领域。比如,tavily-mcp服务器专门提供网络搜索能力,filesystem-mcp服务器提供文件读写能力。你也可以为自己公司的内部系统(如CRM、ERP)构建一个私有的MCP Server。
3. MCP Client(客户端/工具调用方)这是集成AI模型、并负责与用户交互和调度MCP Server的一方。常见的MCP Client包括:
- Claude Desktop App:Anthropic官方桌面应用,内置MCP客户端,可以配置本地或远程的MCP Server。
- Cursor IDE:集成了AI编程助手的编辑器,同样支持MCP,让AI能在编程时使用你提供的工具。
- 其他AI应用或框架:任何集成了MCP SDK的应用都可以作为客户端。
客户端的工作是:理解用户的自然语言请求,决定是否需要以及调用哪个MCP Server的哪个工具,管理多个Server的会话,并将工具执行结果整合进给模型的上下文中。
2.2 技术选型:为什么选择Node.js和官方SDK?
MCP协议本身是语言无关的,你可以用Python、Go、Rust甚至Bash来编写Server。但对于快速上手和生态丰富度来说,我强烈推荐使用Node.js和Anthropic官方提供的@modelcontextprotocol/sdk。
理由如下:
- 官方首选,文档和示例最全:Anthropic的官方示例和文档大量使用Node.js,遇到问题更容易找到参考和社区解答。
- 异步友好,适合IO密集型操作:MCP Server大部分工作是在处理网络请求、数据库查询等IO操作,Node.js的异步非阻塞模型天生适合。
- 生态强大:NPM上有海量的库,可以轻松连接几乎任何你想到的服务(数据库、API、文件系统等)。
- 开发体验流畅:配合
tsx或nodemon,可以实现代码热重载,调试和迭代速度非常快。
当然,如果你团队主力是Python,也有mcp这个Python SDK可供选择,但本文将以Node.js路线进行演示,因为这是目前最主流的快速开发路径。
2.3 环境准备:搭建你的开发舞台
在开始写Server之前,确保你的本地环境已经就绪:
- 安装Node.js:建议安装最新的LTS版本(如v20.x)。你可以从 Node.js官网 下载安装包,或者使用
nvm(Node Version Manager)进行管理。安装后,在终端运行node --version和npm --version确认安装成功。 - 初始化项目:创建一个新的目录作为你的MCP Server项目。
这会在当前目录生成一个mkdir my-first-mcp-server && cd my-first-mcp-server npm init -ypackage.json文件。 - 安装核心依赖:
同时,我们安装npm install @modelcontextprotocol/sdkdotenv来管理环境变量,以及tsx以便直接运行TypeScript代码(如果你写TS的话)。为了更好的开发体验,我们也将TypeScript和类型定义作为开发依赖安装。npm install dotenv npm install -D typescript @types/node tsx - 初始化TypeScript配置(可选但推荐):
这会生成一个npx tsc --inittsconfig.json文件。你可以保持默认,或根据需要进行调整(比如将target改为ES2022)。
至此,你的项目骨架已经搭建完成。接下来,我们将进入核心环节:编写第一个MCP Server。
3. 从“Hello World”到“真实工具”:构建你的第一个MCP Server
我们将遵循一个由浅入深的路径,先构建一个最简单的“回声”服务器来理解流程,然后快速升级为一个有实用价值的“系统信息查询”服务器。
3.1 蓝图:一个MCP Server的基本代码结构
一个最简单的MCP Server代码结构如下所示。创建一个名为server.js(或server.ts)的文件:
// server.js import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; // 1. 创建Server实例,并给它起个名字 const server = new Server( { name: 'my-first-mcp-server', version: '0.1.0', }, { capabilities: { // 这里声明服务器支持的能力,比如工具 tools: {}, }, } ); // 2. 定义工具(Tool) // 这是一个“回声”工具,它接收一个消息参数,并原样返回。 server.setRequestHandler('tools/list', async () => { return { tools: [ { name: 'echo', description: 'Echo back the input message. Useful for testing.', inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'The message to echo back.', }, }, required: ['message'], }, }, ], }; }); // 3. 处理工具调用(Tool Execution) // 当客户端调用‘echo’工具时,执行这里的逻辑。 server.setRequestHandler('tools/call', async (request) => { if (request.params.name === 'echo') { const message = request.params.arguments?.message; return { content: [ { type: 'text', text: `Echo: ${message}`, }, ], }; } // 如果工具名不匹配,抛出错误 throw new Error(`Unknown tool: ${request.params.name}`); }); // 4. 启动服务器,使用标准输入输出进行通信 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Server running on stdio...'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });逐段解析:
- 创建Server实例:我们初始化了一个
Server对象,并给它赋予了名称、版本和初始能力声明。capabilities: { tools: {} }表示“我这个服务器支持提供工具”。 - 声明工具列表(
tools/list):这是MCP协议规定的“握手”步骤之一。客户端启动时会首先调用这个请求,获取服务器提供的所有工具清单。我们返回了一个包含echo工具定义的数组。定义中最重要的部分是inputSchema,它用JSON Schema精确描述了调用这个工具时需要传入什么参数(这里是一个必需的message字符串)。清晰的description对于AI理解工具用途至关重要。 - 处理工具调用(
tools/call):这是核心业务逻辑所在。当AI决定调用echo工具时,客户端会发起一个tools/call请求。我们通过判断request.params.name来路由到对应的处理函数。从request.params.arguments中提取出用户(通过AI)传入的参数,执行逻辑(这里只是简单拼接字符串),然后返回格式化的结果。结果必须包裹在content数组中,通常我们返回type: 'text'的文本内容。 - 启动与传输层:
StdioServerTransport是MCP SDK提供的一个传输层实现,它使用进程的标准输入(stdin)和标准输出(stdout)与客户端通信。这是一种简单、跨平台且安全的本地通信方式。服务器启动后,就会阻塞在这里,等待客户端的连接和请求。
3.2 运行与测试:让你的服务器“活”起来
现在,我们如何测试这个服务器是否工作正常呢?最直接的方法是使用MCP SDK自带的测试工具,或者模拟一个客户端。但有一个更简单直观的方法:使用mcp-cli工具。
首先,全局安装@modelcontextprotocol/cli:
npm install -g @modelcontextprotocol/cli然后,在项目根目录下运行你的服务器,并通过mcpCLI进行交互式测试:
node server.js | mcp dev或者,如果你使用了tsx并编写的是TypeScript文件:
npx tsx server.ts | mcp devmcp dev命令会连接到前一个命令(你的服务器)的标准输出,并提供一个简单的REPL(交互式解释器)界面。在这个界面里,你可以直接输入命令来测试工具。
在mcp dev的提示符下,输入:
tools list你应该能看到返回的JSON,其中列出了你的echo工具。
接着,调用这个工具:
tools call echo '{"message": "Hello MCP!"}'如果一切正常,你会看到返回结果:"Echo: Hello MCP!"。
恭喜!你的第一个MCP Server已经成功运行并响应了请求。但这只是一个开始,echo工具除了测试外并无实际用处。让我们立刻升级它,构建一个真正有用的工具。
3.3 实战升级:构建“系统信息查询”服务器
让我们把“回声”服务器改造成一个能提供真实系统信息的服务器。我们将添加两个工具:一个获取当前时间,另一个获取系统内存使用情况。
修改你的server.js文件:
// server.js - 升级版 import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import os from 'os'; // 引入Node.js内置的os模块 const server = new Server( { name: 'system-info-mcp-server', version: '0.2.0', }, { capabilities: { tools: {}, }, } ); server.setRequestHandler('tools/list', async () => { return { tools: [ { name: 'get_current_time', description: 'Get the current server time in ISO format.', inputSchema: { type: 'object', properties: { // 这个工具不需要参数 }, }, }, { name: 'get_system_memory', description: 'Get the current system memory usage (free, total, used percentage).', inputSchema: { type: 'object', properties: { format: { type: 'string', description: 'Output format, either "human" (readable) or "raw" (in bytes).', enum: ['human', 'raw'], default: 'human', }, }, required: [], }, }, ], }; }); server.setRequestHandler('tools/call', async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'get_current_time': { const now = new Date(); return { content: [ { type: 'text', text: `Current server time is: ${now.toISOString()}`, }, ], }; } case 'get_system_memory': { const format = args?.format || 'human'; const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const usedPercentage = ((usedMem / totalMem) * 100).toFixed(2); let text; if (format === 'human') { const toGB = (bytes) => (bytes / 1024 ** 3).toFixed(2); text = `Memory Usage: - Total: ${toGB(totalMem)} GB - Free: ${toGB(freeMem)} GB - Used: ${toGB(usedMem)} GB (${usedPercentage}%)`; } else { text = `Memory Usage (bytes): - Total: ${totalMem} - Free: ${freeMem} - Used: ${usedMem} (${usedPercentage}%)`; } return { content: [ { type: 'text', text: text, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('System Info MCP Server running on stdio...'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });关键升级点解析:
- 引入Node.js内置模块:我们使用了
os模块来获取真实的系统内存信息。这展示了MCP Server如何将Node.js的生态能力暴露给AI。 - 定义多个工具:在
tools/list中,我们返回了两个工具的数组。每个工具都有清晰的描述和输入模式(inputSchema)。注意get_system_memory工具有一个可选的format参数,并使用了enum来限制其取值,这为AI提供了明确的调用指导。 - 结构化的工具调用处理:使用
switch语句根据工具名进行路由,使代码更清晰易扩展。每个工具执行真实的逻辑并返回结构化的结果。 - 人性化输出:在
get_system_memory工具中,我们根据format参数决定输出是人类可读的GB单位还是原始的字节数。这种对用户体验的考虑非常重要,因为AI最终会将这个结果呈现给用户。
再次使用mcp dev进行测试:
node server.js | mcp dev在REPL中:
tools list tools call get_current_time {} tools call get_system_memory {} tools call get_system_memory '{"format": "raw"}'你应该能看到当前时间和不同格式的内存使用情况报告。
至此,你已经成功构建了一个具备真实功能的MCP Server。它虽然简单,但完整地走通了从定义、声明到执行的全流程。接下来,我们要解决最关键的一步:如何让我们日常使用的AI客户端(如Claude Desktop)认识并使用这个服务器。
4. 连接AI世界:在Claude Desktop中配置你的MCP Server
构建了MCP Server,就像造好了一个功能强大的“外设”。现在,我们需要将它连接到“电脑主机”——也就是我们的AI客户端。这里以Claude Desktop为例,这是目前集成MCP最成熟、最常用的客户端之一。
4.1 理解Claude Desktop的MCP配置机制
Claude Desktop允许用户通过一个配置文件来声明需要连接的MCP Server。这个配置文件通常位于:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
如果这个文件或目录不存在,你需要手动创建它。配置文件的核心是一个JSON对象,其中mcpServers字段是一个对象,键是你给这个服务器起的别名(方便在Claude内部引用),值是该服务器的启动配置。
4.2 为你的服务器创建配置文件
我们需要告诉Claude Desktop如何启动我们刚刚写的那个system-info-mcp-server。假设我们的服务器脚本位于/Users/yourname/Projects/my-first-mcp-server/server.js。
编辑或创建上述路径的claude_desktop_config.json文件,内容如下:
{ "mcpServers": { "system-info": { "command": "node", "args": [ "/Users/yourname/Projects/my-first-mcp-server/server.js" ], "env": { "NODE_ENV": "production" } } } }配置详解:
"system-info":这是你为这个服务器定义的别名。之后在Claude对话中,你可以通过这个名字来指代它(尽管通常AI会自动选择)。"command": "node":指定用于运行服务器的命令。这里就是node。"args":数组,指定传递给命令的参数。最重要的就是你的服务器脚本的绝对路径。使用绝对路径可以避免因工作目录不同导致的找不到文件问题。"env":可选,可以设置服务器进程的环境变量。
重要提示:修改配置文件后,必须完全重启Claude Desktop应用(退出并重新启动),配置才会被加载。
4.3 验证与使用:在对话中调用你的工具
重启Claude Desktop后,打开一个新的对话。你可以通过一些方式来验证服务器是否连接成功:
- 直接询问:你可以尝试问Claude:“你现在有哪些可用的工具或能力?”或者“你能查看系统信息吗?”。一个正确配置的Claude通常会主动提及它连接了MCP服务器,并列出可用的工具。
- 观察界面:在某些版本的Claude Desktop中,当MCP服务器成功连接时,输入框附近可能会有一个微小的图标或提示。
- 发起指令:直接给出需要用到你工具的命令。例如:
- “请告诉我现在的服务器时间。”
- “查看一下当前系统的内存使用情况。”
- “用
raw格式显示内存信息。”
如果一切配置正确,Claude会理解你的请求,在后台调用对应的MCP工具(get_current_time或get_system_memory),并将执行结果整合到它的回复中。你可能会在它的回复里看到类似“我通过系统信息工具查询到...”这样的表述,后面跟着你服务器返回的文本。
第一次连接失败的常见排查点:
- 路径错误:
args中的脚本路径是否正确?最好使用绝对路径。 - Node.js环境:配置中指定的
node命令是否在系统PATH中?你可以尝试在终端中直接用配置中的命令和参数运行,看服务器是否能独立启动。 - 权限问题:确保脚本文件有可执行权限(虽然不是必须,但检查无妨)。
- 端口/进程冲突:MCP over stdio不涉及网络端口,但确保没有其他进程占用了标准输入输出。
- 查看日志:Claude Desktop通常会有日志文件,位于配置目录附近,查看日志可以帮助定位连接或启动失败的原因。
当你在Claude的对话窗口中看到它成功调用了你的工具并返回了信息,那一刻的成就感是非常真实的——你亲手为AI扩展了新的感官和手脚。
5. 进阶实战:构建一个实用的“项目文件搜索”MCP Server
掌握了基础,让我们挑战一个更复杂、也更实用的场景:构建一个项目文件搜索服务器。这个工具将允许AI(比如在Cursor或Claude中辅助编程时)快速搜索你本地项目目录下的文件内容,这对于代码导航、查找日志、定位配置项等任务极其有用。
5.1 需求分析与设计
我们的目标是:让AI能根据关键词,搜索指定目录下的文件内容,并返回匹配的行及其上下文。
功能点设计:
- 工具名称:
search_project_files - 输入参数:
query(字符串,必需):搜索关键词。project_path(字符串,可选):要搜索的项目根目录路径。如果不提供,则使用一个默认路径(可配置)。file_extensions(字符串数组,可选):限制只搜索特定扩展名的文件,如[“.js”, “.ts”, “.md”]。max_results(整数,可选):最多返回的匹配结果数量,避免结果过多。
- 输出:结构化列表,包含文件名、匹配行号、匹配行内容以及前后几行作为上下文。
为了实现这个功能,我们需要用到Node.js的fs(文件系统)和path模块,以及一个简单的文本搜索逻辑。为了提升体验,我们还会引入ignore库来支持类似.gitignore的忽略规则,避免搜索node_modules、.git等目录。
5.2 实现:代码逐行解析
首先,安装新的依赖:
npm install ignore然后,创建新的服务器文件file-search-server.js:
// file-search-server.js import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import ignore from 'ignore'; // 获取当前文件所在目录,作为默认项目路径 const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PROJECT_ROOT = path.join(__dirname, '..'); // 假设项目在上一级目录,可根据需要调整 const server = new Server( { name: 'project-file-search-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); /** * 递归读取目录,收集所有文件路径,并应用忽略规则 */ async function collectFiles(dirPath, ig, fileExtensions) { const files = []; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const relativePath = path.relative(DEFAULT_PROJECT_ROOT, fullPath); // 应用忽略规则 if (ig.ignores(relativePath)) { continue; } if (entry.isDirectory()) { // 递归处理子目录 files.push(...(await collectFiles(fullPath, ig, fileExtensions))); } else if (entry.isFile()) { // 检查文件扩展名过滤 if (fileExtensions && fileExtensions.length > 0) { const ext = path.extname(entry.name); if (!fileExtensions.includes(ext)) { continue; } } files.push(fullPath); } } } catch (error) { console.error(`Error reading directory ${dirPath}:`, error.message); } return files; } /** * 在单个文件中搜索关键词 */ async function searchInFile(filePath, query, maxResultsPerFile = 5) { const results = []; try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes(query)) { // 收集匹配行及其上下文(前后各2行) const start = Math.max(0, i - 2); const end = Math.min(lines.length - 1, i + 2); const context = lines.slice(start, end + 1).join('\n'); const lineNumber = i + 1; // 行号从1开始 results.push({ file: filePath, line: lineNumber, lineContent: lines[i].trim(), context: context, }); if (results.length >= maxResultsPerFile) { break; // 单个文件内限制结果数量 } } } } catch (error) { console.error(`Error reading file ${filePath}:`, error.message); } return results; } server.setRequestHandler('tools/list', async () => { return { tools: [ { name: 'search_project_files', description: 'Search for text content within files of a project directory. Supports filtering by file extension and respects .gitignore rules.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The text string to search for within files.', }, project_path: { type: 'string', description: `Absolute path to the project root directory. Defaults to: ${DEFAULT_PROJECT_ROOT}`, }, file_extensions: { type: 'array', items: { type: 'string' }, description: 'Filter files by extensions, e.g., [".js", ".ts", ".md"]. Leave empty to search all files.', }, max_results: { type: 'number', description: 'Maximum number of matches to return. Default is 20.', }, }, required: ['query'], }, }, ], }; }); server.setRequestHandler('tools/call', async (request) => { if (request.params.name !== 'search_project_files') { throw new Error(`Unknown tool: ${request.params.name}`); } const args = request.params.arguments || {}; const { query, project_path, file_extensions, max_results } = args; if (!query || query.trim() === '') { throw new Error('Search query cannot be empty.'); } const projectRoot = project_path ? path.resolve(project_path) : DEFAULT_PROJECT_ROOT; const maxResults = max_results || 20; // 1. 加载并解析 .gitignore 规则 let ig = ignore(); const gitignorePath = path.join(projectRoot, '.gitignore'); try { const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); ig = ignore().add(gitignoreContent); } catch { // 如果不存在.gitignore,则使用默认忽略规则 ig = ignore().add(['node_modules', '.git', '*.log', 'dist', 'build']); console.error(`No .gitignore found at ${gitignorePath}, using default ignore patterns.`); } // 2. 收集所有符合条件的文件 console.error(`Starting file collection from: ${projectRoot}`); const allFiles = await collectFiles(projectRoot, ig, file_extensions); console.error(`Total files to search: ${allFiles.length}`); // 3. 并行搜索所有文件 const searchPromises = allFiles.map(file => searchInFile(file, query)); const resultsArrays = await Promise.all(searchPromises); let allResults = resultsArrays.flat(); // 4. 排序和限制总数 (按文件名和行号简单排序) allResults.sort((a, b) => { const fileCompare = a.file.localeCompare(b.file); if (fileCompare !== 0) return fileCompare; return a.line - b.line; }); allResults = allResults.slice(0, maxResults); // 5. 格式化输出 if (allResults.length === 0) { return { content: [{ type: 'text', text: `No matches found for query "${query}" in project ${projectRoot}.`, }], }; } let outputText = `Found ${allResults.length} match(es) for "${query}":\n\n`; allResults.forEach((result, index) => { const relativePath = path.relative(projectRoot, result.file); outputText += `**${index + 1}. ${relativePath}:${result.line}**\n`; outputText += `\`\`\`\n${result.context}\n\`\`\`\n\n`; }); return { content: [{ type: 'text', text: outputText, }], }; }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Project File Search MCP Server running on stdio...'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });5.3 核心逻辑与避坑指南
这段代码比之前的例子复杂,包含了几个关键的设计决策和需要注意的坑:
1. 文件收集与忽略规则 (collectFiles函数):
- 递归遍历:使用
fs.readdir的withFileTypes: true选项来提高效率,避免为每个条目额外调用stat。 - 忽略规则是核心:直接遍历整个项目而不加过滤,会搜到
node_modules、.git等大量无关文件,导致性能极差且结果混乱。我们使用ignore库来模拟Git的行为。 - 默认回退:如果项目根目录没有
.gitignore文件,我们提供一个合理的默认忽略列表([‘node_modules’, ‘.git’, ‘*.log’, ‘dist’, ‘build’])。这是一个非常重要的实践,确保了工具的可用性。
2. 文件搜索与上下文 (searchInFile函数):
- 流式读取与内存:对于大文件,一次性读入内存(
fs.readFile)可能有问题。但对于代码项目,文件通常不会巨大,这是一个合理的简化。如果处理日志等大文件,应考虑流式读取。 - 提供上下文:只返回匹配行往往信息不足。我们提供了匹配行的前后各两行作为上下文,这对于理解代码片段或日志块非常有帮助。
- 限制单文件结果:如果一个文件中有大量匹配(比如在
package-lock.json中搜索一个常见的单词),我们通过maxResultsPerFile(代码中硬编码为5)来避免单个文件淹没所有结果。
3. 性能考量与异步处理:
- 并行搜索:使用
Promise.all对收集到的所有文件并行执行搜索,这比串行搜索快得多,尤其是当文件数量很多时。 - 结果排序与截断:对所有结果进行简单排序(先按文件路径,再按行号),然后根据用户指定的
max_results进行截断,确保返回的结果是可控且有序的。
4. 输出格式化:
- 清晰的结构:输出使用了Markdown风格的粗体(
**)和代码块(```)来格式化,这使得在Claude等客户端的回复中,结果的可读性非常高。AI在呈现结果时,会保留这种格式。
5.4 配置与使用
更新Claude Desktop配置:像之前一样,将新的服务器添加到
claude_desktop_config.json中。你可以同时配置多个服务器。{ "mcpServers": { "system-info": { ... }, "file-search": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/your/file-search-server.js"] } } }重启Claude Desktop。
在对话中使用:现在,你可以尝试向Claude提出如下请求:
- “在我的项目中搜索所有包含‘TODO’注释的文件。”
- “查找代码里调用
fetchUserData函数的地方。” - “搜索项目里所有
.md文件,看看有没有提到‘MCP’这个词。” - “在
/src/components目录下,搜索‘useState’。”
Claude会理解这些请求,调用search_project_files工具,并将格式化后的搜索结果清晰地呈现给你。这极大地提升了AI辅助编程、文档检索的效率。
通过这个进阶案例,你已经掌握了构建一个复杂、实用、考虑性能与用户体验的MCP Server的全过程。从简单的系统信息查询到复杂的文件系统操作,MCP的潜力正在被你一步步解锁。