@cyanheads/git-mcp-server 2.1.7 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
  5. package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +44 -143
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +50 -171
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +46 -152
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +75 -257
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +48 -146
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +73 -181
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +37 -121
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +33 -122
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +70 -214
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +66 -188
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. package/package.json +12 -12
@@ -1,78 +1,57 @@
1
- // Import utils from barrel (logger from ../utils/internal/logger.js)
2
- import { logger } from "../../../utils/index.js";
3
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
- import { ErrorHandler } from "../../../utils/index.js";
5
- // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { requestContextService } from "../../../utils/index.js";
7
- // Import the result type along with the function and input schema
8
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Import BaseErrorCode
9
- import { addGitFiles, GitAddInputSchema, } from "./logic.js";
10
- let _getWorkingDirectory;
11
- let _getSessionId;
12
- /**
13
- * Initializes the state accessors needed by the tool registration.
14
- * This should be called once during server setup.
15
- * @param getWdFn - Function to get the working directory for a session.
16
- * @param getSidFn - Function to get the session ID from context.
17
- */
18
- export function initializeGitAddStateAccessors(getWdFn, getSidFn) {
19
- _getWorkingDirectory = getWdFn;
20
- _getSessionId = getSidFn;
21
- logger.info("State accessors initialized for git_add tool registration.");
22
- }
1
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
2
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
3
+ import { addGitFiles, GitAddInputSchema, GitAddOutputSchema, } from "./logic.js";
23
4
  const TOOL_NAME = "git_add";
24
5
  const TOOL_DESCRIPTION = "Stages changes in the Git repository for the next commit by adding file contents to the index (staging area). Can stage specific files/patterns or all changes (default: '.'). Returns the result as a JSON object.";
