@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.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +44 -143
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +50 -171
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +46 -152
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +75 -257
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +48 -146
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +73 -181
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +37 -121
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +33 -122
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +70 -214
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +66 -188
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +12 -12
|
@@ -1,78 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
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.
|
|
41
|
-
|
|
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 =
|
|
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:
|
|
31
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
55
32
|
};
|
|
56
33
|
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitBranch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitBranch/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitBranchTool
|
|
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
|
-
|
|
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
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
12
|
export const GitBranchBaseSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
*
|
|
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(
|
|
72
|
-
const operation = `gitBranchLogic:${
|
|
73
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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:
|
|
284
|
-
mode:
|
|
285
|
-
message: `
|
|
286
|
-
|
|
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 (
|
|
116
|
+
if (params.mode === "show-current") {
|
|
117
|
+
const currentBranchName = stdout.trim() || null;
|
|
290
118
|
return {
|
|
291
|
-
success:
|
|
292
|
-
mode:
|
|
293
|
-
message:
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 (
|
|
314
|
-
|
|
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
|
-
|
|
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
|
}
|