news 2026/8/8 5:35:04

从零构建MCP文件服务器:安全连接大模型与本地文件系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建MCP文件服务器:安全连接大模型与本地文件系统

1. 项目概述:为什么我们需要一个MCP服务器?

最近在和一些做AI应用开发的朋友聊天,发现大家普遍遇到了一个痛点:如何让大语言模型(LLM)安全、高效地访问我们本地的文件系统?无论是想让它帮你分析一份刚下载的财报PDF,还是整理电脑里散乱的代码片段,直接让模型去“看”文件内容,总绕不开安全和权限的坎儿。这时候,模型上下文协议(Model Context Protocol, 简称MCP)就进入了我们的视野。它本质上是一套标准化的“沟通语言”,让AI应用(客户端)能够通过结构化的方式,请求并获取来自各种数据源(服务器)的信息,而文件系统,就是其中最基础、也最实用的数据源之一。

所以,今天我想和你一起动手,从零实现一个MCP文件读取服务器。这个项目听起来有点技术含量,但别担心,我会把它拆解得明明白白。我们不止是写几行代码,更重要的是理解MCP协议的设计哲学,搞清楚服务器与客户端(比如Claude Desktop、Cursor等)之间是如何“握手”和“对话”的。最终,你将获得一个能运行在你本地机器上的服务,它可以安全地让AI助手读取你指定目录下的文本文件(比如.txt,.md,.py),而无需担心隐私泄露或越权访问。这对于提升个人工作效率或构建更智能的本地化AI工具链来说,是一个非常有价值的基建环节。

2. MCP协议核心思想与架构拆解

在动手写代码之前,我们必须先吃透MCP协议的核心。你可以把它想象成AI世界的“USB协议”。不同的AI应用(客户端)就像不同的电脑,而各种数据源(数据库、文件系统、API)就像U盘、移动硬盘等外设。MCP定义了一套标准的“接口形状”和“通信规则”,确保任何符合标准的“外设”都能被任何符合标准的“电脑”即插即用。

2.1 协议基石:资源(Resources)与工具(Tools)

