Skip to main content
Glama

Git MCP Server

index.ts7.58 kB
import Fastify, { FastifyInstance } from "fastify"; import { execFile } from "node:child_process"; import { access } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const allowedRoots: readonly string[] = (process.env.MCP_GIT_ROOTS ?? process.cwd()) .split(":") .map((value) => value.trim()) .filter(Boolean); interface GitStatusResponse { branch: string | null; upstream: string | null; ahead: number; behind: number; changes: Array<{ path: string; statusCode: string; }>; } interface GitCommitRequest { message: string; paths?: string[]; allowEmpty?: boolean; signOff?: boolean; path?: string; } type StatusQuery = { path?: string; }; type DiffQuery = { path?: string; ref?: string; }; function ensureAllowed(targetPath: string): string { const resolved = path.resolve(targetPath); if (allowedRoots.length === 0) { return resolved; } const isAllowed = allowedRoots.some((root) => { const resolvedRoot = path.resolve(root); return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`); }); if (!isAllowed) { throw new Error(`Path '${resolved}' is outside the configured MCP_GIT_ROOTS`); } return resolved; } async function detectRepository(cwd: string): Promise<string> { const resolved = ensureAllowed(cwd); await access(resolved); const { stdout } = await runGit(["rev-parse", "--show-toplevel"], resolved); return stdout.trim(); } async function runGit(args: readonly string[], cwd: string): Promise<{ stdout: string; stderr: string }> { return execFileAsync("git", args, { cwd }) as Promise<{ stdout: string; stderr: string }>; } function parseStatus(output: string): GitStatusResponse { const lines = output.split("\n"); const changes: GitStatusResponse["changes"] = []; let branch: string | null = null; let upstream: string | null = null; let ahead = 0; let behind = 0; for (const line of lines) { if (line.startsWith("# branch.head")) { branch = line.split(" ")[2] ?? null; } if (line.startsWith("# branch.upstream")) { upstream = line.split(" ")[2] ?? null; } if (line.startsWith("# branch.ab")) { const [, aheadStr, behindStr] = line.match(/\+(\d+) -(\d+)/) ?? []; if (aheadStr) { ahead = Number(aheadStr); } if (behindStr) { behind = Number(behindStr); } } if (line.startsWith("1 ") || line.startsWith("2 ")) { const parts = line.split(" "); const statusCode = parts[1]; const filePath = parts.at(-1) ?? ""; changes.push({ path: filePath, statusCode }); } } return { branch, upstream, ahead, behind, changes }; } export function createApp(): FastifyInstance { const app = Fastify({ logger: true, }); app.get("/health", { schema: { response: { 200: { type: "object", properties: { status: { type: "string" }, allowedRoots: { type: "array", items: { type: "string" } }, }, required: ["status", "allowedRoots"], }, }, }, }, () => ({ status: "ok", allowedRoots })); app.get<{ Querystring: StatusQuery }>( "/v1/git/status", { schema: { querystring: { type: "object", properties: { path: { type: "string" }, }, }, response: { 200: { type: "object", properties: { repository: { type: "string" }, status: { type: "object", properties: { branch: { type: ["string", "null"] }, upstream: { type: ["string", "null"] }, ahead: { type: "integer" }, behind: { type: "integer" }, changes: { type: "array", items: { type: "object", properties: { path: { type: "string" }, statusCode: { type: "string" }, }, required: ["path", "statusCode"], }, }, }, required: ["branch", "upstream", "ahead", "behind", "changes"], }, }, required: ["repository", "status"], }, }, }, }, async (request, reply) => { const repoPath = await detectRepository(request.query.path ?? process.cwd()); const { stdout } = await runGit(["status", "--branch", "--porcelain=v2"], repoPath); const statusResponse = parseStatus(stdout); return reply.status(200).send({ repository: repoPath, status: statusResponse }); } ); app.get<{ Querystring: DiffQuery }>( "/v1/git/diff", { schema: { querystring: { type: "object", properties: { path: { type: "string" }, ref: { type: "string", default: "HEAD" }, }, }, response: { 200: { type: "object", properties: { repository: { type: "string" }, ref: { type: "string" }, diff: { type: "string" }, }, required: ["repository", "ref", "diff"], }, }, }, }, async (request, reply) => { const repoPath = await detectRepository(request.query.path ?? process.cwd()); const ref = request.query.ref ?? "HEAD"; const { stdout } = await runGit(["diff", ref], repoPath); return reply.status(200).send({ repository: repoPath, ref, diff: stdout }); } ); app.post<{ Body: GitCommitRequest }>( "/v1/git/commit", { schema: { body: { type: "object", required: ["message"], properties: { message: { type: "string", minLength: 1 }, paths: { type: "array", items: { type: "string" }, }, allowEmpty: { type: "boolean", default: false }, signOff: { type: "boolean", default: false }, path: { type: "string" }, }, additionalProperties: false, }, response: { 200: { type: "object", properties: { repository: { type: "string" }, commit: { type: "string" }, }, required: ["repository", "commit"], }, }, }, }, async (request, reply) => { const { message, paths = [] as string[], allowEmpty = false, signOff = false, path: repoInput, } = request.body; const repoPath = await detectRepository(repoInput ?? process.cwd()); if (paths.length > 0) { await runGit(["add", ...paths], repoPath); } const args = ["commit", "-m", message]; if (allowEmpty) { args.push("--allow-empty"); } if (signOff) { args.push("--signoff"); } const { stdout } = await runGit(args, repoPath); return reply.status(200).send({ repository: repoPath, commit: stdout.trim() }); } ); return app; } if (import.meta.url === `file://${process.argv[1]}`) { const port = Number(process.env.PORT ?? 8082); const app = createApp(); app .listen({ host: "127.0.0.1", port }) .catch((error) => { app.log.error(error); process.exit(1); }); }

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/yevheniikravchuk/git-mcp'

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