#!/usr/bin/env node // Local Git Operations MCP Server import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { readFile, writeFile } from "fs/promises"; import { join } from "path"; import { execSync } from "child_process"; // Get repository information from environment variables const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const BRANCH_NAME = process.env.BRANCH_NAME; const REPO_DIR = process.env.REPO_DIR || process.cwd(); const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const GITEA_API_URL = process.env.GITEA_API_URL || "https://api.github.com"; console.log(`[LOCAL-GIT-MCP] Starting Local Git Operations MCP Server`); console.log(`[LOCAL-GIT-MCP] REPO_OWNER: ${REPO_OWNER}`); console.log(`[LOCAL-GIT-MCP] REPO_NAME: ${REPO_NAME}`); console.log(`[LOCAL-GIT-MCP] BRANCH_NAME: ${BRANCH_NAME}`); console.log(`[LOCAL-GIT-MCP] REPO_DIR: ${REPO_DIR}`); console.log(`[LOCAL-GIT-MCP] GITEA_API_URL: ${GITEA_API_URL}`); console.log(`[LOCAL-GIT-MCP] GITHUB_TOKEN: ${GITHUB_TOKEN ? '***' : 'undefined'}`); if (!REPO_OWNER || !REPO_NAME || !BRANCH_NAME) { console.error( "[LOCAL-GIT-MCP] Error: REPO_OWNER, REPO_NAME, and BRANCH_NAME environment variables are required", ); process.exit(1); } const server = new McpServer({ name: "Local Git Operations Server", version: "0.0.1", }); // Helper function to run git commands function runGitCommand(command: string): string { try { console.log(`[LOCAL-GIT-MCP] Running git command: ${command}`); console.log(`[LOCAL-GIT-MCP] Working directory: ${REPO_DIR}`); const result = execSync(command, { cwd: REPO_DIR, encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], }); console.log(`[LOCAL-GIT-MCP] Git command result: ${result.trim()}`); return result.trim(); } catch (error: any) { console.error(`[LOCAL-GIT-MCP] Git command failed: ${command}`); console.error(`[LOCAL-GIT-MCP] Error: ${error.message}`); if (error.stdout) console.error(`[LOCAL-GIT-MCP] Stdout: ${error.stdout}`); if (error.stderr) console.error(`[LOCAL-GIT-MCP] Stderr: ${error.stderr}`); throw error; } } // Create branch tool server.tool( "create_branch", "Create a new branch from a base branch using local git operations", { branch_name: z.string().describe("Name of the branch to create"), base_branch: z .string() .describe("Base branch to create from (e.g., 'main')"), }, async ({ branch_name, base_branch }) => { try { // Ensure we're on the base branch and it's up to date runGitCommand(`git checkout ${base_branch}`); runGitCommand(`git pull origin ${base_branch}`); // Create and checkout the new branch runGitCommand(`git checkout -b ${branch_name}`); return { content: [ { type: "text", text: `Successfully created and checked out branch: ${branch_name}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error creating branch: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }, ); // Commit files tool server.tool( "commit_files", "Commit one or more files to the current branch using local git operations", { files: z .array(z.string()) .describe( 'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.', ), message: z.string().describe("Commit message"), }, async ({ files, message }) => { console.log(`[LOCAL-GIT-MCP] commit_files called with files: ${JSON.stringify(files)}, message: ${message}`); try { // Add the specified files console.log(`[LOCAL-GIT-MCP] Adding ${files.length} files to git...`); for (const file of files) { const filePath = file.startsWith("/") ? file.slice(1) : file; console.log(`[LOCAL-GIT-MCP] Adding file: ${filePath}`); runGitCommand(`git add "${filePath}"`); } // Commit the changes console.log(`[LOCAL-GIT-MCP] Committing with message: ${message}`); runGitCommand(`git commit -m "${message}"`); console.log(`[LOCAL-GIT-MCP] Successfully committed ${files.length} files`); return { content: [ { type: "text", text: `Successfully committed ${files.length} file(s): ${files.join(", ")}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[LOCAL-GIT-MCP] Error committing files: ${errorMessage}`); return { content: [ { type: "text", text: `Error committing files: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }, ); // Push branch tool server.tool( "push_branch", "Push the current branch to remote origin", { force: z.boolean().optional().describe("Force push (use with caution)"), }, async ({ force = false }) => { try { // Get current branch name const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD"); // Push the branch const pushCommand = force ? `git push -f origin ${currentBranch}` : `git push origin ${currentBranch}`; runGitCommand(pushCommand); return { content: [ { type: "text", text: `Successfully pushed branch: ${currentBranch}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error pushing branch: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }, ); // Create pull request tool (uses Gitea API) server.tool( "create_pull_request", "Create a pull request using Gitea API", { title: z.string().describe("Pull request title"), body: z.string().describe("Pull request body/description"), base_branch: z.string().describe("Base branch (e.g., 'main')"), head_branch: z .string() .optional() .describe("Head branch (defaults to current branch)"), }, async ({ title, body, base_branch, head_branch }) => { try { if (!GITHUB_TOKEN) { throw new Error( "GITHUB_TOKEN environment variable is required for PR creation", ); } // Get current branch if head_branch not specified const currentBranch = head_branch || runGitCommand("git rev-parse --abbrev-ref HEAD"); // Create PR using Gitea API const response = await fetch( `${GITEA_API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls`, { method: "POST", headers: { Authorization: `token ${GITHUB_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ title, body, base: base_branch, head: currentBranch, }), }, ); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to create PR: ${response.status} ${errorText}`); } const prData = await response.json(); return { content: [ { type: "text", text: `Successfully created pull request #${prData.number}: ${prData.html_url}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error creating pull request: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }, ); // Delete files tool server.tool( "delete_files", "Delete one or more files and commit the deletion using local git operations", { files: z .array(z.string()) .describe( 'Array of file paths relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])', ), message: z.string().describe("Commit message for the deletion"), }, async ({ files, message }) => { try { // Remove the specified files for (const file of files) { const filePath = file.startsWith("/") ? file.slice(1) : file; runGitCommand(`git rm "${filePath}"`); } // Commit the deletions runGitCommand(`git commit -m "${message}"`); return { content: [ { type: "text", text: `Successfully deleted and committed ${files.length} file(s): ${files.join(", ")}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error deleting files: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }, ); // Get git status tool server.tool("git_status", "Get the current git status", {}, async () => { console.log(`[LOCAL-GIT-MCP] git_status called`); try { const status = runGitCommand("git status --porcelain"); const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD"); console.log(`[LOCAL-GIT-MCP] Current branch: ${currentBranch}`); console.log(`[LOCAL-GIT-MCP] Git status: ${status || "Working tree clean"}`); return { content: [ { type: "text", text: `Current branch: ${currentBranch}\nStatus:\n${status || "Working tree clean"}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[LOCAL-GIT-MCP] Error getting git status: ${errorMessage}`); return { content: [ { type: "text", text: `Error getting git status: ${errorMessage}`, }, ], error: errorMessage, isError: true, }; } }); async function runServer() { console.log(`[LOCAL-GIT-MCP] Starting MCP server transport...`); const transport = new StdioServerTransport(); console.log(`[LOCAL-GIT-MCP] Connecting to transport...`); await server.connect(transport); console.log(`[LOCAL-GIT-MCP] MCP server connected and ready!`); process.on("exit", () => { console.log(`[LOCAL-GIT-MCP] Server shutting down...`); server.close(); }); } console.log(`[LOCAL-GIT-MCP] Calling runServer()...`); runServer().catch((error) => { console.error(`[LOCAL-GIT-MCP] Server startup failed:`, error); process.exit(1); });