UNPKG

@cyanheads/git-mcp-server

Version:

An MCP (Model Context Protocol) server enabling LLMs and AI agents to interact with Git repositories. Provides tools for comprehensive Git operations including clone, commit, branch, diff, log, status, push, pull, merge, rebase, worktree, tag management,

147 lines 5.36 kB
/** * @fileoverview Defines the core logic, schemas, and types for the git_log tool. * @module src/mcp-server/tools/gitLog/logic */ import { execFile } from "child_process"; import { promisify } from "util"; import { z } from "zod"; import { logger, sanitization, } from "../../../utils/index.js"; import { McpError, BaseErrorCode } from "../../../types-global/errors.js"; const execFileAsync = promisify(execFile); // 1. DEFINE the Zod input schema. export const GitLogInputSchema = z.object({ path: z.string().default(".").describe("Path to the Git repository."), maxCount: z .number() .int() .positive() .optional() .describe("Limit the number of commits to output."), author: z .string() .optional() .describe("Limit commits to those by a specific author."), since: z .string() .optional() .describe("Show commits more recent than a specific date (e.g., '2 weeks ago')."), until: z .string() .optional() .describe("Show commits older than a specific date."), branchOrFile: z .string() .optional() .describe("Show logs for a specific branch, tag, or file path."), showSignature: z .boolean() .default(false) .describe("Show signature verification status for commits."), }); // 2. DEFINE the Zod response schema. const CommitEntrySchema = z.object({ hash: z.string().describe("Full commit hash"), authorName: z.string().describe("Author's name"), authorEmail: z.string().email().describe("Author's email"), timestamp: z .number() .int() .positive() .describe("Commit timestamp (Unix epoch seconds)"), subject: z.string().describe("Commit subject line"), body: z.string().optional().describe("Commit body"), }); export const GitLogOutputSchema = z.object({ success: z.boolean().describe("Indicates if the command was successful."), message: z.string().describe("A summary message of the result."), commits: z.array(CommitEntrySchema).optional().describe("A list of commits."), rawOutput: z .string() .optional() .describe("Raw output from the git log command, used when showSignature is true."), }); const FIELD_SEP = "\x1f"; const RECORD_SEP = "\x1e"; const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`; /** * 4. IMPLEMENT the core logic function. * @throws {McpError} If the logic encounters an unrecoverable issue. */ export async function logGitHistory(params, context) { const operation = "logGitHistory"; logger.debug(`Executing ${operation}`, { ...context, params }); const workingDir = context.getWorkingDirectory(); if (params.path === "." && !workingDir) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first."); } const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath; const args = ["-C", targetPath, "log"]; if (params.showSignature) { args.push("--show-signature"); } else { args.push(GIT_LOG_FORMAT); } if (params.maxCount) args.push(`-n${params.maxCount}`); if (params.author) args.push(`--author=${params.author}`); if (params.since) args.push(`--since=${params.since}`); if (params.until) args.push(`--until=${params.until}`); if (params.branchOrFile) args.push(params.branchOrFile); logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation, }); const { stdout, stderr } = await execFileAsync("git", args, { maxBuffer: 1024 * 1024 * 10, }); if (stderr && stderr.toLowerCase().includes("does not have any commits yet")) { return { success: true, message: "Repository has no commits yet.", commits: [], }; } if (params.showSignature) { return { success: true, message: "Raw log output with signature status.", rawOutput: stdout, }; } const commitRecords = stdout.split(RECORD_SEP).filter((r) => r.trim()); const commits = commitRecords .map((record) => { const fields = record.trim().split(FIELD_SEP); if (fields.length < 5) { return null; // Skip records with insufficient fields } const [hash, authorName, authorEmail, timestampStr, subject, body = undefined,] = fields; if (hash && authorName && authorEmail && timestampStr && subject) { const timestamp = parseInt(timestampStr, 10); if (!isNaN(timestamp)) { return { hash, authorName, authorEmail, timestamp, subject, body: body || undefined, }; } } return null; }) .filter((item) => item !== null); return { success: true, message: `Found ${commits.length} commit(s).`, commits, }; } //# sourceMappingURL=logic.js.map