@cyanheads/git-mcp-server 2.1.8 → 2.2.1

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 +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  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 -144
  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 +47 -173
  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 +45 -154
  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 +18 -32
  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 +47 -187
  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 +75 -310
  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 +72 -264
  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 +38 -136
  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 +40 -162
  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 +71 -266
  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 +45 -191
  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 +39 -156
  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 +65 -192
  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 +59 -207
  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 +76 -200
  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 +33 -133
  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 +39 -144
  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 +28 -133
  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 +59 -219
  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 +79 -236
  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 +57 -198
  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 +102 -328
  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 +25 -43
  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,104 +1,64 @@
1
- // Import utils from barrel (logger from ../utils/internal/logger.js)
2
- import { logger } from "../../../utils/index.js";
3
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
- import { ErrorHandler } from "../../../utils/index.js";
5
- // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { requestContextService } from "../../../utils/index.js";
7
- // Import the schema and types
8
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
9
- import { GitCleanInputSchema, gitCleanLogic, } from "./logic.js";
10
- let _getWorkingDirectory;
11
- let _getSessionId;
12
1
  /**
13
- * Initializes the state accessors needed by the git_clean tool registration.
14
- * @param getWdFn - Function to get the working directory for a session.
15
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_clean tool.
3
+ * @module src/mcp-server/tools/gitClean/registration
16
4
  */