25
- /**
26
- * Registers the git_add tool with the MCP server.
27
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
28
- *
29
- * @param {McpServer} server - The McpServer instance to register the tool with.
30
- * @returns {Promise<void>}
31
- * @throws {Error} If registration fails or state accessors are not initialized.
32
- */
33
- export const registerGitAddTool = async (server) => {
34
- if (!_getWorkingDirectory || !_getSessionId) {
35
- throw new Error("State accessors for git_add must be initialized before registration.");
36
- }
6
+ export const registerGitAddTool = async (server, getWorkingDirectory, getSessionId) => {
37
7
  const operation = "registerGitAddTool";
38
8
  const context = requestContextService.createRequestContext({ operation });
39
9
  await ErrorHandler.tryCatch(async () => {
40
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitAddInputSchema.shape, // Provide the Zod schema shape
41
- async (validatedArgs, callContext) => {
10
+ server.registerTool(TOOL_NAME, {
11
+ title: "Git Add",
12
+ description: TOOL_DESCRIPTION,
13
+ inputSchema: GitAddInputSchema.shape,
14
+ outputSchema: GitAddOutputSchema.shape,
15
+ annotations: {
16
+ readOnlyHint: false,
17
+ destructiveHint: false,
18
+ idempotentHint: true,
19
+ openWorldHint: false,
20
+ },
21
+ }, async (validatedArgs, callContext) => {
42
22
  const toolOperation = "tool:git_add";
43
23
  const requestContext = requestContextService.createRequestContext({
44
24
  operation: toolOperation,
45
25
  parentContext: callContext,
46
26
  });
47
- const sessionId = _getSessionId(requestContext);
48
- const getWorkingDirectoryForSession = () => {
49
- return _getWorkingDirectory(sessionId);
50
- };
27
+ const sessionId = getSessionId(requestContext);
51
28
  const logicContext = {
52
29
  ...requestContext,
53
30
  sessionId: sessionId,
54
- getWorkingDirectory: getWorkingDirectoryForSession,
31
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
55
32
  };
56
33
  logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
57
- return await ErrorHandler.tryCatch(async () => {
58
- // Call the core logic function which now returns a GitAddResult object
59
- const addResult = await addGitFiles(validatedArgs, logicContext);
60
- // Format the successful result as a JSON string within TextContent
61
- const resultContent = {
62
- type: "text",
63
- // Stringify the JSON object for the response content
64
- text: JSON.stringify(addResult, null, 2), // Pretty-print JSON
65
- contentType: "application/json",
34
+ try {
35
+ const result = await addGitFiles(validatedArgs, logicContext);
36
+ return {
37
+ structuredContent: result,
38
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
66
39
  };
67
- logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
68
- return { content: [resultContent] };
69
- }, {
70
- operation: toolOperation,
71
- context: logicContext,
72
- input: validatedArgs,
73
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code
74
- // Let the logic function throw specific errors like NOT_FOUND
75
- });
40
+ }
41
+ catch (error) {
42
+ const handledError = ErrorHandler.handleError(error, {
43
+ operation: "gitAddToolHandler",
44
+ context: logicContext,
45
+ input: validatedArgs,
46
+ });
47
+ const mcpError = handledError instanceof McpError
48
+ ? handledError
49
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while staging files.", { originalErrorName: handledError.name });
50
+ return {
51
+ isError: true,
52
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
53
+ };
54
+ }
76
55
  });
77
56
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
78
57
  }, { operation, context, critical: true });
@@ -1,7 +1,5 @@
1
1
  /**
2
- * @fileoverview Barrel file for the git_branch tool.
3
- * Exports the registration function and state accessor initialization function.
2
+ * @fileoverview Barrel file for the gitBranch tool.
3
+ * @module src/mcp-server/tools/gitBranch/index
4
4
  */
5
- export { registerGitBranchTool, initializeGitBranchStateAccessors, } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitBranchInput, GitBranchResult } from './logic.js';
5
+ export { registerGitBranchTool } from "./registration.js";
@@ -1,324 +1,146 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_branch tool.
3
+ * @module src/mcp-server/tools/gitBranch/logic
4
+ */
1
5
  import { execFile } from "child_process";
2
6
  import { promisify } from "util";
3
7
  import { z } from "zod";
4
- // Import utils from barrel (logger from ../utils/internal/logger.js)
5
- import { logger } from "../../../utils/index.js";
6
- // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
- // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
- import { sanitization } from "../../../utils/index.js";
8
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9
+ import { logger, sanitization } from "../../../utils/index.js";
10
10
  const execFileAsync = promisify(execFile);
11
- // Define the BASE input schema for the git_branch tool using Zod
11
+ // 1. DEFINE the Zod input schema.
12
12
  export const GitBranchBaseSchema = z.object({
13
- path: z
14
- .string()
15
- .min(1)
16
- .optional()
17
- .default(".")
18
- .describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
19
- mode: z
20
- .enum(["list", "create", "delete", "rename", "show-current"])
21
- .describe("The branch operation to perform: 'list', 'create', 'delete', 'rename', 'show-current'."),
22
- branchName: z
23
- .string()
24
- .min(1)
25
- .optional()
26
- .describe("The name of the branch (e.g., 'feat/new-login', 'main'). Required for 'create', 'delete', 'rename' modes."),
27
- newBranchName: z
28
- .string()
29
- .min(1)
30
- .optional()
31
- .describe("The new name for the branch (e.g., 'fix/typo-in-readme'). Required for 'rename' mode."),
32
- startPoint: z
33
- .string()
34
- .min(1)
35
- .optional()
36
- .describe("Optional commit hash, tag, or existing branch name (e.g., 'main', 'v1.0.0', 'commit-hash') to start the new branch from. Used only in 'create' mode. Defaults to HEAD."),
37
- force: z
38
- .boolean()
39
- .default(false)
40
- .describe("Force the operation. Use -D for delete, -M for rename, -f for create (if branch exists). Use with caution, as forcing operations can lead to data loss."),
41
- all: z
42
- .boolean()
43
- .default(false)
44
- .describe("List both local and remote-tracking branches. Used only in 'list' mode."),
45
- remote: z
46
- .boolean()
47
- .default(false)
48
- .describe("Act on remote-tracking branches. Used with 'list' (-r) or 'delete' (-r)."),
13
+ path: z.string().default(".").describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
14
+ mode: z.enum(["list", "create", "delete", "rename", "show-current"]).describe("The branch operation to perform."),
15
+ branchName: z.string().optional().describe("The name of the branch for create, delete, or rename operations."),
16
+ newBranchName: z.string().optional().describe("The new name for the branch when renaming."),
17
+ startPoint: z.string().optional().describe("The starting point (commit, tag, or branch) for a new branch."),
18
+ force: z.boolean().default(false).describe("Force the operation (e.g., overwrite existing branch)."),
19
+ all: z.boolean().default(false).describe("List all branches (local and remote)."),
20
+ remote: z.boolean().default(false).describe("Act on remote-tracking branches."),
49
21
  });
50
- // Apply refinements and export the FINAL schema for validation within the handler
51
- export const GitBranchInputSchema = GitBranchBaseSchema.refine((data) => !(data.mode === "create" && !data.branchName), {
22
+ export const GitBranchInputSchema = GitBranchBaseSchema.refine(data => !(data.mode === "create" && !data.branchName), {
52
23
  message: "A 'branchName' is required for 'create' mode.",
53
24
  path: ["branchName"],
54
- })
55
- .refine((data) => !(data.mode === "delete" && !data.branchName), {
25
+ }).refine(data => !(data.mode === "delete" && !data.branchName), {
56
26
  message: "A 'branchName' is required for 'delete' mode.",
57
27
  path: ["branchName"],
58
- })
59
- .refine((data) => !(data.mode === "rename" && (!data.branchName || !data.newBranchName)), {
60
- message: "Both 'branchName' (old name) and 'newBranchName' are required for 'rename' mode.",
61
- path: ["branchName", "newBranchName"],
28
+ }).refine(data => !(data.mode === "rename" && (!data.branchName || !data.newBranchName)), {
29
+ message: "Both 'branchName' and 'newBranchName' are required for 'rename' mode.",
30
+ path: ["newBranchName"],
31
+ });
32
+ // 2. DEFINE the Zod response schema.
33
+ const BranchInfoSchema = z.object({
34
+ name: z.string(),
35
+ isCurrent: z.boolean(),
36
+ isRemote: z.boolean(),
37
+ commitHash: z.string().optional(),
38
+ commitSubject: z.string().optional(),
39
+ });
40
+ export const GitBranchOutputSchema = z.object({
41
+ success: z.boolean().describe("Indicates if the command was successful."),
42
+ mode: z.string().describe("The mode of operation that was performed."),
43
+ message: z.string().describe("A summary message of the result."),
44
+ branches: z.array(BranchInfoSchema).optional().describe("A list of branches for the 'list' mode."),
45
+ currentBranch: z.string().nullable().optional().describe("The current branch name."),
62
46
  });
63
47
  /**
64
- * Executes git branch commands based on the specified mode.
65
- *
66
- * @param {GitBranchInput} input - The validated input object.
67
- * @param {RequestContext} context - The request context for logging and error handling.
68
- * @returns {Promise<GitBranchResult>} A promise that resolves with the structured result.
69
- * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
48
+ * 4. IMPLEMENT the core logic function.
49
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
70
50
  */
71
- export async function gitBranchLogic(input, context) {
72
- const operation = `gitBranchLogic:${input.mode}`;
73
- logger.debug(`Executing ${operation}`, { ...context, input });
74
- let targetPath;
75
- try {
76
- // Resolve and sanitize the target path
77
- const workingDir = context.getWorkingDirectory();
78
- targetPath =
79
- input.path && input.path !== "." ? input.path : (workingDir ?? ".");
80
- if (targetPath === "." && !workingDir) {
81
- logger.warning("Executing git branch in server's CWD as no path provided and no session WD set.", { ...context, operation });
82
- targetPath = process.cwd();
83
- }
84
- else if (targetPath === "." && workingDir) {
85
- targetPath = workingDir;
86
- logger.debug(`Using session working directory: ${targetPath}`, {
87
- ...context,
88
- operation,
89
- sessionId: context.sessionId,
90
- });
91
- }
92
- else {
93
- logger.debug(`Using provided path: ${targetPath}`, {
94
- ...context,
95
- operation,
96
- });
97
- }
98
- targetPath = sanitization.sanitizePath(targetPath, {
99
- allowAbsolute: true,
100
- }).sanitizedPath;
101
- logger.debug("Sanitized path", {
102
- ...context,
103
- operation,
104
- sanitizedPath: targetPath,
105
- });
51
+ export async function gitBranchLogic(params, context) {
52
+ const operation = `gitBranchLogic:${params.mode}`;
53
+ logger.debug(`Executing ${operation}`, { ...context, params });
54
+ const workingDir = context.getWorkingDirectory();
55
+ if (params.path === "." && !workingDir) {
56
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
106
57
  }
107
- catch (error) {
108
- logger.error("Path resolution or sanitization failed", {
109
- ...context,
110
- operation,
111
- error,
112
- });
113
- if (error instanceof McpError)
114
- throw error;
115
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
58
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
59
+ const args = ["-C", targetPath];
60
+ switch (params.mode) {
61
+ case "list":
62
+ args.push("branch", "--list", "--no-color", "--verbose");
63
+ if (params.all)
64
+ args.push("-a");
65
+ else if (params.remote)
66
+ args.push("-r");
67
+ break;
68
+ case "create":
69
+ args.push("branch");
70
+ if (params.force)
71
+ args.push("-f");
72
+ args.push(params.branchName, params.startPoint || "");
73
+ break;
74
+ case "delete":
75
+ args.push("branch", params.force ? "-D" : "-d");
76
+ if (params.remote)
77
+ args.push("-r");
78
+ args.push(params.branchName);
79
+ break;
80
+ case "rename":
81
+ args.push("branch", params.force ? "-M" : "-m", params.branchName, params.newBranchName);
82
+ break;
83
+ case "show-current":
84
+ args.push("branch", "--show-current");
85
+ break;
116
86
  }
117
87
  try {
118
- let args;
119
- let result;
120
- switch (input.mode) {
121
- case "list":
122
- args = ["-C", targetPath, "branch", "--list", "--no-color"]; // Start with basic list
123
- if (input.all) {
124
- args.push("-a"); // Add -a if requested
125
- }
126
- else if (input.remote) {
127
- args.push("-r"); // Add -r if requested (exclusive with -a)
128
- }
129
- args.push("--verbose"); // Add verbose for commit info
130
- logger.debug(`Executing command: git ${args.join(" ")}`, {
131
- ...context,
132
- operation,
133
- });
134
- const { stdout: listStdout } = await execFileAsync("git", args);
135
- const branches = listStdout
136
- .trim()
137
- .split("\n")
138
- .filter((line) => line && !line.match(/^\s*->\s*/)) // Filter out HEAD pointer lines if any
139
- .map((line) => {
140
- const isCurrent = line.startsWith("* ");
141
- const trimmedLine = line.replace(/^\*?\s+/, ""); // Remove leading '*' and spaces
142
- // Determine isRemote based on the raw trimmed line BEFORE splitting
143
- const isRemote = trimmedLine.startsWith("remotes/");
144
- const parts = trimmedLine.split(/\s+/);
145
- const name = parts[0]; // This might be 'remotes/origin/main' or just 'main'
146
- const commitHash = parts[1] || undefined; // Verbose gives hash
147
- const commitSubject = parts.slice(2).join(" ") || undefined; // Verbose gives subject
148
- // Return the correct name (without 'remotes/' prefix if it was remote) and the isRemote flag
149
- return {
150
- name: isRemote ? name.split("/").slice(2).join("/") : name, // e.g., 'origin/main' or 'main'
151
- isCurrent,
152
- isRemote, // Use the flag determined before splitting
153
- commitHash,
154
- commitSubject,
155
- };
156
- });
157
- const currentBranch = branches.find((b) => b.isCurrent)?.name;
158
- result = { success: true, mode: "list", branches, currentBranch };
159
- break;
160
- case "create":
161
- // branchName is validated by Zod refine
162
- args = ["-C", targetPath, "branch"];
163
- if (input.force) {
164
- args.push("-f");
165
- }
166
- args.push(input.branchName); // branchName is guaranteed by refine
167
- if (input.startPoint) {
168
- args.push(input.startPoint);
169
- }
170
- logger.debug(`Executing command: git ${args.join(" ")}`, {
171
- ...context,
172
- operation,
173
- });
174
- await execFileAsync("git", args);
175
- result = {
176
- success: true,
177
- mode: "create",
178
- branchName: input.branchName,
179
- message: `Branch '${input.branchName}' created successfully.`,
180
- };
181
- break;
182
- case "delete":
183
- // branchName is validated by Zod refine
184
- args = ["-C", targetPath, "branch"];
185
- if (input.remote) {
186
- args.push("-r");
187
- }
188
- args.push(input.force ? "-D" : "-d");
189
- args.push(input.branchName); // branchName is guaranteed by refine
190
- logger.debug(`Executing command: git ${args.join(" ")}`, {
191
- ...context,
192
- operation,
193
- });
194
- const { stdout: deleteStdout } = await execFileAsync("git", args);
195
- result = {
196
- success: true,
197
- mode: "delete",
198
- branchName: input.branchName,
199
- wasRemote: input.remote,
200
- message: deleteStdout.trim() ||
201
- `Branch '${input.branchName}' deleted successfully.`,
202
- };
203
- break;
204
- case "rename":
205
- // branchName and newBranchName validated by Zod refine
206
- args = ["-C", targetPath, "branch"];
207
- args.push(input.force ? "-M" : "-m");
208
- args.push(input.branchName, input.newBranchName);
209
- logger.debug(`Executing command: git ${args.join(" ")}`, {
210
- ...context,
211
- operation,
212
- });
213
- await execFileAsync("git", args);
214
- result = {
215
- success: true,
216
- mode: "rename",
217
- oldBranchName: input.branchName,
218
- newBranchName: input.newBranchName,
219
- message: `Branch '${input.branchName}' renamed to '${input.newBranchName}' successfully.`,
220
- };
221
- break;
222
- case "show-current":
223
- args = ["-C", targetPath, "branch", "--show-current"];
224
- logger.debug(`Executing command: git ${args.join(" ")}`, {
225
- ...context,
226
- operation,
227
- });
228
- try {
229
- const { stdout: currentStdout } = await execFileAsync("git", args);
230
- const currentBranchName = currentStdout.trim();
231
- result = {
232
- success: true,
233
- mode: "show-current",
234
- currentBranch: currentBranchName || null,
235
- message: currentBranchName
236
- ? `Current branch is '${currentBranchName}'.`
237
- : "Currently in detached HEAD state.",
238
- };
239
- }
240
- catch (showError) {
241
- // Handle detached HEAD state specifically if command fails
242
- if (showError.stderr?.includes("HEAD detached")) {
243
- result = {
244
- success: true,
245
- mode: "show-current",
246
- currentBranch: null,
247
- message: "Currently in detached HEAD state.",
248
- };
249
- }
250
- else {
251
- throw showError; // Re-throw other errors
252
- }
253
- }
254
- break;
255
- default:
256
- // Should not happen due to Zod validation
257
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
88
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
89
+ const { stdout, stderr } = await execFileAsync("git", args.filter(Boolean));
90
+ if (stderr && !stderr.includes("HEAD detached")) {
91
+ logger.warning(`Git branch command produced stderr`, { ...context, operation, stderr });
258
92
  }
259
- logger.info(`git branch ${input.mode} executed successfully`, {
260
- ...context,
261
- operation,
262
- path: targetPath,
263
- result,
264
- });
265
- return result;
266
- }
267
- catch (error) {
268
- const errorMessage = error.stderr || error.stdout || error.message || ""; // stdout might contain error messages too
269
- logger.error(`Failed to execute git branch command`, {
270
- ...context,
271
- operation,
272
- path: targetPath,
273
- error: errorMessage,
274
- stderr: error.stderr,
275
- stdout: error.stdout,
276
- });
277
- // Specific error handling
278
- if (errorMessage.toLowerCase().includes("not a git repository")) {
279
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
280
- }
281
- if (input.mode === "create" && errorMessage.includes("already exists")) {
93
+ if (params.mode === "list") {
94
+ const branches = stdout.trim().split("\n").filter(Boolean).map(line => {
95
+ const isCurrent = line.startsWith("* ");
96
+ const trimmedLine = line.replace(/^\*?\s+/, "");
97
+ const isRemote = trimmedLine.startsWith("remotes/");
98
+ const parts = trimmedLine.split(/\s+/);
99
+ const name = parts[0];
100
+ return {
101
+ name: isRemote ? name.split("/").slice(2).join("/") : name,
102
+ isCurrent,
103
+ isRemote,
104
+ commitHash: parts[1],
105
+ commitSubject: parts.slice(2).join(" "),
106
+ };
107
+ });
282
108
  return {
283
- success: false,
284
- mode: "create",
285
- message: `Failed to create branch: Branch '${input.branchName}' already exists. Use force=true to overwrite.`,
286
- error: errorMessage,
109
+ success: true,
110
+ mode: params.mode,
111
+ message: `Found ${branches.length} branches.`,
112
+ branches,
113
+ currentBranch: branches.find(b => b.isCurrent)?.name || null,
287
114
  };
288
115
  }
289
- if (input.mode === "delete" && errorMessage.includes("not found")) {
116
+ if (params.mode === "show-current") {
117
+ const currentBranchName = stdout.trim() || null;
290
118
  return {
291
- success: false,
292
- mode: "delete",
293
- message: `Failed to delete branch: Branch '${input.branchName}' not found.`,
294
- error: errorMessage,
119
+ success: true,
120
+ mode: params.mode,
121
+ message: currentBranchName ? `Current branch is '${currentBranchName}'.` : "Currently in detached HEAD state.",
122
+ currentBranch: currentBranchName,
295
123
  };
296
124
  }
297
- if (input.mode === "delete" && errorMessage.includes("not fully merged")) {
298
- return {
299
- success: false,
300
- mode: "delete",
301
- message: `Failed to delete branch: Branch '${input.branchName}' is not fully merged. Use force=true to delete.`,
302
- error: errorMessage,
303
- };
125
+ return {
126
+ success: true,
127
+ mode: params.mode,
128
+ message: `Operation '${params.mode}' on branch '${params.branchName || params.newBranchName}' completed successfully.`,
129
+ };
130
+ }
131
+ catch (error) {
132
+ const errorMessage = error.stderr || error.stdout || error.message || "";
133
+ logger.error(`Failed to execute git branch command`, { ...context, operation, errorMessage });
134
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
135
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
304
136
  }
305
- if (input.mode === "rename" && errorMessage.includes("already exists")) {
306
- return {
307
- success: false,
308
- mode: "rename",
309
- message: `Failed to rename branch: Branch '${input.newBranchName}' already exists. Use force=true to overwrite.`,
310
- error: errorMessage,
311
- };
137
+ // For specific, non-critical failures, we can throw a specific error code that the handler can interpret.
138
+ if (params.mode === "create" && errorMessage.includes("already exists")) {
139
+ throw new McpError(BaseErrorCode.CONFLICT, `Branch '${params.branchName}' already exists. Use force=true to overwrite.`);
312
140
  }
313
- if (input.mode === "rename" && errorMessage.includes("not found")) {
314
- return {
315
- success: false,
316
- mode: "rename",
317
- message: `Failed to rename branch: Branch '${input.branchName}' not found.`,
318
- error: errorMessage,
319
- };
141
+ if (params.mode === "delete" && errorMessage.includes("not found")) {
142
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Branch '${params.branchName}' not found.`);
320
143
  }
321
- // Throw a generic McpError for other failures
322
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git branch ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
144
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git branch ${params.mode} failed: ${errorMessage}`);
323
145
  }
324
146
  }