@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,30 +1,66 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
7
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import { sanitization } from
|
|
9
|
+
import { sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
// Define the BASE input schema for the git_rebase tool using Zod
|
|
12
12
|
export const GitRebaseBaseSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(["start", "continue", "abort", "skip"])
|
|
21
|
+
.default("start")
|
|
22
|
+
.describe("Rebase operation mode: 'start' (initiate rebase), 'continue', 'abort', 'skip' (manage ongoing rebase)."),
|
|
23
|
+
upstream: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("The upstream branch or commit to rebase onto. Required for 'start' mode unless 'interactive' is true with default base."),
|
|
28
|
+
branch: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("The branch to rebase. Defaults to the current branch if omitted."),
|
|
33
|
+
interactive: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.default(false)
|
|
36
|
+
.describe("Perform an interactive rebase (`-i`). 'upstream' can be omitted to rebase current branch's tracked upstream or use fork-point."),
|
|
37
|
+
strategy: z
|
|
38
|
+
.enum(["recursive", "resolve", "ours", "theirs", "octopus", "subtree"])
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Specifies the merge strategy to use during rebase."),
|
|
41
|
+
strategyOption: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Pass a specific option to the merge strategy (e.g., 'ours', 'theirs' for recursive). Use with -X."),
|
|
45
|
+
onto: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1)
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Rebase onto a specific commit/branch instead of the upstream's base. Requires 'upstream' to be specified."),
|
|
21
50
|
// TODO: Add options like --preserve-merges, --autosquash, --autostash?
|
|
22
51
|
});
|
|
23
52
|
// Apply refinements and export the FINAL schema for validation within the handler
|
|
24
|
-
export const GitRebaseInputSchema = GitRebaseBaseSchema.refine(data => !(data.mode ===
|
|
25
|
-
message: "An 'upstream' branch/commit is required for 'start' mode unless 'interactive' is true.",
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
export const GitRebaseInputSchema = GitRebaseBaseSchema.refine((data) => !(data.mode === "start" && !data.interactive && !data.upstream), {
|
|
54
|
+
message: "An 'upstream' branch/commit is required for 'start' mode unless 'interactive' is true.",
|
|
55
|
+
path: ["upstream"],
|
|
56
|
+
}).refine((data) => !(data.mode !== "start" &&
|
|
57
|
+
(data.upstream ||
|
|
58
|
+
data.branch ||
|
|
59
|
+
data.interactive ||
|
|
60
|
+
data.strategy ||
|
|
61
|
+
data.onto)), {
|
|
62
|
+
message: "Parameters like 'upstream', 'branch', 'interactive', 'strategy', 'onto' are only applicable for 'start' mode.",
|
|
63
|
+
path: ["mode"],
|
|
28
64
|
});
|
|
29
65
|
/**
|
|
30
66
|
* Executes the 'git rebase' command based on the specified mode.
|
|
@@ -41,25 +77,41 @@ export async function gitRebaseLogic(input, context) {
|
|
|
41
77
|
try {
|
|
42
78
|
// Resolve and sanitize the target path
|
|
43
79
|
const workingDir = context.getWorkingDirectory();
|
|
44
|
-
targetPath =
|
|
45
|
-
? input.path
|
|
46
|
-
|
|
47
|
-
if (targetPath === '.' && !workingDir) {
|
|
80
|
+
targetPath =
|
|
81
|
+
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
82
|
+
if (targetPath === "." && !workingDir) {
|
|
48
83
|
logger.warning("Executing git rebase in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
49
84
|
targetPath = process.cwd();
|
|
50
85
|
}
|
|
51
|
-
else if (targetPath ===
|
|
86
|
+
else if (targetPath === "." && workingDir) {
|
|
52
87
|
targetPath = workingDir;
|
|
53
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
88
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
89
|
+
...context,
|
|
90
|
+
operation,
|
|
91
|
+
sessionId: context.sessionId,
|
|
92
|
+
});
|
|
54
93
|
}
|
|
55
94
|
else {
|
|
56
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
95
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
96
|
+
...context,
|
|
97
|
+
operation,
|
|
98
|
+
});
|
|
57
99
|
}
|
|
58
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
59
|
-
|
|
100
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
101
|
+
allowAbsolute: true,
|
|
102
|
+
}).sanitizedPath;
|
|
103
|
+
logger.debug("Sanitized path", {
|
|
104
|
+
...context,
|
|
105
|
+
operation,
|
|
106
|
+
sanitizedPath: targetPath,
|
|
107
|
+
});
|
|
60
108
|
}
|
|
61
109
|
catch (error) {
|
|
62
|
-
logger.error(
|
|
110
|
+
logger.error("Path resolution or sanitization failed", {
|
|
111
|
+
...context,
|
|
112
|
+
operation,
|
|
113
|
+
error,
|
|
114
|
+
});
|
|
63
115
|
if (error instanceof McpError)
|
|
64
116
|
throw error;
|
|
65
117
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -67,9 +119,9 @@ export async function gitRebaseLogic(input, context) {
|
|
|
67
119
|
try {
|
|
68
120
|
let command = `git -C "${targetPath}" rebase`;
|
|
69
121
|
switch (input.mode) {
|
|
70
|
-
case
|
|
122
|
+
case "start":
|
|
71
123
|
if (input.interactive)
|
|
72
|
-
command +=
|
|
124
|
+
command += " -i";
|
|
73
125
|
if (input.strategy)
|
|
74
126
|
command += ` --strategy=${input.strategy}`;
|
|
75
127
|
if (input.strategyOption)
|
|
@@ -82,14 +134,14 @@ export async function gitRebaseLogic(input, context) {
|
|
|
82
134
|
if (input.branch)
|
|
83
135
|
command += ` "${input.branch.replace(/"/g, '\\"')}"`;
|
|
84
136
|
break;
|
|
85
|
-
case
|
|
86
|
-
command +=
|
|
137
|
+
case "continue":
|
|
138
|
+
command += " --continue";
|
|
87
139
|
break;
|
|
88
|
-
case
|
|
89
|
-
command +=
|
|
140
|
+
case "abort":
|
|
141
|
+
command += " --abort";
|
|
90
142
|
break;
|
|
91
|
-
case
|
|
92
|
-
command +=
|
|
143
|
+
case "skip":
|
|
144
|
+
command += " --skip";
|
|
93
145
|
break;
|
|
94
146
|
default:
|
|
95
147
|
// Should not happen due to Zod validation
|
|
@@ -99,74 +151,61 @@ export async function gitRebaseLogic(input, context) {
|
|
|
99
151
|
try {
|
|
100
152
|
const { stdout, stderr } = await execAsync(command);
|
|
101
153
|
const output = stdout + stderr;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (input.mode === 'start' && input.interactive && /noop/i.test(stderr) && /hint: use 'git rebase --edit-todo'/i.test(stderr)) {
|
|
112
|
-
const message = `Interactive rebase started. Edit the todo list in your editor. Output: ${output.trim()}`;
|
|
113
|
-
logger.info(message, { ...context, operation, path: targetPath });
|
|
114
|
-
return { success: true, mode: input.mode, message, rebaseCompleted: false, needsManualAction: true };
|
|
115
|
-
}
|
|
116
|
-
if (input.mode === 'start' && input.interactive && /applying/i.test(stdout)) {
|
|
117
|
-
const message = `Interactive rebase started and processing commits. Output: ${output.trim()}`;
|
|
118
|
-
logger.info(message, { ...context, operation, path: targetPath });
|
|
119
|
-
return { success: true, mode: input.mode, message, rebaseCompleted: false, needsManualAction: false }; // Might complete or hit conflict/edit
|
|
120
|
-
}
|
|
121
|
-
// Check for conflicts even if exit code is 0 (can happen with --continue sometimes)
|
|
122
|
-
if (/conflict/i.test(output)) {
|
|
123
|
-
const message = `Rebase ${input.mode} resulted in conflicts. Resolve conflicts and use 'git rebase --continue'. Output: ${output.trim()}`;
|
|
124
|
-
logger.warning(message, { ...context, operation, path: targetPath });
|
|
125
|
-
return { success: true, mode: input.mode, message, rebaseCompleted: false, needsManualAction: true };
|
|
126
|
-
}
|
|
127
|
-
// Default success message if no specific pattern matched but no error thrown
|
|
128
|
-
const defaultMessage = `Rebase ${input.mode} command finished. Output: ${output.trim()}`;
|
|
129
|
-
logger.info(defaultMessage, { ...context, operation, path: targetPath });
|
|
130
|
-
return { success: true, mode: input.mode, message: defaultMessage, rebaseCompleted: !/applying|stopped/i.test(output), needsManualAction: /stopped at|edit/.test(output) };
|
|
154
|
+
const message = `Rebase ${input.mode} executed successfully. Output: ${output.trim()}`;
|
|
155
|
+
logger.info(message, { ...context, operation, path: targetPath });
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
mode: input.mode,
|
|
159
|
+
message,
|
|
160
|
+
rebaseCompleted: /successfully rebased/.test(output),
|
|
161
|
+
needsManualAction: /conflict|stopped at|edit/i.test(output),
|
|
162
|
+
};
|
|
131
163
|
}
|
|
132
164
|
catch (rebaseError) {
|
|
133
|
-
const errorMessage = rebaseError.stderr || rebaseError.stdout || rebaseError.message ||
|
|
134
|
-
logger.error(`Git rebase ${input.mode} command failed`, {
|
|
165
|
+
const errorMessage = rebaseError.stderr || rebaseError.stdout || rebaseError.message || "";
|
|
166
|
+
logger.error(`Git rebase ${input.mode} command failed`, {
|
|
167
|
+
...context,
|
|
168
|
+
operation,
|
|
169
|
+
path: targetPath,
|
|
170
|
+
error: errorMessage,
|
|
171
|
+
stderr: rebaseError.stderr,
|
|
172
|
+
stdout: rebaseError.stdout,
|
|
173
|
+
});
|
|
135
174
|
// Handle specific error cases
|
|
136
175
|
if (/conflict/i.test(errorMessage)) {
|
|
137
|
-
|
|
176
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Rebase ${input.mode} failed due to conflicts. Resolve conflicts and use 'git rebase --continue'. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
138
177
|
}
|
|
139
178
|
if (/no rebase in progress/i.test(errorMessage)) {
|
|
140
|
-
|
|
179
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to ${input.mode} rebase: No rebase is currently in progress. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
141
180
|
}
|
|
142
181
|
if (/cannot rebase onto multiple branches/i.test(errorMessage)) {
|
|
143
|
-
|
|
182
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to start rebase: Cannot rebase onto multiple branches. Check your 'upstream' parameter. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
144
183
|
}
|
|
145
184
|
if (/does not point to a valid commit/i.test(errorMessage)) {
|
|
146
|
-
|
|
185
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to start rebase: Invalid upstream, branch, or onto reference provided. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
147
186
|
}
|
|
148
187
|
if (/your local changes would be overwritten/i.test(errorMessage)) {
|
|
149
|
-
|
|
188
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to ${input.mode} rebase: Your local changes to tracked files would be overwritten. Please commit or stash them. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
150
189
|
}
|
|
151
190
|
if (/interactive rebase already started/i.test(errorMessage)) {
|
|
152
|
-
|
|
191
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to start rebase: An interactive rebase is already in progress. Use 'continue', 'abort', or 'skip'. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
153
192
|
}
|
|
154
193
|
// Throw McpError for critical issues like non-existent repo
|
|
155
|
-
if (errorMessage.toLowerCase().includes(
|
|
194
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
156
195
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: rebaseError });
|
|
157
196
|
}
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
success: false,
|
|
161
|
-
mode: input.mode,
|
|
162
|
-
message: `Git rebase ${input.mode} failed for path: ${targetPath}.`,
|
|
163
|
-
error: errorMessage
|
|
164
|
-
};
|
|
197
|
+
// Throw a generic McpError for other failures
|
|
198
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git rebase ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: rebaseError });
|
|
165
199
|
}
|
|
166
200
|
}
|
|
167
201
|
catch (error) {
|
|
168
202
|
// Catch errors from path resolution or unexpected issues before command execution
|
|
169
|
-
logger.error(`Unexpected error during git rebase setup or execution`, {
|
|
203
|
+
logger.error(`Unexpected error during git rebase setup or execution`, {
|
|
204
|
+
...context,
|
|
205
|
+
operation,
|
|
206
|
+
path: targetPath,
|
|
207
|
+
error: error.message,
|
|
208
|
+
});
|
|
170
209
|
if (error instanceof McpError)
|
|
171
210
|
throw error;
|
|
172
211
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `An unexpected error occurred during git rebase ${input.mode}: ${error.message}`, { context, operation, originalError: error });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
2
|
-
import { logger } from
|
|
2
|
+
import { logger } from "../../../utils/index.js";
|
|
3
3
|
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
-
import { ErrorHandler } from
|
|
4
|
+
import { ErrorHandler } from "../../../utils/index.js";
|
|
5
5
|
// Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
|
|
6
|
-
import { BaseErrorCode } from
|
|
7
|
-
import { requestContextService } from
|
|
8
|
-
import { GitRebaseBaseSchema, gitRebaseLogic } from
|
|
6
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
8
|
+
import { GitRebaseBaseSchema, gitRebaseLogic, } from "./logic.js";
|
|
9
9
|
let _getWorkingDirectory;
|
|
10
10
|
let _getSessionId;
|
|
11
11
|
/**
|
|
@@ -16,10 +16,10 @@ let _getSessionId;
|
|
|
16
16
|
export function initializeGitRebaseStateAccessors(getWdFn, getSidFn) {
|
|
17
17
|
_getWorkingDirectory = getWdFn;
|
|
18
18
|
_getSessionId = getSidFn;
|
|
19
|
-
logger.info(
|
|
19
|
+
logger.info("State accessors initialized for git_rebase tool registration.");
|
|
20
20
|
}
|
|
21
|
-
const TOOL_NAME =
|
|
22
|
-
const TOOL_DESCRIPTION =
|
|
21
|
+
const TOOL_NAME = "git_rebase";
|
|
22
|
+
const TOOL_DESCRIPTION = "Reapplies commits on top of another base tip. Supports starting a rebase (standard or interactive), continuing, aborting, or skipping steps in an ongoing rebase. Returns results as a JSON object.";
|
|
23
23
|
/**
|
|
24
24
|
* Registers the git_rebase tool with the MCP server.
|
|
25
25
|
*
|
|
@@ -29,9 +29,9 @@ const TOOL_DESCRIPTION = 'Reapplies commits on top of another base tip. Supports
|
|
|
29
29
|
*/
|
|
30
30
|
export const registerGitRebaseTool = async (server) => {
|
|
31
31
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error("State accessors for git_rebase must be initialized before registration.");
|
|
33
33
|
}
|
|
34
|
-
const operation =
|
|
34
|
+
const operation = "registerGitRebaseTool";
|
|
35
35
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
36
|
await ErrorHandler.tryCatch(async () => {
|
|
37
37
|
// Register using the BASE schema shape
|
|
@@ -40,7 +40,10 @@ export const registerGitRebaseTool = async (server) => {
|
|
|
40
40
|
async (validatedArgs, callContext) => {
|
|
41
41
|
const toolInput = validatedArgs; // Cast for use
|
|
42
42
|
const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`;
|
|
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 = () => _getWorkingDirectory(sessionId);
|
|
46
49
|
const logicContext = {
|
|
@@ -52,15 +55,19 @@ export const registerGitRebaseTool = async (server) => {
|
|
|
52
55
|
return await ErrorHandler.tryCatch(async () => {
|
|
53
56
|
const rebaseResult = await gitRebaseLogic(toolInput, logicContext);
|
|
54
57
|
const resultContent = {
|
|
55
|
-
type:
|
|
58
|
+
type: "text",
|
|
56
59
|
text: JSON.stringify(rebaseResult, null, 2), // Pretty-print JSON
|
|
57
|
-
contentType:
|
|
60
|
+
contentType: "application/json",
|
|
58
61
|
};
|
|
59
62
|
if (rebaseResult.success) {
|
|
60
63
|
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully (Needs Manual Action: ${!!rebaseResult.needsManualAction}), returning JSON`, logicContext);
|
|
61
64
|
}
|
|
62
65
|
else {
|
|
63
|
-
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${rebaseResult.message}`, {
|
|
66
|
+
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${rebaseResult.message}`, {
|
|
67
|
+
...logicContext,
|
|
68
|
+
errorDetails: rebaseResult.error,
|
|
69
|
+
conflicts: rebaseResult.conflicts,
|
|
70
|
+
});
|
|
64
71
|
}
|
|
65
72
|
return { content: [resultContent] };
|
|
66
73
|
}, {
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* @fileoverview Barrel file for the git_remote tool.
|
|
3
3
|
* Exports the registration function and state accessor initialization function.
|
|
4
4
|
*/
|
|
5
|
-
export { initializeGitRemoteStateAccessors, registerGitRemoteTool } from
|
|
5
|
+
export { initializeGitRemoteStateAccessors, registerGitRemoteTool, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitRemoteInput, GitRemoteResult } from './logic.js';
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { BaseErrorCode, McpError } from
|
|
5
|
-
import { logger, sanitization } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
5
|
+
import { logger, sanitization } from "../../../utils/index.js"; // Logger (./utils/internal/logger.js) & RequestContext (./utils/internal/requestContext.js) & sanitization (./utils/security/sanitization.js)
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
// Define the input schema for the git_remote tool using Zod
|
|
8
8
|
export const GitRemoteInputSchema = z.object({
|
|
9
|
-
path: z
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
path: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.optional()
|
|
13
|
+
.default(".")
|
|
14
|
+
.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."),
|
|
15
|
+
mode: z
|
|
16
|
+
.enum(["list", "add", "remove", "show"])
|
|
17
|
+
.describe("Operation mode: 'list', 'add', 'remove', 'show'"),
|
|
18
|
+
name: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Remote name (required for 'add', 'remove', 'show')"),
|
|
12
23
|
url: z.string().optional().describe("Remote URL (required for 'add')"), // Removed .url() validation
|
|
13
24
|
});
|
|
14
25
|
/**
|
|
@@ -26,27 +37,43 @@ export async function gitRemoteLogic(input, context) {
|
|
|
26
37
|
try {
|
|
27
38
|
// Resolve and sanitize the target path
|
|
28
39
|
const workingDir = context.getWorkingDirectory();
|
|
29
|
-
targetPath =
|
|
30
|
-
? input.path
|
|
31
|
-
|
|
32
|
-
if (targetPath === '.' && !workingDir) {
|
|
40
|
+
targetPath =
|
|
41
|
+
input.path && input.path !== "." ? input.path : (workingDir ?? "."); // Default to '.' if no working dir set and no path provided
|
|
42
|
+
if (targetPath === "." && !workingDir) {
|
|
33
43
|
logger.warning("Executing git remote in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
34
44
|
// Allow execution in CWD but log it clearly. Consider if an error is more appropriate.
|
|
35
45
|
// For now, let's proceed but be aware.
|
|
36
46
|
targetPath = process.cwd(); // Use actual CWD if '.' was the default
|
|
37
47
|
}
|
|
38
|
-
else if (targetPath ===
|
|
48
|
+
else if (targetPath === "." && workingDir) {
|
|
39
49
|
targetPath = workingDir;
|
|
40
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
50
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
51
|
+
...context,
|
|
52
|
+
operation,
|
|
53
|
+
sessionId: context.sessionId,
|
|
54
|
+
});
|
|
41
55
|
}
|
|
42
56
|
else {
|
|
43
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
57
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
58
|
+
...context,
|
|
59
|
+
operation,
|
|
60
|
+
});
|
|
44
61
|
}
|
|
45
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
46
|
-
|
|
62
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
63
|
+
allowAbsolute: true,
|
|
64
|
+
}).sanitizedPath; // Sanitize the final resolved path
|
|
65
|
+
logger.debug("Sanitized path", {
|
|
66
|
+
...context,
|
|
67
|
+
operation,
|
|
68
|
+
sanitizedPath: targetPath,
|
|
69
|
+
});
|
|
47
70
|
}
|
|
48
71
|
catch (error) {
|
|
49
|
-
logger.error(
|
|
72
|
+
logger.error("Path resolution or sanitization failed", {
|
|
73
|
+
...context,
|
|
74
|
+
operation,
|
|
75
|
+
error,
|
|
76
|
+
});
|
|
50
77
|
if (error instanceof McpError)
|
|
51
78
|
throw error;
|
|
52
79
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -55,26 +82,29 @@ export async function gitRemoteLogic(input, context) {
|
|
|
55
82
|
let command;
|
|
56
83
|
let result;
|
|
57
84
|
switch (input.mode) {
|
|
58
|
-
case
|
|
85
|
+
case "list":
|
|
59
86
|
command = `git -C "${targetPath}" remote -v`;
|
|
60
|
-
logger.debug(`Executing command: ${command}`, {
|
|
87
|
+
logger.debug(`Executing command: ${command}`, {
|
|
88
|
+
...context,
|
|
89
|
+
operation,
|
|
90
|
+
});
|
|
61
91
|
const { stdout: listStdout } = await execAsync(command);
|
|
62
92
|
const remotes = [];
|
|
63
|
-
const lines = listStdout.trim().split(
|
|
93
|
+
const lines = listStdout.trim().split("\n");
|
|
64
94
|
const remoteMap = new Map();
|
|
65
|
-
lines.forEach(line => {
|
|
95
|
+
lines.forEach((line) => {
|
|
66
96
|
const parts = line.split(/\s+/);
|
|
67
97
|
if (parts.length >= 3) {
|
|
68
98
|
const name = parts[0];
|
|
69
99
|
const url = parts[1];
|
|
70
|
-
const type = parts[2].replace(/[()]/g,
|
|
100
|
+
const type = parts[2].replace(/[()]/g, ""); // Remove parentheses around (fetch) or (push)
|
|
71
101
|
if (!remoteMap.has(name)) {
|
|
72
102
|
remoteMap.set(name, {});
|
|
73
103
|
}
|
|
74
|
-
if (type ===
|
|
104
|
+
if (type === "fetch") {
|
|
75
105
|
remoteMap.get(name).fetchUrl = url;
|
|
76
106
|
}
|
|
77
|
-
else if (type ===
|
|
107
|
+
else if (type === "push") {
|
|
78
108
|
remoteMap.get(name).pushUrl = url;
|
|
79
109
|
}
|
|
80
110
|
}
|
|
@@ -83,13 +113,13 @@ export async function gitRemoteLogic(input, context) {
|
|
|
83
113
|
// Ensure both URLs are present, defaulting to fetch URL if push is missing (common case)
|
|
84
114
|
remotes.push({
|
|
85
115
|
name,
|
|
86
|
-
fetchUrl: urls.fetchUrl ||
|
|
87
|
-
pushUrl: urls.pushUrl || urls.fetchUrl ||
|
|
116
|
+
fetchUrl: urls.fetchUrl || "N/A",
|
|
117
|
+
pushUrl: urls.pushUrl || urls.fetchUrl || "N/A",
|
|
88
118
|
});
|
|
89
119
|
});
|
|
90
|
-
result = { success: true, mode:
|
|
120
|
+
result = { success: true, mode: "list", remotes };
|
|
91
121
|
break;
|
|
92
|
-
case
|
|
122
|
+
case "add":
|
|
93
123
|
if (!input.name || !input.url) {
|
|
94
124
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' and 'url' are required for 'add' mode.", { context, operation });
|
|
95
125
|
}
|
|
@@ -98,11 +128,18 @@ export async function gitRemoteLogic(input, context) {
|
|
|
98
128
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
99
129
|
}
|
|
100
130
|
command = `git -C "${targetPath}" remote add "${input.name}" "${input.url}"`;
|
|
101
|
-
logger.debug(`Executing command: ${command}`, {
|
|
131
|
+
logger.debug(`Executing command: ${command}`, {
|
|
132
|
+
...context,
|
|
133
|
+
operation,
|
|
134
|
+
});
|
|
102
135
|
await execAsync(command);
|
|
103
|
-
result = {
|
|
136
|
+
result = {
|
|
137
|
+
success: true,
|
|
138
|
+
mode: "add",
|
|
139
|
+
message: `Remote '${input.name}' added successfully.`,
|
|
140
|
+
};
|
|
104
141
|
break;
|
|
105
|
-
case
|
|
142
|
+
case "remove":
|
|
106
143
|
if (!input.name) {
|
|
107
144
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'remove' mode.", { context, operation });
|
|
108
145
|
}
|
|
@@ -110,11 +147,18 @@ export async function gitRemoteLogic(input, context) {
|
|
|
110
147
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
111
148
|
}
|
|
112
149
|
command = `git -C "${targetPath}" remote remove "${input.name}"`;
|
|
113
|
-
logger.debug(`Executing command: ${command}`, {
|
|
150
|
+
logger.debug(`Executing command: ${command}`, {
|
|
151
|
+
...context,
|
|
152
|
+
operation,
|
|
153
|
+
});
|
|
114
154
|
await execAsync(command);
|
|
115
|
-
result = {
|
|
155
|
+
result = {
|
|
156
|
+
success: true,
|
|
157
|
+
mode: "remove",
|
|
158
|
+
message: `Remote '${input.name}' removed successfully.`,
|
|
159
|
+
};
|
|
116
160
|
break;
|
|
117
|
-
case
|
|
161
|
+
case "show":
|
|
118
162
|
if (!input.name) {
|
|
119
163
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'show' mode.", { context, operation });
|
|
120
164
|
}
|
|
@@ -122,36 +166,48 @@ export async function gitRemoteLogic(input, context) {
|
|
|
122
166
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
123
167
|
}
|
|
124
168
|
command = `git -C "${targetPath}" remote show "${input.name}"`;
|
|
125
|
-
logger.debug(`Executing command: ${command}`, {
|
|
169
|
+
logger.debug(`Executing command: ${command}`, {
|
|
170
|
+
...context,
|
|
171
|
+
operation,
|
|
172
|
+
});
|
|
126
173
|
const { stdout: showStdout } = await execAsync(command);
|
|
127
|
-
result = { success: true, mode:
|
|
174
|
+
result = { success: true, mode: "show", details: showStdout.trim() };
|
|
128
175
|
break;
|
|
129
176
|
default:
|
|
130
177
|
// Should not happen due to Zod validation, but good practice
|
|
131
178
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
132
179
|
}
|
|
133
|
-
logger.info(
|
|
180
|
+
logger.info(`git remote ${input.mode} executed successfully`, {
|
|
181
|
+
...context,
|
|
182
|
+
operation,
|
|
183
|
+
path: targetPath,
|
|
184
|
+
result,
|
|
185
|
+
});
|
|
134
186
|
return result;
|
|
135
187
|
}
|
|
136
188
|
catch (error) {
|
|
137
|
-
const errorMessage = error.stderr || error.message ||
|
|
138
|
-
logger.error(`Failed to execute git remote command`, {
|
|
189
|
+
const errorMessage = error.stderr || error.message || "";
|
|
190
|
+
logger.error(`Failed to execute git remote command`, {
|
|
191
|
+
...context,
|
|
192
|
+
operation,
|
|
193
|
+
path: targetPath,
|
|
194
|
+
error: errorMessage,
|
|
195
|
+
stderr: error.stderr,
|
|
196
|
+
stdout: error.stdout,
|
|
197
|
+
});
|
|
139
198
|
// Specific error handling
|
|
140
|
-
if (errorMessage.toLowerCase().includes(
|
|
199
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
141
200
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
142
201
|
}
|
|
143
|
-
if (input.mode ===
|
|
144
|
-
|
|
202
|
+
if (input.mode === "add" &&
|
|
203
|
+
errorMessage.toLowerCase().includes("already exists")) {
|
|
204
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to add remote: Remote '${input.name}' already exists. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
145
205
|
}
|
|
146
|
-
if ((input.mode ===
|
|
147
|
-
|
|
206
|
+
if ((input.mode === "remove" || input.mode === "show") &&
|
|
207
|
+
errorMessage.toLowerCase().includes("no such remote")) {
|
|
208
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to ${input.mode} remote: Remote '${input.name}' does not exist. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
148
209
|
}
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
success: false,
|
|
152
|
-
mode: input.mode,
|
|
153
|
-
message: `Git remote ${input.mode} failed for path: ${targetPath}.`,
|
|
154
|
-
error: errorMessage
|
|
155
|
-
};
|
|
210
|
+
// Throw a generic McpError for other failures
|
|
211
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git remote ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
156
212
|
}
|
|
157
213
|
}
|