@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.
Files changed (97) hide show
  1. package/README.md +8 -11
  2. package/dist/config/index.js +7 -7
  3. package/dist/index.js +35 -21
  4. package/dist/mcp-server/server.js +72 -56
  5. package/dist/mcp-server/tools/gitAdd/index.js +1 -1
  6. package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
  7. package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
  8. package/dist/mcp-server/tools/gitBranch/index.js +1 -1
  9. package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
  10. package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
  11. package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
  17. package/dist/mcp-server/tools/gitClean/index.js +1 -1
  18. package/dist/mcp-server/tools/gitClean/logic.js +93 -41
  19. package/dist/mcp-server/tools/gitClean/registration.js +19 -16
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
  23. package/dist/mcp-server/tools/gitClone/index.js +1 -1
  24. package/dist/mcp-server/tools/gitClone/logic.js +89 -30
  25. package/dist/mcp-server/tools/gitClone/registration.js +15 -12
  26. package/dist/mcp-server/tools/gitCommit/index.js +1 -1
  27. package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
  28. package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
  29. package/dist/mcp-server/tools/gitDiff/index.js +1 -1
  30. package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
  31. package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
  32. package/dist/mcp-server/tools/gitFetch/index.js +1 -1
  33. package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
  34. package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
  35. package/dist/mcp-server/tools/gitInit/index.js +1 -1
  36. package/dist/mcp-server/tools/gitInit/logic.js +88 -34
  37. package/dist/mcp-server/tools/gitInit/registration.js +32 -18
  38. package/dist/mcp-server/tools/gitLog/index.js +1 -1
  39. package/dist/mcp-server/tools/gitLog/logic.js +133 -47
  40. package/dist/mcp-server/tools/gitLog/registration.js +16 -14
  41. package/dist/mcp-server/tools/gitMerge/index.js +1 -1
  42. package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
  43. package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
  44. package/dist/mcp-server/tools/gitPull/index.js +1 -1
  45. package/dist/mcp-server/tools/gitPull/logic.js +90 -69
  46. package/dist/mcp-server/tools/gitPull/registration.js +16 -14
  47. package/dist/mcp-server/tools/gitPush/index.js +1 -1
  48. package/dist/mcp-server/tools/gitPush/logic.js +116 -100
  49. package/dist/mcp-server/tools/gitPush/registration.js +16 -14
  50. package/dist/mcp-server/tools/gitRebase/index.js +1 -1
  51. package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
  52. package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
  53. package/dist/mcp-server/tools/gitRemote/index.js +1 -1
  54. package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
  55. package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
  56. package/dist/mcp-server/tools/gitReset/index.js +1 -1
  57. package/dist/mcp-server/tools/gitReset/logic.js +65 -37
  58. package/dist/mcp-server/tools/gitReset/registration.js +14 -12
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
  62. package/dist/mcp-server/tools/gitShow/index.js +1 -1
  63. package/dist/mcp-server/tools/gitShow/logic.js +78 -35
  64. package/dist/mcp-server/tools/gitShow/registration.js +17 -12
  65. package/dist/mcp-server/tools/gitStash/index.js +1 -1
  66. package/dist/mcp-server/tools/gitStash/logic.js +143 -58
  67. package/dist/mcp-server/tools/gitStash/registration.js +19 -12
  68. package/dist/mcp-server/tools/gitStatus/index.js +1 -1
  69. package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
  70. package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
  71. package/dist/mcp-server/tools/gitTag/index.js +1 -1
  72. package/dist/mcp-server/tools/gitTag/logic.js +124 -51
  73. package/dist/mcp-server/tools/gitTag/registration.js +14 -11
  74. package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
  75. package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
  76. package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
  77. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
  78. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
  79. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
  80. package/dist/mcp-server/transports/httpTransport.js +187 -79
  81. package/dist/mcp-server/transports/stdioTransport.js +14 -8
  82. package/dist/types-global/errors.js +9 -4
  83. package/dist/utils/index.js +4 -4
  84. package/dist/utils/internal/errorHandler.js +62 -40
  85. package/dist/utils/internal/index.js +3 -3
  86. package/dist/utils/internal/logger.js +97 -54
  87. package/dist/utils/internal/requestContext.js +7 -5
  88. package/dist/utils/metrics/index.js +1 -1
  89. package/dist/utils/metrics/tokenCounter.js +18 -14
  90. package/dist/utils/parsing/dateParser.js +5 -5
  91. package/dist/utils/parsing/index.js +2 -2
  92. package/dist/utils/parsing/jsonParser.js +20 -11
  93. package/dist/utils/security/idGenerator.js +8 -10
  94. package/dist/utils/security/index.js +3 -3
  95. package/dist/utils/security/rateLimiter.js +16 -14
  96. package/dist/utils/security/sanitization.js +139 -82
  97. package/package.json +45 -23
