@cyanheads/git-mcp-server 2.1.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
  5. package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +44 -143
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +50 -171
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +46 -152
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +75 -257
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +48 -146
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +73 -181
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +37 -121
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +33 -122
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +70 -214
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +66 -188
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. package/package.json +8 -8
@@ -1,89 +1,64 @@
1
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
2
- import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), ErrorHandler (./utils/internal/errorHandler.js), requestContextService (./utils/internal/requestContext.js)
3
- // Import the final schema and types for handler logic
4
- // Import the BASE schema separately for registration shape
5
- import { GitStashBaseSchema, gitStashLogic, } from "./logic.js";
6
- let _getWorkingDirectory;
7
- let _getSessionId;
8
1
  /**
9
- * Initializes the state accessors needed by the git_stash tool registration.
10
- * @param getWdFn - Function to get the working directory for a session.
11
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_stash tool.
3
+ * @module src/mcp-server/tools/gitStash/registration
12
4
  */
13
- export function initializeGitStashStateAccessors(getWdFn, getSidFn) {
14
- _getWorkingDirectory = getWdFn;
15
- _getSessionId = getSidFn;
16
- logger.info("State accessors initialized for git_stash tool registration.");
17
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
7
+ import { gitStashLogic, GitStashOutputSchema, GitStashBaseSchema, } from "./logic.js";
18
8
  const TOOL_NAME = "git_stash";
19
9
  const TOOL_DESCRIPTION = "Manages stashed changes in the working directory. Supports listing stashes, applying/popping specific stashes (with conflict detection), dropping stashes, and saving current changes to a new stash with an optional message. Returns results as a JSON object.";
20
10
  /**
21
- * Registers the git_stash tool with the MCP server.
22
- *
23
- * @param {McpServer} server - The McpServer instance to register the tool with.
24
- * @returns {Promise<void>}
25
- * @throws {Error} If registration fails or state accessors are not initialized.
11
+ * Registers the git_stash tool with the MCP server instance.
12
+ * @param server The MCP server instance.
13
+ * @param getWorkingDirectory Function to get the session's working directory.
14
+ * @param getSessionId Function to get the session ID from context.
26
15
  */
27
- export const registerGitStashTool = async (server) => {
28
- if (!_getWorkingDirectory || !_getSessionId) {
29
- throw new Error("State accessors for git_stash must be initialized before registration.");
30
- }
16
+ export const registerGitStashTool = async (server, getWorkingDirectory, getSessionId) => {
31
17
  const operation = "registerGitStashTool";
32
18
  const context = requestContextService.createRequestContext({ operation });
33
- await ErrorHandler.tryCatch(async () => {
34
- // Register the tool using the *base* schema's shape for definition
35
- server.tool(// Use BASE schema shape here
36
- TOOL_NAME, TOOL_DESCRIPTION, GitStashBaseSchema.shape, // Use the shape from the BASE schema
37
- // Let TypeScript infer handler argument types.
38
- // The SDK validates against the full GitStashInputSchema before calling this.
39
- async (validatedArgs, callContext) => {
40
- // Cast validatedArgs to the specific input type for use within the handler
41
- const toolInput = validatedArgs;
42
- const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`; // Include mode in operation
43
- const requestContext = requestContextService.createRequestContext({
44
- operation: toolOperation,
45
- parentContext: callContext,
19
+ server.registerTool(TOOL_NAME, {
20
+ title: "Git Stash",
21
+ description: TOOL_DESCRIPTION,
22
+ inputSchema: GitStashBaseSchema.shape,
23
+ outputSchema: GitStashOutputSchema.shape,
24
+ annotations: {
25
+ readOnlyHint: false,
26
+ destructiveHint: true, // Can drop stashes or cause conflicts
27
+ idempotentHint: false,
28
+ openWorldHint: false,
29
+ },
30
+ }, async (params, callContext) => {
31
+ const handlerContext = requestContextService.createRequestContext({
32
+ toolName: TOOL_NAME,
33
+ parentContext: callContext,
34
+ });
35
+ try {
36
+ const sessionId = getSessionId(handlerContext);
37
+ const result = await gitStashLogic(params, {
38
+ ...handlerContext,
39
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
46
40
  });
47
- const sessionId = _getSessionId(requestContext);
48
- const getWorkingDirectoryForSession = () => {
49
- return _getWorkingDirectory(sessionId);
50
- };
51
- const logicContext = {
52
- ...requestContext,
53
- sessionId: sessionId,
54
- getWorkingDirectory: getWorkingDirectoryForSession,
41
+ return {
42
+ structuredContent: result,
43
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
55
44
  };
56
- logger.info(`Executing tool: ${TOOL_NAME} (mode: ${toolInput.mode})`, logicContext);
57
- return await ErrorHandler.tryCatch(async () => {
58
- // Call the core logic function which returns a GitStashResult object
59
- const stashResult = await gitStashLogic(toolInput, logicContext);
60
- // Format the result as a JSON string within TextContent
61
- const resultContent = {
62
- type: "text",
63
- text: JSON.stringify(stashResult, null, 2), // Pretty-print JSON
64
- contentType: "application/json",
65
- };
66
- // Log based on the success flag in the result
67
- if (stashResult.success) {
68
- logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
69
- }
70
- else {
71
- // Log specific failure message from the result
72
- logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${stashResult.message}`, {
73
- ...logicContext,
74
- errorDetails: stashResult.error,
75
- conflicts: stashResult.conflicts,
76
- });
77
- }
78
- // Return the result, whether success or structured failure
79
- return { content: [resultContent] };
80
- }, {
81
- operation: toolOperation,
82
- context: logicContext,
83
- input: validatedArgs, // Log the raw validated args
84
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
45
+ }
46
+ catch (error) {
47
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
48
+ const handledError = ErrorHandler.handleError(error, {
49
+ operation: `tool:${TOOL_NAME}`,
50
+ context: handlerContext,
51
+ input: params,
85
52
  });
86
- });
87
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
88
- }, { operation, context, critical: true }); // Mark registration as critical
53
+ const mcpError = handledError instanceof McpError
54
+ ? handledError
55
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
56
+ return {
57
+ isError: true,
58
+ content: [{ type: "text", text: mcpError.message }],
59
+ structuredContent: mcpError.details,
60
+ };
61
+ }
62
+ });
63
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
89
64
  };
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitStatus tool.
3
- * Exports the registration function and state accessor initialization function.
3
+ * @module src/mcp-server/tools/gitStatus/index
4
4
  */
