尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MCP协议实战:构建AI可调用的本地文件读取服务

MCP协议实战:构建AI可调用的本地文件读取服务 1. 从“玩具”到“工具”为什么我们需要MCP如果你最近在AI编程工具比如Cursor、Claude Desktop的社区里混迹大概率会频繁看到一个词MCP。它全称是Model Context Protocol直译过来是“模型上下文协议”。听起来很唬人但它的核心目标其实非常朴素让AI助手大模型能够安全、可控地访问和使用你电脑上的工具和数据。想象一个典型的开发场景你想让AI帮你分析一个本地的日志文件。传统做法是你得手动把文件内容复制粘贴到聊天框里受限于上下文长度大文件根本处理不了。或者你想让它调用一个本地的API服务你不得不把复杂的curl命令和响应结果来回搬运。这个过程是割裂的、低效的。MCP协议就是为了解决这个“最后一公里”的问题而生的。它定义了一套标准让开发者可以编写一个个轻量的“服务”MCP Server这些服务就像一个个插件专门负责与特定的资源如本地文件系统、数据库、API打交道。而AI客户端MCP Client如集成了该协议的编辑器则通过这个协议安全地向这些服务发出请求获取处理后的信息或执行操作。所以当标题提到“写一个能读本地文件的极简服务”时这其实就是MCP最经典、最核心的应用场景之一。它不是一个庞大的后端系统而是一个聚焦于单一能力文件读取的轻量级进程。通过构建这样一个服务你可以让AI助手直接“看到”并处理你指定目录下的文件无需再手动搬运数据。这不仅仅是方便更是一种工作流的质变。接下来我们就从零开始拆解如何构建这样一个服务并深入理解其背后的设计逻辑与实战细节。2. 环境搭建与核心依赖解析在动手写代码之前我们需要把舞台搭好。由于MCP是一个新兴协议官方和社区提供了多种语言的SDK来降低开发门槛。对于前端或全栈开发者来说基于Node.js的modelcontextprotocol/sdk是一个非常好的起点它封装了协议通信的底层细节让我们可以专注于业务逻辑。2.1 初始化项目与安装依赖首先创建一个新的项目目录并初始化。这里我推荐使用pnpm它在管理Monorepo和依赖方面表现更优但npm或yarn也同样适用。mkdir simple-file-reader-mcp cd simple-file-reader-mcp pnpm init -y接下来安装核心的MCP SDK。同时我们会用到zod这个库它在MCP生态中至关重要用于严格定义和验证工具Tools的输入输出参数确保AI客户端和服务器之间传递的数据结构是清晰且类型安全的。pnpm add modelcontextprotocol/sdk zod此外我们还需要安装TypeScript及相关类型定义以获得更好的开发体验。虽然这不是强制要求但对于确保代码质量非常有帮助。pnpm add -D typescript types/node npx tsc --init初始化TypeScript配置后你可以在生成的tsconfig.json中确保module: ESNext和target: ES2022等设置以适应现代Node.js环境。2.2 理解MCP Server的基本骨架一个MCP Server的核心生命周期非常简单可以概括为初始化 - 声明能力 - 处理请求 - 返回结果。SDK为我们抽象了与Stdio标准输入输出传输层通信的复杂性我们只需要关注三件事定义工具Tools告诉客户端我这个服务器能提供哪些“能力”。每个工具都需要一个名字、描述和参数模式schema。例如我们的“读文件”工具需要定义一个名为read_file的工具并说明它需要一个path参数。实现工具处理逻辑当客户端调用某个工具时服务器需要执行相应的操作。对于read_file就是使用Node.js的fs模块读取指定路径的文件内容。处理资源Resources可选除了主动调用的工具MCP还支持“资源”概念可以理解为服务器主动向客户端“推送”的上下文信息。例如你可以将一个目录下的文件列表定义为一个资源客户端在初始化时就能获取到。本篇我们聚焦于工具资源将在后续进阶部分探讨。基于这个理解我们先创建一个最简单的服务器入口文件src/server.ts并搭建起基础结构。3. 构建极简文件读取工具从定义到实现现在我们进入核心环节打造那个能让AI助手读取本地文件的工具。这个过程不仅仅是写几行读取文件的代码更重要的是按照MCP协议的要求严谨地定义工具契约并处理好边界情况。3.1 使用Zod定义工具契约在MCP中工具的参数和返回值都需要用JSON Schema来描述。zod库完美地扮演了这个角色。它让我们能用TypeScript风格的方式定义模式并且能自动推导出TypeScript类型实现“一处定义多处使用”。首先我们在src/tools目录下创建readFile.ts定义我们的工具// src/tools/readFile.ts import { z } from “zod”; // 1. 定义输入参数的模式 // 我们要求客户端必须传递一个 path 参数它是字符串类型。 // 通过 .describe() 方法我们可以为参数添加人类可读的描述这会被AI客户端用来理解如何填写参数。 export const ReadFileArgsSchema z.object({ path: z.string().describe(“The absolute or relative path to the file to read.”), }); // 从Schema推导出TypeScript类型方便在实现逻辑中使用 export type ReadFileArgs z.infertypeof ReadFileArgsSchema; // 2. 定义工具本身的元数据 // 这包括工具的名称、描述和参数模式。 // 名称是客户端调用时使用的标识符描述帮助AI理解工具的作用。 export const readFileTool { name: “read_file”, // 工具名通常使用蛇形命名 description: “Read the contents of a file from the local filesystem.”, inputSchema: ReadFileArgsSchema, };这里有一个关键点path参数是相对路径还是绝对路径为了服务的可预测性和安全性我强烈建议在实现时优先处理绝对路径。你可以通过约定一个“根目录”或者要求客户端传入基于项目根目录的路径然后在服务器端解析为绝对路径。这避免了因工作目录不同导致的“文件找不到”问题。3.2 实现文件读取逻辑定义了契约接下来就是实现。在src/tools/readFile.ts中继续添加// src/tools/readFile.ts (续) import { promises as fs } from “fs”; // 使用Promise-based的fs API import path from “path”; // 这是一个简单的实现函数 export async function executeReadFile(args: ReadFileArgs): Promisestring { const { path: filePath } args; // 安全考虑这里可以添加路径校验逻辑防止读取系统敏感文件。 // 例如可以检查 filePath 是否在某个允许的目录范围内。 // const safeBaseDir process.cwd(); // 例如限制在当前工作目录 // const resolvedPath path.resolve(safeBaseDir, filePath); // if (!resolvedPath.startsWith(safeBaseDir)) { // throw new Error(“Access to files outside the allowed directory is prohibited.”); // } // 为了简单起见我们直接解析传入的路径。 // 注意如果filePath是相对路径它将相对于服务器进程的当前工作目录(process.cwd())进行解析。 const resolvedPath path.resolve(filePath); try { // 使用fs.readFile读取文件内容默认编码为utf-8 const content await fs.readFile(resolvedPath, “utf-8”); return content; } catch (error: any) { // 错误处理至关重要需要将Node.js错误转化为对AI客户端友好的信息。 if (error.code “ENOENT”) { throw new Error(File not found at path: ${resolvedPath}); } else if (error.code “EACCES”) { throw new Error(Permission denied when reading file: ${resolvedPath}); } else if (error.code “EISDIR”) { throw new Error(The path is a directory, not a file: ${resolvedPath}); } // 其他未知错误 throw new Error(Failed to read file: ${error.message}); } }实操心得错误处理是服务健壮性的关键。AI客户端尤其是大模型需要清晰的错误信息来理解哪里出错了并可能尝试其他方案。像ENOENT文件不存在、EACCES权限不足这类系统错误码转换成明确的英文描述能极大提升交互体验。此外在生产环境中路径安全校验是必须的绝不能允许服务随意读取文件系统的任何位置。3.3 组装MCP服务器工具定义和实现都有了现在需要将它们集成到MCP服务器实例中。创建src/server.ts// src/server.ts import { Server } from “modelcontextprotocol/sdk/server/index.js”; import { StdioServerTransport } from “modelcontextprotocol/sdk/server/stdio.js”; import { readFileTool, executeReadFile } from “./tools/readFile.js”; // 注意导入编译后的JS文件或使用ts-node等 // 1. 创建Server实例 // 需要提供服务器名称和版本这些信息会告知客户端。 const server new Server( { name: “simple-file-reader”, version: “0.1.0”, }, { // 可选的服务器能力声明我们在这里声明工具。 capabilities: { tools: {}, // 工具列表将在后面通过setRequestHandler动态关联这里先留空。 }, } ); // 2. 注册工具请求处理器 // 当客户端调用 read_file 工具时这个函数会被触发。 server.setRequestHandler(“tools/call”, async (request) { // 根据工具名路由到不同的处理函数 if (request.params.name readFileTool.name) { // 使用Zod Schema验证客户端传入的参数 const args readFileTool.inputSchema.parse(request.params.arguments); // 执行我们的业务逻辑 const content await executeReadFile(args); // 返回结果给客户端。MCP协议要求工具调用返回一个包含content的数组。 // 每个content项可以包含type如“text”和具体的值。 return { content: [ { type: “text”, text: content, }, ], }; } // 如果收到未知的工具调用请求抛出错误 throw new Error(Unknown tool: ${request.params.name}); }); // 3. 启动服务器使用Stdio传输层 // 这是MCP Server的标准运行方式通过标准输入输出与父进程如AI客户端通信。 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(“MCP File Reader Server running on stdio…”); } main().catch((error) { console.error(“Server fatal error:”, error); process.exit(1); });关键点解析server.setRequestHandler(“tools/call”, …)是核心。它监听着所有工具调用请求。当请求进来时我们首先根据request.params.name判断是哪个工具然后用对应的Zod Schema去验证参数。验证通过后才执行业务函数。最后按照MCP协议规定的格式返回结果。这种模式使得添加新工具变得非常容易——只需定义新的工具Schema和函数并在此处添加一个if分支即可。4. 配置、运行与在Claude Desktop中测试服务器代码写好了但它如何被AI客户端发现和调用呢这就需要配置文件。不同的客户端配置方式略有不同我们以目前集成度较高的Claude Desktop为例。4.1 创建MCP服务器配置文件对于Claude Desktop它会在特定目录查找MCP服务器的配置。我们需要创建一个JSON文件来告诉Claude“我有一个叫simple-file-reader的服务器你可以通过运行这个Node.js脚本来启动它。”在Claude Desktop的配置目录下通常是~/Library/Application Support/Claude或%APPDATA%\Claude找到或创建claude_desktop_config.json文件。其结构如下{ “mcpServers”: { “simple-file-reader”: { “command”: “node”, “args”: [ “/ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/server.js” ], “env”: { “NODE_ENV”: “production” } } } }重要提示command启动服务器的命令这里是node。args命令的参数即我们编译后的JavaScript入口文件路径。必须使用绝对路径。env可选项可以设置环境变量。为了让这个配置生效我们需要先将TypeScript代码编译成JavaScript。在package.json中添加构建脚本{ “scripts”: { “build”: “tsc”, “start”: “node dist/server.js” } }然后运行pnpm run build它会在dist目录下生成编译后的server.js文件。将上述配置中args的路径替换为你本地dist/server.js的绝对路径。4.2 运行与调试技巧配置完成后重启Claude Desktop。如果一切正常Claude Desktop会在后台启动我们的MCP服务器进程。如何验证呢查看日志Claude Desktop通常有日志输出位置。在macOS上你可以通过Console.app查看。更直接的方式是在我们的服务器代码中使用console.error输出信息如上例中的“MCP File Reader Server running on stdio…”这些信息会输出到标准错误流可以被客户端捕获并记录。在Claude中尝试打开Claude Desktop新建一个对话。理论上Claude现在应该知道它多了一个read_file工具。你可以尝试用自然语言说“请读取我桌面上的notes.txt文件。” 或者更直接地“使用read_file工具路径是/Users/YourName/Desktop/notes.txt。”处理常见启动错误命令未找到确保node在系统PATH中或者使用node的绝对路径如/usr/local/bin/node。文件路径错误确保args中的JS文件路径绝对正确并且文件存在。权限问题确保Claude Desktop有权限执行该Node.js脚本和读取目标文件。踩坑实录我第一次配置时使用了相对路径“./dist/server.js”这导致了启动失败。因为Claude Desktop启动子进程时其当前工作目录CWD可能不是项目目录。因此在配置文件中所有路径都应使用绝对路径这是最稳妥的做法。另一个坑是如果服务器代码有语法错误或启动时崩溃Claude Desktop可能会静默失败。此时最有效的调试方法是先脱离客户端直接在终端用node dist/server.js运行服务器看是否能正常启动并等待输入。4.3 进阶添加目录列表工具一个只能读单个文件的服务有点单薄。让我们快速扩展一下添加一个list_directory工具让AI能先“浏览”目录结构再决定读哪个文件。这更符合真实的使用场景。创建src/tools/listDirectory.ts// src/tools/listDirectory.ts import { z } from “zod”; import { promises as fs } from “fs”; import path from “path”; export const ListDirectoryArgsSchema z.object({ path: z.string().describe(“The path to the directory to list. Defaults to current directory if not provided.”).optional(), }); export type ListDirectoryArgs z.infertypeof ListDirectoryArgsSchema; export const listDirectoryTool { name: “list_directory”, description: “List files and directories within a specified directory.”, inputSchema: ListDirectoryArgsSchema, }; export async function executeListDirectory(args: ListDirectoryArgs): Promisestring { const targetPath args.path ? path.resolve(args.path) : process.cwd(); try { const items await fs.readdir(targetPath, { withFileTypes: true }); const result items.map((dirent) { const type dirent.isDirectory() ? “[DIR] ” : “[FILE]”; return ${type} ${dirent.name}; }).join(“\n”); return Contents of directory: ${targetPath}\n\n${result}; } catch (error: any) { if (error.code “ENOENT”) { throw new Error(Directory not found: ${targetPath}); } else if (error.code “ENOTDIR”) { throw new Error(The path is not a directory: ${targetPath}); } else if (error.code “EACCES”) { throw new Error(Permission denied when accessing directory: ${targetPath}); } throw new Error(Failed to list directory: ${error.message}); } }然后在src/server.ts中导入并注册这个新工具// 在server.ts顶部导入 import { listDirectoryTool, executeListDirectory } from “./tools/listDirectory.js”; // 在 server.setRequestHandler 中增加一个分支 server.setRequestHandler(“tools/call”, async (request) { if (request.params.name readFileTool.name) { // … 原有逻辑 … } // 新增list_directory工具处理 if (request.params.name listDirectoryTool.name) { const args listDirectoryTool.inputSchema.parse(request.params.arguments); const listing await executeListDirectory(args); return { content: [ { type: “text”, text: listing, }, ], }; } throw new Error(Unknown tool: ${request.params.name}); });重新构建 (pnpm run build) 并重启Claude Desktop现在你的AI助手就同时具备了浏览目录和读取文件的能力。你可以这样使用“先列出我的项目根目录然后帮我读取src/server.ts文件。”
返回列表