@@ -1,11 +1,11 @@
1
1
  // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from '../../../utils/index.js';
2
+ import { ErrorHandler } from "../../../utils/index.js";
3
3
  // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from '../../../utils/index.js';
4
+ import { logger } from "../../../utils/index.js";
5
5
  // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
- import { requestContextService } from '../../../utils/index.js';
8
- import { GitLogInputSchema, logGitHistory } from './logic.js';
6
+ import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
7
+ import { requestContextService } from "../../../utils/index.js";
8
+ import { GitLogInputSchema, logGitHistory, } from "./logic.js";
9
9
  let _getWorkingDirectory;
10
10
  let _getSessionId;
11
11
  /**
@@ -17,9 +17,9 @@ let _getSessionId;
17
17
  export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
18
18
  _getWorkingDirectory = getWdFn;
19
19
  _getSessionId = getSidFn;
20
- logger.info('State accessors initialized for git_log tool registration.');
20
+ logger.info("State accessors initialized for git_log tool registration.");
21
21
  }
22
- const TOOL_NAME = 'git_log';
22
+ const TOOL_NAME = "git_log";
23
23
  const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a JSON object containing a list of commit objects (`commits` array) by default. If `showSignature: true` is used, it returns a JSON object where the `commits` array is empty and the raw signature verification output is included in the `message` field.";
24
24
  /**
25
25
  * Registers the git_log tool with the MCP server.
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limitin
29
29
  */
