Git MCP Server

by cyanheads
Verified
/** * Branch Tools * =========== * * MCP tools for Git branch operations. */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { GitService } from '../services/git-service.js'; import { Schemas, PathValidation } from '../utils/validation.js'; /** * Registers branch tools with the MCP server * * @param server - MCP server instance */ export function setupBranchTools(server: McpServer): void { // List branches server.tool( "git_branch_list", { path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"), all: z.boolean().optional().default(false).describe("Whether to include remote branches in the list") }, async ({ path, all }) => { try { const normalizedPath = PathValidation.normalizePath(path); const gitService = new GitService(normalizedPath); // Check if this is a git repository const isRepo = await gitService.isGitRepository(); if (!isRepo) { return { content: [{ type: "text", text: `Error: Not a Git repository: ${normalizedPath}` }], isError: true }; } const result = await gitService.listBranches(all); if (!result.resultSuccessful) { return { content: [{ type: "text", text: `Error: ${result.resultError.errorMessage}` }], isError: true }; } if (result.resultData.length === 0) { return { content: [{ type: "text", text: `No branches found in repository at: ${normalizedPath}` }] }; } // Get status to determine current branch const statusResult = await gitService.getStatus(); const currentBranch = statusResult.resultSuccessful ? statusResult.resultData.current : null; // Format output let output = `Branches in repository at: ${normalizedPath}\n\n`; result.resultData.forEach(branch => { if (branch === currentBranch) { output += `* ${branch} (current)\n`; } else { output += ` ${branch}\n`; } }); return { content: [{ type: "text", text: output }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Create branch server.tool( "git_branch_create", { path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"), name: z.string().min(1, "Branch name is required").describe("Name of the new branch to create"), startPoint: z.string().optional().describe("Reference (commit, branch) to create the branch from"), checkout: z.boolean().optional().default(false).describe("Whether to checkout the newly created branch") }, async ({ path, name, startPoint, checkout }) => { try { const normalizedPath = PathValidation.normalizePath(path); const gitService = new GitService(normalizedPath); // Check if this is a git repository const isRepo = await gitService.isGitRepository(); if (!isRepo) { return { content: [{ type: "text", text: `Error: Not a Git repository: ${normalizedPath}` }], isError: true }; } const result = await gitService.createBranch({ name, startPoint, checkout }); if (!result.resultSuccessful) { return { content: [{ type: "text", text: `Error: ${result.resultError.errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Successfully created branch '${name}'${checkout ? ' and checked it out' : ''}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Checkout branch server.tool( "git_checkout", { path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"), target: z.string().min(1, "Branch or commit to checkout is required").describe("Branch name, tag, or commit hash to checkout"), createBranch: z.boolean().optional().default(false).describe("Whether to create a new branch with the specified name") }, async ({ path, target, createBranch }) => { try { const normalizedPath = PathValidation.normalizePath(path); const gitService = new GitService(normalizedPath); // Check if this is a git repository const isRepo = await gitService.isGitRepository(); if (!isRepo) { return { content: [{ type: "text", text: `Error: Not a Git repository: ${normalizedPath}` }], isError: true }; } const result = await gitService.checkout(target, createBranch); if (!result.resultSuccessful) { return { content: [{ type: "text", text: `Error: ${result.resultError.errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Successfully checked out '${target}'${createBranch ? ' (new branch)' : ''}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Delete branch server.tool( "git_branch_delete", { path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"), branch: z.string().min(1, "Branch name is required").describe("Name of the branch to delete"), force: z.boolean().optional().default(false).describe("Force deletion even if branch is not fully merged") }, async ({ path, branch, force }) => { try { const normalizedPath = PathValidation.normalizePath(path); const gitService = new GitService(normalizedPath); // Check if this is a git repository const isRepo = await gitService.isGitRepository(); if (!isRepo) { return { content: [{ type: "text", text: `Error: Not a Git repository: ${normalizedPath}` }], isError: true }; } const result = await gitService.deleteBranch(branch, force); if (!result.resultSuccessful) { return { content: [{ type: "text", text: `Error: ${result.resultError.errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Successfully deleted branch '${branch}'` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Merge branch server.tool( "git_merge", { path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"), branch: z.string().min(1, "Branch to merge is required").describe("Name of the branch to merge into the current branch"), message: z.string().optional().describe("Custom commit message for the merge commit"), fastForwardOnly: z.boolean().optional().default(false).describe("Only allow fast-forward merges (fail if not possible)"), noFastForward: z.boolean().optional().default(false).describe("Create a merge commit even when fast-forward is possible") }, async ({ path, branch, message, fastForwardOnly, noFastForward }) => { try { const normalizedPath = PathValidation.normalizePath(path); const gitService = new GitService(normalizedPath); // Check if this is a git repository const isRepo = await gitService.isGitRepository(); if (!isRepo) { return { content: [{ type: "text", text: `Error: Not a Git repository: ${normalizedPath}` }], isError: true }; } // Can't have both fastForwardOnly and noFastForward if (fastForwardOnly && noFastForward) { return { content: [{ type: "text", text: `Error: Cannot specify both fastForwardOnly and noFastForward` }], isError: true }; } const result = await gitService.merge({ branch, message, fastForwardOnly, noFastForward }); if (!result.resultSuccessful) { return { content: [{ type: "text", text: `Error: ${result.resultError.errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: `Successfully merged branch '${branch}' into current branch` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); }