Skip to main content
Glama
by Cytrogen
index.ts21.2 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import path from "node:path"; import fs from "node:fs/promises"; import { glob } from "glob"; // --- 配置区 --- const SYNC_PATHS = [ "D:\\Programs\\Novus\\frontend\\src", "D:\\Programs\\Novus\\backend\\src" ]; const IGNORED_PATTERNS = new Set(['node_modules', '.git', '.idea', 'dist', 'build', '.DS_Store']); const pathRegistry = new Map<string, string>(); SYNC_PATHS.forEach(p => { if (!p) return; // 跳过空路径 const parentDir = path.basename(path.dirname(p)); const selfDir = path.basename(p); const prefix = `[${parentDir}/${selfDir}]`; pathRegistry.set(prefix, p); }); console.error("Configuration loaded. Syncing paths:", SYNC_PATHS); async function getFilesRecursive(directory: string): Promise<string[]> { let files: string[] = []; try { const dirents = await fs.readdir(directory, { withFileTypes: true }); for (const dirent of dirents) { if (IGNORED_PATTERNS.has(dirent.name)) continue; const fullPath = path.join(directory, dirent.name); const relativePath = path.relative(directory, fullPath); if (dirent.isDirectory()) { const subFiles = await getFilesRecursive(fullPath); files.push(...subFiles.map(sf => path.join(relativePath, sf).replace(/\\/g, '/'))); } else { files.push(relativePath.replace(/\\/g, '/')); } } } catch (error) { console.error(`Error reading directory ${directory}:`, error); } return files; } async function analyzeDirectory(dirPath: string, maxDepth: number, currentDepth = 0): Promise<any> { if (currentDepth >= maxDepth) return null; const result: any = { name: path.basename(dirPath), type: 'directory', children: [] }; try { const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { if (IGNORED_PATTERNS.has(item.name)) continue; const fullPath = path.join(dirPath, item.name); if (item.isDirectory()) { const subResult = await analyzeDirectory(fullPath, maxDepth, currentDepth + 1); if (subResult) result.children.push(subResult); } else { result.children.push({ name: item.name, type: 'file', extension: path.extname(item.name) }); } } } catch (error) { console.error(`分析目录 ${dirPath} 时出错:`, error); } return result; } function formatStructure(structure: any, indent: number): string { if (!structure) return ''; const spaces = ' '.repeat(indent); let result = `${spaces}${structure.type === 'directory' ? '📁' : '📄'} ${structure.name}\n`; if (structure.children) { for (const child of structure.children) { result += formatStructure(child, indent + 1); } } return result; } async function getDirectoryStats(dirPath: string): Promise<any> { const stats = { tsFiles: 0, jsFiles: 0, components: 0, services: 0, directories: 0 }; async function countFiles(dir: string) { try { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { if (IGNORED_PATTERNS.has(item.name)) continue; const fullPath = path.join(dir, item.name); if (item.isDirectory()) { stats.directories++; await countFiles(fullPath); } else { const ext = path.extname(item.name); const name = item.name.toLowerCase(); if (ext === '.ts' || ext === '.tsx') stats.tsFiles++; if (ext === '.js' || ext === '.jsx') stats.jsFiles++; if (name.includes('component') || ext === '.tsx' || ext === '.jsx') stats.components++; if (name.includes('service')) stats.services++; } } } catch (error) { // 忽略错误,继续统计 } } await countFiles(dirPath); return stats; } /** * 提取函数定义的辅助函数 */ function extractFunctionDefinition( lines: string[], functionName: string, includeComments: boolean, includeDecorators: boolean ): { found: boolean; content: string; startLine: number; endLine: number } { const result = { found: false, content: '', startLine: 0, endLine: 0 }; // 匹配函数定义的正则表达式 const functionPatterns = [ // async methodName( new RegExp(`^\\s*async\\s+${functionName}\\s*\\(`), // methodName( new RegExp(`^\\s*${functionName}\\s*\\(`), // private/public/protected async methodName( new RegExp(`^\\s*(private|public|protected)\\s+(async\\s+)?${functionName}\\s*\\(`), // function functionName( new RegExp(`^\\s*(export\\s+)?(async\\s+)?function\\s+${functionName}\\s*\\(`), // const functionName = new RegExp(`^\\s*(export\\s+)?const\\s+${functionName}\\s*=`), // functionName: new RegExp(`^\\s*${functionName}\\s*:`), ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 检查是否匹配函数定义 const isMatch = functionPatterns.some(pattern => pattern.test(line)); if (isMatch) { let startIndex = i; let endIndex = i; // 向上查找注释和装饰器 if (includeComments || includeDecorators) { let searchIndex = i - 1; while (searchIndex >= 0) { const prevLine = lines[searchIndex].trim(); // 跳过空行 if (prevLine === '') { searchIndex--; continue; } // 包含注释 if (includeComments && (prevLine.startsWith('//') || prevLine.startsWith('/*') || prevLine.startsWith('*') || prevLine.endsWith('*/'))) { startIndex = searchIndex; searchIndex--; continue; } // 包含装饰器 if (includeDecorators && prevLine.startsWith('@')) { startIndex = searchIndex; searchIndex--; continue; } // 如果不是注释或装饰器,停止向上搜索 break; } } // 向下查找函数结束位置 let braceCount = 0; let inFunction = false; for (let j = i; j < lines.length; j++) { const currentLine = lines[j]; // 计算大括号 for (const char of currentLine) { if (char === '{') { braceCount++; inFunction = true; } else if (char === '}') { braceCount--; } } endIndex = j; // 如果找到了函数开始的大括号,并且括号已经平衡,则结束 if (inFunction && braceCount === 0) { break; } // 对于箭头函数或单行函数,特殊处理 if (!inFunction && (currentLine.includes('=>') || currentLine.includes(';'))) { break; } } // 提取内容 const extractedLines = lines.slice(startIndex, endIndex + 1); result.found = true; result.content = extractedLines.join('\n'); result.startLine = startIndex + 1; result.endLine = endIndex + 1; return result; } } return result; } const server = new McpServer({ name: "local-project-sync", version: "3.0.0", }); console.error("MCP Server 'local-project-sync' is starting..."); server.tool( "list_project_files", "递归列出所有已配置同步目录中的文件", {}, async () => { let allFilesText = "项目文件列表:\n---\n"; for (const [prefix, absolutePath] of pathRegistry.entries()) { const files = await getFilesRecursive(absolutePath); if (files.length > 0) { allFilesText += files.map(file => `${prefix}/${file}`).join('\n') + '\n'; } } return { content: [{ type: "text", text: allFilesText }] }; } ); server.tool( "read_file_content", "读取项目内指定文件的内容,文件路径必须包含前缀,例如 '[backend/src]/main.ts'", { filePath: z.string().describe("带前缀的完整文件路径, e.g., '[backend/src]/main.ts'"), }, async ({ filePath }) => { const match = filePath.match(/^(\[.*?\])\/(.*)$/s); if (!match) return { content: [{ type: "text", text: "错误:文件路径格式不正确,必须包含如 '[backend/src]/' 的前缀。" }] }; const prefix = match[1]; const relativePath = match[2]; const rootPath = pathRegistry.get(prefix); if (!rootPath) return { content: [{ type: "text", text: `错误:未知的路径前缀 '${prefix}'。` }] }; const resolvedPath = path.resolve(rootPath, relativePath); if (!resolvedPath.startsWith(path.resolve(rootPath))) return { content: [{ type: "text", text: "错误:禁止访问项目目录之外的文件。" }] }; try { const fileContent = await fs.readFile(resolvedPath, "utf-8"); return { content: [{ type: "text", text: `文件 '${filePath}' 的内容:\n---\n${fileContent}` }] }; } catch (error: any) { let errorMessage = `读取文件 '${filePath}' 时发生错误。`; if (error.code === 'ENOENT') errorMessage = `错误:文件 '${filePath}' 未找到。`; console.error(errorMessage, error); return { content: [{ type: "text", text: errorMessage }] }; } } ); server.tool( "search_code_content", "在项目代码中搜索指定内容,支持正则表达式", { query: z.string().describe("搜索关键词或正则表达式"), fileTypes: z.array(z.string()).optional().describe("文件类型过滤,如 ['.ts', '.tsx', '.js']"), maxResults: z.number().optional().default(50).describe("最大结果数量"), caseSensitive: z.boolean().optional().default(false).describe("是否区分大小写"), useRegex: z.boolean().optional().default(false).describe("是否启用正则表达式模式"), contextLines: z.number().optional().default(0).describe("返回匹配行前后的上下文行数"), }, async ({ query, fileTypes = ['.ts', '.tsx', '.js', '.jsx'], maxResults = 50, caseSensitive = false, useRegex = false, contextLines = 0 }) => { let results: Array<{file: string, lineNumber: number, content: string}> = []; for (const [prefix, absolutePath] of pathRegistry.entries()) { try { const files = await getFilesRecursive(absolutePath); const filteredFiles = files.filter(file => fileTypes.some((ext: string) => file.endsWith(ext)) ); for (const file of filteredFiles) { if (results.length >= maxResults) break; try { const fullPath = path.join(absolutePath, file); const stats = await fs.stat(fullPath); if (!stats.isFile()) continue; const content = await fs.readFile(fullPath, 'utf-8'); const lines = content.split('\n'); const flags = caseSensitive ? 'g' : 'gi'; // 根据useRegex参数决定是否转义 const processedQuery = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(processedQuery, flags); lines.forEach((line, index) => { if (results.length >= maxResults) return; if (regex.test(line)) { let resultContent: string; if (contextLines > 0) { // 计算上下文范围 const startLine = Math.max(0, index - contextLines); const endLine = Math.min(lines.length - 1, index + contextLines); // 提取上下文,并添加行号 const contextContent = lines.slice(startLine, endLine + 1) .map((l, i) => { const lineNum = startLine + i + 1; const marker = lineNum === (index + 1) ? '>>> ' : ' '; // 标记匹配行 return `${marker}${lineNum}: ${l}`; }) .join('\n'); resultContent = contextContent; } else { // 原来的逻辑:只返回匹配行 resultContent = line.trim(); } results.push({ file: `${prefix}/${file}`, lineNumber: index + 1, content: line.trim() }); } }); } catch (error: any) { if (process.env.NODE_ENV === 'development') { console.error(`搜索文件 ${file} 时出错:`, error.message); } continue; } } } catch (error: any) { console.error(`搜索路径 ${absolutePath} 时出错:`, error.message); } } const resultText = results.length > 0 ? `找到 ${results.length} 个匹配结果:\n---\n` + results.map(r => `${r.file}:${r.lineNumber}\n${r.content}`).join('\n\n') : `未找到包含 "${query}" 的代码`; return { content: [{ type: "text", text: resultText }] }; } ); server.tool( "read_multiple_files", "批量读取多个文件内容,支持glob模式", { patterns: z.array(z.string()).describe("文件模式数组,如 ['modules/*/services/*.ts', 'config/*.ts']"), maxFiles: z.number().optional().default(20).describe("最大文件数量限制"), }, async ({ patterns, maxFiles = 20 }) => { let allContent = "批量文件内容:\n" + "=".repeat(50) + "\n"; let fileCount = 0; for (const [prefix, absolutePath] of pathRegistry.entries()) { for (const pattern of patterns) { try { const fullPattern = path.join(absolutePath, pattern); const matchedFiles = await glob(fullPattern, { ignore: ['**/node_modules/**', '**/.git/**'] }); for (const file of matchedFiles) { if (fileCount >= maxFiles) break; try { const relativePath = path.relative(absolutePath, file); const content = await fs.readFile(file, 'utf-8'); allContent += `\n📄 ${prefix}/${relativePath}\n`; allContent += "-".repeat(30) + "\n"; allContent += content + "\n"; fileCount++; } catch (error) { allContent += `\n无法读取: ${file}\n`; } } } catch (error) { console.error(`处理模式 ${pattern} 时出错:`, error); } if (fileCount >= maxFiles) break; } if (fileCount >= maxFiles) break; } return { content: [{ type: "text", text: allContent }] }; } ); server.tool( "analyze_project_structure", "分析项目结构,生成模块和功能概览", { scope: z.enum(['frontend', 'backend', 'all']).optional().default('all').describe("分析范围"), depth: z.number().optional().default(3).describe("目录深度"), }, async ({ scope, depth = 3 }) => { let analysis = "项目结构分析:\n" + "=".repeat(50) + "\n"; for (const [prefix, absolutePath] of pathRegistry.entries()) { // 根据scope过滤 if (scope !== 'all') { if (scope === 'frontend' && !prefix.includes('frontend')) continue; if (scope === 'backend' && !prefix.includes('backend')) continue; } analysis += `\n📁 ${prefix}\n`; analysis += "-".repeat(30) + "\n"; try { const structure = await analyzeDirectory(absolutePath, depth); analysis += formatStructure(structure, 0); // 添加统计信息 const stats = await getDirectoryStats(absolutePath); analysis += `\n📊 统计信息:\n`; analysis += ` - TypeScript文件: ${stats.tsFiles}\n`; analysis += ` - JavaScript文件: ${stats.jsFiles}\n`; analysis += ` - 组件文件: ${stats.components}\n`; analysis += ` - 服务文件: ${stats.services}\n`; analysis += ` - 总目录数: ${stats.directories}\n`; } catch (error) { analysis += `分析失败: ${error}\n`; } } return { content: [{ type: "text", text: analysis }] }; } ); server.tool( "extract_function_definition", "提取指定函数/方法的完整定义,包括注释和装饰器", { filePath: z.string().describe("带前缀的完整文件路径, e.g., '[backend/src]/main.ts'"), functionName: z.string().describe("函数/方法名"), includeComments: z.boolean().optional().default(true).describe("是否包含上方的注释"), includeDecorators: z.boolean().optional().default(true).describe("是否包含装饰器"), }, async ({ filePath, functionName, includeComments = true, includeDecorators = true }) => { const match = filePath.match(/^(\[.*?\])\/(.*)$/s); if (!match) return { content: [{ type: "text", text: "错误:文件路径格式不正确,必须包含如 '[backend/src]/' 的前缀。" }] }; const prefix = match[1]; const relativePath = match[2]; const rootPath = pathRegistry.get(prefix); if (!rootPath) return { content: [{ type: "text", text: `错误:未知的路径前缀 '${prefix}'。` }] }; const resolvedPath = path.resolve(rootPath, relativePath); if (!resolvedPath.startsWith(path.resolve(rootPath))) return { content: [{ type: "text", text: "错误:禁止访问项目目录之外的文件。" }] }; try { const fileContent = await fs.readFile(resolvedPath, "utf-8"); const lines = fileContent.split('\n'); // 查找函数定义 const functionResult = extractFunctionDefinition(lines, functionName, includeComments, includeDecorators); if (functionResult.found) { const resultText = `函数 '${functionName}' 在 '${filePath}' 中的定义:\n` + `行号 ${functionResult.startLine}-${functionResult.endLine}\n` + "---\n" + functionResult.content; return { content: [{ type: "text", text: resultText }] }; } else { return { content: [{ type: "text", text: `未在 '${filePath}' 中找到函数 '${functionName}'` }] }; } } catch (error: any) { let errorMessage = `读取文件 '${filePath}' 时发生错误。`; if (error.code === 'ENOENT') errorMessage = `错误:文件 '${filePath}' 未找到。`; console.error(errorMessage, error); return { content: [{ type: "text", text: errorMessage }] }; } } ); server.tool( "read_file_section", "读取文件的指定行范围", { filePath: z.string().describe("带前缀的完整文件路径, e.g., '[backend/src]/main.ts'"), startLine: z.number().describe("起始行号(从1开始)"), endLine: z.number().describe("结束行号(包含)"), showLineNumbers: z.boolean().optional().default(true).describe("是否显示行号"), }, async ({ filePath, startLine, endLine, showLineNumbers = true }) => { const match = filePath.match(/^(\[.*?\])\/(.*)$/s); if (!match) return { content: [{ type: "text", text: "错误:文件路径格式不正确,必须包含如 '[backend/src]/' 的前缀。" }] }; const prefix = match[1]; const relativePath = match[2]; const rootPath = pathRegistry.get(prefix); if (!rootPath) return { content: [{ type: "text", text: `错误:未知的路径前缀 '${prefix}'。` }] }; const resolvedPath = path.resolve(rootPath, relativePath); if (!resolvedPath.startsWith(path.resolve(rootPath))) return { content: [{ type: "text", text: "错误:禁止访问项目目录之外的文件。" }] }; try { const fileContent = await fs.readFile(resolvedPath, "utf-8"); const lines = fileContent.split('\n'); // 验证行号范围 if (startLine < 1) startLine = 1; if (endLine > lines.length) endLine = lines.length; if (startLine > endLine) { return { content: [{ type: "text", text: `错误:起始行号 ${startLine} 大于结束行号 ${endLine}` }] }; } // 提取指定行范围(转换为0-based索引) const selectedLines = lines.slice(startLine - 1, endLine); let resultContent: string; if (showLineNumbers) { resultContent = selectedLines .map((line, index) => `${startLine + index}: ${line}`) .join('\n'); } else { resultContent = selectedLines.join('\n'); } const resultText = `文件 '${filePath}' 第 ${startLine}-${endLine} 行内容:\n---\n${resultContent}`; return { content: [{ type: "text", text: resultText }] }; } catch (error: any) { let errorMessage = `读取文件 '${filePath}' 时发生错误。`; if (error.code === 'ENOENT') errorMessage = `错误:文件 '${filePath}' 未找到。`; console.error(errorMessage, error); return { content: [{ type: "text", text: errorMessage }] }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Local Project Sync MCP Server is running and connected via stdio."); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Cytrogen/local-project-sync'

If you have feedback or need assistance with the MCP directory API, please join our Discord server