5
- export { registerGitStatusTool, initializeGitStatusStateAccessors, } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitStatusInput, GitStatusResult } from './logic.js';
5
+ export { registerGitStatusTool } from "./registration.js";
@@ -1,267 +1,120 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_status tool.
3
+ * @module src/mcp-server/tools/gitStatus/logic
4
+ */
1
5
  import { execFile } from "child_process";
2
6
  import { promisify } from "util";
3
7
  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)
8
+ import { logger, sanitization } from "../../../utils/index.js";
9
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
6
10
  const execFileAsync = promisify(execFile);
7
- // Define the input schema for the git_status tool using Zod
11
+ // 1. DEFINE the Zod input schema.
8
12
  export const GitStatusInputSchema = z.object({
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."),
13
+ path: z.string().default(".").describe("Path to the Git repository."),
15
14
  });
16
- /**
17
- * Parses the output of 'git status --porcelain=v1 -b'.
18
- * See: https://git-scm.com/docs/git-status#_porcelain_format_version_1
19
- *
20
- * @param {string} porcelainOutput - The raw output from the git command.
21
- * @returns {GitStatusResult} - Structured status information.
22
- */
23
- function parseGitStatusPorcelainV1(porcelainOutput) {
24
- const lines = porcelainOutput.trim().split("\n");
15
+ // 2. DEFINE the Zod response schema.
16
+ const ChangesSchema = z.object({
17
+ Added: z.array(z.string()).optional(),
18
+ Modified: z.array(z.string()).optional(),
19
+ Deleted: z.array(z.string()).optional(),
20
+ Renamed: z.array(z.string()).optional(),
21
+ Copied: z.array(z.string()).optional(),
22
+ TypeChanged: z.array(z.string()).optional(),
23
+ });
24
+ export const GitStatusOutputSchema = z.object({
25
+ current_branch: z.string().nullable().describe("The current branch, or null for detached HEAD."),
26
+ staged_changes: ChangesSchema.describe("Changes staged for the next commit."),
27
+ unstaged_changes: ChangesSchema.describe("Changes not staged for commit."),
28
+ untracked_files: z.array(z.string()).describe("Files not tracked by Git."),
29
+ conflicted_files: z.array(z.string()).describe("Files with merge conflicts."),
30
+ is_clean: z.boolean().describe("True if there are no pending changes."),
31
+ });
32
+ function parseGitStatus(porcelainOutput) {
33
+ const lines = porcelainOutput.trim().split("\n").filter(Boolean);
25
34
  const result = {
26
35
  current_branch: null,
27
36
  staged_changes: {},
28
37
  unstaged_changes: {},
29
38
  untracked_files: [],
30
39
  conflicted_files: [],
31
- is_clean: true, // Assume clean initially
40
+ is_clean: true,
32
41
  };
33
- if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
34
- return result;
35
- }
36
- if (lines[0].startsWith("## ")) {
42
+ if (lines.length > 0 && lines[0].startsWith("## ")) {
37
43
  const branchLine = lines.shift();
38
- const standardBranchMatch = branchLine.match(/^## ([^ ]+?)(?:\.\.\.| \[.*\]|$)/);
39
- const noCommitsMatch = branchLine.match(/^## No commits yet on (.+)/);
40
- const detachedMatch = branchLine.match(/^## HEAD \(no branch\)/);
41
- if (standardBranchMatch) {
42
- result.current_branch = standardBranchMatch[1];
43
- }
44
- else if (noCommitsMatch) {
45
- result.current_branch = `${noCommitsMatch[1]} (no commits yet)`;
46
- }
47
- else if (detachedMatch) {
48
- result.current_branch = "HEAD (detached)";
49
- }
50
- else {
51
- logger.warning("Could not parse branch information from line:", {
52
- branchLine,
53
- });
54
- result.current_branch = "(unknown)";
55
- }
44
+ const branchMatch = branchLine.match(/^## (.*?)(?:\.\.\..*)?$/);
45
+ result.current_branch = branchMatch ? branchMatch[1] : "HEAD (detached)";
56
46
  }
57
- for (const line of lines) {
58
- if (!line)
59
- continue;
60
- result.is_clean = false; // Any line indicates non-clean state
47
+ lines.forEach(line => {
48
+ result.is_clean = false;
61
49
  const xy = line.substring(0, 2);
62
50
  const file = line.substring(3);
63
- const stagedStatusChar = xy[0];
64
- const unstagedStatusChar = xy[1];
65
- // Handle untracked files
66
- if (xy === "??") {
51
+ const staged = xy[0];
52
+ const unstaged = xy[1];
53
+ if (xy === '??') {
67
54
  result.untracked_files.push(file);
68
- continue;
69
55
  }
70
- // Handle conflicted files (unmerged paths)
71
- // DD = both deleted, AU = added by us, UD = deleted by them, UA = added by them, DU = deleted by us
72
- // AA = both added, UU = both modified
73
- if (stagedStatusChar === "U" ||
74
- unstagedStatusChar === "U" ||
75
- (stagedStatusChar === "D" && unstagedStatusChar === "D") ||
76
- (stagedStatusChar === "A" && unstagedStatusChar === "A")) {
56
+ else if (staged === 'U' || unstaged === 'U' || (staged === 'A' && unstaged === 'A') || (staged === 'D' && unstaged === 'D')) {
77
57
  result.conflicted_files.push(file);
78
- continue; // Conflicted files are handled separately and not in staged/unstaged
79
58
  }
80
- // Handle staged changes (index status)
81
- if (stagedStatusChar !== " " && stagedStatusChar !== "?") {
82
- let statusDesc = undefined;
83
- switch (stagedStatusChar) {
84
- case "M":
85
- statusDesc = "Modified";
86
- break;
87
- case "A":
88
- statusDesc = "Added";
89
- break;
90
- case "D":
91
- statusDesc = "Deleted";
92
- break;
93
- case "R":
94
- statusDesc = "Renamed";
95
- break;
96
- case "C":
97
- statusDesc = "Copied";
98
- break;
99
- case "T":
100
- statusDesc = "TypeChanged";
101
- break;
102
- }
103
- if (statusDesc) {
104
- if (!result.staged_changes[statusDesc]) {
105
- result.staged_changes[statusDesc] = [];
59
+ else {
60
+ const mapStatus = (char, changeSet) => {
61
+ let statusKey;
62
+ switch (char) {
63
+ case 'M':
64
+ statusKey = 'Modified';
65
+ break;
66
+ case 'A':
67
+ statusKey = 'Added';
68
+ break;
69
+ case 'D':
70
+ statusKey = 'Deleted';
71
+ break;
72
+ case 'R':
73
+ statusKey = 'Renamed';
74
+ break;
75
+ case 'C':
76
+ statusKey = 'Copied';
77
+ break;
78
+ case 'T':
79
+ statusKey = 'TypeChanged';
80
+ break;
81
+ default: return;
106
82
  }
107
- result.staged_changes[statusDesc].push(file);
108
- }
109
- }
110
- // Handle unstaged changes (worktree status)
111
- if (unstagedStatusChar !== " " && unstagedStatusChar !== "?") {
112
- let statusDesc = undefined;
113
- switch (unstagedStatusChar) {
114
- case "M":
115
- statusDesc = "Modified";
116
- break;
117
- case "D":
118
- statusDesc = "Deleted";
119
- break;
120
- case "T":
121
- statusDesc = "TypeChanged";
122
- break;
123
- // 'A' (Added but not committed) is handled by '??' (untracked)
124
- // 'R' and 'C' in worktree without being staged are complex, often appear as deleted + untracked
125
- }
126
- if (statusDesc) {
127
- if (!result.unstaged_changes[statusDesc]) {
128
- result.unstaged_changes[statusDesc] = [];
83
+ if (!changeSet[statusKey]) {
84
+ changeSet[statusKey] = [];
129
85
  }
130
- result.unstaged_changes[statusDesc].push(file);
131
- }
86
+ changeSet[statusKey].push(file);
87
+ };
88
+ mapStatus(staged, result.staged_changes);
89
+ mapStatus(unstaged, result.unstaged_changes);
132
90
  }
133
- }
134
- result.is_clean =
135
- Object.keys(result.staged_changes).length === 0 &&
136
- Object.keys(result.unstaged_changes).length === 0 &&
137
- result.untracked_files.length === 0 &&
138
- result.conflicted_files.length === 0;
91
+ });
139
92
  return result;
140
93
  }
141
94
  /**
142
- * Executes the 'git status --porcelain=v1 -b' command and returns structured JSON output.
143
- *
144
- * @param {GitStatusInput} input - The validated input object containing the repository path.
145
- * @param {RequestContext} context - The request context for logging and error handling.
146
- * @returns {Promise<GitStatusResult>} A promise that resolves with the structured git status.
147
- * @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails.
95
+ * 4. IMPLEMENT the core logic function.
96
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
148
97
  */
149
- export async function getGitStatus(input, context) {
98
+ export async function getGitStatus(params, context) {
150
99
  const operation = "getGitStatus";
151
- logger.debug(`Executing ${operation}`, { ...context, input });
152
- let targetPath;
153
- try {
154
- // Resolve the target path
155
- if (input.path && input.path !== ".") {
156
- // Use the provided path directly
157
- targetPath = input.path;
158
- logger.debug(`Using provided path: ${targetPath}`, {
159
- ...context,
160
- operation,
161
- });
162
- }
163
- else {
164
- // Path is '.' or undefined, try to get the session's working directory
165
- const workingDir = context.getWorkingDirectory();
166
- if (!workingDir) {
167
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
168
- }
169
- targetPath = workingDir;
170
- logger.debug(`Using session working directory: ${targetPath}`, {
171
- ...context,
172
- operation,
173
- sessionId: context.sessionId,
174
- });
175
- }
176
- // Sanitize the resolved path
177
- const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
178
- allowAbsolute: true,
179
- });
180
- logger.debug("Sanitized path", {
181
- ...context,
182
- operation,
183
- sanitizedPathInfo,
184
- });
185
- targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
186
- }
187
- catch (error) {
188
- logger.error("Path resolution or sanitization failed", {
189
- ...context,
190
- operation,
191
- error,
192
- });
193
- if (error instanceof McpError) {
194
- throw error;
195
- }
196
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
100
+ logger.debug(`Executing ${operation}`, { ...context, params });
101
+ const workingDir = context.getWorkingDirectory();
102
+ if (params.path === "." && !workingDir) {
103
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
197
104
  }
105
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
106
+ const args = ["-C", targetPath, "status", "--porcelain=v1", "-b"];
198
107
  try {
199
- // Using --porcelain=v1 for stable, scriptable output and -b for branch info
200
- const args = ["-C", targetPath, "status", "--porcelain=v1", "-b"];
201
- logger.debug(`Executing command: git ${args.join(" ")}`, {
202
- ...context,
203
- operation,
204
- });
205
- const { stdout, stderr } = await execFileAsync("git", args);
206
- if (stderr) {
207
- // Log stderr as warning but proceed to parse stdout
208
- logger.warning(`Git status command produced stderr (may be informational)`, { ...context, operation, stderr });
209
- }
210
- logger.debug(`${operation} command executed, parsing output...`, {
211
- ...context,
212
- operation,
213
- path: targetPath,
214
- });
215
- // Parse the porcelain output
216
- const structuredResult = parseGitStatusPorcelainV1(stdout);
217
- // If parsing resulted in clean state but no branch, re-check branch explicitly
218
- // This handles the case of an empty repo after init but before first commit
219
- if (structuredResult.is_clean && !structuredResult.current_branch) {
220
- try {
221
- const branchArgs = [
222
- "-C",
223
- targetPath,
224
- "rev-parse",
225
- "--abbrev-ref",
226
- "HEAD",
227
- ];
228
- const { stdout: branchStdout } = await execFileAsync("git", branchArgs);
229
- const currentBranchName = branchStdout.trim(); // Renamed variable for clarity
230
- if (currentBranchName && currentBranchName !== "HEAD") {
231
- structuredResult.current_branch = currentBranchName;
232
- }
233
- else if (currentBranchName === "HEAD" &&
234
- !structuredResult.current_branch) {
235
- // If rev-parse returns HEAD and we still don't have a branch (e.g. detached from no-commits branch)
236
- structuredResult.current_branch = "HEAD (detached)";
237
- }
238
- }
239
- catch (branchError) {
240
- // Ignore error if rev-parse fails (e.g., still no commits)
241
- logger.debug("Could not determine branch via rev-parse, likely no commits yet.", { ...context, operation, branchError });
242
- }
243
- }
244
- logger.info("git status parsed successfully", {
245
- ...context,
246
- operation,
247
- path: targetPath,
248
- isClean: structuredResult.is_clean,
249
- currentBranch: structuredResult.current_branch,
250
- });
251
- return structuredResult; // Return the structured JSON object
108
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
109
+ const { stdout } = await execFileAsync("git", args);
110
+ return parseGitStatus(stdout);
252
111
  }
253
112
  catch (error) {
254
- logger.error(`Failed to execute or parse git status command`, {
255
- ...context,
256
- operation,
257
- path: targetPath,
258
- error: error.message,
259
- stderr: error.stderr,
260
- });
261
113
  const errorMessage = error.stderr || error.message || "";
114
+ logger.error(`Failed to execute git status command`, { ...context, operation, errorMessage });
262
115
  if (errorMessage.toLowerCase().includes("not a git repository")) {
263
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
116
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
264
117
  }
265
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git status for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
118
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git status failed: ${errorMessage}`);
266
119
  }
267
120
  }
@@ -1,78 +1,64 @@
1
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
2
- import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), ErrorHandler (./utils/internal/errorHandler.js), requestContextService (./utils/internal/requestContext.js)
3
- // Import the result type along with the function and input schema
4
- import { getGitStatus, GitStatusInputSchema, } from "./logic.js";
5
- let _getWorkingDirectory;
6
- let _getSessionId;
7
1
  /**
8
- * Initializes the state accessors needed by the tool registration.
9
- * This should be called once during server setup.
10
- * @param getWdFn - Function to get the working directory for a session.
11
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_status tool.
3
+ * @module src/mcp-server/tools/gitStatus/registration
12
4
  */
13
- export function initializeGitStatusStateAccessors(getWdFn, getSidFn) {
14
- _getWorkingDirectory = getWdFn;
15
- _getSessionId = getSidFn;
16
- logger.info("State accessors initialized for git_status tool registration.");
17
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
7
+ import { getGitStatus, GitStatusInputSchema, GitStatusOutputSchema, } from "./logic.js";
18
8
  const TOOL_NAME = "git_status";
19
9
  const TOOL_DESCRIPTION = "Retrieves the status of a Git repository. Returns a JSON object detailing the current branch, cleanliness, and changes. Staged and unstaged changes are grouped by status (e.g., Added, Modified), alongside lists of untracked and conflicted files.";
20
10
  /**
21
- * Registers the git_status tool with the MCP server.
22
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
23
- *
24
- * @param {McpServer} server - The McpServer instance to register the tool with.
25
- * @returns {Promise<void>}
26
- * @throws {Error} If registration fails or state accessors are not initialized.
11
+ * Registers the git_status tool with the MCP server instance.
12
+ * @param server The MCP server instance.
13
+ * @param getWorkingDirectory Function to get the session's working directory.
14
+ * @param getSessionId Function to get the session ID from context.
27
15
  */
28
- export const registerGitStatusTool = async (server) => {
29
- if (!_getWorkingDirectory || !_getSessionId) {
30
- throw new Error("State accessors for git_status must be initialized before registration.");
31
- }
16
+ export const registerGitStatusTool = async (server, getWorkingDirectory, getSessionId) => {
32
17
  const operation = "registerGitStatusTool";
33
18
  const context = requestContextService.createRequestContext({ operation });
34
- await ErrorHandler.tryCatch(async () => {
35
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitStatusInputSchema.shape, // Provide the Zod schema shape
36
- async (validatedArgs, callContext) => {
37
- const toolOperation = "tool:git_status";
38
- // Create context, potentially inheriting from callContext
39
- const requestContext = requestContextService.createRequestContext({
40
- operation: toolOperation,
41
- parentContext: callContext,
19
+ server.registerTool(TOOL_NAME, {
20
+ title: "Git Status",
21
+ description: TOOL_DESCRIPTION,
22
+ inputSchema: GitStatusInputSchema.shape,
23
+ outputSchema: GitStatusOutputSchema.shape,
24
+ annotations: {
25
+ readOnlyHint: true,
26
+ destructiveHint: false,
27
+ idempotentHint: true,
28
+ openWorldHint: false,
29
+ },
30
+ }, async (params, callContext) => {
31
+ const handlerContext = requestContextService.createRequestContext({
32
+ toolName: TOOL_NAME,
33
+ parentContext: callContext,
34
+ });
35
+ try {
36
+ const sessionId = getSessionId(handlerContext);
37
+ const result = await getGitStatus(params, {
38
+ ...handlerContext,
39
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
42
40
  });
43
- // Get session ID
44
- const sessionId = _getSessionId(requestContext);
45
- // Define the session-specific getter function
46
- const getWorkingDirectoryForSession = () => {
47
- return _getWorkingDirectory(sessionId);
48
- };
49
- // Enhance context for the logic function
50
- const logicContext = {
51
- ...requestContext,
52
- sessionId: sessionId,
53
- getWorkingDirectory: getWorkingDirectoryForSession,
41
+ return {
42
+ structuredContent: result,
43
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
54
44
  };
55
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
56
- // Use ErrorHandler.tryCatch to wrap the logic execution
57
- return await ErrorHandler.tryCatch(async () => {
58
- // Call the core logic function with validated args and enhanced context
59
- const statusResult = await getGitStatus(validatedArgs, logicContext);
60
- // Format the successful result as a JSON string within TextContent
61
- const resultContent = {
62
- type: "text",
63
- // Stringify the JSON object for the response content
64
- text: JSON.stringify(statusResult, null, 2), // Pretty-print JSON
65
- contentType: "application/json", // Specify content type
66
- };
67
- logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
68
- return { content: [resultContent] }; // isError defaults to false
69
- }, {
70
- operation: toolOperation,
71
- context: logicContext,
72
- input: validatedArgs, // Log sanitized input
73
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code if logic fails unexpectedly
45
+ }
46
+ catch (error) {
47
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
48
+ const handledError = ErrorHandler.handleError(error, {
49
+ operation: `tool:${TOOL_NAME}`,
50
+ context: handlerContext,
51
+ input: params,
74
52
  });
75
- });
76
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
77
- }, { operation, context, critical: true }); // Treat registration failure as critical
53
+ const mcpError = handledError instanceof McpError
54
+ ? handledError
55
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
56
+ return {
57
+ isError: true,
58
+ content: [{ type: "text", text: mcpError.message }],
59
+ structuredContent: mcpError.details,
60
+ };
61
+ }
62
+ });
63
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
78
64
  };