UNPKG

repo-analysis-mcp

Version:

Repository Analysis MCP for analyzing GitHub repositories

368 lines (339 loc) 10 kB
#!/usr/bin/env node import { Octokit } from "@octokit/rest"; import dotenv from "dotenv"; import * as path from "path"; import fs from "fs"; import readline from "readline"; // Disable debug logging const DEBUG = false; const DEBUG_FILE = "/tmp/repo-analysis-mcp-debug.log"; function debug(message, data = null) { if (DEBUG) { const logMessage = `${new Date().toISOString()} - ${message}${ data ? ": " + JSON.stringify(data, null, 2) : "" }`; fs.appendFileSync(DEBUG_FILE, logMessage + "\n"); console.error(`DEBUG: ${message}`); } } // Load environment variables dotenv.config(); if (DEBUG) { debug("Environment loaded", { GITHUB_TOKEN_SET: !!process.env.GITHUB_TOKEN, SOURCE_REPO: process.env.SOURCE_REPO, TARGET_REPOS: process.env.TARGET_REPOS, }); } // Create a GitHub client const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, }); // Define our tools const tools = [ { name: "get_repo_info", description: "Get basic information about a GitHub repository", parameters: { type: "object", properties: { owner: { type: "string", description: "Repository owner (username or organization)", }, repo: { type: "string", description: "Repository name", }, }, required: ["owner", "repo"], }, handler: async (params) => { try { const { data } = await octokit.repos.get({ owner: params.owner, repo: params.repo, }); return { name: data.name, full_name: data.full_name, description: data.description, language: data.language, stars: data.stargazers_count, forks: data.forks_count, issues: data.open_issues_count, created_at: data.created_at, updated_at: data.updated_at, default_branch: data.default_branch, }; } catch (error) { console.error("Error getting repository info:", error); return { error: "Failed to get repository info: " + error.message }; } }, }, { name: "get_file_content", description: "Get the content of a file from a GitHub repository", parameters: { type: "object", properties: { owner: { type: "string", description: "Repository owner (username or organization)", }, repo: { type: "string", description: "Repository name", }, path: { type: "string", description: "Path to the file", }, branch: { type: "string", description: "Branch to get the file from (defaults to main)", }, }, required: ["owner", "repo", "path"], }, handler: async (params) => { try { const response = await octokit.repos.getContent({ owner: params.owner, repo: params.repo, path: params.path, ref: params.branch || "main", }); if ( "content" in response.data && "type" in response.data && response.data.type === "file" ) { const content = Buffer.from( response.data.content, "base64" ).toString(); return { content, fileType: path.extname(params.path).substring(1) || "unknown", }; } return { error: "Not a file or file not found" }; } catch (error) { console.error("Error getting file content:", error); return { error: "Failed to get file content: " + error.message }; } }, }, { name: "list_files", description: "List files in a directory of a GitHub repository", parameters: { type: "object", properties: { owner: { type: "string", description: "Repository owner (username or organization)", }, repo: { type: "string", description: "Repository name", }, path: { type: "string", description: "Path to the directory (defaults to root)", }, branch: { type: "string", description: "Branch to list files from (defaults to main)", }, }, required: ["owner", "repo"], }, handler: async (params) => { try { const response = await octokit.repos.getContent({ owner: params.owner, repo: params.repo, path: params.path || "", ref: params.branch || "main", }); if (Array.isArray(response.data)) { return { files: response.data.map((item) => ({ name: item.name, path: item.path, type: item.type, size: item.size, })), }; } return { error: "Not a directory or directory not found" }; } catch (error) { console.error("Error listing files:", error); return { error: "Failed to list files: " + error.message }; } }, }, { name: "analyze_repository", description: "Analyze a GitHub repository for patterns and architecture", parameters: { type: "object", properties: { owner: { type: "string", description: "Repository owner (username or organization)", }, repo: { type: "string", description: "Repository name", }, branch: { type: "string", description: "Branch to analyze (defaults to main)", }, }, required: ["owner", "repo"], }, handler: async (params) => { try { // Get package.json to analyze dependencies const packageJsonResponse = await octokit.repos.getContent({ owner: params.owner, repo: params.repo, path: "package.json", ref: params.branch || "main", }); let dependencies = {}; if ( "content" in packageJsonResponse.data && packageJsonResponse.data.type === "file" ) { try { const content = Buffer.from( packageJsonResponse.data.content, "base64" ).toString(); const parsed = JSON.parse(content); dependencies = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}), }; } catch (e) { console.error("Error parsing package.json:", e); } } return { repository: `${params.owner}/${params.repo}`, dependencies, analysis: { hasReact: "react" in dependencies, hasTypeScript: "typescript" in dependencies, hasNextJs: "next" in dependencies, hasRedux: "redux" in dependencies || "@reduxjs/toolkit" in dependencies, hasStyledComponents: "styled-components" in dependencies, hasTailwind: "tailwindcss" in dependencies, hasJest: "jest" in dependencies, hasEslint: "eslint" in dependencies, }, }; } catch (error) { console.error("Error analyzing repository:", error); return { error: "Failed to analyze repository: " + error.message }; } }, }, ]; // Simple stdio-based MCP server async function main() { console.log("Starting Repository Analysis MCP Server"); // Send server info in MCP format - using a very simple format const serverInfo = { tools: tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters, })), }; process.stdout.write(JSON.stringify(serverInfo) + "\n"); // Listen for tool calls on stdin using line-by-line reading const rl = readline.createInterface({ input: process.stdin, terminal: false, }); rl.on("line", async (line) => { try { if (!line.trim()) return; const request = JSON.parse(line); // Handle different request types if (request.type === "tool_call") { const { id, name, parameters } = request; const tool = tools.find((t) => t.name === name); if (!tool) { process.stdout.write( JSON.stringify({ id, type: "tool_response", status: "error", error: `Tool not found: ${name}`, }) + "\n" ); return; } try { // Send a "running" status process.stdout.write( JSON.stringify({ id, type: "tool_response", status: "running", }) + "\n" ); // Execute the tool const result = await tool.handler(parameters); // Send the result process.stdout.write( JSON.stringify({ id, type: "tool_response", status: "success", result, }) + "\n" ); } catch (error) { process.stdout.write( JSON.stringify({ id, type: "tool_response", status: "error", error: error.message || "Unknown error", }) + "\n" ); } } } catch (error) { console.error("Error parsing request:", error); } }); // Handle stdin end rl.on("close", () => { process.exit(0); }); } // Handle errors process.on("uncaughtException", (error) => { debug("Uncaught exception", { error: error.message, stack: error.stack }); console.error("Uncaught exception:", error); }); process.on("unhandledRejection", (reason, promise) => { debug("Unhandled rejection", { reason, promise }); console.error("Unhandled rejection at:", promise, "reason:", reason); }); // Start the server main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });