ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手写MCP文件读写Server:为AI大模型打造安全可控的本地文件操作能力

手写MCP文件读写Server:为AI大模型打造安全可控的本地文件操作能力 1. 项目概述为什么大模型需要“手”去“触摸”硬盘最近在折腾大模型应用开发的朋友估计都绕不开一个词MCP Server。你可能已经用上了各种现成的MCP工具比如读取网页、查询天气但有没有想过如果能让大模型直接、安全地操作你本地硬盘上的文件会是什么场景想象一下你只需要对大模型说“帮我把上周的会议纪要整理成摘要并保存到‘工作总结’文件夹”它就能自动完成——这不再是科幻。今天我们就来动手实现这个核心能力手写一个文件读写的MCP Server让大模型真正拥有“触摸”你硬盘的“手”。简单来说MCPModel Context Protocol是大模型与外部工具和数据的“接线员”。一个MCP Server就是一个专门的服务它定义了一套标准接口让大模型可以安全、可控地调用它背后的功能。我们这次要做的就是一个专门处理文件读、写、列表等操作的Server。这不仅仅是调用一个API那么简单它涉及到权限边界、安全沙箱、数据格式转换等一系列工程问题。为什么非要自己写因为现成的文件操作MCP可能不符合你的具体安全策略或者你想深度定制操作逻辑比如只允许操作特定目录、自动备份修改前的文件等。自己动手才能完全掌控大模型与你的数据世界交互的每一道关卡。这个项目适合谁如果你是对大模型应用开发感兴趣的开发者已经了解了基本的API调用想深入Agent或工具调用层或者你是某个垂直领域的从业者希望将大模型能力深度集成到自己的文件管理、知识库构建等 workflows 中那么跟着走一遍这个从零到一的构建过程会让你对MCP的机制、安全设计和系统集成有透彻的理解。我们将使用最通用的技术栈TypeScript/Node.js来构建确保思路可以平移到Python、Go等其他语言。核心不是语法而是设计理念和避坑经验。2. 核心设计在安全笼子里给大模型一把“钥匙”在让大模型操作你的文件之前第一个蹦进脑子的问题肯定是这安全吗太危险了吧没错直接给大模型一个rm -rf /的权限无疑是灾难。因此我们整个MCP Server的设计核心就是**“最小权限原则”和“操作透明化”**。我们不是给大模型开放一个终端而是为它精心打造一套仅包含几个特定动作的、有严格边界和审计日志的“工具套件”。2.1 协议与接口设计定义大模型能“说”的话MCP协议的核心是工具Tools和资源Resources。对于文件读写Server我们主要定义工具。工具定义我们需要告诉大模型我这个Server提供了哪些“手部动作”。至少需要三个read_file读取文件内容。输入是文件路径path输出是文件内容字符串。write_file写入或创建文件。输入是文件路径path和内容content输出是操作成功状态。list_directory列出目录内容。输入是目录路径path输出是文件/子目录列表。在设计工具输入时要尽可能明确和受限。例如path参数可以设计为只接受相对路径相对于一个预先配置好的根目录或者必须匹配某个白名单模式从源头杜绝跨目录访问。通信协议MCP Server通常通过stdio标准输入输出或HTTP与MCP客户端如Claude Desktop、支持MCP的AI应用通信。我们选择stdio因为它部署简单适合本地一体化应用。通信消息是JSON-RPC格式。这意味着我们的Server需要持续监听process.stdin解析收到的JSON-RPC请求调用对应的工具函数再将结果封装成JSON-RPC响应写入process.stdout。2.2 安全沙箱设计划定不可逾越的边界这是项目的重中之重。我们需要在代码层面构建多道防线。根目录锁定Chroot思想Server启动时从一个配置项或环境变量中读取一个绝对路径作为BASE_DIR例如/Users/YourName/AIManagedDocs。所有文件操作的工具函数在解析完路径参数后第一件事就是将用户传入的路径与BASE_DIR进行解析和校验确保最终的操作路径不会逃逸出BASE_DIR。这可以通过Node.js的path.resolve和path.relative方法来实现。如果解析后的路径不在BASE_DIR下直接返回错误。const path require(path); const BASE_DIR process.env.MCP_FILE_BASE || /safe/root; function resolveSafePath(userPath) { const resolvedPath path.resolve(BASE_DIR, userPath); const relativePath path.relative(BASE_DIR, resolvedPath); // 检查是否试图向上穿越根目录 if (relativePath.startsWith(..) || path.isAbsolute(relativePath)) { throw new Error(Access denied: Path outside of allowed directory.); } return resolvedPath; }操作白名单与黑名单可以在BASE_DIR内进一步限制。例如通过一个配置文件设置禁止写入的目录如/system/backup或只允许读取特定扩展名的文件如.txt,.md,.json。在工具函数执行前增加一层校验。操作审计日志所有成功或失败的操作都必须以结构化的格式如JSON记录到日志文件或发送到监控服务。日志至少包含时间戳、工具名、请求路径解析前后、用户或会话ID、操作结果。这样即使出了问题也能快速追溯。2.3 错误处理与用户反馈让大模型“知道”发生了什么大模型需要清晰的操作反馈来决定下一步动作。我们的工具函数不能只在出错时抛出异常而应该返回结构化的错误信息。成功响应{“success”: true, “content”: “文件内容...”}或{“success”: true, “files”: […]}错误响应{“success”: false, “error”: “错误类型”, “message”: “人类可读的描述”}错误类型可以细分例如“PATH_NOT_FOUND”,“PERMISSION_DENIED”,“INVALID_ENCODING”。这样大模型在收到错误后可以尝试更精确的补救措施比如请求一个存在的路径而不是笼统地报告“出错了”。3. 分步实现从零搭建一个健壮的MCP文件Server理论说完了我们开始动手。我们将使用TypeScript和Node.js来构建因为它有丰富的生态和清晰的类型提示有助于构建可靠的服务。3.1 环境准备与项目初始化首先确保你安装了Node.js建议18版本和npm。然后创建一个新目录并初始化项目。mkdir mcp-file-server cd mcp-file-server npm init -y安装核心依赖。我们需要modelcontextprotocol/sdk这是官方提供的SDK能极大简化MCP Server的构建。同时安装TypeScript和相关类型定义。npm install modelcontextprotocol/sdk npm install -D typescript types/node tsx初始化TypeScript配置。npx tsc --init在生成的tsconfig.json中确保“module”设置为“NodeNext”“target”设置为“ES2022”或更高并打开“outDir”选项如“./dist”。3.2 构建Server核心骨架创建一个src/server.ts文件。首先导入SDK并创建Server实例。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from modelcontextprotocol/sdk/types.js; // 1. 创建Server实例 const server new Server( { name: file-operations-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明我们支持工具 }, } );接下来定义我们要暴露的工具列表。这是一个Tool对象的数组每个对象描述一个工具的名称、描述、输入参数模式JSON Schema。const tools: Tool[] [ { name: read_file, description: 读取指定路径的文本文件内容。, inputSchema: { type: object, properties: { path: { type: string, description: 相对于配置根目录的文件路径例如 docs/report.md, }, encoding: { type: string, description: 文件编码默认为 utf-8, default: utf-8, }, }, required: [path], }, }, { name: write_file, description: 将内容写入指定路径的文件。如果文件不存在则创建存在则覆盖。, inputSchema: { type: object, properties: { path: { type: string, description: 相对于配置根目录的文件路径, }, content: { type: string, description: 要写入的文本内容, }, encoding: { type: string, description: 文件编码默认为 utf-8, default: utf-8, }, }, required: [path, content], }, }, { name: list_directory, description: 列出指定目录下的文件和子目录。, inputSchema: { type: object, properties: { path: { type: string, description: 相对于配置根目录的目录路径默认为根目录., default: ., }, }, }, }, ];然后实现我们前面讨论的安全路径解析函数。import * as path from path; import * as fs from fs/promises; const BASE_DIR process.env.MCP_FILE_BASE ? path.resolve(process.env.MCP_FILE_BASE) : path.resolve(process.cwd(), mcp_workspace); function resolveSafePath(userPath: string): string { const resolvedPath path.resolve(BASE_DIR, userPath); const relativePath path.relative(BASE_DIR, resolvedPath); // 安全检查防止目录穿越攻击 if (relativePath.startsWith(..) || path.isAbsolute(relativePath)) { throw new Error(安全违规路径“${userPath}”试图访问根目录“${BASE_DIR}”之外的内容。); } // 可选检查路径是否存在对于write_file的父目录需要单独检查 // 这里我们先不检查留给具体工具函数处理。 return resolvedPath; }3.3 实现工具处理逻辑现在我们需要为Server设置请求处理器。当客户端调用listTools时返回工具列表当调用callTool时执行对应的文件操作。// 处理列出工具的请求 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools, }; }); // 处理调用工具的请求 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; try { switch (name) { case read_file: { const safePath resolveSafePath(args.path as string); const encoding (args.encoding as string) || utf-8; // 检查路径是否为文件且存在 const stats await fs.stat(safePath); if (!stats.isFile()) { throw new Error(路径“${args.path}”不是一个文件。); } const content await fs.readFile(safePath, { encoding: encoding as BufferEncoding }); return { content: [ { type: text, text: 文件读取成功。\n路径${args.path}\n内容\n\\\\n${content}\n\\\, }, ], }; } case write_file: { const safePath resolveSafePath(args.path as string); const encoding (args.encoding as string) || utf-8; const content args.content as string; // 确保目标目录存在 const dir path.dirname(safePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(safePath, content, { encoding: encoding as BufferEncoding }); // 记录审计日志简单示例写入控制台 console.error([AUDIT] WRITE ${safePath} (${content.length} chars)); return { content: [ { type: text, text: 文件写入成功。\n路径${args.path}, }, ], }; } case list_directory: { const targetPath (args.path as string) || .; const safePath resolveSafePath(targetPath); const stats await fs.stat(safePath); if (!stats.isDirectory()) { throw new Error(路径“${targetPath}”不是一个目录。); } const items await fs.readdir(safePath, { withFileTypes: true }); const list items.map((dirent) { const type dirent.isDirectory() ? [DIR] : [FILE]; const name dirent.name; return ${type} ${name}; }).join(\n); return { content: [ { type: text, text: 目录列表${targetPath}\n${list}, }, ], }; } default: throw new Error(未知工具${name}); } } catch (error: any) { // 统一错误处理返回给大模型清晰的信息 return { content: [ { type: text, text: 操作失败${error.message}, }, ], isError: true, }; } });最后启动Server使用stdio传输。async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP 文件读写服务器已启动根目录, BASE_DIR); } runServer().catch((error) { console.error(服务器启动失败, error); process.exit(1); });3.4 编译与运行在package.json中添加启动脚本。scripts: { build: tsc, start: node dist/server.js, dev: tsx watch src/server.ts }现在你可以通过环境变量设置根目录并运行开发版本。export MCP_FILE_BASE/Users/YourName/Documents/AI_Sandbox npm run dev服务器将在标准输入输出上运行等待MCP客户端如配置了MCP的Claude Desktop连接。4. 客户端配置与实战测试构建好Server只是第一步让它真正被大模型使用起来还需要在客户端进行配置。这里以目前支持MCP较成熟的Claude Desktop为例。4.1 配置Claude Desktop找到Claude Desktop的配置文件位置。macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.json编辑这个JSON文件添加我们的MCP Server配置。{ mcpServers: { file-server: { command: node, args: [ /absolute/path/to/your/mcp-file-server/dist/server.js ], env: { MCP_FILE_BASE: /Users/YourName/Documents/AI_Sandbox } } } }关键提示command和args必须指向你编译后的JS文件。使用npm run dev那种tsx方式在开发时方便但在生产配置中更推荐先用npm run build编译成JS然后指向编译后的文件这样无需依赖TypeScript运行时。配置完成后重启Claude Desktop。4.2 与大模型对话测试重启后在Claude Desktop中新建对话你应该能在输入框上方或工具菜单中看到可用的工具。尝试以下对话你“请使用list_directory工具看看根目录下有什么。”Claude会调用工具并返回目录列表。你“请读取README.md文件的内容。”Claude调用read_file返回文件内容。你“请帮我把‘今天天气真好’这句话写入到notes/test.txt文件中。”Claude调用write_file创建文件并写入内容。在这个过程中观察Claude是如何组织请求、解析结果并基于结果进行下一步推理和操作的。你会发现一个设计良好的工具和清晰的错误反馈能让大模型表现得像一个真正理解文件系统的助手。4.3 扩展功能思路基础读写列表功能实现后你可以根据需求扩展这个Server让它更强大、更智能文件搜索工具添加一个search_files工具接收关键词和目录使用glob或递归遍历返回匹配的文件列表和片段。这能让大模型快速定位信息。文件信息工具添加一个get_file_info工具返回文件大小、修改时间、MIME类型等信息。批量操作添加move_files、copy_files工具但务必谨慎设计避免误操作。可以加入“确认”机制或者限制一次操作的文件数量。内容预处理在read_file时如果是特定格式如Markdown、JSON可以尝试解析并返回结构化数据而不仅仅是纯文本方便大模型理解。版本控制集成在write_file前后自动执行git add/commit为每次AI修改留下记录。5. 避坑指南与安全强化在实际开发和部署中你会遇到一些预料之外的问题。以下是我踩过坑后总结的经验。5.1 路径解析的陷阱我们之前的resolveSafePath函数基本够用但在Windows系统或处理符号链接时可能有问题。Windows路径分隔符Node.js的path模块会自动处理但如果你在字符串层面进行手动处理要小心。始终使用path.join(),path.resolve()。符号链接fs.stat会跟随符号链接。如果你不想让Server通过符号链接逃逸出沙箱需要使用fs.lstat和fs.realpath.native进行更严格的检查。一个更健壮的方案是在解析路径后使用fs.realpath.native获取真实路径再检查这个真实路径是否仍在BASE_DIR下。5.2 文件编码与二进制文件我们的工具默认使用UTF-8编码。但如果大模型试图读取一个二进制文件如图片、PDF会得到乱码甚至错误。方案一在read_file中如果检测到文件不是纯文本可以通过简单试探或file-type库可以返回一个错误提示“此文件为二进制格式无法直接读取文本内容”。或者可以返回文件的Base64编码让大模型知道这是一个二进制块。方案二专门为二进制文件设计工具如read_file_binary返回Base64和write_file_binary。这需要大模型客户端能处理这类响应。5.3 性能与资源限制想象一下如果大模型请求list_directory你的整个用户目录或者读取一个几GB的日志文件会发生什么目录列表限制在list_directory中可以限制返回的条目数量比如前100个或者对递归深度进行限制。文件大小限制在read_file中先用fs.stat检查文件大小如果超过一个阈值如10MB直接拒绝并返回错误。大模型通常不需要一次性处理非常大的文件。内存与阻塞文件读写是I/O操作使用fs.promisesAPI是异步的避免阻塞事件循环。但对于超大目录的递归操作仍需小心。5.4 审计日志的实战化之前我们只是把日志打印到控制台。在生产环境中这远远不够。结构化日志使用Winston或Pino等日志库将每条操作记录以JSON格式输出到文件或日志收集系统如Loki、ELK。关键信息除了操作本身还应记录请求的会话ID如果MCP协议传递了的话、来源IP如果是HTTP传输、工具参数脱敏后等。这对于安全事件回溯至关重要。告警对于高风险操作如写入系统目录、删除文件可以设置实时告警。5.5 权限模型的细化我们的BASE_DIR模型是“一刀切”的。更精细的权限控制可以考虑基于角色的访问控制在Server启动时加载一个配置文件定义不同“角色”可能对应不同的大模型会话或API密钥对BASE_DIR下不同子目录的读写权限。这需要MCP客户端在连接时提供身份信息目前标准协议支持有限可能需要自定义。操作前确认对于写、删除等危险操作可以实现一个两阶段提交。工具先返回一个“预执行”结果包含操作详情需要用户或一个确认工具明确确认后才真正执行。这增加了安全性但降低了自动化程度。手写一个MCP Server尤其是文件读写这种“高危”操作是一个绝佳的练习它能强迫你思考大模型与真实世界交互中最核心的问题信任、边界与控制。完成这个项目后你不仅得到了一个有用的工具更重要的是掌握了一套为AI设计安全、可靠“手”和“眼”的方法论。这套方法论可以应用到数据库操作、内部API调用、硬件控制等任何你希望大模型触及的领域。记住给AI能力的同时锁好每一扇不该打开的门是构建下一代AI应用的基础技能。
返回列表