17
- export function initializeGitCleanStateAccessors(getWdFn, getSidFn) {
18
- _getWorkingDirectory = getWdFn;
19
- _getSessionId = getSidFn;
20
- logger.info("State accessors initialized for git_clean tool registration.");
21
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { gitCleanLogic, GitCleanInputSchema, GitCleanOutputSchema, } from "./logic.js";
22
7
  const TOOL_NAME = "git_clean";
23
8
  const TOOL_DESCRIPTION = "Removes untracked files from the working directory. Supports dry runs, removing directories, and removing ignored files. CRITICAL: Requires explicit `force: true` parameter for safety as this is a destructive operation. Returns results as a JSON object.";
24
9
  /**
25
- * Registers the git_clean tool with the MCP server.
26
- *
27
- * @param {McpServer} server - The McpServer instance to register the tool with.
28
- * @returns {Promise<void>}
29
- * @throws {Error} If registration fails or state accessors are not initialized.
10
+ * Registers the git_clean tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param getWorkingDirectory Function to get the session's working directory.
13
+ * @param getSessionId Function to get the session ID from context.
30
14
  */
31
- export const registerGitCleanTool = async (server) => {
32
- if (!_getWorkingDirectory || !_getSessionId) {
33
- throw new Error("State accessors for git_clean must be initialized before registration.");
34
- }
15
+ export const registerGitCleanTool = async (server, getWorkingDirectory, getSessionId) => {
35
16
  const operation = "registerGitCleanTool";
36
17
  const context = requestContextService.createRequestContext({ operation });
37
- await ErrorHandler.tryCatch(async () => {
38
- // Register the tool using the schema's shape (no refinements here)
39
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCleanInputSchema.shape, // Use the shape directly
40
- // Let TypeScript infer handler argument types.
41
- async (validatedArgs, callContext) => {
42
- // Cast validatedArgs to the specific input type for use within the handler
43
- const toolInput = validatedArgs;
44
- const toolOperation = `tool:${TOOL_NAME}`;
45
- const requestContext = requestContextService.createRequestContext({
46
- operation: toolOperation,
47
- parentContext: callContext,
18
+ server.registerTool(TOOL_NAME, {
19
+ title: "Git Clean",
20
+ description: TOOL_DESCRIPTION,
21
+ inputSchema: GitCleanInputSchema.shape,
22
+ outputSchema: GitCleanOutputSchema.shape,
23
+ annotations: {
24
+ readOnlyHint: false,
25
+ destructiveHint: true,
26
+ idempotentHint: true, // Running clean again does nothing if already clean
27
+ openWorldHint: false,
28
+ },
29
+ }, async (params, callContext) => {
30
+ const handlerContext = requestContextService.createRequestContext({
31
+ toolName: TOOL_NAME,
32
+ parentContext: callContext,
33
+ });
34
+ try {
35
+ const sessionId = getSessionId(handlerContext);
36
+ const result = await gitCleanLogic(params, {
37
+ ...handlerContext,
38
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
48
39
  });
49
- // --- SAFETY CHECK (Redundant but good practice) ---
50
- // The core logic already checks this, but adding a check here ensures
51
- // the intent is clear even before calling the logic.
52
- if (!toolInput.force) {
53
- logger.error(`Tool ${TOOL_NAME} called without force=true. Aborting.`, requestContext);
54
- // Return a structured error via CallToolResult
55
- const errorContent = {
56
- type: "text",
57
- text: JSON.stringify({
58
- success: false,
59
- message: "Operation aborted: 'force' parameter must be explicitly set to true to execute 'git clean'.",
60
- dryRun: toolInput.dryRun, // Include dryRun status
61
- }, null, 2),
62
- contentType: "application/json",
63
- };
64
- return { content: [errorContent], isError: true }; // Indicate it's an error result
65
- }
66
- const sessionId = _getSessionId(requestContext);
67
- const getWorkingDirectoryForSession = () => {
68
- return _getWorkingDirectory(sessionId);
69
- };
70
- const logicContext = {
71
- ...requestContext,
72
- sessionId: sessionId,
73
- getWorkingDirectory: getWorkingDirectoryForSession,
40
+ return {
41
+ structuredContent: result,
42
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
74
43
  };
75
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
76
- return await ErrorHandler.tryCatch(async () => {
77
- // Call the core logic function which returns a GitCleanResult object
78
- const cleanResult = await gitCleanLogic(toolInput, logicContext);
79
- // Format the result as JSON within TextContent
80
- const resultContent = {
81
- type: "text",
82
- text: JSON.stringify(cleanResult, null, 2),
83
- contentType: "application/json",
84
- };
85
- // Log based on the success flag in the result
86
- if (cleanResult.success) {
87
- logger.info(`Tool ${TOOL_NAME} executed successfully (DryRun: ${cleanResult.dryRun})`, logicContext);
88
- }
89
- else {
90
- // Log specific failure message from the result
91
- logger.warning(`Tool ${TOOL_NAME} failed: ${cleanResult.message}`, { ...logicContext, errorDetails: cleanResult.error });
92
- }
93
- // Return the result, whether success or structured failure
94
- return { content: [resultContent] };
95
- }, {
96
- operation: toolOperation,
97
- context: logicContext,
98
- input: validatedArgs, // Log the raw validated args
99
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
44
+ }
45
+ catch (error) {
46
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
47
+ const mcpError = ErrorHandler.handleError(error, {
48
+ operation: `tool:${TOOL_NAME}`,
49
+ context: handlerContext,
50
+ input: params,
100
51
  });
101
- });
102
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
103
- }, { operation, context, critical: true }); // Mark registration as critical
52
+ return {
53
+ isError: true,
54
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
55
+ structuredContent: {
56
+ code: mcpError.code,
57
+ message: mcpError.message,
58
+ details: mcpError.details,
59
+ },
60
+ };
61
+ }
62
+ });
63
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
104
64
  };
@@ -1,7 +1,5 @@
1
1
  /**
2
- * @fileoverview Barrel file for the git_clear_working_dir tool.
3
- * Exports the registration function and related components.
2
+ * @fileoverview Barrel file for the gitClearWorkingDir tool.
3
+ * @module src/mcp-server/tools/gitClearWorkingDir/index
4
4
  */
5
- export { registerGitClearWorkingDirTool, initializeGitClearWorkingDirStateAccessors, } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitClearWorkingDirInput, GitClearWorkingDirResult } from './logic.js';
5
+ export { registerGitClearWorkingDirTool } from "./registration.js";
@@ -1,39 +1,25 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_clear_working_dir tool.
3
+ * @module src/mcp-server/tools/gitClearWorkingDir/logic
4
+ */
1
5
  import { z } from "zod";
2
- // Import utils from barrel (logger from ../utils/internal/logger.js)
3
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
4
6
  import { logger } from "../../../utils/index.js";
5
- // Define the Zod schema for input validation (no arguments needed)
7
+ // 1. DEFINE the Zod input schema.
6
8
  export const GitClearWorkingDirInputSchema = z.object({});
9
+ // 2. DEFINE the Zod response schema.
10
+ export const GitClearWorkingDirOutputSchema = z.object({
11
+ success: z.boolean().describe("Indicates if the command was successful."),
12
+ message: z.string().describe("A summary message of the result."),
13
+ });
7
14
  /**
8
- * Logic for the git_clear_working_dir tool.
9
- * Clears the global working directory setting for the current session.
10
- *
11
- * @param {GitClearWorkingDirInput} input - The validated input arguments (empty object).
12
- * @param {RequestContext} context - The request context, containing session ID and the clear function.
13
- * @returns {Promise<GitClearWorkingDirResult>} The result of the operation.
14
- * @throws {McpError} Throws McpError for operational errors.
15
+ * 4. IMPLEMENT the core logic function.
16
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
15
17
  */
16
- export async function gitClearWorkingDirLogic(input, context) {
18
+ export async function gitClearWorkingDirLogic(params, context) {
17
19
  const operation = "gitClearWorkingDirLogic";
18
- logger.debug(`Executing ${operation}`, { ...context, input });
19
- // --- Update Session State ---
20
- // This part needs access to the session state mechanism defined in server.ts
21
- // We assume the context provides a way to clear the working directory for the current session.
22
- try {
23
- context.clearWorkingDirectory();
24
- const message = `Working directory cleared for session ${context.sessionId || "stdio"}`;
25
- logger.info(message, { ...context, operation });
26
- }
27
- catch (error) {
28
- logger.error("Failed to clear working directory in session state", error, {
29
- ...context,
30
- operation,
31
- });
32
- // This indicates an internal logic error in how state is passed/managed.
33
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to update session state.", { context, operation });
34
- }
35
- return {
36
- success: true,
37
- message: "Global working directory setting cleared.",
38
- };
20
+ logger.debug(`Executing ${operation}`, { ...context, params });
21
+ context.clearWorkingDirectory();
22
+ const message = "Session working directory cleared successfully.";
23
+ logger.info(message, { ...context, operation });
24
+ return { success: true, message };
39
25
  }
@@ -1,82 +1,64 @@
1
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from "../../../utils/index.js";
3
- // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from "../../../utils/index.js";
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 { GitClearWorkingDirInputSchema, gitClearWorkingDirLogic, } from "./logic.js";
9
- let _clearWorkingDirectory;
10
- let _getSessionId;
11
1
  /**
12
- * Initializes the state accessors needed by the tool registration.
13
- * This should be called once during server setup.
14
- * @param clearFn - Function to clear the working directory for a session.
15
- * @param getFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_clear_working_dir tool.
3
+ * @module src/mcp-server/tools/gitClearWorkingDir/registration
16
4
  */
17
- export function initializeGitClearWorkingDirStateAccessors(clearFn, getFn) {
18
- _clearWorkingDirectory = clearFn;
19
- _getSessionId = getFn; // Can reuse the getter from the set tool
20
- logger.info("State accessors initialized for git_clear_working_dir tool registration.");
21
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { gitClearWorkingDirLogic, GitClearWorkingDirInputSchema, GitClearWorkingDirOutputSchema, } from "./logic.js";
22
7
  const TOOL_NAME = "git_clear_working_dir";
23
8
  const TOOL_DESCRIPTION = "Clears the session-specific working directory previously set by `git_set_working_dir`. Subsequent Git tool calls in this session will require an explicit `path` parameter or will default to the server's current working directory. Returns the result as a JSON object.";
24
9
  /**
25
- * Registers the git_clear_working_dir tool with the MCP server.
26
- *
27
- * @param {McpServer} server - The MCP server instance.
28
- * @throws {Error} If state accessors are not initialized.
10
+ * Registers the git_clear_working_dir tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param clearWorkingDirectory Function to clear the session's working directory.
13
+ * @param getSessionId Function to get the session ID from context.
29
14
  */
30
- export async function registerGitClearWorkingDirTool(server) {
31
- if (!_clearWorkingDirectory || !_getSessionId) {
32
- throw new Error("State accessors for git_clear_working_dir must be initialized before registration.");
33
- }
34
- try {
35
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitClearWorkingDirInputSchema.shape, // Empty shape
36
- async (validatedArgs, callContext) => {
37
- const operation = "tool:git_clear_working_dir";
38
- const requestContext = requestContextService.createRequestContext({
39
- operation,
40
- parentContext: callContext,
15
+ export const registerGitClearWorkingDirTool = async (server, clearWorkingDirectory, getSessionId) => {
16
+ const operation = "registerGitClearWorkingDirTool";
17
+ const context = requestContextService.createRequestContext({ operation });
18
+ server.registerTool(TOOL_NAME, {
19
+ title: "Git Clear Working Directory",
20
+ description: TOOL_DESCRIPTION,
21
+ inputSchema: GitClearWorkingDirInputSchema.shape,
22
+ outputSchema: GitClearWorkingDirOutputSchema.shape,
23
+ annotations: {
24
+ readOnlyHint: true, // Modifies session state, but not external files
25
+ destructiveHint: false,
26
+ idempotentHint: true,
27
+ openWorldHint: false,
28
+ },
29
+ }, async (params, callContext) => {
30
+ const handlerContext = requestContextService.createRequestContext({
31
+ toolName: TOOL_NAME,
32
+ parentContext: callContext,
33
+ });
34
+ try {
35
+ const sessionId = getSessionId(handlerContext);
36
+ const result = await gitClearWorkingDirLogic(params, {
37
+ ...handlerContext,
38
+ clearWorkingDirectory: () => clearWorkingDirectory(sessionId),
41
39
  });
42
- const sessionId = _getSessionId(requestContext);
43
- // Define the session-specific clear function
44
- const clearWorkingDirectoryForSession = () => {
45
- _clearWorkingDirectory(sessionId);
46
- };
47
- const logicContext = {
48
- ...requestContext,
49
- sessionId: sessionId,
50
- clearWorkingDirectory: clearWorkingDirectoryForSession,
40
+ return {
41
+ structuredContent: result,
42
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
51
43
  };
52
- return await ErrorHandler.tryCatch(async () => {
53
- // Call the core logic function
54
- const result = await gitClearWorkingDirLogic(validatedArgs, logicContext);
55
- // Format the successful result
56
- const responseContent = {
57
- type: "text",
58
- text: JSON.stringify(result, null, 2),
59
- contentType: "application/json",
60
- };
61
- logger.info(`Tool ${TOOL_NAME} executed successfully`, {
62
- ...logicContext,
63
- result,
64
- });
65
- return { content: [responseContent] };
66
- }, {
67
- operation,
68
- context: logicContext,
69
- input: validatedArgs,
70
- errorCode: BaseErrorCode.INTERNAL_ERROR,
44
+ }
45
+ catch (error) {
46
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
47
+ const mcpError = ErrorHandler.handleError(error, {
48
+ operation: `tool:${TOOL_NAME}`,
49
+ context: handlerContext,
50
+ input: params,
71
51
  });
72
- });
73
- logger.info(`Tool registered: ${TOOL_NAME}`);
74
- }
75
- catch (error) {
76
- logger.error(`Failed to register tool: ${TOOL_NAME}`, {
77
- error: error instanceof Error ? error.message : String(error),
78
- stack: error instanceof Error ? error.stack : undefined,
79
- });
80
- throw error;
81
- }
82
- }
52
+ return {
53
+ isError: true,
54
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
55
+ structuredContent: {
56
+ code: mcpError.code,
57
+ message: mcpError.message,
58
+ details: mcpError.details,
59
+ },
60
+ };
61
+ }
62
+ });
63
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
64
+ };
@@ -1,7 +1,5 @@
1
1
  /**
2
- * @fileoverview Barrel file for the git_clone tool.
3
- * Exports the registration function.
2
+ * @fileoverview Barrel file for the gitClone tool.
3
+ * @module src/mcp-server/tools/gitClone/index
4
4
  */
5
5
  export { registerGitCloneTool } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitCloneInput, GitCloneResult } from './logic.js';
@@ -1,202 +1,62 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_clone tool.
3
+ * @module src/mcp-server/tools/gitClone/logic
4
+ */
1
5
  import { execFile } from "child_process";
2
6
  import fs from "fs/promises";
3
7
  import { promisify } from "util";
4
8
  import { z } from "zod";
5
- // Import utils from barrel (logger from ../utils/internal/logger.js)
6
- import { logger } from "../../../utils/index.js";
7
- // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
8
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
9
- // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
10
- import { sanitization } from "../../../utils/index.js";
9
+ import { logger, sanitization } from "../../../utils/index.js";
10
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
11
11
  const execFileAsync = promisify(execFile);
12
- // Define the input schema for the git_clone tool using Zod
12
+ // 1. DEFINE the Zod input schema.
13
13
  export const GitCloneInputSchema = z.object({
14
- repositoryUrl: z
15
- .string()
16
- .url("Invalid repository URL format.")
17
- .describe("The URL of the repository to clone (e.g., https://github.com/cyanheads/git-mcp-server, git@github.com:cyanheads/git-mcp-server.git)."),
18
- targetPath: z
19
- .string()
20
- .min(1)
21
- .describe("The absolute path to the directory where the repository should be cloned."),
22
- branch: z
23
- .string()
24
- .optional()
25
- .describe("Specify a specific branch to checkout after cloning."),
26
- depth: z
27
- .number()
28
- .int()
29
- .positive()
30
- .optional()
31
- .describe("Create a shallow clone with a history truncated to the specified number of commits."),
32
- // recursive: z.boolean().default(false).describe("After the clone is created, initialize all submodules within, using their default settings."), // Consider adding later
33
- quiet: z
34
- .boolean()
35
- .default(false)
36
- .describe("Operate quietly. Progress is not reported to the standard error stream."),
14
+ repositoryUrl: z.string().url("Invalid repository URL format.").describe("The URL of the repository to clone."),
15
+ targetPath: z.string().min(1).describe("The absolute path where the repository should be cloned."),
16
+ branch: z.string().optional().describe("The specific branch to checkout after cloning."),
17
+ depth: z.number().int().positive().optional().describe("Create a shallow clone with a truncated history."),
18
+ quiet: z.boolean().default(false).describe("Operate quietly, suppressing progress output."),
19
+ });
20
+ // 2. DEFINE the Zod response schema.
21
+ export const GitCloneOutputSchema = z.object({
22
+ success: z.boolean().describe("Indicates if the command was successful."),
23
+ message: z.string().describe("A summary message of the result."),
24
+ path: z.string().describe("The path where the repository was cloned."),
37
25
  });
38
26
  /**
39
- * Executes the 'git clone' command to clone a repository.
40
- *
41
- * @param {GitCloneInput} input - The validated input object.
42
- * @param {RequestContext} context - The request context for logging and error handling.
43
- * @returns {Promise<GitCloneResult>} A promise that resolves with the structured clone result.
44
- * @throws {McpError} Throws an McpError if path/URL validation fails or the git command fails unexpectedly.
27
+ * 4. IMPLEMENT the core logic function.
28
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
45
29
  */
46
- export async function gitCloneLogic(input, context) {
30
+ export async function gitCloneLogic(params, context) {
47
31
  const operation = "gitCloneLogic";
48
- logger.debug(`Executing ${operation}`, { ...context, input });
49
- let sanitizedTargetPath;
50
- let sanitizedRepoUrl;
51
- try {
52
- // Sanitize the target path (must be absolute)
53
- sanitizedTargetPath = sanitization.sanitizePath(input.targetPath, {
54
- allowAbsolute: true,
55
- }).sanitizedPath;
56
- logger.debug("Sanitized target path", {
57
- ...context,
58
- operation,
59
- sanitizedTargetPath,
60
- });
61
- // Basic sanitization/validation for URL (Zod already checks format)
62
- // Further sanitization might be needed depending on how it's used in the shell command
63
- // For now, rely on Zod's URL validation and careful command construction.
64
- sanitizedRepoUrl = input.repositoryUrl; // Assume Zod validation is sufficient for now
65
- logger.debug("Validated repository URL", {
66
- ...context,
67
- operation,
68
- sanitizedRepoUrl,
69
- });
70
- // Check if target directory already exists and is not empty
71
- try {
72
- const stats = await fs.stat(sanitizedTargetPath);
73
- if (stats.isDirectory()) {
74
- const files = await fs.readdir(sanitizedTargetPath);
75
- if (files.length > 0) {
76
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target directory already exists and is not empty: ${sanitizedTargetPath}`, { context, operation });
77
- }
78
- }
79
- else {
80
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target path exists but is not a directory: ${sanitizedTargetPath}`, { context, operation });
81
- }
82
- }
83
- catch (error) {
84
- if (error instanceof McpError)
85
- throw error; // Re-throw our specific validation errors
86
- if (error.code !== "ENOENT") {
87
- // If error is not "does not exist", it's unexpected
88
- logger.error(`Error checking target directory ${sanitizedTargetPath}`, {
89
- ...context,
90
- operation,
91
- error: error.message,
92
- });
93
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to check target directory: ${error.message}`, { context, operation });
32
+ logger.debug(`Executing ${operation}`, { ...context, params });
33
+ const sanitizedTargetPath = sanitization.sanitizePath(params.targetPath, { allowAbsolute: true }).sanitizedPath;
34
+ const stats = await fs.stat(sanitizedTargetPath).catch(err => {
35
+ if (err.code === 'ENOENT')
36
+ return null;
37
+ throw err;
38
+ });
39
+ if (stats) {
40
+ if (stats.isDirectory()) {
41
+ const files = await fs.readdir(sanitizedTargetPath);
42
+ if (files.length > 0) {
43
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target directory already exists and is not empty: ${sanitizedTargetPath}`);
94
44
  }
95
- // ENOENT is expected - directory doesn't exist, which is fine for clone
96
- logger.debug(`Target directory ${sanitizedTargetPath} does not exist, proceeding with clone.`, { ...context, operation });
97
- }
98
- }
99
- catch (error) {
100
- logger.error("Path/URL validation or sanitization failed", {
101
- ...context,
102
- operation,
103
- error,
104
- });
105
- if (error instanceof McpError)
106
- throw error;
107
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid input: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
108
- }
109
- try {
110
- // Construct the git clone command
111
- const args = ["clone"];
112
- if (input.quiet) {
113
- args.push("--quiet");
114
- }
115
- if (input.branch) {
116
- args.push("--branch", input.branch);
117
- }
118
- if (input.depth) {
119
- args.push("--depth", String(input.depth));
120
- }
121
- // Add repo URL and target path
122
- args.push(sanitizedRepoUrl, sanitizedTargetPath);
123
- logger.debug(`Executing command: git ${args.join(" ")}`, {
124
- ...context,
125
- operation,
126
- });
127
- // Increase timeout for clone operations as they can take time
128
- const { stdout, stderr } = await execFileAsync("git", args, {
129
- timeout: 300000,
130
- }); // 5 minutes timeout
131
- if (stderr && !input.quiet) {
132
- // Stderr often contains progress info, log as info if quiet is false
133
- logger.info(`Git clone command produced stderr (progress/info)`, {
134
- ...context,
135
- operation,
136
- stderr,
137
- });
138
- }
139
- if (stdout && !input.quiet) {
140
- logger.info(`Git clone command produced stdout`, {
141
- ...context,
142
- operation,
143
- stdout,
144
- });
145
- }
146
- // Verify the target directory exists after clone
147
- let repoDirExists = false;
148
- try {
149
- await fs.access(sanitizedTargetPath);
150
- repoDirExists = true;
151
- }
152
- catch (e) {
153
- logger.error(`Could not verify existence of target directory ${sanitizedTargetPath} after git clone`, { ...context, operation });
154
- // This indicates a potential failure despite exec not throwing
155
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Clone command finished but target directory ${sanitizedTargetPath} not found.`, { context, operation });
156
- }
157
- const successMessage = `Repository cloned successfully into ${sanitizedTargetPath}`;
158
- logger.info(successMessage, {
159
- ...context,
160
- operation,
161
- path: sanitizedTargetPath,
162
- });
163
- return {
164
- success: true,
165
- message: successMessage,
166
- path: sanitizedTargetPath,
167
- repoDirExists: repoDirExists,
168
- };
169
- }
170
- catch (error) {
171
- const errorMessage = error.stderr || error.message || "";
172
- logger.error(`Failed to execute git clone command`, {
173
- ...context,
174
- operation,
175
- path: sanitizedTargetPath,
176
- error: errorMessage,
177
- stderr: error.stderr,
178
- stdout: error.stdout,
179
- });
180
- // Handle specific error cases
181
- if (errorMessage.toLowerCase().includes("repository not found") ||
182
- errorMessage
183
- .toLowerCase()
184
- .includes("could not read from remote repository")) {
185
- throw new McpError(BaseErrorCode.NOT_FOUND, `Repository not found or access denied: ${sanitizedRepoUrl}. Error: ${errorMessage}`, { context, operation, originalError: error });
186
- }
187
- if (errorMessage
188
- .toLowerCase()
189
- .includes("already exists and is not an empty directory")) {
190
- // This should have been caught by our pre-check, but handle defensively
191
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target directory already exists and is not empty: ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
192
- }
193
- if (errorMessage.toLowerCase().includes("permission denied")) {
194
- throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied during clone operation for path: ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
195
45
  }
196
- if (errorMessage.toLowerCase().includes("timeout")) {
197
- throw new McpError(BaseErrorCode.TIMEOUT, `Git clone operation timed out for repository: ${sanitizedRepoUrl}. Error: ${errorMessage}`, { context, operation, originalError: error });
46
+ else {
47
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target path exists but is not a directory: ${sanitizedTargetPath}`);
198
48
  }
199
- // Generic internal error for other failures
200
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to clone repository ${sanitizedRepoUrl} to ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
201
49
  }
50
+ const args = ["clone"];
51
+ if (params.quiet)
52
+ args.push("--quiet");
53
+ if (params.branch)
54
+ args.push("--branch", params.branch);
55
+ if (params.depth)
56
+ args.push("--depth", String(params.depth));
57
+ args.push(params.repositoryUrl, sanitizedTargetPath);
58
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
59
+ await execFileAsync("git", args, { timeout: 300000 }); // 5 minutes timeout
60
+ const successMessage = `Repository cloned successfully into ${sanitizedTargetPath}`;
61
+ return { success: true, message: successMessage, path: sanitizedTargetPath };
202
62
  }