MCP Local File Reader
by sworddut
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import path from "path";
// 定义支持的工具名称常量
const TOOL_NAMES = {
READ_FILE: "read_file",
LIST_FILES: "list_files",
GET_FILE_INFO: "get_file_info"
};
/**
* 创建 MCP 服务器
*/
const server = new Server(
{
name: "mcp-local-file-reader",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
);
/**
* 列出 `/data` 目录中的文件
*/
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
// 为 ListResourcesRequestSchema 使用默认路径,因为它没有 arguments 参数
const dataDir = "C:\\Users\\24067\\webProject\\mcp-local-file-reader\\data";
console.log(`尝试列出目录: ${dataDir}`);
try {
const files = fs.readdirSync(dataDir);
console.log(`列出目录 ${dataDir} 中的文件:`, files);
return {
resources: files.map(file => ({
uri: `file:///${file}`,
mimeType: getMimeType(file),
name: file,
description: `File in data directory: ${file}`,
}))
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`无法读取目录: ${dataDir}`, error);
throw new Error(`无法读取目录: ${dataDir} - ${errorMessage}`);
}
});
/**
* 读取指定文件
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
const filePath = path.resolve(url.pathname.slice(1));
console.log(`尝试读取文件: ${filePath}`);
try {
if (!fs.existsSync(filePath)) {
console.error(`文件不存在: ${filePath}`);
throw new Error(`文件不存在: ${filePath}`);
}
const content = readFileContent(filePath);
return {
contents: [
{
uri: request.params.uri,
mimeType: getMimeType(filePath),
text: content
}
]
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`读取文件失败: ${filePath}`, error);
throw new Error(`读取文件失败: ${filePath} - ${errorMessage}`);
}
});
/**
* 提供工具以便客户端按需读取文件
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.log("列出可用工具");
return {
tools: [
{
name: TOOL_NAMES.READ_FILE,
description: "读取指定文件内容",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "要读取的文件路径(绝对路径)"
}
},
required: ["filePath"]
}
},
{
name: TOOL_NAMES.LIST_FILES,
description: "列出指定目录中的所有文件",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "要列出文件的目录路径(绝对路径)"
}
},
required: ["filePath"]
}
},
{
name: TOOL_NAMES.GET_FILE_INFO,
description: "获取指定文件的详细信息",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "要获取信息的文件路径(绝对路径)"
}
},
required: ["filePath"]
}
}
]
};
});
/**
* 处理工具请求
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
console.log("工具调用请求:", request.params);
try {
switch (request.params.name) {
case TOOL_NAMES.READ_FILE: {
const filePath = String(request.params.arguments?.filePath || "");
console.log(`尝试读取文件: ${filePath}`);
if (!filePath) {
throw new Error("文件路径不能为空");
}
const fullPath = path.resolve(filePath);
console.log(`完整文件路径: ${fullPath}`);
if (!fs.existsSync(fullPath)) {
console.error(`文件不存在: ${filePath}`);
throw new Error(`文件不存在: ${filePath}`);
}
const content = readFileContent(fullPath);
return {
content: [
{
type: "text",
text: content
}
]
};
}
case TOOL_NAMES.LIST_FILES: {
const filesPath = String(request.params.arguments?.filePath || "");
console.log(`尝试列出目录: ${filesPath}`);
if (!filesPath) {
throw new Error("目录路径不能为空");
}
if (!fs.existsSync(filesPath)) {
console.error(`目录不存在: ${filesPath}`);
throw new Error(`目录不存在: ${filesPath}`);
}
const files = fs.readdirSync(filesPath);
console.log(`找到文件:`, files);
return {
content: [
{
type: "text",
text: files.join("\n")
}
]
};
}
case TOOL_NAMES.GET_FILE_INFO: {
const filePath = String(request.params.arguments?.filePath || "");
console.log(`尝试获取文件信息: ${filePath}`);
if (!filePath) {
throw new Error("文件路径不能为空");
}
const fullPath = path.resolve(filePath);
if (!fs.existsSync(fullPath)) {
console.error(`文件不存在: ${filePath}`);
throw new Error(`文件不存在: ${filePath}`);
}
const stats = fs.statSync(fullPath);
const info = {
path: fullPath,
size: stats.size,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
created: stats.birthtime,
modified: stats.mtime,
extension: path.extname(fullPath),
mimeType: getMimeType(fullPath)
};
return {
content: [
{
type: "text",
text: JSON.stringify(info, null, 2)
}
]
};
}
default:
console.error(`未知工具: ${request.params.name}`);
throw new Error(`未知工具: ${request.params.name}。可用工具: ${Object.values(TOOL_NAMES).join(", ")}`);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("工具调用错误:", error);
throw new Error(`工具调用错误: ${errorMessage}`);
}
});
/**
* 获取文件的MIME类型
* @param filePath 文件路径
* @returns MIME类型
*/
function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
'.txt': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.xls': 'application/vnd.ms-excel',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.ppt': 'application/vnd.ms-powerpoint',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.md': 'text/markdown',
'.xml': 'application/xml',
'.zip': 'application/zip',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.wav': 'audio/wav',
'.avi': 'video/x-msvideo',
'.csv': 'text/csv'
};
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* 读取文件内容,根据文件类型进行适当处理
* @param filePath 文件路径
* @returns 文件内容
*/
function readFileContent(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
// 二进制文件类型列表
const binaryExtensions = [
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.zip', '.mp3',
'.mp4', '.wav', '.avi'
];
try {
if (binaryExtensions.includes(ext)) {
// 对于二进制文件,返回提示信息
return `[二进制文件: ${path.basename(filePath)}] - 文件大小: ${fs.statSync(filePath).size} 字节`;
} else {
// 对于文本文件,直接读取内容
return fs.readFileSync(filePath, 'utf8');
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`读取文件失败: ${filePath}`, error);
return `[读取文件失败: ${errorMessage}]`;
}
}
/**
* 启动服务器
*/
async function main() {
console.log("启动 MCP 服务器...");
console.log(`支持的工具: ${Object.values(TOOL_NAMES).join(", ")}`);
const transport = new StdioServerTransport();
console.log("连接到传输层...");
await server.connect(transport);
console.log("MCP 服务器已启动并准备接收请求");
}
main().catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Server error:", errorMessage);
process.exit(1);
});