UNPKG

xliff-mcp

Version:

MCP server for XLIFF bilingual file processing

700 lines (699 loc) 31.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; import os from 'os'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { minimatch } from 'minimatch'; import { XliffProcessor } from './xliff-processor.js'; import { fileURLToPath } from 'url'; // 获取版本信息 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageJsonPath = path.join(__dirname, '..', 'package.json'); let packageInfo = { name: 'xliff-mcp', version: '1.0.0', description: 'MCP server for XLIFF bilingual file processing' }; try { const packageContent = await fs.readFile(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(packageContent); packageInfo = { name: packageJson.name || packageInfo.name, version: packageJson.version || packageInfo.version, description: packageJson.description || packageInfo.description }; } catch (error) { console.error('无法读取package.json文件,使用默认版本信息'); } // 命令行参数解析 const args = process.argv.slice(2); if (args.length === 0) { console.error("使用方法: xliff-mcp <允许的目录> [其他目录...]"); process.exit(1); } // 检测是否在WSL环境中 function isWSLEnvironment() { try { // 检查 /proc/version 文件是否包含 Microsoft 或 WSL const fs = require('fs'); if (fs.existsSync('/proc/version')) { const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase(); return version.includes('microsoft') || version.includes('wsl'); } return false; } catch { return false; } } // 规范化路径 function normalizePath(p) { return path.normalize(p); } function expandHome(filepath) { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } // 智能路径处理函数 function smartPathConversion(filepath) { const isWSL = isWSLEnvironment(); // Windows路径格式检测 (如 C:\... 或 C:/...) const windowsPathRegex = /^[A-Za-z]:(\\|\/)/; const isWindowsPath = windowsPathRegex.test(filepath); // 如果在WSL环境中,且路径是Windows格式,则转换为WSL路径 if (isWSL && isWindowsPath) { const drive = filepath.charAt(0).toLowerCase(); const pathWithoutDrive = filepath.slice(2).replace(/\\/g, '/'); return `/mnt/${drive}${pathWithoutDrive}`; } // 如果在WSL环境中,但路径已经是WSL格式(/mnt/...),保持不变 if (isWSL && filepath.startsWith('/mnt/')) { return filepath; } // 如果不是WSL环境,或者路径不是Windows格式,保持原样 return filepath; } // 存储允许的目录 const allowedDirectories = args.map(dir => { const converted = smartPathConversion(dir); const expanded = expandHome(converted); return normalizePath(path.resolve(expanded)); }); // 验证所有目录是否存在且可访问 await Promise.all(args.map(async (dir) => { try { const converted = smartPathConversion(dir); const expanded = expandHome(converted); const stats = await fs.stat(expanded); if (!stats.isDirectory()) { console.error(`错误: ${dir} 不是一个目录`); process.exit(1); } } catch (error) { console.error(`访问目录 ${dir} 时出错:`, error); process.exit(1); } })); // 在 validatePath 函数之前添加 async function findFileInAllowedDirectories(fileName) { // 如果已经是绝对路径,直接返回 if (path.isAbsolute(fileName)) { return fileName; } // 在所有允许的目录中搜索文件 for (const allowedDir of allowedDirectories) { const fullPath = path.join(allowedDir, fileName); try { await fs.access(fullPath); return fullPath; } catch { // 继续查找下一个目录 } } // 递归搜索子目录 for (const allowedDir of allowedDirectories) { try { const found = await searchForFile(allowedDir, fileName); if (found) return found; } catch { // 继续查找下一个目录 } } return null; } async function searchForFile(dir, fileName) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isFile() && entry.name === fileName) { return fullPath; } else if (entry.isDirectory()) { const found = await searchForFile(fullPath, fileName); if (found) return found; } } } catch { // 忽略访问错误 } return null; } // 修改 validatePath 函数 async function validatePath(requestedPath) { const convertedPath = smartPathConversion(requestedPath); const expandedPath = expandHome(convertedPath); // 如果不是绝对路径,先尝试在允许目录中查找 if (!path.isAbsolute(expandedPath)) { const foundPath = await findFileInAllowedDirectories(expandedPath); if (foundPath) { return foundPath; } } // 原有的验证逻辑保持不变 const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); // 检查路径是否在允许的目录中 const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir)); if (!isAllowed) { // 提供更友好的错误信息 const debugInfo = { 原始路径: requestedPath, 转换后路径: convertedPath, 扩展后路径: expandedPath, 绝对路径: absolute, 规范化路径: normalizedRequested, 允许的目录: allowedDirectories, 是否WSL环境: isWSLEnvironment() }; console.error('路径验证失败,调试信息:', JSON.stringify(debugInfo, null, 2)); throw new Error(`❌ 文件未找到:'${requestedPath}'\n\n🔍 我在以下目录中查找了该文件:\n${allowedDirectories.map(dir => `• ${dir}`).join('\n')}\n\n💡 建议:\n1. 确认文件名是否正确\n2. 确认文件是否在允许访问的目录中\n3. 使用 'search_files' 工具搜索文件\n4. 使用 'list_directory' 工具查看目录内容`); } // 其余验证逻辑保持不变... try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir)); if (!isRealPathAllowed) { throw new Error("访问被拒绝 - 符号链接目标在允许的目录之外"); } return realPath; } catch (error) { // 对于尚不存在的新文件,验证父目录 const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir)); if (!isParentAllowed) { throw new Error("访问被拒绝 - 父目录在允许的目录之外"); } return absolute; } catch { throw new Error(`父目录不存在: ${parentDir}`); } } } // Schema定义 const ReadFileArgsSchema = z.object({ path: z.string().describe('文件路径'), }); const WriteFileArgsSchema = z.object({ path: z.string().describe('文件路径'), content: z.string().describe('文件内容'), }); const ListDirectoryArgsSchema = z.object({ path: z.string().describe('目录路径'), }); const SearchFilesArgsSchema = z.object({ path: z.string().describe('搜索起始目录'), pattern: z.string().describe('搜索模式'), excludePatterns: z.array(z.string()).optional().default([]).describe('排除模式'), }); // XLIFF特定的Schema const ParseXliffArgsSchema = z.object({ path: z.string().describe('XLIFF文件路径'), }); const CreateXliffArgsSchema = z.object({ path: z.string().describe('输出文件路径'), data: z.array(z.object({ fileName: z.string(), segNumber: z.number(), percent: z.number(), source: z.string(), target: z.string(), srcLang: z.string(), tgtLang: z.string(), id: z.string().optional(), approved: z.boolean().optional(), state: z.string().optional(), resname: z.string().optional(), })).describe('XLIFF数据数组'), options: z.object({ version: z.enum(['1.2', '2.0', '2.1']).optional().default('1.2'), sourceLanguage: z.string(), targetLanguage: z.string(), original: z.string().optional(), datatype: z.string().optional(), }).describe('创建选项'), }); const UpdateXliffArgsSchema = z.object({ path: z.string().describe('XLIFF文件路径'), updates: z.array(z.object({ fileName: z.string(), segNumber: z.number(), percent: z.number(), source: z.string(), target: z.string(), srcLang: z.string(), tgtLang: z.string(), id: z.string().optional(), approved: z.boolean().optional(), state: z.string().optional(), resname: z.string().optional(), })).describe('更新数据数组'), options: z.object({ updateMode: z.enum(['merge', 'replace']).default('merge'), preserveState: z.boolean().optional().default(false), preserveApproval: z.boolean().optional().default(false), }).describe('更新选项'), }); const ValidateXliffArgsSchema = z.object({ path: z.string().describe('XLIFF文件路径'), }); const BatchProcessXliffArgsSchema = z.object({ directory: z.string().describe('包含XLIFF文件的目录'), pattern: z.string().optional().default('*.xlf').describe('文件匹配模式'), operation: z.enum(['parse', 'validate']).describe('批处理操作类型'), }); const SmartAssistantArgsSchema = z.object({ task: z.string().describe('用自然语言描述您想要完成的任务,例如:"我想看看这个目录下有什么文件"、"帮我检查这个xliff文件是否正确"、"解析这个XLIFF文件"等'), filePath: z.string().optional().describe('相关的文件路径(如果有的话)'), }); const ToolInputSchema = ToolSchema.shape.inputSchema; // 服务器设置 const server = new Server({ name: "xliff-mcp-server", version: packageInfo.version, }, { capabilities: { tools: {}, }, }); // 工具实现 async function searchFiles(rootPath, pattern, excludePatterns = []) { const results = []; async function search(currentPath) { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); try { await validatePath(fullPath); const relativePath = path.relative(rootPath, fullPath); const shouldExclude = excludePatterns.some(pattern => { const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`; return minimatch(relativePath, globPattern, { dot: true }); }); if (shouldExclude) { continue; } if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { results.push(fullPath); } if (entry.isDirectory()) { await search(fullPath); } } catch (error) { continue; } } } await search(rootPath); return results; } // 工具处理程序 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "smart_assistant", description: "🤖 智能助手 - 用自然语言告诉我您想做什么,我会帮您选择合适的工具来完成任务。支持查看文件内容、检查文件格式、解析XLIFF文件等各种操作。", inputSchema: zodToJsonSchema(SmartAssistantArgsSchema), }, { name: "get_version", description: "🔍 查看版本信息 - 了解当前XLIFF工具的版本和功能", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "read_file", description: "📖 读取文件内容 - 查看任何文本文件的完整内容", inputSchema: zodToJsonSchema(ReadFileArgsSchema), }, { name: "write_file", description: "✏️ 写入文件 - 创建新文件或修改现有文件", inputSchema: zodToJsonSchema(WriteFileArgsSchema), }, { name: "list_directory", description: "📁 查看文件夹内容 - 列出指定文件夹中的所有文件和子文件夹", inputSchema: zodToJsonSchema(ListDirectoryArgsSchema), }, { name: "search_files", description: "🔍 搜索文件 - 在文件夹中查找特定名称的文件", inputSchema: zodToJsonSchema(SearchFilesArgsSchema), }, { name: "parse_xliff", description: "📊 解析XLIFF文件 - 把XLIFF文件转换成易读的JSON格式,可以看到所有翻译单元的详细信息", inputSchema: zodToJsonSchema(ParseXliffArgsSchema), }, { name: "create_xliff", description: "🆕 创建XLIFF文件 - 根据JSON数据生成新的XLIFF文件", inputSchema: zodToJsonSchema(CreateXliffArgsSchema), }, { name: "update_xliff", description: "📝 更新XLIFF文件 - 修改现有XLIFF文件中的内容", inputSchema: zodToJsonSchema(UpdateXliffArgsSchema), }, { name: "validate_xliff", description: "✅ 检查XLIFF文件 - 验证XLIFF文件格式是否正确", inputSchema: zodToJsonSchema(ValidateXliffArgsSchema), }, { name: "batch_process_xliff", description: "📦 批量处理XLIFF文件 - 一次性处理多个XLIFF文件", inputSchema: zodToJsonSchema(BatchProcessXliffArgsSchema), }, { name: "list_allowed_directories", description: "📋 查看可访问的文件夹 - 显示我可以访问的所有文件夹路径", inputSchema: { type: "object", properties: {}, required: [], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case "smart_assistant": { const parsed = SmartAssistantArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`智能助手参数解析失败: ${parsed.error}`); } const { task, filePath } = parsed.data; // 根据用户的自然语言任务描述,提供智能指导 let guidance = `🤖 我来帮您完成这个任务:${task}\n\n`; // 分析任务类型 const taskLower = task.toLowerCase(); if (taskLower.includes('查看') || taskLower.includes('看') || taskLower.includes('读取')) { guidance += `📖 **文件查看任务**\n`; if (filePath) { guidance += `我会帮您查看文件:${filePath}\n`; guidance += `正在读取文件内容...`; } else { guidance += `请告诉我您想查看哪个文件的路径。`; } } else if (taskLower.includes('检查') || taskLower.includes('验证') || taskLower.includes('validate')) { guidance += `✅ **文件检查任务**\n`; if (filePath) { guidance += `我会检查XLIFF文件的格式是否正确:${filePath}\n`; guidance += `正在验证文件格式...`; } else { guidance += `请告诉我您想检查哪个XLIFF文件的路径。`; } } else if (taskLower.includes('解析') || taskLower.includes('parse')) { guidance += `📊 **文件解析任务**\n`; if (filePath) { guidance += `我会解析XLIFF文件并显示其结构:${filePath}\n`; guidance += `正在解析文件...`; } else { guidance += `请告诉我您想解析哪个XLIFF文件的路径。`; } } else if (taskLower.includes('文件夹') || taskLower.includes('目录') || taskLower.includes('folder')) { guidance += `📁 **文件夹浏览任务**\n`; if (filePath) { guidance += `我会列出文件夹中的所有内容:${filePath}\n`; guidance += `正在扫描文件夹...`; } else { guidance += `请告诉我您想查看哪个文件夹的路径。`; } } else if (taskLower.includes('搜索') || taskLower.includes('查找') || taskLower.includes('search')) { guidance += `🔍 **文件搜索任务**\n`; if (filePath) { guidance += `我会在指定目录中搜索文件:${filePath}\n`; guidance += `请告诉我您要搜索什么文件名或模式。`; } else { guidance += `请告诉我您想在哪个目录中搜索,以及要搜索什么文件。`; } } else { guidance += `🤔 **任务分析**\n`; guidance += `我理解您想要:${task}\n\n`; guidance += `💡 **建议**\n`; guidance += `我可以帮您完成以下类型的任务:\n`; guidance += `• 查看文件内容\n`; guidance += `• 检查XLIFF文件格式\n`; guidance += `• 解析XLIFF文件结构\n`; guidance += `• 浏览文件夹内容\n`; guidance += `• 搜索特定文件\n`; guidance += `• 批量处理XLIFF文件\n\n`; guidance += `请用更具体的描述告诉我您的需求,我会提供详细的执行方案!`; } return { content: [{ type: "text", text: guidance }], }; } case "get_version": { const versionInfo = { name: packageInfo.name, version: packageInfo.version, description: packageInfo.description, environment: { nodeVersion: process.version, platform: process.platform, arch: process.arch, isWSL: isWSLEnvironment(), }, allowedDirectories: allowedDirectories, supportedXliffVersions: ['1.2', '2.0', '2.1'], capabilities: { fileOperations: "支持文件读写、目录浏览、文件搜索", xliffProcessing: "支持XLIFF文件解析、验证、创建、更新", naturalLanguage: "支持自然语言指令", smartAssistant: "智能任务分析和指导" } }; return { content: [{ type: "text", text: `🔍 **XLIFF MCP 服务器信息**\n\n📦 **基本信息**\n名称:${versionInfo.name}\n版本:${versionInfo.version}\n描述:${versionInfo.description}\n\n🖥️ **运行环境**\nNode.js版本:${versionInfo.environment.nodeVersion}\n操作系统:${versionInfo.environment.platform}\n架构:${versionInfo.environment.arch}\nWSL环境:${versionInfo.environment.isWSL ? '是' : '否'}\n\n📁 **可访问目录**\n${versionInfo.allowedDirectories.join('\n')}\n\n✨ **功能特性**\n• 支持XLIFF版本:${versionInfo.supportedXliffVersions.join(', ')}\n• 文件操作:${versionInfo.capabilities.fileOperations}\n• XLIFF处理:${versionInfo.capabilities.xliffProcessing}\n• 自然语言:${versionInfo.capabilities.naturalLanguage}\n• 智能助手:${versionInfo.capabilities.smartAssistant}` }], }; } case "read_file": { const parsed = ReadFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`read_file参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const content = await fs.readFile(validPath, "utf-8"); return { content: [{ type: "text", text: content }], }; } case "write_file": { const parsed = WriteFileArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`write_file参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); await fs.writeFile(validPath, parsed.data.content, "utf-8"); return { content: [{ type: "text", text: `成功写入文件 ${parsed.data.path}` }], }; } case "list_directory": { const parsed = ListDirectoryArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`list_directory参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const entries = await fs.readdir(validPath, { withFileTypes: true }); const formatted = entries .map((entry) => `${entry.isDirectory() ? "[目录]" : "[文件]"} ${entry.name}`) .join("\n"); return { content: [{ type: "text", text: formatted }], }; } case "search_files": { const parsed = SearchFilesArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`search_files参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns); return { content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "未找到匹配项" }], }; } case "parse_xliff": { const parsed = ParseXliffArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`解析XLIFF文件参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const content = await fs.readFile(validPath, "utf-8"); const fileName = path.basename(validPath); const xliffData = XliffProcessor.parseXliff(fileName, content); const summary = `📊 **XLIFF文件解析完成!**\n\n📄 **文件信息**\n• 文件名:${fileName}\n• 翻译单元数量:${xliffData.length}\n• 源语言:${xliffData[0]?.srcLang || '未知'}\n• 目标语言:${xliffData[0]?.tgtLang || '未设置'}\n\n📋 **详细数据**\n`; return { content: [{ type: "text", text: summary + JSON.stringify(xliffData, null, 2) }], }; } case "create_xliff": { const parsed = CreateXliffArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`创建XLIFF文件参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const xliffContent = XliffProcessor.createXliff(parsed.data.data, parsed.data.options); await fs.writeFile(validPath, xliffContent, "utf-8"); const successMessage = `✅ **XLIFF文件创建成功!**\n\n📄 **新文件信息**\n• 文件路径:${parsed.data.path}\n• 翻译单元数:${parsed.data.data.length}\n• 源语言:${parsed.data.options.sourceLanguage}\n• 目标语言:${parsed.data.options.targetLanguage}\n• XLIFF版本:${parsed.data.options.version || '1.2'}\n\n🎉 新的XLIFF文件已成功创建!`; return { content: [{ type: "text", text: successMessage }], }; } case "update_xliff": { const parsed = UpdateXliffArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`update_xliff参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const originalContent = await fs.readFile(validPath, "utf-8"); const updatedContent = XliffProcessor.updateXliff(originalContent, parsed.data.updates, parsed.data.options); await fs.writeFile(validPath, updatedContent, "utf-8"); return { content: [{ type: "text", text: `成功更新XLIFF文件 ${parsed.data.path}` }], }; } case "validate_xliff": { const parsed = ValidateXliffArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`validate_xliff参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.path); const content = await fs.readFile(validPath, "utf-8"); const validation = XliffProcessor.validateXliff(content); const result = { valid: validation.valid, errors: validation.errors, file: parsed.data.path }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "batch_process_xliff": { const parsed = BatchProcessXliffArgsSchema.safeParse(args); if (!parsed.success) { throw new Error(`batch_process_xliff参数无效: ${parsed.error}`); } const validPath = await validatePath(parsed.data.directory); const xliffFiles = await searchFiles(validPath, parsed.data.pattern); const results = []; for (const filePath of xliffFiles) { try { const content = await fs.readFile(filePath, "utf-8"); const fileName = path.basename(filePath); if (parsed.data.operation === 'parse') { const xliffData = XliffProcessor.parseXliff(fileName, content); results.push({ file: filePath, success: true, data: xliffData }); } else if (parsed.data.operation === 'validate') { const validation = XliffProcessor.validateXliff(content); results.push({ file: filePath, success: validation.valid, valid: validation.valid, errors: validation.errors }); } } catch (error) { results.push({ file: filePath, success: false, error: error instanceof Error ? error.message : String(error) }); } } return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } case "list_allowed_directories": { return { content: [{ type: "text", text: `允许访问的目录:\n${allowedDirectories.join('\n')}` }], }; } default: throw new Error(`未知工具: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `❌ 出现错误: ${errorMessage}` }], isError: true, }; } }); // 启动服务器 async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("XLIFF MCP服务器在stdio上运行"); console.error(`版本: ${packageInfo.version}`); console.error("允许的目录:", allowedDirectories); } runServer().catch((error) => { console.error("服务器运行出现致命错误:", error); process.exit(1); });