@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,144 +1,55 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_show tool.
3
+ * @module src/mcp-server/tools/gitShow/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_show tool using Zod
8
- // No refinements needed here, so we don't need a separate BaseSchema
11
+ // 1. DEFINE the Zod input schema.
9
12
  export const GitShowInputSchema = z.object({
10
- path: z
11
- .string()
12
- .min(1)
13
- .optional()
14
- .default(".")
15
- .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."),
16
- ref: z
17
- .string()
18
- .min(1)
19
- .describe("The object reference (commit hash, tag name, branch name, HEAD, etc.) to show."),
20
- filePath: z
21
- .string()
22
- .optional()
23
- .describe("Optional specific file path within the ref to show (e.g., show a file's content at a specific commit). If provided, use the format '<ref>:<filePath>'."),
24
- // format: z.string().optional().describe("Optional format string for the output"), // Consider adding later
13
+ path: z.string().default(".").describe("Path to the local Git repository."),
14
+ ref: z.string().min(1).describe("The object reference (commit hash, tag, branch, etc.) to show."),
15
+ filePath: z.string().optional().describe("Optional specific file path within the ref to show."),
16
+ });
17
+ // 2. DEFINE the Zod response schema.
18
+ export const GitShowOutputSchema = z.object({
19
+ success: z.boolean().describe("Indicates if the command was successful."),
20
+ content: z.string().describe("Raw output from the git show command."),
25
21
  });
26
22
  /**
27
- * Executes the 'git show' command for a given reference and optional file path.
28
- *
29
- * @param {GitShowInput} input - The validated input object.
30
- * @param {RequestContext} context - The request context for logging and error handling.
31
- * @returns {Promise<GitShowResult>} A promise that resolves with the structured result.
32
- * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
23
+ * 4. IMPLEMENT the core logic function.
24
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
33
25
  */
