xliff-mcp
Version:
MCP server for XLIFF bilingual file processing
700 lines (699 loc) • 31.6 kB
JavaScript
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);
});