@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.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
- package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +45 -154
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +47 -187
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +40 -162
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +71 -266
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +39 -156
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +65 -192
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +33 -133
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +28 -133
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +59 -219
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +57 -198
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- 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
|
-
*
|
|
14
|
-
* @
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
28
|
-
* @
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitClearWorkingDir tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitClearWorkingDir/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitClearWorkingDirTool
|
|
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
|
-
//
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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(
|
|
18
|
+
export async function gitClearWorkingDirLogic(params, context) {
|
|
17
19
|
const operation = "gitClearWorkingDirLogic";
|
|
18
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
28
|
-
* @
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
3
|
-
*
|
|
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
|
-
|
|
6
|
-
import {
|
|
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
|
-
//
|
|
12
|
+
// 1. DEFINE the Zod input schema.
|
|
13
13
|
export const GitCloneInputSchema = z.object({
|
|
14
|
-
repositoryUrl: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
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(
|
|
30
|
+
export async function gitCloneLogic(params, context) {
|
|
47
31
|
const operation = "gitCloneLogic";
|
|
48
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
197
|
-
throw new McpError(BaseErrorCode.
|
|
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
|
}
|