@cyanheads/git-mcp-server 2.1.8 → 2.2.1

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 +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  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 -144
  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 +47 -173
  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 +45 -154
  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 +18 -32
  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 +47 -187
  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 +75 -310
  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 +72 -264
  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 +38 -136
  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 +40 -162
  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 +71 -266
  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 +45 -191
  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 +39 -156
  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 +65 -192
  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 +59 -207
  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 +76 -200
  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 +33 -133
  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 +39 -144
  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 +28 -133
  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 +59 -219
  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 +79 -236
  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 +57 -198
  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 +102 -328
  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 +25 -43
  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 +8 -8
@@ -1,78 +1,58 @@
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 { addGitFiles, GitAddInputSchema, GitAddOutputSchema, } from "./logic.js";
23
3
  const TOOL_NAME = "git_add";
24
4
  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
- }
5
+ export const registerGitAddTool = async (server, getWorkingDirectory, getSessionId) => {
37
6
  const operation = "registerGitAddTool";
38
7
  const context = requestContextService.createRequestContext({ operation });
39
8
  await ErrorHandler.tryCatch(async () => {
40
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitAddInputSchema.shape, // Provide the Zod schema shape
41
- async (validatedArgs, callContext) => {
9
+ server.registerTool(TOOL_NAME, {
10
+ title: "Git Add",
11
+ description: TOOL_DESCRIPTION,
12
+ inputSchema: GitAddInputSchema.shape,
13
+ outputSchema: GitAddOutputSchema.shape,
14
+ annotations: {
15
+ readOnlyHint: false,
16
+ destructiveHint: false,
17
+ idempotentHint: true,
18
+ openWorldHint: false,
19
+ },
20
+ }, async (validatedArgs, callContext) => {
42
21
  const toolOperation = "tool:git_add";
43
22
  const requestContext = requestContextService.createRequestContext({
44
23
  operation: toolOperation,
45
24
  parentContext: callContext,
46
25
  });
47
- const sessionId = _getSessionId(requestContext);
48
- const getWorkingDirectoryForSession = () => {
49
- return _getWorkingDirectory(sessionId);
50
- };
26
+ const sessionId = getSessionId(requestContext);
51
27
  const logicContext = {
52
28
  ...requestContext,
53
29
  sessionId: sessionId,
54
- getWorkingDirectory: getWorkingDirectoryForSession,
30
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
55
31
  };
56
32
  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",
33
+ try {
34
+ const result = await addGitFiles(validatedArgs, logicContext);
35
+ return {
36
+ structuredContent: result,
37
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
66
38
  };
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
- });
39
+ }
40
+ catch (error) {
41
+ const mcpError = ErrorHandler.handleError(error, {
42
+ operation: "gitAddToolHandler",
43
+ context: logicContext,
44
+ input: validatedArgs,
45
+ });
46
+ return {
47
+ isError: true,
48
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
49
+ structuredContent: {
50
+ code: mcpError.code,
51
+ message: mcpError.message,
52
+ details: mcpError.details,
53
+ },
54
+ };
55
+ }
76
56
  });
77
57
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
78
58
  }, { 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,129 @@
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
- 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 });
258
- }
259
- logger.info(`git branch ${input.mode} executed successfully`, {
260
- ...context,
261
- operation,
262
- path: targetPath,
263
- result,
264
- });
265
- return result;
87
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
88
+ const { stdout, stderr } = await execFileAsync("git", args.filter(Boolean));
89
+ if (stderr && !stderr.includes("HEAD detached")) {
90
+ logger.warning(`Git branch command produced stderr`, { ...context, operation, stderr });
266
91
  }
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")) {
282
- 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,
287
- };
288
- }
289
- if (input.mode === "delete" && errorMessage.includes("not found")) {
290
- return {
291
- success: false,
292
- mode: "delete",
293
- message: `Failed to delete branch: Branch '${input.branchName}' not found.`,
294
- error: errorMessage,
295
- };
296
- }
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
- };
304
- }
305
- if (input.mode === "rename" && errorMessage.includes("already exists")) {
92
+ if (params.mode === "list") {
93
+ const branches = stdout.trim().split("\n").filter(Boolean).map(line => {
94
+ const isCurrent = line.startsWith("* ");
95
+ const trimmedLine = line.replace(/^\*?\s+/, "");
96
+ const isRemote = trimmedLine.startsWith("remotes/");
97
+ const parts = trimmedLine.split(/\s+/);
98
+ const name = parts[0];
306
99
  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,
100
+ name: isRemote ? name.split("/").slice(2).join("/") : name,
101
+ isCurrent,
102
+ isRemote,
103
+ commitHash: parts[1],
104
+ commitSubject: parts.slice(2).join(" "),
311
105
  };
312
- }
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
- };
320
- }
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 });
106
+ });
107
+ return {
108
+ success: true,
109
+ mode: params.mode,
110
+ message: `Found ${branches.length} branches.`,
111
+ branches,
112
+ currentBranch: branches.find(b => b.isCurrent)?.name || null,
113
+ };
114
+ }
115
+ if (params.mode === "show-current") {
116
+ const currentBranchName = stdout.trim() || null;
117
+ return {
118
+ success: true,
119
+ mode: params.mode,
120
+ message: currentBranchName ? `Current branch is '${currentBranchName}'.` : "Currently in detached HEAD state.",
121
+ currentBranch: currentBranchName,
122
+ };
323
123
  }
124
+ return {
125
+ success: true,
126
+ mode: params.mode,
127
+ message: `Operation '${params.mode}' on branch '${params.branchName || params.newBranchName}' completed successfully.`,
128
+ };
324
129
  }