34
- export async function gitShowLogic(input, context) {
26
+ export async function gitShowLogic(params, context) {
35
27
  const operation = "gitShowLogic";
36
- logger.debug(`Executing ${operation}`, { ...context, input });
37
- let targetPath;
38
- try {
39
- // Resolve and sanitize the target path
40
- const workingDir = context.getWorkingDirectory();
41
- targetPath =
42
- input.path && input.path !== "." ? input.path : (workingDir ?? ".");
43
- if (targetPath === "." && !workingDir) {
44
- logger.warning("Executing git show in server's CWD as no path provided and no session WD set.", { ...context, operation });
45
- targetPath = process.cwd();
46
- }
47
- else if (targetPath === "." && workingDir) {
48
- targetPath = workingDir;
49
- logger.debug(`Using session working directory: ${targetPath}`, {
50
- ...context,
51
- operation,
52
- sessionId: context.sessionId,
53
- });
54
- }
55
- else {
56
- logger.debug(`Using provided path: ${targetPath}`, {
57
- ...context,
58
- operation,
59
- });
60
- }
61
- targetPath = sanitization.sanitizePath(targetPath, {
62
- allowAbsolute: true,
63
- }).sanitizedPath;
64
- logger.debug("Sanitized path", {
65
- ...context,
66
- operation,
67
- sanitizedPath: targetPath,
68
- });
69
- }
70
- catch (error) {
71
- logger.error("Path resolution or sanitization failed", {
72
- ...context,
73
- operation,
74
- error,
75
- });
76
- if (error instanceof McpError)
77
- throw error;
78
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
79
- }
80
- // Validate ref format (simple validation)
81
- if (!/^[a-zA-Z0-9_./~^:-]+$/.test(input.ref)) {
82
- // Allow ':' for filePath combination
83
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid reference format: ${input.ref}`, { context, operation });
84
- }
85
- // Validate filePath format if provided (basic path chars)
86
- if (input.filePath && !/^[a-zA-Z0-9_./-]+$/.test(input.filePath)) {
87
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid file path format: ${input.filePath}`, { context, operation });
28
+ logger.debug(`Executing ${operation}`, { ...context, params });
29
+ const workingDir = context.getWorkingDirectory();
30
+ if (params.path === "." && !workingDir) {
31
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
88
32
  }
33
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
34
+ const refSpec = params.filePath ? `${params.ref}:${params.filePath}` : params.ref;
35
+ const args = ["-C", targetPath, "show", refSpec];
89
36
  try {
90
- // Construct the refspec, combining ref and filePath if needed
91
- const refSpec = input.filePath
92
- ? `${input.ref}:${input.filePath}`
93
- : input.ref;
94
- // Construct the command
95
- const args = ["-C", targetPath, "show", refSpec];
96
- logger.debug(`Executing command: git ${args.join(" ")}`, {
97
- ...context,
98
- operation,
99
- });
100
- // Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
101
- // We primarily care about stdout for the content. Errors usually have non-zero exit code.
102
- const { stdout, stderr } = await execFileAsync("git", args);
103
- if (stderr) {
104
- // Log stderr as debug info, as it might contain commit details etc.
105
- logger.debug(`Git show command produced stderr (may be informational)`, {
106
- ...context,
107
- operation,
108
- stderr,
109
- });
110
- }
111
- logger.info(`git show executed successfully for ref: ${refSpec}`, {
112
- ...context,
113
- operation,
114
- path: targetPath,
115
- });
116
- return { success: true, content: stdout }; // Return raw stdout content
37
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
38
+ const { stdout } = await execFileAsync("git", args);
39
+ return { success: true, content: stdout };
117
40
  }
118
41
  catch (error) {
119
42
  const errorMessage = error.stderr || error.message || "";
120
- logger.error(`Failed to execute git show command`, {
121
- ...context,
122
- operation,
123
- path: targetPath,
124
- error: errorMessage,
125
- stderr: error.stderr,
126
- stdout: error.stdout,
127
- });
128
- // Specific error handling
43
+ logger.error(`Failed to execute git show command`, { ...context, operation, errorMessage });
129
44
  if (errorMessage.toLowerCase().includes("not a git repository")) {
130
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
45
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
131
46
  }
132
47
  if (/unknown revision or path not in the working tree/i.test(errorMessage)) {
133
- const target = input.filePath
134
- ? `${input.ref}:${input.filePath}`
135
- : input.ref;
136
- throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to show: Reference or pathspec '${target}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
48
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Reference or pathspec not found: '${refSpec}'.`);
137
49
  }
138
50
  if (/ambiguous argument/i.test(errorMessage)) {
139
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to show: Reference '${input.ref}' is ambiguous. Provide a more specific reference. Error: ${errorMessage}`, { context, operation, originalError: error });
51
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Reference '${params.ref}' is ambiguous.`);
140
52
  }
141
- // Throw a generic McpError for other failures
142
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git show failed for path: ${targetPath}, ref: ${input.ref}. Error: ${errorMessage}`, { context, operation, originalError: error });
53
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git show failed: ${errorMessage}`);
143
54
  }
144
55
  }
@@ -1,86 +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 schema and types
4
- import { GitShowInputSchema, gitShowLogic, } from "./logic.js";
5
- let _getWorkingDirectory;
6
- let _getSessionId;
7
1
  /**
8
- * Initializes the state accessors needed by the git_show tool registration.
9
- * @param getWdFn - Function to get the working directory for a session.
10
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_show tool.
3
+ * @module src/mcp-server/tools/gitShow/registration
11
4
  */
12
- export function initializeGitShowStateAccessors(getWdFn, getSidFn) {
13
- _getWorkingDirectory = getWdFn;
14
- _getSessionId = getSidFn;
15
- logger.info("State accessors initialized for git_show tool registration.");
16
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
7
+ import { gitShowLogic, GitShowInputSchema, GitShowOutputSchema, } from "./logic.js";
17
8
  const TOOL_NAME = "git_show";
18
9
  const TOOL_DESCRIPTION = "Shows information about Git objects (commits, tags, blobs, trees) based on a reference. Can optionally show the content of a specific file at that reference. Returns the raw output.";
19
10
  /**
20
- * Registers the git_show tool with the MCP server.
21
- *
22
- * @param {McpServer} server - The McpServer instance to register the tool with.
23
- * @returns {Promise<void>}
24
- * @throws {Error} If registration fails or state accessors are not initialized.
11
+ * Registers the git_show 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.
25
15
  */
26
- export const registerGitShowTool = async (server) => {
27
- if (!_getWorkingDirectory || !_getSessionId) {
28
- throw new Error("State accessors for git_show must be initialized before registration.");
29
- }
16
+ export const registerGitShowTool = async (server, getWorkingDirectory, getSessionId) => {
30
17
  const operation = "registerGitShowTool";
31
18
  const context = requestContextService.createRequestContext({ operation });
32
- await ErrorHandler.tryCatch(async () => {
33
- // Register the tool using the schema's shape (no refinements here)
34
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitShowInputSchema.shape, // Use the shape directly
35
- // Let TypeScript infer handler argument types.
36
- async (validatedArgs, callContext) => {
37
- // Cast validatedArgs to the specific input type for use within the handler
38
- const toolInput = validatedArgs;
39
- const toolOperation = `tool:${TOOL_NAME}`;
40
- const requestContext = requestContextService.createRequestContext({
41
- operation: toolOperation,
42
- parentContext: callContext,
19
+ server.registerTool(TOOL_NAME, {
20
+ title: "Git Show",
21
+ description: TOOL_DESCRIPTION,
22
+ inputSchema: GitShowInputSchema.shape,
23
+ outputSchema: GitShowOutputSchema.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 gitShowLogic(params, {
38
+ ...handlerContext,
39
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
43
40
  });
44
- const sessionId = _getSessionId(requestContext);
45
- const getWorkingDirectoryForSession = () => {
46
- return _getWorkingDirectory(sessionId);
47
- };
48
- const logicContext = {
49
- ...requestContext,
50
- sessionId: sessionId,
51
- getWorkingDirectory: getWorkingDirectoryForSession,
41
+ return {
42
+ structuredContent: result,
43
+ content: [{ type: "text", text: result.content, contentType: "text/plain; charset=utf-8" }],
52
44
  };
53
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
54
- return await ErrorHandler.tryCatch(async () => {
55
- // Call the core logic function which returns a GitShowResult object
56
- const showResult = await gitShowLogic(toolInput, logicContext);
57
- // Format the result within TextContent
58
- const resultContent = {
59
- type: "text",
60
- // Return raw content on success, or error message on failure
61
- text: showResult.success
62
- ? showResult.content
63
- : `Error: ${showResult.message}${showResult.error ? `\nDetails: ${showResult.error}` : ""}`,
64
- // Use plain text content type, unless we decide to return JSON later
65
- contentType: "text/plain",
66
- };
67
- // Log based on the success flag in the result
68
- if (showResult.success) {
69
- logger.info(`Tool ${TOOL_NAME} executed successfully`, logicContext);
70
- }
71
- else {
72
- // Log specific failure message from the result
73
- logger.warning(`Tool ${TOOL_NAME} failed: ${showResult.message}`, { ...logicContext, errorDetails: showResult.error });
74
- }
75
- // Return the result, whether success or structured failure
76
- return { content: [resultContent] };
77
- }, {
78
- operation: toolOperation,
79
- context: logicContext,
80
- input: validatedArgs, // Log the raw validated args
81
- 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,
82
52
  });
83
- });
84
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
85
- }, { 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);
86
64
  };
@@ -1,7 +1,5 @@
1
1
  /**
2
- * @fileoverview Barrel file for the git_stash tool.
3
- * Exports the registration function and state accessor initialization function.
2
+ * @fileoverview Barrel file for the gitStash tool.
3
+ * @module src/mcp-server/tools/gitStash/index
4
4
  */
5
- export { registerGitStashTool, initializeGitStashStateAccessors, } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitStashInput, GitStashResult } from './logic.js';
5
+ export { registerGitStashTool } from "./registration.js";
@@ -1,244 +1,100 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_stash tool.
3
+ * @module src/mcp-server/tools/gitStash/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 BASE input schema for the git_stash tool using Zod
11
+ // 1. DEFINE the Zod input schema.
8
12
  export const GitStashBaseSchema = z.object({
9
- path: z
10
- .string()
11
- .min(1)
12
- .optional()
13
- .default(".")
14
- .describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
15
- mode: z
16
- .enum(["list", "apply", "pop", "drop", "save"])
17
- .describe("The stash operation to perform: 'list', 'apply', 'pop', 'drop', 'save'."),
18
- stashRef: z
19
- .string()
20
- .optional()
21
- .describe("Stash reference (e.g., 'stash@{1}'). Required for 'apply', 'pop', 'drop' modes."),
22
- message: z
23
- .string()
24
- .optional()
25
- .describe("Optional descriptive message used only for 'save' mode."),
26
- // includeUntracked: z.boolean().default(false).describe("Include untracked files in 'save' mode (-u)"), // Consider adding later
27
- // keepIndex: z.boolean().default(false).describe("Keep staged changes in 'save' mode (--keep-index)"), // Consider adding later
13
+ path: z.string().default(".").describe("Path to the local Git repository."),
14
+ mode: z.enum(["list", "apply", "pop", "drop", "save"]).describe("The stash operation to perform."),
15
+ stashRef: z.string().optional().describe("Stash reference (e.g., 'stash@{1}')."),
16
+ message: z.string().optional().describe("Optional descriptive message for 'save' mode."),
28
17
  });
29
- // Apply refinements and export the FINAL schema for validation within the handler
30
18
  export const GitStashInputSchema = GitStashBaseSchema.refine((data) => !(["apply", "pop", "drop"].includes(data.mode) && !data.stashRef), {
31
- message: "A 'stashRef' (e.g., 'stash@{0}') is required for 'apply', 'pop', and 'drop' modes.",
32
- path: ["stashRef"], // Point error to the stashRef field
19
+ message: "A 'stashRef' is required for 'apply', 'pop', and 'drop' modes.",
20
+ path: ["stashRef"],
21
+ });
22
+ // 2. DEFINE the Zod response schema.
23
+ const StashInfoSchema = z.object({
24
+ ref: z.string(),
25
+ branch: z.string(),
26
+ description: z.string(),
27
+ });
28
+ export const GitStashOutputSchema = z.object({
29
+ success: z.boolean().describe("Indicates if the command was successful."),
30
+ mode: z.string().describe("The mode of operation that was performed."),
31
+ message: z.string().optional().describe("A summary message of the result."),
32
+ stashes: z.array(StashInfoSchema).optional().describe("A list of stashes for the 'list' mode."),
33
+ conflicts: z.boolean().optional().describe("Indicates if a merge conflict occurred."),
34
+ stashCreated: z.boolean().optional().describe("Indicates if a stash was created."),
33
35
  });
34
36
  /**
35
- * Executes git stash commands based on the specified mode.
36
- *
37
- * @param {GitStashInput} input - The validated input object (validated against GitStashInputSchema).
38
- * @param {RequestContext} context - The request context for logging and error handling.
39
- * @returns {Promise<GitStashResult>} A promise that resolves with the structured result.
40
- * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
37
+ * 4. IMPLEMENT the core logic function.
38
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
41
39
  */
42
- export async function gitStashLogic(input, context) {
43
- const operation = `gitStashLogic:${input.mode}`;
44
- logger.debug(`Executing ${operation}`, { ...context, input });
45
- let targetPath;
46
- try {
47
- // Resolve and sanitize the target path
48
- const workingDir = context.getWorkingDirectory();
49
- targetPath =
50
- input.path && input.path !== "." ? input.path : (workingDir ?? ".");
51
- if (targetPath === "." && !workingDir) {
52
- logger.warning("Executing git stash in server's CWD as no path provided and no session WD set.", { ...context, operation });
53
- targetPath = process.cwd();
54
- }
55
- else if (targetPath === "." && workingDir) {
56
- targetPath = workingDir;
57
- logger.debug(`Using session working directory: ${targetPath}`, {
58
- ...context,
59
- operation,
60
- sessionId: context.sessionId,
61
- });
62
- }
63
- else {
64
- logger.debug(`Using provided path: ${targetPath}`, {
65
- ...context,
66
- operation,
67
- });
68
- }
69
- targetPath = sanitization.sanitizePath(targetPath, {
70
- allowAbsolute: true,
71
- }).sanitizedPath;
72
- logger.debug("Sanitized path", {
73
- ...context,
74
- operation,
75
- sanitizedPath: targetPath,
76
- });
40
+ export async function gitStashLogic(params, context) {
41
+ const operation = `gitStashLogic:${params.mode}`;
42
+ logger.debug(`Executing ${operation}`, { ...context, params });
43
+ const workingDir = context.getWorkingDirectory();
44
+ if (params.path === "." && !workingDir) {
45
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
77
46
  }
78
- catch (error) {
79
- logger.error("Path resolution or sanitization failed", {
80
- ...context,
81
- operation,
82
- error,
83
- });
84
- if (error instanceof McpError)
85
- throw error;
86
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
87
- }
88
- // Validate stashRef format if provided (simple validation)
89
- if (input.stashRef && !/^stash@\{[0-9]+\}$/.test(input.stashRef)) {
90
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid stash reference format: ${input.stashRef}. Expected format: stash@{n}`, { context, operation });
91
- }
92
- try {
93
- let args;
94
- let result;
95
- switch (input.mode) {
47
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
48
+ const buildArgs = () => {
49
+ const baseArgs = ["-C", targetPath, "stash", params.mode];
50
+ switch (params.mode) {
96
51
  case "list":
97
- args = ["-C", targetPath, "stash", "list"];
98
- logger.debug(`Executing command: git ${args.join(" ")}`, {
99
- ...context,
100
- operation,
101
- });
102
- const { stdout: listStdout } = await execFileAsync("git", args);
103
- const stashes = listStdout
104
- .trim()
105
- .split("\n")
106
- .filter((line) => line)
107
- .map((line) => {
108
- // Improved regex to handle different stash list formats
109
- const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
110
- return match
111
- ? {
112
- ref: match[1],
113
- branch: match[3] || "unknown",
114
- description: match[4],
115
- }
116
- : { ref: "unknown", branch: "unknown", description: line }; // Fallback parsing
117
- });
118
- result = { success: true, mode: "list", stashes };
52
+ // No extra args needed
119
53
  break;
120
54
  case "apply":
121
55
  case "pop":
122
- // stashRef is validated by Zod refine
123
- const stashRefApplyPop = input.stashRef;
124
- args = ["-C", targetPath, "stash", input.mode, stashRefApplyPop];
125
- logger.debug(`Executing command: git ${args.join(" ")}`, {
126
- ...context,
127
- operation,
128
- });
129
- try {
130
- const { stdout, stderr } = await execFileAsync("git", args);
131
- // Check stdout/stderr for conflict messages, although exit code 0 usually means success
132
- const conflicts = /conflict/i.test(stdout) || /conflict/i.test(stderr);
133
- const message = conflicts
134
- ? `Stash ${input.mode} resulted in conflicts that need manual resolution.`
135
- : `Stash ${stashRefApplyPop} ${input.mode === "apply" ? "applied" : "popped"} successfully.`;
136
- logger.info(message, {
137
- ...context,
138
- operation,
139
- path: targetPath,
140
- conflicts,
141
- });
142
- result = { success: true, mode: input.mode, message, conflicts };
143
- }
144
- catch (applyError) {
145
- const applyErrorMessage = applyError.stderr || applyError.message || "";
146
- if (/conflict/i.test(applyErrorMessage)) {
147
- logger.warning(`Stash ${input.mode} failed due to conflicts.`, {
148
- ...context,
149
- operation,
150
- path: targetPath,
151
- error: applyErrorMessage,
152
- });
153
- // Return failure but indicate conflicts
154
- return {
155
- success: false,
156
- mode: input.mode,
157
- message: `Failed to ${input.mode} stash ${stashRefApplyPop} due to conflicts. Resolve conflicts manually.`,
158
- error: applyErrorMessage,
159
- conflicts: true,
160
- };
161
- }
162
- // Rethrow other errors
163
- throw applyError;
164
- }
165
- break;
166
56
  case "drop":
167
- // stashRef is validated by Zod refine
168
- const stashRefDrop = input.stashRef;
169
- args = ["-C", targetPath, "stash", "drop", stashRefDrop];
170
- logger.debug(`Executing command: git ${args.join(" ")}`, {
171
- ...context,
172
- operation,
173
- });
174
- await execFileAsync("git", args);
175
- result = {
176
- success: true,
177
- mode: "drop",
178
- message: `Dropped ${stashRefDrop} successfully.`,
179
- stashRef: stashRefDrop,
180
- };
57
+ baseArgs.push(params.stashRef);
181
58
  break;
182
59
  case "save":
183
- args = ["-C", targetPath, "stash", "save"];
184
- if (input.message) {
185
- args.push(input.message);
60
+ if (params.message) {
61
+ baseArgs.push(params.message);
186
62
  }
187
- logger.debug(`Executing command: git ${args.join(" ")}`, {
188
- ...context,
189
- operation,
190
- });
191
- const { stdout: saveStdout } = await execFileAsync("git", args);
192
- const stashCreated = !/no local changes to save/i.test(saveStdout);
193
- const saveMessage = stashCreated
194
- ? `Changes stashed successfully.` +
195
- (input.message ? ` Message: "${input.message}"` : "")
196
- : "No local changes to save.";
197
- result = {
198
- success: true,
199
- mode: "save",
200
- message: saveMessage,
201
- stashCreated,
202
- };
203
63
  break;
204
- default:
205
- // Should not happen due to Zod validation
206
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
207
64
  }
208
- logger.info(`git stash ${input.mode} executed successfully`, {
209
- ...context,
210
- operation,
211
- path: targetPath,
212
- result,
213
- });
214
- return result;
65
+ return baseArgs;
66
+ };
67
+ const args = buildArgs();
68
+ try {
69
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
70
+ const { stdout, stderr } = await execFileAsync("git", args);
71
+ if (params.mode === 'list') {
72
+ const stashes = stdout.trim().split("\n").filter(Boolean).map(line => {
73
+ const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
74
+ return match ? { ref: match[1], branch: match[3] || "unknown", description: match[4] } : { ref: "unknown", branch: "unknown", description: line };
75
+ });
76
+ return { success: true, mode: params.mode, stashes };
77
+ }
78
+ const output = stdout + stderr;
79
+ const conflicts = /conflict/i.test(output);
80
+ if (params.mode === 'save') {
81
+ const stashCreated = !/no local changes to save/i.test(output);
82
+ return { success: true, mode: params.mode, message: stashCreated ? "Changes stashed." : "No local changes to save.", stashCreated };
83
+ }
84
+ return { success: true, mode: params.mode, message: `${params.mode} operation successful.`, conflicts };
215
85
  }
216
86
  catch (error) {
217
87
  const errorMessage = error.stderr || error.message || "";
218
- logger.error(`Failed to execute git stash command`, {
219
- ...context,
220
- operation,
221
- path: targetPath,
222
- error: errorMessage,
223
- stderr: error.stderr,
224
- stdout: error.stdout,
225
- });
226
- // Specific error handling
88
+ logger.error(`Failed to execute git stash command`, { ...context, operation, errorMessage });
227
89
  if (errorMessage.toLowerCase().includes("not a git repository")) {
228
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
90
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
229
91
  }
230
- if ((input.mode === "apply" ||
231
- input.mode === "pop" ||
232
- input.mode === "drop") &&
233
- /no such stash/i.test(errorMessage)) {
234
- throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to ${input.mode} stash: Stash '${input.stashRef}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
92
+ if (/no such stash/i.test(errorMessage)) {
93
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Stash '${params.stashRef}' not found.`);
235
94
  }
236
- if ((input.mode === "apply" || input.mode === "pop") &&
237
- /conflict/i.test(errorMessage)) {
238
- // This case might be caught above, but double-check here
239
- throw new McpError(BaseErrorCode.CONFLICT, `Failed to ${input.mode} stash '${input.stashRef}' due to conflicts. Resolve conflicts manually. Error: ${errorMessage}`, { context, operation, originalError: error });
95
+ if (/conflict/i.test(errorMessage)) {
96
+ throw new McpError(BaseErrorCode.CONFLICT, `Stash ${params.mode} failed due to conflicts.`);
240
97
  }
241
- // Throw a generic McpError for other failures
242
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git stash ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
98
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git stash ${params.mode} failed: ${errorMessage}`);
243
99
  }
244
100
  }