@cyanheads/git-mcp-server 2.1.0 → 2.1.2
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 +8 -11
- package/dist/config/index.js +7 -7
- package/dist/index.js +35 -21
- package/dist/mcp-server/server.js +72 -56
- package/dist/mcp-server/tools/gitAdd/index.js +1 -1
- package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
- package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
- package/dist/mcp-server/tools/gitBranch/index.js +1 -1
- package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
- package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
- package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
- package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
- package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
- package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
- package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
- package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
- package/dist/mcp-server/tools/gitClean/index.js +1 -1
- package/dist/mcp-server/tools/gitClean/logic.js +93 -41
- package/dist/mcp-server/tools/gitClean/registration.js +19 -16
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
- package/dist/mcp-server/tools/gitClone/index.js +1 -1
- package/dist/mcp-server/tools/gitClone/logic.js +89 -30
- package/dist/mcp-server/tools/gitClone/registration.js +15 -12
- package/dist/mcp-server/tools/gitCommit/index.js +1 -1
- package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
- package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
- package/dist/mcp-server/tools/gitDiff/index.js +1 -1
- package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
- package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
- package/dist/mcp-server/tools/gitFetch/index.js +1 -1
- package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
- package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
- package/dist/mcp-server/tools/gitInit/index.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +88 -34
- package/dist/mcp-server/tools/gitInit/registration.js +32 -18
- package/dist/mcp-server/tools/gitLog/index.js +1 -1
- package/dist/mcp-server/tools/gitLog/logic.js +133 -47
- package/dist/mcp-server/tools/gitLog/registration.js +16 -14
- package/dist/mcp-server/tools/gitMerge/index.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
- package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
- package/dist/mcp-server/tools/gitPull/index.js +1 -1
- package/dist/mcp-server/tools/gitPull/logic.js +90 -69
- package/dist/mcp-server/tools/gitPull/registration.js +16 -14
- package/dist/mcp-server/tools/gitPush/index.js +1 -1
- package/dist/mcp-server/tools/gitPush/logic.js +116 -100
- package/dist/mcp-server/tools/gitPush/registration.js +16 -14
- package/dist/mcp-server/tools/gitRebase/index.js +1 -1
- package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
- package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
- package/dist/mcp-server/tools/gitRemote/index.js +1 -1
- package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
- package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
- package/dist/mcp-server/tools/gitReset/index.js +1 -1
- package/dist/mcp-server/tools/gitReset/logic.js +65 -37
- package/dist/mcp-server/tools/gitReset/registration.js +14 -12
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
- package/dist/mcp-server/tools/gitShow/index.js +1 -1
- package/dist/mcp-server/tools/gitShow/logic.js +78 -35
- package/dist/mcp-server/tools/gitShow/registration.js +17 -12
- package/dist/mcp-server/tools/gitStash/index.js +1 -1
- package/dist/mcp-server/tools/gitStash/logic.js +143 -58
- package/dist/mcp-server/tools/gitStash/registration.js +19 -12
- package/dist/mcp-server/tools/gitStatus/index.js +1 -1
- package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
- package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
- package/dist/mcp-server/tools/gitTag/index.js +1 -1
- package/dist/mcp-server/tools/gitTag/logic.js +124 -51
- package/dist/mcp-server/tools/gitTag/registration.js +14 -11
- package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
- package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
- package/dist/mcp-server/transports/httpTransport.js +187 -79
- package/dist/mcp-server/transports/stdioTransport.js +14 -8
- package/dist/types-global/errors.js +9 -4
- package/dist/utils/index.js +4 -4
- package/dist/utils/internal/errorHandler.js +62 -40
- package/dist/utils/internal/index.js +3 -3
- package/dist/utils/internal/logger.js +97 -54
- package/dist/utils/internal/requestContext.js +7 -5
- package/dist/utils/metrics/index.js +1 -1
- package/dist/utils/metrics/tokenCounter.js +18 -14
- package/dist/utils/parsing/dateParser.js +5 -5
- package/dist/utils/parsing/index.js +2 -2
- package/dist/utils/parsing/jsonParser.js +20 -11
- package/dist/utils/security/idGenerator.js +8 -10
- package/dist/utils/security/index.js +3 -3
- package/dist/utils/security/rateLimiter.js +16 -14
- package/dist/utils/security/sanitization.js +139 -82
- package/package.json +45 -23
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseErrorCode } from
|
|
2
|
-
import { ErrorHandler, logger, requestContextService } from
|
|
1
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
2
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // ErrorHandler (./utils/internal/errorHandler.js), logger (./utils/internal/logger.js), requestContextService (./utils/internal/requestContext.js)
|
|
3
3
|
// Import the final schema and types for handler logic
|
|
4
4
|
// Import the BASE schema separately for registration shape
|
|
5
|
-
import { GitTagBaseSchema, gitTagLogic } from
|
|
5
|
+
import { GitTagBaseSchema, gitTagLogic, } from "./logic.js";
|
|
6
6
|
let _getWorkingDirectory;
|
|
7
7
|
let _getSessionId;
|
|
8
8
|
/**
|
|
@@ -13,10 +13,10 @@ let _getSessionId;
|
|
|
13
13
|
export function initializeGitTagStateAccessors(getWdFn, getSidFn) {
|
|
14
14
|
_getWorkingDirectory = getWdFn;
|
|
15
15
|
_getSessionId = getSidFn;
|
|
16
|
-
logger.info(
|
|
16
|
+
logger.info("State accessors initialized for git_tag tool registration.");
|
|
17
17
|
}
|
|
18
|
-
const TOOL_NAME =
|
|
19
|
-
const TOOL_DESCRIPTION =
|
|
18
|
+
const TOOL_NAME = "git_tag";
|
|
19
|
+
const TOOL_DESCRIPTION = "Manages Git tags. Supports listing existing tags, creating new lightweight or annotated tags against specific commits, and deleting local tags. Returns results as a JSON object.";
|
|
20
20
|
/**
|
|
21
21
|
* Registers the git_tag tool with the MCP server.
|
|
22
22
|
*
|
|
@@ -26,9 +26,9 @@ const TOOL_DESCRIPTION = 'Manages Git tags. Supports listing existing tags, crea
|
|
|
26
26
|
*/
|
|
27
27
|
export const registerGitTagTool = async (server) => {
|
|
28
28
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error("State accessors for git_tag must be initialized before registration.");
|
|
30
30
|
}
|
|
31
|
-
const operation =
|
|
31
|
+
const operation = "registerGitTagTool";
|
|
32
32
|
const context = requestContextService.createRequestContext({ operation });
|
|
33
33
|
await ErrorHandler.tryCatch(async () => {
|
|
34
34
|
// Register the tool using the *base* schema's shape for definition
|
|
@@ -40,7 +40,10 @@ export const registerGitTagTool = async (server) => {
|
|
|
40
40
|
// Cast validatedArgs to the specific input type for use within the handler
|
|
41
41
|
const toolInput = validatedArgs;
|
|
42
42
|
const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`; // Include mode in operation
|
|
43
|
-
const requestContext = requestContextService.createRequestContext({
|
|
43
|
+
const requestContext = requestContextService.createRequestContext({
|
|
44
|
+
operation: toolOperation,
|
|
45
|
+
parentContext: callContext,
|
|
46
|
+
});
|
|
44
47
|
const sessionId = _getSessionId(requestContext);
|
|
45
48
|
const getWorkingDirectoryForSession = () => {
|
|
46
49
|
return _getWorkingDirectory(sessionId);
|
|
@@ -56,9 +59,9 @@ export const registerGitTagTool = async (server) => {
|
|
|
56
59
|
const tagResult = await gitTagLogic(toolInput, logicContext);
|
|
57
60
|
// Format the result as a JSON string within TextContent
|
|
58
61
|
const resultContent = {
|
|
59
|
-
type:
|
|
62
|
+
type: "text",
|
|
60
63
|
text: JSON.stringify(tagResult, null, 2), // Pretty-print JSON
|
|
61
|
-
contentType:
|
|
64
|
+
contentType: "application/json",
|
|
62
65
|
};
|
|
63
66
|
// Log based on the success flag in the result
|
|
64
67
|
if (tagResult.success) {
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* @fileoverview Barrel file for the git_worktree tool.
|
|
3
3
|
* Exports the registration function and state accessor initialization function.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitWorktreeTool, initializeGitWorktreeStateAccessors } from
|
|
5
|
+
export { registerGitWorktreeTool, initializeGitWorktreeStateAccessors, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitWorktreeInput, GitWorktreeResult } from './logic.js';
|
|
@@ -1,71 +1,115 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { logger, sanitization } from
|
|
5
|
-
import { BaseErrorCode, McpError } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
5
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
// Define the BASE input schema for the git_worktree tool using Zod
|
|
8
8
|
export const GitWorktreeBaseSchema = z.object({
|
|
9
|
-
path: z
|
|
10
|
-
|
|
9
|
+
path: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.optional()
|
|
13
|
+
.default(".")
|
|
14
|
+
.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."),
|
|
15
|
+
mode: z
|
|
16
|
+
.enum(["list", "add", "remove", "move", "prune"])
|
|
17
|
+
.describe("The worktree operation to perform: 'list', 'add', 'remove', 'move', 'prune'."),
|
|
11
18
|
// Common optional path for operations
|
|
12
|
-
worktreePath: z
|
|
19
|
+
worktreePath: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Path of the worktree. Required for 'add', 'remove', 'move' modes."),
|
|
13
24
|
// 'add' mode specific
|
|
14
|
-
commitish: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
commitish: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Branch or commit to checkout in the new worktree. Used only in 'add' mode. Defaults to HEAD."),
|
|
30
|
+
newBranch: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Create a new branch in the worktree. Used only in 'add' mode."),
|
|
35
|
+
force: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.default(false)
|
|
38
|
+
.describe("Force the operation (e.g., for 'add' if branch exists, or 'remove' if uncommitted changes)."),
|
|
39
|
+
detach: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(false)
|
|
42
|
+
.describe("Detach HEAD in the new worktree. Used only in 'add' mode."),
|
|
18
43
|
// 'move' mode specific
|
|
19
|
-
newPath: z
|
|
44
|
+
newPath: z
|
|
45
|
+
.string()
|
|
46
|
+
.min(1)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("The new path for the worktree. Required for 'move' mode."),
|
|
20
49
|
// 'prune' mode specific
|
|
21
|
-
verbose: z
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
verbose: z
|
|
51
|
+
.boolean()
|
|
52
|
+
.default(false)
|
|
53
|
+
.describe("Provide more detailed output. Used in 'list' and 'prune' modes."),
|
|
54
|
+
dryRun: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.default(false)
|
|
57
|
+
.describe("Show what would be done without actually doing it. Used in 'prune' mode."),
|
|
58
|
+
expire: z
|
|
59
|
+
.string()
|
|
60
|
+
.min(1)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Prune entries older than this time (e.g., '1.month.ago'). Used in 'prune' mode."),
|
|
24
63
|
});
|
|
25
64
|
// Apply refinements and export the FINAL schema
|
|
26
|
-
export const GitWorktreeInputSchema = GitWorktreeBaseSchema.refine(data => !(data.mode ===
|
|
27
|
-
message: "A 'worktreePath' is required for 'add' mode.",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
message: "
|
|
65
|
+
export const GitWorktreeInputSchema = GitWorktreeBaseSchema.refine((data) => !(data.mode === "add" && !data.worktreePath), {
|
|
66
|
+
message: "A 'worktreePath' is required for 'add' mode.",
|
|
67
|
+
path: ["worktreePath"],
|
|
68
|
+
})
|
|
69
|
+
.refine((data) => !(data.mode === "remove" && !data.worktreePath), {
|
|
70
|
+
message: "A 'worktreePath' is required for 'remove' mode.",
|
|
71
|
+
path: ["worktreePath"],
|
|
72
|
+
})
|
|
73
|
+
.refine((data) => !(data.mode === "move" && (!data.worktreePath || !data.newPath)), {
|
|
74
|
+
message: "Both 'worktreePath' (old path) and 'newPath' are required for 'move' mode.",
|
|
75
|
+
path: ["worktreePath", "newPath"],
|
|
32
76
|
});
|
|
33
77
|
/**
|
|
34
78
|
* Parses the output of `git worktree list --porcelain`.
|
|
35
79
|
*/
|
|
36
80
|
function parsePorcelainWorktreeList(stdout) {
|
|
37
81
|
const worktrees = [];
|
|
38
|
-
const entries = stdout.trim().split(
|
|
82
|
+
const entries = stdout.trim().split("\n\n"); // Entries are separated by double newlines
|
|
39
83
|
for (const entry of entries) {
|
|
40
|
-
const lines = entry.trim().split(
|
|
41
|
-
let path =
|
|
42
|
-
let head =
|
|
84
|
+
const lines = entry.trim().split("\n");
|
|
85
|
+
let path = "";
|
|
86
|
+
let head = "";
|
|
43
87
|
let branch;
|
|
44
88
|
let isBare = false;
|
|
45
89
|
let isLocked = false;
|
|
46
90
|
let isPrunable = false;
|
|
47
91
|
let prunableReason;
|
|
48
92
|
for (const line of lines) {
|
|
49
|
-
if (line.startsWith(
|
|
50
|
-
path = line.substring(
|
|
93
|
+
if (line.startsWith("worktree ")) {
|
|
94
|
+
path = line.substring("worktree ".length);
|
|
51
95
|
}
|
|
52
|
-
else if (line.startsWith(
|
|
53
|
-
head = line.substring(
|
|
96
|
+
else if (line.startsWith("HEAD ")) {
|
|
97
|
+
head = line.substring("HEAD ".length);
|
|
54
98
|
}
|
|
55
|
-
else if (line.startsWith(
|
|
56
|
-
branch = line.substring(
|
|
99
|
+
else if (line.startsWith("branch ")) {
|
|
100
|
+
branch = line.substring("branch ".length);
|
|
57
101
|
}
|
|
58
|
-
else if (line.startsWith(
|
|
102
|
+
else if (line.startsWith("bare")) {
|
|
59
103
|
isBare = true;
|
|
60
104
|
}
|
|
61
|
-
else if (line.startsWith(
|
|
105
|
+
else if (line.startsWith("locked")) {
|
|
62
106
|
isLocked = true;
|
|
63
107
|
const reasonMatch = line.match(/locked(?: (.+))?/);
|
|
64
108
|
if (reasonMatch && reasonMatch[1]) {
|
|
65
109
|
prunableReason = reasonMatch[1]; // Using prunableReason for lock reason too
|
|
66
110
|
}
|
|
67
111
|
}
|
|
68
|
-
else if (line.startsWith(
|
|
112
|
+
else if (line.startsWith("prunable")) {
|
|
69
113
|
isPrunable = true;
|
|
70
114
|
const reasonMatch = line.match(/prunable(?: (.+))?/);
|
|
71
115
|
if (reasonMatch && reasonMatch[1]) {
|
|
@@ -73,8 +117,17 @@ function parsePorcelainWorktreeList(stdout) {
|
|
|
73
117
|
}
|
|
74
118
|
}
|
|
75
119
|
}
|
|
76
|
-
if (path) {
|
|
77
|
-
|
|
120
|
+
if (path) {
|
|
121
|
+
// Only add if a path was found
|
|
122
|
+
worktrees.push({
|
|
123
|
+
path,
|
|
124
|
+
head,
|
|
125
|
+
branch,
|
|
126
|
+
isBare,
|
|
127
|
+
isLocked,
|
|
128
|
+
isPrunable,
|
|
129
|
+
prunableReason,
|
|
130
|
+
});
|
|
78
131
|
}
|
|
79
132
|
}
|
|
80
133
|
return worktrees;
|
|
@@ -88,24 +141,36 @@ export async function gitWorktreeLogic(input, context) {
|
|
|
88
141
|
let targetPath;
|
|
89
142
|
try {
|
|
90
143
|
const workingDir = context.getWorkingDirectory();
|
|
91
|
-
targetPath =
|
|
92
|
-
? input.path
|
|
93
|
-
|
|
94
|
-
if (targetPath === '.' && !workingDir) {
|
|
144
|
+
targetPath =
|
|
145
|
+
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
146
|
+
if (targetPath === "." && !workingDir) {
|
|
95
147
|
logger.warning("Executing git worktree in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
96
148
|
targetPath = process.cwd();
|
|
97
149
|
}
|
|
98
|
-
else if (targetPath ===
|
|
150
|
+
else if (targetPath === "." && workingDir) {
|
|
99
151
|
targetPath = workingDir;
|
|
100
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
152
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
153
|
+
...context,
|
|
154
|
+
operation,
|
|
155
|
+
sessionId: context.sessionId,
|
|
156
|
+
});
|
|
101
157
|
}
|
|
102
158
|
else {
|
|
103
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
159
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
160
|
+
...context,
|
|
161
|
+
operation,
|
|
162
|
+
});
|
|
104
163
|
}
|
|
105
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
164
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
165
|
+
allowAbsolute: true,
|
|
166
|
+
}).sanitizedPath;
|
|
106
167
|
}
|
|
107
168
|
catch (error) {
|
|
108
|
-
logger.error(
|
|
169
|
+
logger.error("Path resolution or sanitization failed", {
|
|
170
|
+
...context,
|
|
171
|
+
operation,
|
|
172
|
+
error,
|
|
173
|
+
});
|
|
109
174
|
if (error instanceof McpError)
|
|
110
175
|
throw error;
|
|
111
176
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -114,91 +179,126 @@ export async function gitWorktreeLogic(input, context) {
|
|
|
114
179
|
let command = `git -C "${targetPath}" worktree `;
|
|
115
180
|
let result;
|
|
116
181
|
switch (input.mode) {
|
|
117
|
-
case
|
|
118
|
-
command +=
|
|
182
|
+
case "list":
|
|
183
|
+
command += "list";
|
|
119
184
|
if (input.verbose)
|
|
120
|
-
command +=
|
|
121
|
-
logger.debug(`Executing command: ${command}`, {
|
|
185
|
+
command += " --porcelain"; // Use porcelain for structured output
|
|
186
|
+
logger.debug(`Executing command: ${command}`, {
|
|
187
|
+
...context,
|
|
188
|
+
operation,
|
|
189
|
+
});
|
|
122
190
|
const { stdout: listStdout } = await execAsync(command);
|
|
123
191
|
if (input.verbose) {
|
|
124
192
|
const worktrees = parsePorcelainWorktreeList(listStdout);
|
|
125
|
-
result = { success: true, mode:
|
|
193
|
+
result = { success: true, mode: "list", worktrees };
|
|
126
194
|
}
|
|
127
195
|
else {
|
|
128
196
|
// Simple list output parsing (less structured)
|
|
129
|
-
const worktrees = listStdout
|
|
197
|
+
const worktrees = listStdout
|
|
198
|
+
.trim()
|
|
199
|
+
.split("\n")
|
|
200
|
+
.map((line) => {
|
|
130
201
|
const parts = line.split(/\s+/);
|
|
131
202
|
return {
|
|
132
203
|
path: parts[0],
|
|
133
204
|
head: parts[1],
|
|
134
|
-
branch: parts[2]?.replace(/[\[\]]/g,
|
|
205
|
+
branch: parts[2]?.replace(/[\[\]]/g, ""), // Remove brackets from branch name
|
|
135
206
|
isBare: false, // Cannot determine from simple list
|
|
136
207
|
isLocked: false, // Cannot determine
|
|
137
|
-
isPrunable: false // Cannot determine
|
|
208
|
+
isPrunable: false, // Cannot determine
|
|
138
209
|
};
|
|
139
210
|
});
|
|
140
|
-
result = { success: true, mode:
|
|
211
|
+
result = { success: true, mode: "list", worktrees };
|
|
141
212
|
}
|
|
142
213
|
break;
|
|
143
|
-
case
|
|
214
|
+
case "add":
|
|
144
215
|
// worktreePath is guaranteed by refine
|
|
145
216
|
const sanitizedWorktreePathAdd = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
146
217
|
command += `add `;
|
|
147
218
|
if (input.force)
|
|
148
|
-
command +=
|
|
219
|
+
command += "--force ";
|
|
149
220
|
if (input.detach)
|
|
150
|
-
command +=
|
|
221
|
+
command += "--detach ";
|
|
151
222
|
if (input.newBranch)
|
|
152
223
|
command += `-b "${input.newBranch}" `;
|
|
153
224
|
command += `"${sanitizedWorktreePathAdd}"`;
|
|
154
225
|
if (input.commitish)
|
|
155
226
|
command += ` "${input.commitish}"`;
|
|
156
|
-
logger.debug(`Executing command: ${command}`, {
|
|
227
|
+
logger.debug(`Executing command: ${command}`, {
|
|
228
|
+
...context,
|
|
229
|
+
operation,
|
|
230
|
+
});
|
|
157
231
|
await execAsync(command);
|
|
158
232
|
// To get the HEAD of the new worktree, we might need another command or parse output if available
|
|
159
233
|
// For simplicity, we'll report success. A more robust solution might `git -C new_worktree_path rev-parse HEAD`
|
|
160
234
|
result = {
|
|
161
235
|
success: true,
|
|
162
|
-
mode:
|
|
236
|
+
mode: "add",
|
|
163
237
|
worktreePath: sanitizedWorktreePathAdd,
|
|
164
238
|
branch: input.newBranch,
|
|
165
|
-
head:
|
|
166
|
-
message: `Worktree '${sanitizedWorktreePathAdd}' added successfully
|
|
239
|
+
head: "HEAD", // Placeholder, actual SHA would require another call
|
|
240
|
+
message: `Worktree '${sanitizedWorktreePathAdd}' added successfully.`,
|
|
167
241
|
};
|
|
168
242
|
break;
|
|
169
|
-
case
|
|
243
|
+
case "remove":
|
|
170
244
|
// worktreePath is guaranteed by refine
|
|
171
245
|
const sanitizedWorktreePathRemove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
172
246
|
command += `remove `;
|
|
173
247
|
if (input.force)
|
|
174
|
-
command +=
|
|
248
|
+
command += "--force ";
|
|
175
249
|
command += `"${sanitizedWorktreePathRemove}"`;
|
|
176
|
-
logger.debug(`Executing command: ${command}`, {
|
|
250
|
+
logger.debug(`Executing command: ${command}`, {
|
|
251
|
+
...context,
|
|
252
|
+
operation,
|
|
253
|
+
});
|
|
177
254
|
const { stdout: removeStdout } = await execAsync(command);
|
|
178
|
-
result = {
|
|
255
|
+
result = {
|
|
256
|
+
success: true,
|
|
257
|
+
mode: "remove",
|
|
258
|
+
worktreePath: sanitizedWorktreePathRemove,
|
|
259
|
+
message: removeStdout.trim() ||
|
|
260
|
+
`Worktree '${sanitizedWorktreePathRemove}' removed successfully.`,
|
|
261
|
+
};
|
|
179
262
|
break;
|
|
180
|
-
case
|
|
263
|
+
case "move":
|
|
181
264
|
// worktreePath and newPath are guaranteed by refine
|
|
182
265
|
const sanitizedOldPathMove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
183
|
-
const sanitizedNewPathMove = sanitization.sanitizePath(input.newPath, {
|
|
266
|
+
const sanitizedNewPathMove = sanitization.sanitizePath(input.newPath, {
|
|
267
|
+
allowAbsolute: true,
|
|
268
|
+
rootDir: targetPath,
|
|
269
|
+
}).sanitizedPath;
|
|
184
270
|
command += `move "${sanitizedOldPathMove}" "${sanitizedNewPathMove}"`;
|
|
185
|
-
logger.debug(`Executing command: ${command}`, {
|
|
271
|
+
logger.debug(`Executing command: ${command}`, {
|
|
272
|
+
...context,
|
|
273
|
+
operation,
|
|
274
|
+
});
|
|
186
275
|
await execAsync(command);
|
|
187
|
-
result = {
|
|
276
|
+
result = {
|
|
277
|
+
success: true,
|
|
278
|
+
mode: "move",
|
|
279
|
+
oldPath: sanitizedOldPathMove,
|
|
280
|
+
newPath: sanitizedNewPathMove,
|
|
281
|
+
message: `Worktree moved from '${sanitizedOldPathMove}' to '${sanitizedNewPathMove}' successfully.`,
|
|
282
|
+
};
|
|
188
283
|
break;
|
|
189
|
-
case
|
|
190
|
-
command +=
|
|
284
|
+
case "prune":
|
|
285
|
+
command += "prune ";
|
|
191
286
|
if (input.dryRun)
|
|
192
|
-
command +=
|
|
287
|
+
command += "--dry-run ";
|
|
193
288
|
if (input.verbose)
|
|
194
|
-
command +=
|
|
289
|
+
command += "--verbose ";
|
|
195
290
|
if (input.expire)
|
|
196
291
|
command += `--expire "${input.expire}" `;
|
|
197
|
-
logger.debug(`Executing command: ${command}`, {
|
|
292
|
+
logger.debug(`Executing command: ${command}`, {
|
|
293
|
+
...context,
|
|
294
|
+
operation,
|
|
295
|
+
});
|
|
198
296
|
const { stdout: pruneStdout, stderr: pruneStderr } = await execAsync(command);
|
|
199
297
|
// Prune often outputs to stderr even on success for verbose/dry-run
|
|
200
|
-
const pruneMessage =
|
|
201
|
-
|
|
298
|
+
const pruneMessage = pruneStdout.trim() ||
|
|
299
|
+
pruneStderr.trim() ||
|
|
300
|
+
"Worktree prune operation completed.";
|
|
301
|
+
result = { success: true, mode: "prune", message: pruneMessage };
|
|
202
302
|
if (input.verbose && pruneStdout.trim()) {
|
|
203
303
|
// Attempt to parse verbose output if needed, for now just return raw message
|
|
204
304
|
// result.prunedItems = ...
|
|
@@ -207,33 +307,42 @@ export async function gitWorktreeLogic(input, context) {
|
|
|
207
307
|
default:
|
|
208
308
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
209
309
|
}
|
|
210
|
-
logger.info(
|
|
310
|
+
logger.info(`git worktree ${input.mode} executed successfully`, {
|
|
311
|
+
...context,
|
|
312
|
+
operation,
|
|
313
|
+
path: targetPath,
|
|
314
|
+
result,
|
|
315
|
+
});
|
|
211
316
|
return result;
|
|
212
317
|
}
|
|
213
318
|
catch (error) {
|
|
214
|
-
const errorMessage = error.stderr || error.stdout ||
|
|
215
|
-
logger.error(`Failed to execute git worktree command`, {
|
|
216
|
-
|
|
319
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
320
|
+
logger.error(`Failed to execute git worktree command`, {
|
|
321
|
+
...context,
|
|
322
|
+
path: targetPath,
|
|
323
|
+
error: errorMessage,
|
|
324
|
+
stderr: error.stderr,
|
|
325
|
+
stdout: error.stdout,
|
|
326
|
+
});
|
|
327
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
217
328
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
218
329
|
}
|
|
219
330
|
// Add more specific error handling based on `git worktree` messages
|
|
220
|
-
if (input.mode ===
|
|
221
|
-
|
|
331
|
+
if (input.mode === "add" && errorMessage.includes("already exists")) {
|
|
332
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to add worktree: Path '${input.worktreePath}' already exists or is a worktree. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
222
333
|
}
|
|
223
|
-
if (input.mode ===
|
|
224
|
-
|
|
334
|
+
if (input.mode === "add" && errorMessage.includes("is a submodule")) {
|
|
335
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to add worktree: Path '${input.worktreePath}' is a submodule. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
225
336
|
}
|
|
226
|
-
if (input.mode ===
|
|
227
|
-
|
|
337
|
+
if (input.mode === "remove" &&
|
|
338
|
+
errorMessage.includes("cannot remove the current worktree")) {
|
|
339
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to remove worktree: Cannot remove the current worktree. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
228
340
|
}
|
|
229
|
-
if (input.mode ===
|
|
230
|
-
|
|
341
|
+
if (input.mode === "remove" &&
|
|
342
|
+
errorMessage.includes("has unclean changes")) {
|
|
343
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to remove worktree: '${input.worktreePath}' has uncommitted changes. Use force=true to remove. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
231
344
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
mode: input.mode,
|
|
235
|
-
message: `Git worktree ${input.mode} failed for path: ${targetPath}.`,
|
|
236
|
-
error: errorMessage
|
|
237
|
-
};
|
|
345
|
+
// Throw a generic McpError for other failures
|
|
346
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git worktree ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
238
347
|
}
|
|
239
348
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { logger, ErrorHandler, requestContextService } from
|
|
2
|
-
import { BaseErrorCode } from
|
|
3
|
-
import { GitWorktreeBaseSchema, gitWorktreeLogic } from
|
|
1
|
+
import { logger, ErrorHandler, requestContextService, } from "../../../utils/index.js";
|
|
2
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
3
|
+
import { GitWorktreeBaseSchema, gitWorktreeLogic, } from "./logic.js";
|
|
4
4
|
let _getWorkingDirectory;
|
|
5
5
|
let _getSessionId;
|
|
6
6
|
/**
|
|
@@ -11,10 +11,10 @@ let _getSessionId;
|
|
|
11
11
|
export function initializeGitWorktreeStateAccessors(getWdFn, getSidFn) {
|
|
12
12
|
_getWorkingDirectory = getWdFn;
|
|
13
13
|
_getSessionId = getSidFn;
|
|
14
|
-
logger.info(
|
|
14
|
+
logger.info("State accessors initialized for git_worktree tool registration.");
|
|
15
15
|
}
|
|
16
|
-
const TOOL_NAME =
|
|
17
|
-
const TOOL_DESCRIPTION =
|
|
16
|
+
const TOOL_NAME = "git_worktree";
|
|
17
|
+
const TOOL_DESCRIPTION = "Manages Git worktrees. Supports listing, adding, removing, moving, and pruning worktrees. Returns results as a JSON object.";
|
|
18
18
|
/**
|
|
19
19
|
* Registers the git_worktree tool with the MCP server.
|
|
20
20
|
*
|
|
@@ -24,15 +24,18 @@ const TOOL_DESCRIPTION = 'Manages Git worktrees. Supports listing, adding, remov
|
|
|
24
24
|
*/
|
|
25
25
|
export const registerGitWorktreeTool = async (server) => {
|
|
26
26
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error("State accessors for git_worktree must be initialized before registration.");
|
|
28
28
|
}
|
|
29
|
-
const operation =
|
|
29
|
+
const operation = "registerGitWorktreeTool";
|
|
30
30
|
const context = requestContextService.createRequestContext({ operation });
|
|
31
31
|
await ErrorHandler.tryCatch(async () => {
|
|
32
32
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitWorktreeBaseSchema.shape, async (validatedArgs, callContext) => {
|
|
33
33
|
const toolInput = validatedArgs; // Cast for use
|
|
34
34
|
const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`;
|
|
35
|
-
const requestContext = requestContextService.createRequestContext({
|
|
35
|
+
const requestContext = requestContextService.createRequestContext({
|
|
36
|
+
operation: toolOperation,
|
|
37
|
+
parentContext: callContext,
|
|
38
|
+
});
|
|
36
39
|
const sessionId = _getSessionId(requestContext);
|
|
37
40
|
const getWorkingDirectoryForSession = () => _getWorkingDirectory(sessionId);
|
|
38
41
|
const logicContext = {
|
|
@@ -44,9 +47,9 @@ export const registerGitWorktreeTool = async (server) => {
|
|
|
44
47
|
return await ErrorHandler.tryCatch(async () => {
|
|
45
48
|
const worktreeResult = await gitWorktreeLogic(toolInput, logicContext);
|
|
46
49
|
const resultContent = {
|
|
47
|
-
type:
|
|
50
|
+
type: "text",
|
|
48
51
|
text: JSON.stringify(worktreeResult, null, 2), // Pretty-print JSON
|
|
49
|
-
contentType:
|
|
52
|
+
contentType: "application/json",
|
|
50
53
|
};
|
|
51
54
|
if (worktreeResult.success) {
|
|
52
55
|
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { registerGitWrapupInstructionsTool, initializeGitWrapupInstructionsStateAccessors } from
|
|
1
|
+
export { registerGitWrapupInstructionsTool, initializeGitWrapupInstructionsStateAccessors, } from "./registration.js";
|
|
2
2
|
// This tool now requires session-specific state accessors (getWorkingDirectory, getSessionId)
|
|
3
3
|
// to fetch git status, so initializeGitWrapupInstructionsStateAccessors is exported for server setup.
|