@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,
142 lines • 5.21 kB
JavaScript
/**
* @fileoverview Defines the core logic, schemas, and types for the git_stash tool.
* @module src/mcp-server/tools/gitStash/logic
*/
import { execFile } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import { logger, sanitization, } from "../../../utils/index.js";
import { getGitStatus, GitStatusOutputSchema } from "../gitStatus/logic.js";
const execFileAsync = promisify(execFile);
// 1. DEFINE the Zod input schema.
export const GitStashBaseSchema = z.object({
path: z.string().default(".").describe("Path to the local Git repository."),
mode: z
.enum(["list", "apply", "pop", "drop", "save"])
.describe("The stash operation to perform."),
stashRef: z
.string()
.optional()
.describe("Stash reference (e.g., 'stash@{1}')."),
message: z
.string()
.optional()
.describe("Optional descriptive message for 'save' mode."),
});
export const GitStashInputSchema = GitStashBaseSchema.refine((data) => !(["apply", "pop", "drop"].includes(data.mode) && !data.stashRef), {
message: "A 'stashRef' is required for 'apply', 'pop', and 'drop' modes.",
path: ["stashRef"],
});
// 2. DEFINE the Zod response schema.
const StashInfoSchema = z.object({
ref: z.string(),
branch: z.string(),
description: z.string(),
});
export const GitStashOutputSchema = z.object({
success: z.boolean().describe("Indicates if the command was successful."),
mode: z.string().describe("The mode of operation that was performed."),
message: z.string().optional().describe("A summary message of the result."),
stashes: z
.array(StashInfoSchema)
.optional()
.describe("A list of stashes for the 'list' mode."),
conflicts: z
.boolean()
.optional()
.describe("Indicates if a merge conflict occurred."),
stashCreated: z
.boolean()
.optional()
.describe("Indicates if a stash was created."),
status: GitStatusOutputSchema.optional().describe("The status of the repository after the stash operation."),
});
/**
* Parses the raw output of `git stash list` into a structured array.
* @private
*/
function _parseStashList(stdout) {
return stdout
.trim()
.split("\n")
.map((line) => {
const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
if (match) {
const ref = match[1];
const description = match[4] !== undefined ? match[4] : null;
if (ref && description !== null) {
return {
ref,
branch: match[3] || "unknown",
description,
};
}
}
return null;
})
.filter((item) => item !== null);
}
/**
* 4. IMPLEMENT the core logic function.
* @throws {McpError} If the logic encounters an unrecoverable issue.
*/
export async function gitStashLogic(params, context) {
const operation = `gitStashLogic:${params.mode}`;
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 buildArgs = () => {
const baseArgs = ["-C", targetPath, "stash", params.mode];
switch (params.mode) {
case "list":
// No extra args needed
break;
case "apply":
case "pop":
case "drop":
baseArgs.push(params.stashRef);
break;
case "save":
if (params.message) {
baseArgs.push(params.message);
}
break;
}
return baseArgs;
};
const args = buildArgs();
logger.debug(`Executing command: git ${args.join(" ")}`, {
...context,
operation,
});
const { stdout, stderr } = await execFileAsync("git", args);
const status = await getGitStatus({ path: targetPath }, context);
if (params.mode === "list") {
const stashes = _parseStashList(stdout);
return { success: true, mode: params.mode, stashes, status };
}
const output = stdout + stderr;
const conflicts = /conflict/i.test(output);
if (params.mode === "save") {
const stashCreated = !/no local changes to save/i.test(output);
return {
success: true,
mode: params.mode,
message: stashCreated ? "Changes stashed." : "No local changes to save.",
stashCreated,
status,
};
}
return {
success: true,
mode: params.mode,
message: `${params.mode} operation successful.`,
conflicts,
status,
};
}
//# sourceMappingURL=logic.js.map