30
30
  export async function registerGitLogTool(server) {
31
31
  if (!_getWorkingDirectory || !_getSessionId) {
32
- throw new Error('State accessors for git_log must be initialized before registration.');
32
+ throw new Error("State accessors for git_log must be initialized before registration.");
33
33
  }
34
- const operation = 'registerGitLogTool';
34
+ const operation = "registerGitLogTool";
35
35
  const context = requestContextService.createRequestContext({ operation });
36
36
  await ErrorHandler.tryCatch(async () => {
37
37
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitLogInputSchema.shape, // Provide the Zod schema shape
38
38
  async (validatedArgs, callContext) => {
39
- const toolOperation = 'tool:git_log';
40
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
39
+ const toolOperation = "tool:git_log";
40
+ const requestContext = requestContextService.createRequestContext({
41
+ operation: toolOperation,
42
+ parentContext: callContext,
43
+ });
41
44
  const sessionId = _getSessionId(requestContext);
42
45
  const getWorkingDirectoryForSession = () => {
43
46
  return _getWorkingDirectory(sessionId);
@@ -53,10 +56,10 @@ export async function registerGitLogTool(server) {
53
56
  const logResult = await logGitHistory(validatedArgs, logicContext);
54
57
  // Format the result (array of commits) as a JSON string within TextContent
55
58
  const resultContent = {
56
- type: 'text',
59
+ type: "text",
57
60
  // Stringify the entire GitLogResult object which includes the commits array and success flag
58
61
  text: JSON.stringify(logResult, null, 2), // Pretty-print JSON
59
- contentType: 'application/json',
62
+ contentType: "application/json",
60
63
  };
61
64
  logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
62
65
  // Success is determined by the logic function and included in the result object
@@ -71,4 +74,3 @@ export async function registerGitLogTool(server) {
71
74
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
72
75
  }, { operation, context, critical: true });
73
76
  }
74
- ;
@@ -2,6 +2,6 @@
2
2
  * @fileoverview Barrel file for the git_merge tool.
3
3
  * Exports the registration function and state accessor initialization function.
4
4
  */
5
- export { registerGitMergeTool, initializeGitMergeStateAccessors } from './registration.js';
5
+ export { registerGitMergeTool, initializeGitMergeStateAccessors, } from "./registration.js";
6
6
  // Export types if needed elsewhere, e.g.:
7
7
  // export type { GitMergeInput, GitMergeResult } from './logic.js';
@@ -1,22 +1,42 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { z } from 'zod';
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 '../../../utils/index.js';
5
+ import { logger } from "../../../utils/index.js";
6
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
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 path from 'path'; // Import path module
10
- import { sanitization } from '../../../utils/index.js';
9
+ import path from "path"; // Import path module
10
+ import { sanitization } from "../../../utils/index.js";
11
11
  const execAsync = promisify(exec);
12
12
  // Define the input schema for the git_merge tool
13
13
  export const GitMergeInputSchema = z.object({
14
- path: z.string().min(1).optional().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."),
15
- branch: z.string().min(1).describe('The name of the branch to merge into the current branch.'),
16
- commitMessage: z.string().optional().describe('Commit message to use for the merge commit (if required, e.g., not fast-forward).'),
17
- noFf: z.boolean().default(false).describe('Create a merge commit even when the merge resolves as a fast-forward (`--no-ff`).'),
18
- squash: z.boolean().default(false).describe('Combine merged changes into a single commit (`--squash`). Requires manual commit afterwards.'),
19
- abort: z.boolean().default(false).describe('Abort the current merge process (resolves conflicts).'),
14
+ path: z
15
+ .string()
16
+ .min(1)
17
+ .optional()
18
+ .default(".")
19
+ .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."),
20
+ branch: z
21
+ .string()
22
+ .min(1)
23
+ .describe("The name of the branch to merge into the current branch."),
24
+ commitMessage: z
25
+ .string()
26
+ .optional()
27
+ .describe("Commit message to use for the merge commit (if required, e.g., not fast-forward)."),
28
+ noFf: z
29
+ .boolean()
30
+ .default(false)
31
+ .describe("Create a merge commit even when the merge resolves as a fast-forward (`--no-ff`)."),
32
+ squash: z
33
+ .boolean()
34
+ .default(false)
35
+ .describe("Combine merged changes into a single commit (`--squash`). Requires manual commit afterwards."),
36
+ abort: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe("Abort the current merge process (resolves conflicts)."),
20
40
  // 'continue' might be too complex for initial implementation due to requiring index manipulation
21
41
  });
22
42
  /**
@@ -28,13 +48,13 @@ export const GitMergeInputSchema = z.object({
28
48
  * @throws {McpError} Throws an McpError for path issues, command failures, or unexpected errors.
29
49
  */
30
50
  export async function gitMergeLogic(input, context) {
31
- const operation = 'gitMergeLogic';
51
+ const operation = "gitMergeLogic";
32
52
  logger.debug(`Executing ${operation}`, { ...context, input });
33
53
  let targetPath;
34
54
  try {
35
55
  // Resolve the target path
36
56
  let resolvedPath;
37
- if (input.path && input.path !== '.') {
57
+ if (input.path && input.path !== ".") {
38
58
  // If a specific path is given, resolve it absolutely first
39
59
  // Assuming input.path could be relative *to the server's CWD* if no session WD is set,
40
60
  // but it's safer to require absolute paths or rely on session WD.
@@ -50,7 +70,10 @@ export async function gitMergeLogic(input, context) {
50
70
  // If relative path given without session WD, it's ambiguous. Error out.
51
71
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Relative path provided but no working directory set for the session.", { context, operation });
52
72
  }
53
- logger.debug(`Resolved provided path: ${resolvedPath}`, { ...context, operation });
73
+ logger.debug(`Resolved provided path: ${resolvedPath}`, {
74
+ ...context,
75
+ operation,
76
+ });
54
77
  }
55
78
  else {
56
79
  const workingDir = context.getWorkingDirectory();
@@ -58,16 +81,26 @@ export async function gitMergeLogic(input, context) {
58
81
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
59
82
  }
60
83
  resolvedPath = workingDir; // Use session working directory
61
- logger.debug(`Using session working directory: ${resolvedPath}`, { ...context, operation, sessionId: context.sessionId });
84
+ logger.debug(`Using session working directory: ${resolvedPath}`, {
85
+ ...context,
86
+ operation,
87
+ sessionId: context.sessionId,
88
+ });
62
89
  }
63
90
  // Sanitize the resolved path
64
91
  // We assume the resolved path should be absolute for git commands.
65
92
  // sanitizePath checks for traversal and normalizes.
66
- targetPath = sanitization.sanitizePath(resolvedPath, { allowAbsolute: true }).sanitizedPath;
93
+ targetPath = sanitization.sanitizePath(resolvedPath, {
94
+ allowAbsolute: true,
95
+ }).sanitizedPath;
67
96
  logger.debug(`Sanitized path: ${targetPath}`, { ...context, operation });
68
97
  }
69
98
  catch (error) {
70
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
99
+ logger.error("Path resolution or sanitization failed", {
100
+ ...context,
101
+ operation,
102
+ error,
103
+ });
71
104
  if (error instanceof McpError) {
72
105
  throw error;
73
106
  }
@@ -76,15 +109,16 @@ export async function gitMergeLogic(input, context) {
76
109
  // --- Construct the git merge command ---
77
110
  let command = `git -C "${targetPath}" merge`;
78
111
  if (input.abort) {
79
- command += ' --abort';
112
+ command += " --abort";
80
113
  }
81
114
  else {
82
115
  // Standard merge options
83
116
  if (input.noFf)
84
- command += ' --no-ff';
117
+ command += " --no-ff";
85
118
  if (input.squash)
86
- command += ' --squash';
87
- if (input.commitMessage && !input.squash) { // Commit message only relevant if not squashing (squash requires separate commit)
119
+ command += " --squash";
120
+ if (input.commitMessage && !input.squash) {
121
+ // Commit message only relevant if not squashing (squash requires separate commit)
88
122
  command += ` -m "${input.commitMessage.replace(/"/g, '\\"')}"`;
89
123
  }
90
124
  else if (input.squash && input.commitMessage) {
@@ -99,62 +133,69 @@ export async function gitMergeLogic(input, context) {
99
133
  logger.debug(`Command stdout: ${stdout}`, { ...context, operation });
100
134
  if (stderr)
101
135
  logger.debug(`Command stderr: ${stderr}`, { ...context, operation }); // Log stderr even on success
136
+ const result = {
137
+ success: true,
138
+ message: stdout.trim() || stderr.trim() || "Merge command executed.",
139
+ };
102
140
  if (input.abort) {
103
- return { success: true, message: 'Merge aborted successfully.', aborted: true };
104
- }
105
- // Check stdout/stderr for specific success messages
106
- if (stdout.includes('Fast-forward')) {
107
- return { success: true, message: `Merge successful (fast-forward): ${stdout.trim()}`, fastForward: true };
108
- }
109
- if (stdout.includes('Merge made by') || stdout.includes('merging')) { // Check for recursive strategy message etc.
110
- const match = stdout.match(/Merge commit '([a-f0-9]+)'/); // Try to get merge commit hash
111
- return {
112
- success: true,
113
- message: `Merge successful: ${stdout.trim()}`,
114
- mergedCommitHash: match ? match[1] : undefined,
115
- fastForward: false, // Explicitly not FF
116
- };
117
- }
118
- if (stdout.includes('Squash commit -- not updating HEAD')) {
119
- return {
120
- success: true,
121
- message: `Merge successful (--squash): Changes staged. Manual commit required. ${stdout.trim()}`,
122
- needsManualCommit: true,
123
- };
124
- }
125
- if (stdout.includes('Already up to date')) {
126
- return { success: true, message: `Already up to date.`, fastForward: true }; // Treat as success/FF
127
- }
128
- // If none of the above, return generic success based on exit code 0
129
- return { success: true, message: `Merge command completed: ${stdout.trim()}` };
141
+ result.aborted = true;
142
+ result.message = "Merge aborted successfully.";
143
+ }
144
+ else if (stdout.includes("Fast-forward")) {
145
+ result.fastForward = true;
146
+ }
147
+ else if (stdout.includes("Merge made by") || stdout.includes("merging")) {
148
+ const match = stdout.match(/Merge commit '([a-f0-9]+)'/);
149
+ result.mergedCommitHash = match ? match[1] : undefined;
150
+ result.fastForward = false;
151
+ }
152
+ else if (stdout.includes("Squash commit -- not updating HEAD")) {
153
+ result.needsManualCommit = true;
154
+ }
155
+ else if (stdout.includes("Already up to date")) {
156
+ result.fastForward = true;
157
+ }
158
+ logger.info("git merge executed successfully", {
159
+ ...context,
160
+ operation,
161
+ path: targetPath,
162
+ result,
163
+ });
164
+ return result;
130
165
  }
131
166
  catch (error) {
132
- const errorMessage = error.stderr || error.stdout || error.message || ''; // Git often puts errors in stdout/stderr
133
- logger.error(`Git merge command failed`, { ...context, operation, path: targetPath, error: error.message, output: errorMessage });
167
+ const errorMessage = error.stderr || error.stdout || error.message || ""; // Git often puts errors in stdout/stderr
168
+ logger.error(`Git merge command failed`, {
169
+ ...context,
170
+ operation,
171
+ path: targetPath,
172
+ error: error.message,
173
+ output: errorMessage,
174
+ });
134
175
  if (input.abort) {
135
176
  // If abort failed, it's likely there was no merge in progress
136
- if (errorMessage.includes('fatal: There is no merge to abort')) {
137
- return { success: false, message: 'No merge in progress to abort.', aborted: false };
177
+ if (errorMessage.includes("fatal: There is no merge to abort")) {
178
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `No merge in progress to abort.`, { context, operation, originalError: error });
138
179
  }
139
180
  throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to abort merge: ${errorMessage}`, { context, operation, originalError: error });
140
181
  }
141
182
  // Check for specific failure scenarios
142
- if (errorMessage.includes('CONFLICT')) {
143
- return { success: false, message: `Merge failed due to conflicts. Please resolve conflicts and commit. Output: ${errorMessage}`, conflict: true };
183
+ if (errorMessage.includes("CONFLICT")) {
184
+ throw new McpError(BaseErrorCode.CONFLICT, `Merge failed due to conflicts. Please resolve conflicts and commit. Output: ${errorMessage}`, { context, operation, originalError: error });
144
185
  }
145
- if (errorMessage.includes('refusing to merge unrelated histories')) {
186
+ if (errorMessage.includes("refusing to merge unrelated histories")) {
146
187
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Merge failed: Refusing to merge unrelated histories. Consider using '--allow-unrelated-histories'.`, { context, operation, originalError: error });
147
188
  }
148
- if (errorMessage.includes('fatal: Not possible to fast-forward, aborting.')) {
189
+ if (errorMessage.includes("fatal: Not possible to fast-forward, aborting.")) {
149
190
  throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Not possible to fast-forward. Merge required.`, { context, operation, originalError: error });
150
191
  }
151
192
  if (errorMessage.match(/fatal: '.*?' does not point to a commit/)) {
152
193
  throw new McpError(BaseErrorCode.NOT_FOUND, `Merge failed: Branch '${input.branch}' not found or does not point to a commit.`, { context, operation, originalError: error });
153
194
  }
154
- if (errorMessage.includes('fatal: You have not concluded your merge')) {
155
- return { success: false, message: `Merge failed: Conflicts still exist from a previous merge. Resolve conflicts or abort. Output: ${errorMessage}`, conflict: true };
195
+ if (errorMessage.includes("fatal: You have not concluded your merge")) {
196
+ throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Conflicts still exist from a previous merge. Resolve conflicts or abort. Output: ${errorMessage}`, { context, operation, originalError: error });
156
197
  }
157
- if (errorMessage.includes('error: Your local changes to the following files would be overwritten by merge')) {
198
+ if (errorMessage.includes("error: Your local changes to the following files would be overwritten by merge")) {
158
199
  throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Local changes would be overwritten. Please commit or stash them.`, { context, operation, originalError: error });
159
200
  }
160
201
  // Generic error
@@ -1,11 +1,11 @@
1
1
  // Import utils from barrel (logger from ../utils/internal/logger.js)
2
- import { logger } from '../../../utils/index.js';
2
+ import { logger } from "../../../utils/index.js";
3
3
  // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
- import { ErrorHandler } from '../../../utils/index.js';
4
+ import { ErrorHandler } from "../../../utils/index.js";
5
5
  // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
- import { requestContextService } from '../../../utils/index.js';
8
- import { GitMergeInputSchema, gitMergeLogic } from './logic.js';
6
+ import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
7
+ import { requestContextService } from "../../../utils/index.js";
8
+ import { GitMergeInputSchema, gitMergeLogic, } from "./logic.js";
9
9
  let _getWorkingDirectory;
10
10
  let _getSessionId;
11
11
  /**
@@ -16,10 +16,10 @@ let _getSessionId;
16
16
  export function initializeGitMergeStateAccessors(getWdFn, getSidFn) {
17
17
  _getWorkingDirectory = getWdFn;
18
18
  _getSessionId = getSidFn;
19
- logger.info('State accessors initialized for git_merge tool registration.');
19
+ logger.info("State accessors initialized for git_merge tool registration.");
20
20
  }
21
- const TOOL_NAME = 'git_merge';
22
- const TOOL_DESCRIPTION = 'Merges the specified branch into the current branch. Supports options like --no-ff, --squash, and --abort. Returns the merge result as a JSON object.';
21
+ const TOOL_NAME = "git_merge";
22
+ const TOOL_DESCRIPTION = "Merges the specified branch into the current branch. Supports options like --no-ff, --squash, and --abort. Returns the merge result as a JSON object.";
23
23
  /**
24
24
  * Registers the git_merge tool with the MCP server.
25
25
  *
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = 'Merges the specified branch into the current branch. S
29
29
  */
30
30
  export const registerGitMergeTool = async (server) => {
31
31
  if (!_getWorkingDirectory || !_getSessionId) {
32
- throw new Error('State accessors for git_merge must be initialized before registration.');
32
+ throw new Error("State accessors for git_merge must be initialized before registration.");
33
33
  }
34
- const operation = 'registerGitMergeTool';
34
+ const operation = "registerGitMergeTool";
35
35
  const context = requestContextService.createRequestContext({ operation });
36
36
  await ErrorHandler.tryCatch(async () => {
37
37
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitMergeInputSchema.shape, // Provide the Zod schema shape
38
38
  async (validatedArgs, callContext) => {
39
- const toolOperation = 'tool:git_merge';
40
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
39
+ const toolOperation = "tool:git_merge";
40
+ const requestContext = requestContextService.createRequestContext({
41
+ operation: toolOperation,
42
+ parentContext: callContext,
43
+ });
41
44
  const sessionId = _getSessionId(requestContext);
42
45
  const getWorkingDirectoryForSession = () => {
43
46
  return _getWorkingDirectory(sessionId);
@@ -53,9 +56,9 @@ export const registerGitMergeTool = async (server) => {
53
56
  const mergeResult = await gitMergeLogic(validatedArgs, logicContext);
54
57
  // Format the result as a JSON string within TextContent
55
58
  const resultContent = {
56
- type: 'text',
59
+ type: "text",
57
60
  text: JSON.stringify(mergeResult, null, 2), // Pretty-print JSON
58
- contentType: 'application/json',
61
+ contentType: "application/json",
59
62
  };
60
63
  // Log based on the success flag in the result
61
64
  if (mergeResult.success) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitPull tool.
3
3
  */
4
- export { registerGitPullTool, initializeGitPullStateAccessors } from './registration.js';
4
+ export { registerGitPullTool, initializeGitPullStateAccessors, } from "./registration.js";
5
5
  // Export types if needed elsewhere, e.g.:
6
6
  // export type { GitPullInput, GitPullResult } from './logic.js';
@@ -1,20 +1,39 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { z } from 'zod';
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 '../../../utils/index.js';
5
+ import { logger } from "../../../utils/index.js";
6
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
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 '../../../utils/index.js';
9
+ import { sanitization } from "../../../utils/index.js";
10
10
  const execAsync = promisify(exec);
11
11
  // Define the input schema for the git_pull tool using Zod
12
12
  export const GitPullInputSchema = z.object({
13
- path: z.string().min(1).optional().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
- remote: z.string().optional().describe("The remote repository to pull from (e.g., 'origin'). Defaults to the tracked upstream or 'origin'."),
15
- branch: z.string().optional().describe("The remote branch to pull (e.g., 'main'). Defaults to the current branch's upstream."),
16
- rebase: z.boolean().optional().default(false).describe("Use 'git pull --rebase' instead of merge."),
17
- ffOnly: z.boolean().optional().default(false).describe("Use '--ff-only' to only allow fast-forward merges."),
13
+ path: z
14
+ .string()
15
+ .min(1)
16
+ .optional()
17
+ .default(".")
18
+ .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."),
19
+ remote: z
20
+ .string()
21
+ .optional()
22
+ .describe("The remote repository to pull from (e.g., 'origin'). Defaults to the tracked upstream or 'origin'."),
23
+ branch: z
24
+ .string()
25
+ .optional()
26
+ .describe("The remote branch to pull (e.g., 'main'). Defaults to the current branch's upstream."),
27
+ rebase: z
28
+ .boolean()
29
+ .optional()
30
+ .default(false)
31
+ .describe("Use 'git pull --rebase' instead of merge."),
32
+ ffOnly: z
33
+ .boolean()
34
+ .optional()
35
+ .default(false)
36
+ .describe("Use '--ff-only' to only allow fast-forward merges."),
18
37
  // Add other relevant git pull options as needed (e.g., --prune, --tags, --depth)
19
38
  });
20
39
  /**
@@ -26,14 +45,17 @@ export const GitPullInputSchema = z.object({
26
45
  * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
27
46
  */
28
47
  export async function pullGitChanges(input, context) {
29
- const operation = 'pullGitChanges';
48
+ const operation = "pullGitChanges";
30
49
  logger.debug(`Executing ${operation}`, { ...context, input });
31
50
  let targetPath;
32
51
  try {
33
52
  // Resolve the target path
34
- if (input.path && input.path !== '.') {
53
+ if (input.path && input.path !== ".") {
35
54
  targetPath = input.path;
36
- logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
55
+ logger.debug(`Using provided path: ${targetPath}`, {
56
+ ...context,
57
+ operation,
58
+ });
37
59
  }
38
60
  else {
39
61
  const workingDir = context.getWorkingDirectory();
@@ -41,14 +63,28 @@ export async function pullGitChanges(input, context) {
41
63
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
42
64
  }
43
65
  targetPath = workingDir;
44
- logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
66
+ logger.debug(`Using session working directory: ${targetPath}`, {
67
+ ...context,
68
+ operation,
69
+ sessionId: context.sessionId,
70
+ });
45
71
  }
46
72
  // Sanitize the resolved path
47
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
48
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
73
+ targetPath = sanitization.sanitizePath(targetPath, {
74
+ allowAbsolute: true,
75
+ }).sanitizedPath;
76
+ logger.debug("Sanitized path", {
77
+ ...context,
78
+ operation,
79
+ sanitizedPath: targetPath,
80
+ });
49
81
  }
50
82
  catch (error) {
51
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
83
+ logger.error("Path resolution or sanitization failed", {
84
+ ...context,
85
+ operation,
86
+ error,
87
+ });
52
88
  if (error instanceof McpError)
53
89
  throw error;
54
90
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
@@ -57,88 +93,73 @@ export async function pullGitChanges(input, context) {
57
93
  // Construct the git pull command
58
94
  let command = `git -C "${targetPath}" pull`;
59
95
  if (input.rebase) {
60
- command += ' --rebase';
96
+ command += " --rebase";
61
97
  }
62
98
  if (input.ffOnly) {
63
- command += ' --ff-only';
99
+ command += " --ff-only";
64
100
  }
65
101
  if (input.remote) {
66
102
  // Sanitize remote and branch names - basic alphanumeric + common chars
67
- const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, '');
103
+ const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "");
68
104
  command += ` ${safeRemote}`;
69
105
  if (input.branch) {
70
- const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, '');
106
+ const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
71
107
  command += ` ${safeBranch}`;
72
108
  }
73
109
  }
74
110
  else if (input.branch) {
75
111
  // If only branch is specified, assume 'origin' or tracked remote
76
- const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, '');
112
+ const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
77
113
  command += ` origin ${safeBranch}`; // Defaulting to origin if remote not specified but branch is
78
114
  logger.warning(`Remote not specified, defaulting to 'origin' for branch pull`, { ...context, operation });
79
115
  }
80
116
  logger.debug(`Executing command: ${command}`, { ...context, operation });
81
117
  const { stdout, stderr } = await execAsync(command);
82
- logger.info(`Git pull stdout: ${stdout}`, { ...context, operation });
118
+ logger.debug(`Git pull stdout: ${stdout}`, { ...context, operation });
83
119
  if (stderr) {
84
- // Stderr might contain progress or non-error info, log as warning unless it indicates a clear failure handled below
85
- logger.warning(`Git pull stderr: ${stderr}`, { ...context, operation });
120
+ logger.debug(`Git pull stderr: ${stderr}`, { ...context, operation });
86
121
  }
87
122
  // Analyze stdout/stderr to determine the outcome
88
- let message = stdout.trim() || stderr.trim(); // Use stdout first, fallback to stderr for message
89
- let success = true;
90
- let conflict = false;
91
- let summary = undefined;
92
- if (message.includes('Already up to date')) {
93
- message = 'Already up to date.';
94
- }
95
- else if (message.includes('Fast-forward')) {
96
- message = 'Pull successful (fast-forward).';
97
- // Try to extract summary
98
- const summaryMatch = stdout.match(/(\d+ files? changed.*)/);
99
- if (summaryMatch)
100
- summary = summaryMatch[1];
101
- }
102
- else if (message.includes('Merge made by the') || message.includes('merging')) { // Covers recursive and octopus
103
- message = 'Pull successful (merge).';
104
- const summaryMatch = stdout.match(/(\d+ files? changed.*)/);
105
- if (summaryMatch)
106
- summary = summaryMatch[1];
107
- }
108
- else if (message.includes('Automatic merge failed; fix conflicts and then commit the result.')) {
109
- message = 'Pull resulted in merge conflicts.';
110
- success = false; // Indicate failure due to conflicts
111
- conflict = true;
112
- }
113
- else if (message.includes('fatal:')) {
114
- // If a fatal error wasn't caught by the execAsync catch block but is in stdout/stderr
115
- success = false;
116
- message = `Pull failed: ${message}`;
117
- logger.error(`Git pull command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
118
- }
119
- // Add more specific checks based on git pull output variations if needed
120
- logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message, conflict });
121
- return { success, message, summary, conflict };
123
+ const message = stdout.trim() || stderr.trim() || "Pull command executed.";
124
+ const summary = message;
125
+ const conflict = message.includes("CONFLICT");
126
+ logger.info("git pull executed successfully", {
127
+ ...context,
128
+ operation,
129
+ path: targetPath,
130
+ summary,
131
+ conflict,
132
+ });
133
+ return { success: true, message, summary, conflict };
122
134
  }
123
135
  catch (error) {
124
- logger.error(`Failed to execute git pull command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
125
- const errorMessage = error.stderr || error.stdout || error.message || ''; // Check stdout too for errors
136
+ logger.error(`Failed to execute git pull command`, {
137
+ ...context,
138
+ operation,
139
+ path: targetPath,
140
+ error: error.message,
141
+ stderr: error.stderr,
142
+ stdout: error.stdout,
143
+ });
144
+ const errorMessage = error.stderr || error.stdout || error.message || ""; // Check stdout too for errors
126
145
  // Handle specific error cases
127
- if (errorMessage.toLowerCase().includes('not a git repository')) {
146
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
128
147
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
129
148
  }
130
- if (errorMessage.includes('resolve host') || errorMessage.includes('Could not read from remote repository')) {
149
+ if (errorMessage.includes("resolve host") ||
150
+ errorMessage.includes("Could not read from remote repository")) {
131
151
  throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository. Error: ${errorMessage}`, { context, operation, originalError: error });
132
152
  }
133
- if (errorMessage.includes('merge conflict') || errorMessage.includes('fix conflicts')) {
153
+ if (errorMessage.includes("merge conflict") ||
154
+ errorMessage.includes("fix conflicts")) {
134
155
  // This might be caught here if execAsync throws due to non-zero exit code during conflict
135
- logger.warning('Pull resulted in merge conflicts (caught as error)', { ...context, operation, path: targetPath, errorMessage });
136
- return { success: false, message: 'Pull resulted in merge conflicts.', conflict: true };
156
+ throw new McpError(BaseErrorCode.CONFLICT, `Pull resulted in merge conflicts. Error: ${errorMessage}`, { context, operation, originalError: error });
137
157
  }
138
- if (errorMessage.includes('You have unstaged changes') || errorMessage.includes('Your local changes to the following files would be overwritten by merge')) {
158
+ if (errorMessage.includes("You have unstaged changes") ||
159
+ errorMessage.includes("Your local changes to the following files would be overwritten by merge")) {
139
160
  throw new McpError(BaseErrorCode.CONFLICT, `Pull failed due to uncommitted local changes. Please commit or stash them first. Error: ${errorMessage}`, { context, operation, originalError: error });
140
161
  }
141
- if (errorMessage.includes('refusing to merge unrelated histories')) {
162
+ if (errorMessage.includes("refusing to merge unrelated histories")) {
142
163
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Pull failed: Refusing to merge unrelated histories. Use '--allow-unrelated-histories' if intended.`, { context, operation, originalError: error });
143
164
  }
144
165
  // Generic internal error for other failures