repo-analysis-mcp
Version:
Repository Analysis MCP for analyzing GitHub repositories
368 lines (339 loc) • 10 kB
JavaScript
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);
});