Skip to main content
Glama

Git Workflow Automation MCP Server

by Arcia125
index.ts18.1 kB
#!/usr/bin/env node /** * Git Workflow Automation MCP Server * * Provides automated Git workflow tools for committing, creating PRs, and merging. * Handles GitHub authentication issues and provides reliable workflow automation. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { simpleGit, SimpleGit } from 'simple-git'; import { exec } from 'child_process'; import { promisify } from 'util'; import { writeFileSync, unlinkSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; const execAsync = promisify(exec); /** * Interface for Git workflow results */ interface WorkflowResult { success: boolean; message: string; details?: any; error?: string; } /** * Create an MCP server with Git workflow automation capabilities */ const server = new Server( { name: "git-workflow-server", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); /** * Escape PowerShell string literals to prevent command injection */ function escapePowerShellString(str: string): string { return str .replace(/'/g, "''") // Escape single quotes .replace(/`/g, "``") // Escape backticks .replace(/\$/g, "`$") // Escape dollar signs .replace(/"/g, '""'); // Escape double quotes } /** * Execute GitHub CLI command with environment variable clearing * Uses temporary files for complex text to avoid PowerShell parsing issues */ async function executeGitHubCommand( baseCommand: string, options?: { title?: string; body?: string; cwd?: string; } ): Promise<{ stdout: string; stderr: string }> { let tempBodyFile: string | null = null; try { let finalCommand = baseCommand; // If we have a body, write it to a temporary file to avoid PowerShell parsing issues if (options?.body) { tempBodyFile = join(tmpdir(), `gh-pr-body-${Date.now()}.txt`); writeFileSync(tempBodyFile, options.body, 'utf8'); // Replace --body with --body-file, handling both quoted and unquoted body content finalCommand = finalCommand.replace(/--body\s+"[^"]*"/, `--body-file "${tempBodyFile}"`); finalCommand = finalCommand.replace(/--body\s+[^\s]+/, `--body-file "${tempBodyFile}"`); } // If we have a title, properly escape it for PowerShell if (options?.title) { const escapedTitle = escapePowerShellString(options.title); finalCommand = finalCommand.replace(/--title\s+"([^"]*)"/, `--title '${escapedTitle}'`); } // Build the PowerShell command with proper token clearing const powerShellCommand = `powershell -Command "Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue; Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue; ${finalCommand}"`; const result = await execAsync(powerShellCommand, { cwd: options?.cwd || process.cwd(), maxBuffer: 1024 * 1024 // 1MB buffer }); return result; } catch (error: any) { throw new Error(`Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`); } finally { // Clean up temporary file if (tempBodyFile) { try { unlinkSync(tempBodyFile); } catch (e) { // Ignore cleanup errors } } } } /** * Execute simple command with environment variable clearing for GitHub CLI */ async function executeWithClearedTokens(command: string, cwd?: string): Promise<{ stdout: string; stderr: string }> { const powerShellCommand = `powershell -Command "Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue; Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue; ${command}"`; try { const result = await execAsync(powerShellCommand, { cwd: cwd || process.cwd(), maxBuffer: 1024 * 1024 // 1MB buffer }); return result; } catch (error: any) { throw new Error(`Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`); } } /** * Get Git instance for the specified directory */ function getGit(workingDir?: string): SimpleGit { return simpleGit(workingDir || process.cwd()); } /** * Commit and push changes */ async function commitAndPush( files: string[], commitMessage: string, branch?: string, workingDir?: string, dryRun: boolean = false ): Promise<WorkflowResult> { try { const git = getGit(workingDir); if (dryRun) { return { success: true, message: "Dry run: Would commit and push changes", details: { files, commitMessage, branch, workingDir } }; } // Check if we're in a git repository const isRepo = await git.checkIsRepo(); if (!isRepo) { return { success: false, message: "Failed to commit and push", error: "Not a Git repository" }; } // Get current status const status = await git.status(); // Create branch if specified and doesn't exist if (branch) { try { await git.checkoutBranch(branch, 'HEAD'); } catch (error) { // Branch might already exist, try to switch to it try { await git.checkout(branch); } catch (switchError) { return { success: false, message: "Failed to commit and push", error: `Failed to create or switch to branch ${branch}: ${switchError}` }; } } } // Add specified files if (files.length > 0) { await git.add(files); } else { // Add all modified files if no specific files provided await git.add('.'); } // Commit changes const commitResult = await git.commit(commitMessage); // Push changes const currentBranch = await git.revparse(['--abbrev-ref', 'HEAD']); await git.push('origin', currentBranch); return { success: true, message: "Successfully committed and pushed changes", details: { commit: commitResult.commit, branch: currentBranch, files: commitResult.summary.changes, insertions: commitResult.summary.insertions, deletions: commitResult.summary.deletions } }; } catch (error: any) { return { success: false, message: "Failed to commit and push", error: error.message }; } } /** * Create a pull request using GitHub CLI */ async function createPullRequest( title: string, body: string, baseBranch: string = 'main', headBranch?: string, workingDir?: string, dryRun: boolean = false ): Promise<WorkflowResult> { try { if (dryRun) { return { success: true, message: "Dry run: Would create pull request", details: { title, body, baseBranch, headBranch } }; } // Get current branch if not specified if (!headBranch) { const git = getGit(workingDir); headBranch = await git.revparse(['--abbrev-ref', 'HEAD']); } // Create PR using GitHub CLI with cleared tokens and proper escaping const command = `gh pr create --title "${title}" --body "${body}" --base ${baseBranch} --head ${headBranch}`; const result = await executeGitHubCommand(command, { title, body, cwd: workingDir }); // Extract PR URL from output const prUrl = result.stdout.trim(); return { success: true, message: "Successfully created pull request", details: { url: prUrl, title, baseBranch, headBranch } }; } catch (error: any) { return { success: false, message: "Failed to create pull request", error: `Failed to create pull request: ${error.message}` }; } } /** * Merge a pull request */ async function mergePullRequest( prNumber: string, mergeMethod: 'merge' | 'squash' | 'rebase' = 'merge', deleteBranch: boolean = true, workingDir?: string, dryRun: boolean = false ): Promise<WorkflowResult> { try { if (dryRun) { return { success: true, message: "Dry run: Would merge pull request", details: { prNumber, mergeMethod, deleteBranch } }; } // Merge PR using GitHub CLI with cleared tokens const mergeFlag = mergeMethod === 'squash' ? '--squash' : mergeMethod === 'rebase' ? '--rebase' : '--merge'; const deleteFlag = deleteBranch ? '--delete-branch' : ''; const command = `gh pr merge ${prNumber} ${mergeFlag} ${deleteFlag}`.trim(); const result = await executeWithClearedTokens(command, workingDir); return { success: true, message: "Successfully merged pull request", details: { prNumber, mergeMethod, output: result.stdout } }; } catch (error: any) { return { success: false, message: "Failed to merge pull request", error: `Failed to merge pull request: ${error.message}` }; } } /** * Complete Git workflow: commit, push, create PR, and optionally merge */ async function completeGitWorkflow( files: string[], commitMessage: string, prTitle: string, prBody: string, branch?: string, baseBranch: string = 'main', autoMerge: boolean = false, workingDir?: string, dryRun: boolean = false ): Promise<WorkflowResult> { try { if (dryRun) { return { success: true, message: "Dry run: Would execute complete Git workflow", details: { files, commitMessage, prTitle, prBody, branch, baseBranch, autoMerge } }; } // Step 1: Commit and push const commitResult = await commitAndPush(files, commitMessage, branch, workingDir); if (!commitResult.success) { return commitResult; } // Step 2: Create pull request const prResult = await createPullRequest(prTitle, prBody, baseBranch, branch, workingDir); if (!prResult.success) { return prResult; } let mergeResult = null; if (autoMerge && prResult.details?.url) { // Extract PR number from URL const prMatch = prResult.details.url.match(/\/pull\/(\d+)$/); if (prMatch) { const prNumber = prMatch[1]; mergeResult = await mergePullRequest(prNumber, 'merge', true, workingDir); } } return { success: true, message: "Successfully completed Git workflow", details: { commit: commitResult.details, pullRequest: prResult.details, merge: mergeResult?.details } }; } catch (error: any) { return { success: false, message: "Git workflow failed", error: `Git workflow failed: ${error.message}` }; } } /** * Handler that lists available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "git_commit_and_push", description: "Commit staged changes and push to remote repository", inputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" }, description: "Array of file paths to commit (empty for all changes)" }, commitMessage: { type: "string", description: "Commit message (use conventional commit format)" }, branch: { type: "string", description: "Branch name to create/switch to (optional)" }, workingDir: { type: "string", description: "Working directory path (defaults to current directory)" }, dryRun: { type: "boolean", description: "Preview changes without executing", default: false } }, required: ["commitMessage"] } }, { name: "create_pull_request", description: "Create a GitHub pull request with proper authentication handling", inputSchema: { type: "object", properties: { title: { type: "string", description: "Pull request title" }, body: { type: "string", description: "Pull request description" }, baseBranch: { type: "string", description: "Base branch (target)", default: "main" }, headBranch: { type: "string", description: "Head branch (source, defaults to current branch)" }, workingDir: { type: "string", description: "Working directory path" }, dryRun: { type: "boolean", description: "Preview without executing", default: false } }, required: ["title", "body"] } }, { name: "merge_pull_request", description: "Merge a GitHub pull request", inputSchema: { type: "object", properties: { prNumber: { type: "string", description: "Pull request number" }, mergeMethod: { type: "string", enum: ["merge", "squash", "rebase"], description: "Merge method", default: "merge" }, deleteBranch: { type: "boolean", description: "Delete branch after merge", default: true }, workingDir: { type: "string", description: "Working directory path" }, dryRun: { type: "boolean", description: "Preview without executing", default: false } }, required: ["prNumber"] } }, { name: "complete_git_workflow", description: "Execute complete Git workflow: commit, push, create PR, and optionally merge", inputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" }, description: "Files to commit" }, commitMessage: { type: "string", description: "Commit message (conventional format)" }, prTitle: { type: "string", description: "Pull request title" }, prBody: { type: "string", description: "Pull request description" }, branch: { type: "string", description: "Feature branch name" }, baseBranch: { type: "string", description: "Base branch", default: "main" }, autoMerge: { type: "boolean", description: "Automatically merge PR after creation", default: false }, workingDir: { type: "string", description: "Working directory path" }, dryRun: { type: "boolean", description: "Preview without executing", default: false } }, required: ["commitMessage", "prTitle", "prBody"] } } ] }; }); /** * Handler for tool execution */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result: WorkflowResult; switch (name) { case "git_commit_and_push": result = await commitAndPush( (args?.files as string[]) || [], args?.commitMessage as string, args?.branch as string, args?.workingDir as string, (args?.dryRun as boolean) || false ); break; case "create_pull_request": result = await createPullRequest( args?.title as string, args?.body as string, (args?.baseBranch as string) || 'main', args?.headBranch as string, args?.workingDir as string, (args?.dryRun as boolean) || false ); break; case "merge_pull_request": result = await mergePullRequest( args?.prNumber as string, (args?.mergeMethod as 'merge' | 'squash' | 'rebase') || 'merge', (args?.deleteBranch as boolean) ?? true, args?.workingDir as string, (args?.dryRun as boolean) || false ); break; case "complete_git_workflow": result = await completeGitWorkflow( (args?.files as string[]) || [], args?.commitMessage as string, args?.prTitle as string, args?.prBody as string, args?.branch as string, (args?.baseBranch as string) || 'main', (args?.autoMerge as boolean) || false, args?.workingDir as string, (args?.dryRun as boolean) || false ); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error: any) { return { content: [ { type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) } ], isError: true }; } }); /** * Start the server using stdio transport */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error("Git Workflow MCP Server error:", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arcia125/git-workflow-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server