UNPKG

repomix-mcp

Version:

MCP server for intelligent context gathering using repomix

258 lines (257 loc) 10.7 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { execSync } from "child_process"; import { glob } from "glob"; import path from "path"; import fs from "fs"; class RepomixServer { server; constructor() { this.server = new Server({ name: "repomix-mcp", version: "1.0.3", }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "gather_context", description: "Gather context from specified directories using repomix", inputSchema: { type: "object", properties: { baseDir: { type: "string", description: "Base directory to search in", }, targetDirs: { type: "array", items: { type: "string" }, description: "Target directories to find (e.g. ['translations', 'src'])", }, patterns: { type: "array", items: { type: "string" }, description: "File patterns to include", }, depth: { type: "string", enum: ["shallow", "dependencies", "full"], description: "Context gathering depth", }, includeConfigs: { type: "boolean", description: "Include configuration files", }, excludePatterns: { type: "array", items: { type: "string" }, description: "Patterns to exclude", }, }, required: ["baseDir"], }, }, { name: "analyze_project", description: "Analyze project structure and suggest context gathering strategy", inputSchema: { type: "object", properties: { projectDir: { type: "string", description: "Project directory to analyze", }, }, required: ["projectDir"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = request.params.arguments; switch (request.params.name) { case "gather_context": { if (!args || typeof args.baseDir !== "string") { throw new McpError(ErrorCode.InvalidParams, "baseDir is required and must be a string"); } const config = { baseDir: args.baseDir, targetDirs: Array.isArray(args.targetDirs) ? args.targetDirs.map(String) : undefined, patterns: Array.isArray(args.patterns) ? args.patterns.map(String) : undefined, depth: args.depth, includeConfigs: typeof args.includeConfigs === "boolean" ? args.includeConfigs : undefined, excludePatterns: Array.isArray(args.excludePatterns) ? args.excludePatterns.map(String) : undefined, }; return await this.handleGatherContext(config); } case "analyze_project": { if (!args || typeof args.projectDir !== "string") { throw new McpError(ErrorCode.InvalidParams, "projectDir is required and must be a string"); } return await this.handleAnalyzeProject({ projectDir: args.projectDir, }); } default: throw new McpError(ErrorCode.MethodNotFound, "Unknown tool"); } }); } async findTargetDirs(baseDir, targetDirs) { if (!targetDirs || targetDirs.length === 0) { return [baseDir]; } const results = []; for (const dir of targetDirs) { if (dir === ".") { results.push(baseDir); } else { const matches = await glob(`**/${dir}`, { cwd: baseDir, ignore: ["**/node_modules/**"], dot: true, }); results.push(...matches.map((match) => path.join(baseDir, match))); } } return results; } async handleGatherContext(config) { try { const targetDirs = await this.findTargetDirs(config.baseDir, config.targetDirs); if (targetDirs.length === 0) { throw new McpError(ErrorCode.InvalidRequest, "No matching directories found"); } let combinedOutput = ""; // Process each directory separately for (const dir of targetDirs) { let repomixCommand = `npx repomix "${dir}"`; // Add patterns if specified if (config.patterns && config.patterns.length > 0) { config.patterns.forEach((pattern) => { repomixCommand += ` --include "${pattern}"`; }); } // Add depth if specified if (config.depth) { repomixCommand += ` --depth ${config.depth}`; } try { // Execute repomix for this directory const output = execSync(repomixCommand, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, // 50MB buffer }); combinedOutput += output + "\n"; } catch (error) { console.error(`Failed to process directory ${dir}:`, error); // Continue with other directories even if one fails } } return { content: [ { type: "text", text: combinedOutput, }, ], }; } catch (error) { if (error instanceof McpError) throw error; throw new McpError(ErrorCode.InternalError, `Failed to gather context: ${error instanceof Error ? error.message : String(error)}`); } } async handleAnalyzeProject(args) { try { const projectDir = args.projectDir; // Check for package.json to determine project type const packageJsonPath = path.join(projectDir, "package.json"); let projectType = "unknown"; let suggestedDirs = ["src"]; let suggestedPatterns = []; if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies, }; if (deps.react) { projectType = "react"; suggestedDirs.push("components", "pages", "hooks"); suggestedPatterns.push("*.tsx", "*.jsx"); } else if (deps.vue) { projectType = "vue"; suggestedDirs.push("components", "views", "store"); suggestedPatterns.push("*.vue"); } else if (deps.angular) { projectType = "angular"; suggestedDirs.push("app", "components", "services"); suggestedPatterns.push("*.ts"); } } return { content: [ { type: "text", text: JSON.stringify({ projectType, suggestedConfig: { baseDir: projectDir, targetDirs: suggestedDirs, patterns: suggestedPatterns, includeConfigs: true, depth: "dependencies", excludePatterns: [ "**/*.test.*", "**/*.spec.*", "**/node_modules/**", ], }, }, null, 2), }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to analyze project: ${error instanceof Error ? error.message : String(error)}`); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Repomix MCP server running on stdio"); } } const server = new RepomixServer(); server.run().catch((error) => { console.error("Server error:", error); process.exit(1); });