MCP协议的核心抽象只有两个:资源(Resources)工具(Tools)。这是服务器向客户端暴露能力的两种基本方式。

  1. 资源(Resources):代表可供读取的静态或动态数据。每个资源有一个唯一的URI(如file:///home/user/notes/project_plan.md)和对应的MIME类型(如text/markdown)。客户端可以“列出”可用的资源,也可以“读取”特定资源的内容。在我们的文件服务器里,一个文件就是一个资源。

  2. 工具(Tools):代表可供调用的函数或操作。每个工具有一个名称、描述、输入参数(JSON Schema定义)。客户端可以“列出”可用的工具,并“调用”它们。比如,一个“搜索文件”工具,接收关键词参数,返回匹配的文件列表。在我们的初版服务器中,我们会先聚焦于实现资源相关的功能,这是更基础的需求。

2.2 通信桥梁:SSE与Stdio传输层

客户端和服务器之间需要一种双向通信机制。MCP主要支持两种传输层:

  1. Server-Sent Events (SSE):这是一种基于HTTP的协议,服务器可以主动向客户端推送事件。在这种模式下,服务器通常作为一个HTTP服务运行,客户端通过HTTP连接与之通信。它更适合于网络环境下的远程调用。

  2. Stdio (标准输入/输出):这是我们将要采用的模式,也是本地集成中最简单、最直接的方式。服务器作为一个独立的进程启动,通过标准输入(stdin)接收来自客户端的JSON-RPC请求,并通过标准输出(stdout)发送JSON-RPC响应和通知。Claude Desktop默认就使用这种方式加载本地MCP服务器。它的好处是隔离性好,部署简单,非常适合本地文件访问这种场景。

我们的项目将采用Stdio传输层,并遵循JSON-RPC 2.0规范进行所有通信。这意味着我们编写的服务器,本质上是一个不断从stdin读JSON、向stdout写JSON的控制台程序。

2.3 会话流程:初始化与能力交换

一个典型的MCP会话,遵循着清晰的“握手-交互”流程:

  1. 初始化(Initialize):客户端启动服务器进程后,发送的第一个请求必然是initialize。这个请求中包含了客户端的名称、版本、协议版本等信息。服务器必须回应,在回应中声明自己支持哪些能力(比如“资源读取”、“工具调用”),并附上一个唯一的服务器实例ID。

  2. 能力通告(Notifications):初始化完成后,服务器需要立即通过通知(notifications)告诉客户端自己提供了哪些“资源”和“工具”。对于资源,服务器会发送resources/list通知,列出所有资源的URI和类型。对于工具,则发送tools/list通知。客户端收到这些列表后,就知道自己可以向服务器请求什么了。

  3. 请求-响应循环:此后,客户端会根据用户的需求,发送具体的请求。例如:

    • resources/read:请求读取某个URI对应的资源内容。
    • tools/call:请求调用某个工具。 服务器处理这些请求并返回结果。
  4. 心跳与关闭:客户端可能会发送ping请求来检测服务器是否存活。当客户端要关闭时,它会发送shutdown请求,服务器应妥善清理并退出。

理解了这个流程,我们就能勾勒出服务器代码的基本骨架:一个循环,不断解析stdin来的JSON,根据方法名(如initialize,resources/read)路由到对应的处理函数,执行逻辑,然后将结果JSON写入stdout

3. 项目环境搭建与核心依赖选择

工欲善其事,必先利其器。我们选择Node.js作为实现语言,主要是因为其异步IO特性非常适合这种通信密集型服务,而且生态丰富,JSON处理天然友好。当然,你也可以用Python、Go等任何你熟悉的语言,协议的核心思想是通用的。

3.1 初始化项目与安装依赖

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

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

接下来,安装我们最核心的依赖。我们不需要任何特定的“MCP SDK”,因为协议本身足够简洁,我们可以用基础库来构建。

npm install zod @types/node typescript ts-node --save-dev

这里解释一下选型理由:

  • zod:这是一个功能强大、零依赖的TypeScript模式验证库。MCP协议中充满了结构化的JSON数据(请求、响应、参数)。使用Zod来定义和验证这些数据结构,能极大提升代码的健壮性和开发体验,避免因数据格式错误导致的诡异问题。
  • typescript & @types/node & ts-node:我们将使用TypeScript来编写代码,以获得更好的类型安全和代码提示。ts-node用于直接运行.ts文件。

然后,初始化TypeScript配置:

npx tsc --init

你可以根据习惯修改生成的tsconfig.json,一个适用于本项目的简单配置如下:

{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }

3.2 构建协议类型定义

这是保证我们与客户端“说同一种语言”的关键一步。我们在src目录下创建一个protocol.ts文件,用Zod来定义MCP协议的核心类型。

// src/protocol.ts import { z } from 'zod'; // 1. 定义通用结构 export const URI = z.string().url(); // 资源URI,实际上我们主要用file://协议 export const MimeType = z.string(); // 如 "text/plain", "text/markdown" // 资源模板,用于动态资源路径 export const ResourceTemplate = z.object({ uriTemplate: z.string(), name: z.string(), description: z.string().optional(), mimeType: MimeType, }); // 工具输入参数定义 export const ToolInputSchema = z.record(z.any()); // 简化,实际应为JSON Schema对象 export const Tool = z.object({ name: z.string(), description: z.string(), inputSchema: ToolInputSchema, }); // 2. 定义请求(Requests)和通知(Notifications)的结构 // 初始化请求 export const InitializeRequest = z.object({ method: z.literal('initialize'), params: z.object({ protocolVersion: z.string(), clientInfo: z.object({ name: z.string(), version: z.string(), }).optional(), capabilities: z.record(z.any()).optional(), // 客户端能力,我们初版暂不深究 }), jsonrpc: z.literal('2.0'), id: z.number().or(z.string()), }); // 初始化成功响应 export const InitializeResult = z.object({ protocolVersion: z.string(), serverInfo: z.object({ name: z.string(), version: z.string(), }), capabilities: z.object({ resources: z.object({}).optional(), // 表明支持资源功能 tools: z.object({}).optional(), // 表明支持工具功能 }), instanceId: z.string().uuid(), }); // 读取资源请求 export const ReadResourceRequest = z.object({ method: z.literal('resources/read'), params: z.object({ uri: URI, }), jsonrpc: z.literal('2.0'), id: z.number().or(z.string()), }); // 读取资源成功响应 export const ReadResourceResult = z.object({ contents: z.array(z.object({ uri: URI, mimeType: MimeType, // 注意:文本内容放在 `text` 字段,二进制内容用 `blob` (base64编码) text: z.string().optional(), blob: z.string().optional(), })), }); // 列出资源通知(服务器主动发送) export const ListResourcesNotification = z.object({ method: z.literal('notifications/resources/list'), params: z.object({ resources: z.array(z.object({ uri: URI, name: z.string().optional(), description: z.string().optional(), mimeType: MimeType, })), }), jsonrpc: z.literal('2.0'), }); // 3. 定义错误响应 export const ErrorResponse = z.object({ jsonrpc: z.literal('2.0'), id: z.number().or(z.string()).or(z.null()), error: z.object({ code: z.number(), message: z.string(), data: z.any().optional(), }), }); // 4. 导出类型推断 export type InitializeRequest = z.infer<typeof InitializeRequest>; export type InitializeResult = z.infer<typeof InitializeResult>; export type ReadResourceRequest = z.infer<typeof ReadResourceRequest>; export type ReadResourceResult = z.infer<typeof ReadResourceResult>; export type ListResourcesNotification = z.infer<typeof ListResourcesNotification>; // ... 其他类型推断

这个文件是我们的“协议字典”,确保了进出我们服务器的每一条消息都符合MCP的规范。使用Zod的好处是,我们不仅能获得TypeScript类型,还能在运行时进行验证,如果客户端发来了畸形数据,我们能立刻发现并返回友好的错误。

注意:以上是核心类型的精简版。完整的MCP协议还包含tools/listtools/callresources/list(客户端请求)、ping/pong等。为了聚焦主线,我们先实现资源读取相关部分。你可以根据官方协议文档逐步补充。

4. 服务器核心逻辑实现

现在进入最核心的部分:编写服务器主循环。我们在src目录下创建server.ts

4.1 搭建服务器骨架与主循环

服务器的核心是一个永不结束(直到收到shutdown)的循环,监听process.stdin,解析JSON-RPC消息,路由处理,再写入process.stdout

// src/server.ts import * as readline from 'readline'; import { InitializeRequest, InitializeResult, ReadResourceRequest, ReadResourceResult, ListResourcesNotification, ErrorResponse, } from './protocol'; // 定义配置接口,例如允许访问的根目录 interface ServerConfig { rootDirectory: string; allowedExtensions: string[]; // 如 ['.txt', '.md', '.py', '.js'] } // 全局状态 let serverInstanceId: string; let config: ServerConfig; // 创建readline接口,用于逐行读取stdin const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false, // 关键!避免readline处理输出,我们直接写stdout }); // 主消息处理循环 rl.on('line', async (line) => { try { const message = JSON.parse(line); await handleMessage(message); } catch (error) { console.error('Failed to parse or handle message:', error); // 发送一个JSON-RPC错误响应 sendError(null, -32700, 'Parse error', { originalLine: line }); } }); // 发送JSON-RPC响应或通知的辅助函数 function sendResponse(id: number | string | null, result: any) { const response = { jsonrpc: '2.0', id, result, }; process.stdout.write(JSON.stringify(response) + '\n'); } function sendError(id: number | string | null, code: number, message: string, data?: any) { const errorResponse: z.infer<typeof ErrorResponse> = { jsonrpc: '2.0', id, error: { code, message, data }, }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); } function sendNotification(method: string, params: any) { const notification = { jsonrpc: '2.0', method, params, }; process.stdout.write(JSON.stringify(notification) + '\n'); } // 核心消息路由器 async function handleMessage(message: any) { // 首先,根据方法名路由 switch (message.method) { case 'initialize': await handleInitialize(message); break; case 'resources/read': await handleReadResource(message); break; case 'shutdown': await handleShutdown(message); break; case 'ping': handlePing(message); break; // 可以在这里添加更多方法,如 `tools/call` default: sendError(message.id, -32601, `Method not found: ${message.method}`); } }

这个骨架搭建了基本的I/O和路由机制。readline模块帮助我们以行为单位读取stdin,因为JSON-RPC消息通常以换行符分隔。terminal: false这个设置至关重要,它阻止readline接管stdout的格式化,让我们能直接输出纯净的JSON。

4.2 实现初始化(initialize)处理

初始化是会话的起点,我们必须在这里设置服务器状态,并告知客户端我们的能力。

// 在 server.ts 中继续添加 import { randomUUID } from 'crypto'; // 初始化配置(实际项目中可以从环境变量或配置文件读取) config = { rootDirectory: process.env.MCP_ROOT_DIR || '/Users/yourname/mcp_shared', // 替换为你的目录 allowedExtensions: ['.txt', '.md', '.py', '.js', '.json', '.csv'], }; async function handleInitialize(request: any) { // 1. 用Zod验证请求结构 const parsedRequest = InitializeRequest.safeParse(request); if (!parsedRequest.success) { sendError(request.id, -32600, 'Invalid Request', parsedRequest.error.errors); return; } // 2. 生成服务器实例ID serverInstanceId = randomUUID(); // 3. 构建响应 const result: InitializeResult = { protocolVersion: parsedRequest.data.params.protocolVersion, serverInfo: { name: 'mcp-file-server', version: '0.1.0', }, capabilities: { resources: {}, // 声明支持资源功能 // tools: {}, // 初版暂不声明工具支持 }, instanceId: serverInstanceId, }; sendResponse(request.id, result); // 4. 初始化后,立即发送资源列表通知 await notifyResourceList(); } async function notifyResourceList() { // 扫描配置的根目录,获取文件列表 const resourceList = await scanDirectoryForResources(config.rootDirectory, config.allowedExtensions); const notification: ListResourcesNotification = { method: 'notifications/resources/list', params: { resources: resourceList, }, jsonrpc: '2.0', }; sendNotification(notification.method, notification.params); }

handleInitialize函数做了四件事:验证请求格式、生成唯一会话ID、返回服务器信息和支持的能力、最后主动推送当前可用的资源列表。notifyResourceList函数依赖于一个尚未实现的scanDirectoryForResources函数,它的职责是遍历指定目录,将符合条件的文件转换为MCP资源描述。

4.3 实现目录扫描与资源列表构建

这是连接本地文件系统和MCP资源抽象的关键一步。我们需要递归扫描目录,过滤文件,并生成规范的URI。

// src/file-utils.ts import * as fs from 'fs/promises'; import * as path from 'path'; import { MimeType } from './protocol'; // 扩展名到MIME类型的简单映射 const EXTENSION_TO_MIME: Record<string, string> = { '.txt': 'text/plain', '.md': 'text/markdown', '.py': 'text/x-python', '.js': 'text/javascript', '.json': 'application/json', '.csv': 'text/csv', '.html': 'text/html', '.css': 'text/css', }; export interface ResourceDescriptor { uri: string; name?: string; description?: string; mimeType: string; } export async function scanDirectoryForResources( rootDir: string, allowedExtensions: string[] ): Promise<ResourceDescriptor[]> { const resources: ResourceDescriptor[] = []; async function scan(dir: string, relativePath = '') { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const currentRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name; if (entry.isDirectory()) { // 递归扫描子目录 await scan(fullPath, currentRelativePath); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (allowedExtensions.includes(ext)) { // 构建符合MCP规范的file:// URI // 注意:需要将路径转换为绝对路径,并确保格式正确 const uri = `file://${path.resolve(fullPath)}`; // 简单处理:文件名(不含扩展名)作为资源名 const name = path.basename(entry.name, ext); const mimeType = EXTENSION_TO_MIME[ext] || 'application/octet-stream'; resources.push({ uri, name, mimeType, // 可以在这里添加更多逻辑,例如从文件首行提取description }); } } } } await scan(rootDir); return resources; } // 根据URI读取文件内容 export async function readResourceContent(uri: string): Promise<string> { // 从 file:// URI 中提取本地文件路径 if (!uri.startsWith('file://')) { throw new Error(`Unsupported URI scheme: ${uri}`); } const filePath = uri.slice('file://'.length); // 简单的安全校验:确保请求的文件路径在配置的根目录下(防止目录遍历攻击) const resolvedRoot = path.resolve(config.rootDirectory); const resolvedPath = path.resolve(filePath); if (!resolvedPath.startsWith(resolvedRoot)) { throw new Error(`Access denied: File is outside of allowed root directory.`); } // 读取文件内容 const content = await fs.readFile(filePath, 'utf-8'); return content; }

scanDirectoryForResources函数递归遍历目录,为每个允许扩展名的文件创建一个资源描述符。这里有两个关键点:

  1. URI生成:我们使用file://协议头加上文件的绝对路径来构造URI。这是MCP客户端识别本地文件资源的通用方式。
  2. MIME类型映射:根据文件扩展名推断MIME类型,帮助客户端正确理解内容格式。

readResourceContent函数是实际读取文件的函数,它包含了一个至关重要的安全校验:检查请求的文件路径是否在我们声明的根目录之下。这是防止恶意客户端通过构造类似file:///etc/passwd的URI来越权访问系统文件的核心防线。

4.4 实现资源读取(resources/read)处理

现在,我们可以实现最核心的功能:响应客户端的文件读取请求。

// 在 server.ts 中继续添加 import { readResourceContent } from './file-utils'; import { ReadResourceRequest, ReadResourceResult } from './protocol'; async function handleReadResource(request: any) { // 1. 验证请求 const parsedRequest = ReadResourceRequest.safeParse(request); if (!parsedRequest.success) { sendError(request.id, -32600, 'Invalid Request', parsedRequest.error.errors); return; } const { uri } = parsedRequest.data.params; try { // 2. 读取文件内容 const textContent = await readResourceContent(uri); // 3. 构建响应 const result: ReadResourceResult = { contents: [{ uri, mimeType: EXTENSION_TO_MIME[path.extname(new URL(uri).pathname).toLowerCase()] || 'text/plain', text: textContent, // 因为是文本文件,我们使用text字段 }], }; sendResponse(request.id, result); } catch (error: any) { // 4. 错误处理 console.error(`Failed to read resource ${uri}:`, error); let errorCode = -32000; // 默认服务器错误 let errorMessage = 'Server error'; if (error.code === 'ENOENT') { errorCode = -32602; // 类似于 Invalid params, 资源未找到 errorMessage = `Resource not found: ${uri}`; } else if (error.message.includes('Access denied')) { errorCode = -32602; errorMessage = error.message; } sendError(request.id, errorCode, errorMessage, { uri }); } }

这个处理函数清晰展示了JSON-RPC服务器的模式:验证、执行业务逻辑、返回结果或错误。错误处理部分特别重要,我们将文件系统错误(如文件不存在)和业务逻辑错误(如越权访问)映射为合适的JSON-RPC错误码,让客户端能理解发生了什么。

4.5 实现其他必要方法

为了让服务器更完整,我们还需要实现shutdownping

// 在 server.ts 中继续添加 async function handleShutdown(request: any) { console.log('Received shutdown request.'); sendResponse(request.id, null); // 先响应 // 可以在这里进行一些清理工作 process.exit(0); // 然后退出进程 } function handlePing(request: any) { // ping请求只需要返回一个pong响应,result通常为null或空对象 sendResponse(request.id, {}); }

5. 配置、运行与客户端连接测试

服务器代码已经完成,接下来我们需要让它能运行起来,并连接到真正的MCP客户端(如Claude Desktop)进行测试。

5.1 创建启动脚本与配置

首先,在package.json中添加启动脚本:

{ "scripts": { "start": "ts-node src/server.ts", "build": "tsc", "serve": "node dist/server.js" } }

为了更灵活地配置服务器,我们可以创建一个简单的配置文件或使用环境变量。这里我们使用环境变量,在启动脚本前设置:

# 在命令行中设置(Linux/macOS) export MCP_ROOT_DIR="/path/to/your/shared/folder" export MCP_ALLOWED_EXTENSIONS=".txt,.md,.py,.js" npm start # 或者在package.json脚本中嵌入(Windows需调整语法) # "start": "cross-env MCP_ROOT_DIR=/path/to/shared ts-node src/server.ts"

同时,我们需要修改server.ts开头的配置部分,使其读取环境变量:

// server.ts 顶部 const config: ServerConfig = { rootDirectory: process.env.MCP_ROOT_DIR || path.join(os.homedir(), 'mcp_shared'), allowedExtensions: process.env.MCP_ALLOWED_EXTENSIONS?.split(',').map(ext => ext.trim()) || ['.txt', '.md', '.py', '.js', '.json'], };

5.2 配置Claude Desktop进行连接测试

Claude Desktop是目前体验MCP服务器最方便的工具之一。我们需要为它创建一个服务器配置文件。

  1. 找到Claude Desktop的配置目录

    • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows:%APPDATA%\Claude\claude_desktop_config.json
    • Linux:~/.config/Claude/claude_desktop_config.json
  2. 编辑配置文件:如果文件不存在就创建它。添加我们的MCP服务器配置。

{ "mcpServers": { "my-file-server": { "command": "node", "args": [ "/absolute/path/to/your/mcp-file-server/dist/server.js" // 如果是ts-node开发,可以是 "/path/to/ts-node", "/absolute/path/to/src/server.ts" ], "env": { "MCP_ROOT_DIR": "/path/to/your/shared/folder", "MCP_ALLOWED_EXTENSIONS": ".txt,.md,.py" } } } }

关键配置解析

  • command: 启动服务器的命令,这里是node
  • args: 传递给命令的参数。如果你用ts-node直接运行TypeScript源码,第一个参数就是ts-node的路径,第二个是server.ts的绝对路径。如果先编译成JS (npm run build),则指向编译后的server.js
  • env: 设置环境变量,这样我们的服务器就能读到MCP_ROOT_DIR等配置。
  1. 重启Claude Desktop:保存配置文件后,完全关闭并重新打开Claude Desktop。

5.3 测试与验证

重启后,Claude Desktop会自动启动我们配置的MCP服务器进程。你可以通过以下方式验证:

  1. 查看Claude Desktop日志:在Claude Desktop中,通常可以通过Cmd/Ctrl + Shift + I打开开发者工具,在控制台(Console)里可以看到MCP服务器初始化的日志,比如[MCP] Server initialized: my-file-server。如果启动失败,这里也会有错误信息,是排查问题的第一现场。

  2. 与Claude对话:在聊天框中,你可以尝试输入:

    • “你都能访问哪些文件?” – Claude可能会调用服务器列出的资源列表来回答。
    • “请读取project_plan.md文件并总结要点。” – Claude会发送resources/read请求给我们的服务器,获取文件内容后进行分析。
  3. 服务器端日志:为了调试,我们可以在服务器代码的关键位置添加console.log(输出到stderr,避免干扰JSON-RPC通信),然后在Claude Desktop的开发者工具控制台查看。

    // 在handleInitialize, handleReadResource等处添加 console.error(`[Debug] Handling ${message.method} request...`);

6. 高级功能拓展与优化建议

一个基础可用的文件服务器已经完成了。但要让它在生产环境中更可靠、更强大,我们还需要考虑很多方面。

6.1 实现工具(Tools)功能

资源是“拉取”模式,工具则是“调用”模式。让我们添加一个简单的文件搜索工具。

首先,在protocol.ts中补充工具相关的类型定义(Tool,ListToolsNotification,CallToolRequest等)。然后在初始化时,在capabilities中声明支持tools,并在初始化后发送notifications/tools/list通知。

接着,实现一个tools/call处理器:

// 在 handleMessage 的 switch 中添加 case 'tools/call': await handleCallTool(message); break; async function handleCallTool(request: any) { // 验证请求... const { name, arguments: args } = request.params; switch (name) { case 'search_files': // 实现搜索逻辑:遍历资源,检查文件名或内容是否包含关键词 const keyword = args.keyword; const allResources = await scanDirectoryForResources(...); const matched = allResources.filter(r => r.name?.includes(keyword) || (await readResourceContent(r.uri)).includes(keyword) ); sendResponse(request.id, { matches: matched.map(r => ({uri: r.uri, name: r.name})) }); break; default: sendError(request.id, -32601, `Tool not found: ${name}`); } }

6.2 性能优化:资源列表缓存与增量更新

每次客户端连接都全盘扫描目录,对于文件很多的情况效率低下。我们可以引入缓存机制。

  • 缓存资源列表:在服务器启动或目录变化时扫描一次,将结果缓存起来。notifyResourceList直接返回缓存。
  • 监听文件系统变化:使用fs.watch或更高效的库(如chokidar)监听配置的根目录。当文件增删改时,更新缓存,并主动向客户端发送notifications/resources/list通知,实现资源列表的动态更新。
  • 分页与过滤:如果资源数量巨大,可以在resources/list请求(如果实现)或工具调用中支持分页和过滤参数。

6.3 增强安全性与错误处理

  1. 更严格的路径校验:我们之前的路径检查是基础。更安全的做法是使用path.relative()计算相对路径,并检查是否包含..
    const relativePath = path.relative(config.rootDirectory, resolvedFilePath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { throw new Error('Access denied: Path traversal attempt detected.'); }
  2. 文件大小限制:防止客户端读取超大文件拖垮服务器。
    const stats = await fs.stat(filePath); if (stats.size > 10 * 1024 * 1024) { // 例如10MB throw new Error('File too large.'); }
  3. 更精细的MIME类型处理:可以尝试通过文件内容(魔数)而不仅仅是扩展名来判断类型,更准确。
  4. 请求限流与超时:防止客户端恶意发送大量请求。

6.4 支持二进制文件与分块传输

目前我们只处理文本文件(text字段)。对于图片、PDF等二进制文件,MCP协议支持通过blob字段(Base64编码)传输。我们需要在readResourceContent中根据MIME类型决定读取方式(utf-8binary),并转换为Base64。

对于超大文件,可以考虑实现分块读取(range请求),但这需要扩展MCP协议或自定义工具。

6.5 日志与监控

为服务器添加结构化的日志(如使用winstonpino库),记录请求方法、URI、处理时间、错误等信息,便于运维和问题排查。可以将日志输出到文件或标准错误流。

7. 常见问题与排查技巧实录

在实际开发和集成过程中,你肯定会遇到各种问题。以下是我踩过的一些坑和解决方案:

问题1:Claude Desktop连接服务器失败,日志显示"Failed to start server""Exited with code 1"

  • 排查思路
    1. 路径问题:检查claude_desktop_config.json中的commandargs路径是否是绝对路径?在macOS/Linux上是否可执行?可以尝试在终端中手动运行该命令来验证。
    2. 权限问题:确保Node.js脚本有执行权限,并且对MCP_ROOT_DIR指向的目录有读取权限。
    3. 依赖缺失:如果你的服务器需要ts-node,确保它在全局或项目本地已安装,并且在args中路径正确。最稳妥的方式是在项目内编译成JS (npm run build),然后直接运行node dist/server.js
    4. 立即崩溃:服务器可能在启动阶段就因未捕获的异常而崩溃。在服务器入口点添加try-catch,并将错误打印到console.error,然后在Claude Desktop控制台查看具体错误信息。

问题2:Claude能连接,但说“没有可用的资源”或读取文件失败。

  • 排查思路
    1. 资源列表未发送:确认服务器在initialize响应后,是否立即发送了notifications/resources/list。检查Claude Desktop控制台网络或MCP日志,看是否有这条出站通知。
    2. URI格式错误:检查服务器生成的file://URI是否正确。在scanDirectoryForResources函数中打印出生成的URI看看。注意Windows路径的file://格式通常是file:///C:/Users/...(三个斜杠)。
    3. MIME类型不匹配:客户端可能根据MIME类型决定如何处理内容。确保你映射的MIME类型是通用的(如text/plain最安全)。可以尝试将所有文本文件都设为text/plain进行测试。
    4. 安全校验拦截:确认你要读取的文件确实在MCP_ROOT_DIR目录下,并且路径没有触发我们的安全规则。可以在readResourceContent函数中打印resolvedPathresolvedRoot进行比对。

问题3:服务器进程僵死或无响应。

  • 排查思路
    1. 未处理异常:确保所有异步操作都有.catch或放在try-catch中,避免未处理的Promise拒绝导致进程崩溃。可以在进程顶层添加:
      process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); });
    2. 输入流阻塞:检查readlinestdin读取逻辑是否正确。确保在收到shutdown请求后正确退出循环,避免僵尸进程。
    3. 长时间操作阻塞主线程:文件扫描或大文件读取如果是同步的,会阻塞处理其他请求。确保所有I/O操作都是异步的(使用fs.promises)。

问题4:想调试服务器端的详细通信过程。

  • 技巧:可以将所有经过stdin/stdout的JSON消息都记录到日志文件中。在handleMessage开头和sendResponse前,将消息对象用JSON.stringify美化后输出到console.error(或一个日志文件)。这样你就能看到完整的请求-响应对话,对于理解协议流程和定位问题非常有帮助。

实现一个MCP服务器,最有趣的不是代码本身,而是这种“标准化接口”思维。一旦你理解了资源和工具这两个核心抽象,以及JSON-RPC over Stdio的通信模式,你就可以将任何本地能力——数据库、内部API、硬件状态——封装成AI可安全调用的服务。这个文件服务器只是一个起点,你可以在此基础上,轻松扩展出“数据库查询服务器”、“项目管理工具服务器”甚至“智能家居控制服务器”,真正让大模型成为你个人工作流和数字生活的强大副驾驶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/8 5:34:55

JavaScript 快速入门实战:2小时掌握核心语法与DOM交互

JavaScript 是前端开发的基石&#xff0c;也是现代 Web 应用的核心。无论你是想入门前端&#xff0c;还是希望系统性地夯实基础&#xff0c;一份高效、直接、能快速上手的教程都至关重要。这篇文章不是泛泛而谈的概念介绍&#xff0c;而是为你准备的一份“实战驱动”的快速入门…

作者头像 李华
网站建设 2026/8/8 5:32:21

从Prompt到Skill:AI技能工程化实践与架构设计指南

1. 项目概述&#xff1a;从“技能”到“创造者”的范式转变最近在跟几个做AI应用开发的朋友聊天&#xff0c;大家不约而同地提到了一个词&#xff1a;skill-creator。这听起来像是一个工具或者框架的名字&#xff0c;但深入聊下去才发现&#xff0c;它背后代表的是一种全新的工…

作者头像 李华
网站建设 2026/8/8 5:32:02

NightX-Client终极指南:3步打造你的赛博朋克Minecraft体验

NightX-Client终极指南&#xff1a;3步打造你的赛博朋克Minecraft体验 【免费下载链接】NightX-Client Minecraft Forge 1.8.9 hacked client, Based on LiquidBounce 项目地址: https://gitcode.com/gh_mirrors/ni/NightX-Client 想要为你的Minecraft游戏注入未来科技感…

作者头像 李华
网站建设 2026/8/8 5:31:37

Java应用打包利器jpackage:从JAR到原生安装包的完整指南

1. 项目概述&#xff1a;为什么我们需要一个独立的打包工具&#xff1f;如果你是一个Java开发者&#xff0c;尤其是开发过桌面应用的&#xff0c;肯定对“打包部署”这四个字又爱又恨。爱的是&#xff0c;终于可以把辛苦写好的程序交给用户了&#xff1b;恨的是&#xff0c;这个…

作者头像 李华
网站建设 2026/8/8 5:27:44

Python编程中Flag标志位的核心用法:从布尔变量到枚举与特性开关

1. 从“开关”到“信使”&#xff1a;理解Python中的Flag在编程世界里&#xff0c;尤其是当你从Python入门&#xff0c;开始接触一些稍微复杂的逻辑时&#xff0c;你可能会频繁地遇到一个词&#xff1a;flag。它听起来很神秘&#xff0c;像是某种旗帜或标志&#xff0c;但在代码…

作者头像 李华