@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,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitFetch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitFetch/index
|
|
3
4
|
*/
|
|
4
|
-
export { registerGitFetchTool
|
|
5
|
-
// Export types if needed elsewhere, e.g.:
|
|
6
|
-
// export type { GitFetchInput, GitFetchResult } from './logic.js';
|
|
5
|
+
export { registerGitFetchTool } from "./registration.js";
|
|
@@ -1,149 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_fetch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitFetch/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
|
-
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import { sanitization } from "../../../utils/index.js";
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
12
|
export const GitFetchInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
prune: z
|
|
24
|
-
.boolean()
|
|
25
|
-
.optional()
|
|
26
|
-
.default(false)
|
|
27
|
-
.describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
|
|
28
|
-
tags: z
|
|
29
|
-
.boolean()
|
|
30
|
-
.optional()
|
|
31
|
-
.default(false)
|
|
32
|
-
.describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
|
|
33
|
-
all: z.boolean().optional().default(false).describe("Fetch all remotes."),
|
|
34
|
-
// Add options like --depth, specific refspecs if needed
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
14
|
+
remote: z.string().optional().describe("The remote repository to fetch from (e.g., 'origin')."),
|
|
15
|
+
prune: z.boolean().default(false).describe("Remove remote-tracking references that no longer exist on the remote."),
|
|
16
|
+
tags: z.boolean().default(false).describe("Fetch all tags from the remote."),
|
|
17
|
+
all: z.boolean().default(false).describe("Fetch all remotes."),
|
|
18
|
+
});
|
|
19
|
+
// 2. DEFINE the Zod response schema.
|
|
20
|
+
export const GitFetchOutputSchema = z.object({
|
|
21
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
22
|
+
message: z.string().describe("A summary message of the result."),
|
|
35
23
|
});
|
|
36
24
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param {GitFetchInput} input - The validated input object.
|
|
40
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
41
|
-
* @returns {Promise<GitFetchResult>} A promise that resolves with the structured fetch result.
|
|
42
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
25
|
+
* 4. IMPLEMENT the core logic function.
|
|
26
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
43
27
|
*/
|
|
44
|
-
export async function fetchGitRemote(
|
|
28
|
+
export async function fetchGitRemote(params, context) {
|
|
45
29
|
const operation = "fetchGitRemote";
|
|
46
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (input.path && input.path !== ".") {
|
|
51
|
-
targetPath = input.path;
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
const workingDir = context.getWorkingDirectory();
|
|
55
|
-
if (!workingDir) {
|
|
56
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
57
|
-
}
|
|
58
|
-
targetPath = workingDir;
|
|
59
|
-
}
|
|
60
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
61
|
-
allowAbsolute: true,
|
|
62
|
-
}).sanitizedPath;
|
|
63
|
-
logger.debug("Sanitized path", {
|
|
64
|
-
...context,
|
|
65
|
-
operation,
|
|
66
|
-
sanitizedPath: targetPath,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
logger.error("Path resolution or sanitization failed", {
|
|
71
|
-
...context,
|
|
72
|
-
operation,
|
|
73
|
-
error,
|
|
74
|
-
});
|
|
75
|
-
if (error instanceof McpError)
|
|
76
|
-
throw error;
|
|
77
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
30
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
31
|
+
const workingDir = context.getWorkingDirectory();
|
|
32
|
+
if (params.path === "." && !workingDir) {
|
|
33
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
78
34
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
if (input.all) {
|
|
89
|
-
args.push("--all");
|
|
90
|
-
}
|
|
91
|
-
else if (input.remote) {
|
|
92
|
-
args.push(input.remote); // Fetch specific remote if 'all' is not used
|
|
93
|
-
}
|
|
94
|
-
// If neither 'all' nor 'remote' is specified, git fetch defaults to 'origin' or configured upstream.
|
|
95
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
96
|
-
...context,
|
|
97
|
-
operation,
|
|
98
|
-
});
|
|
99
|
-
// Execute command. Fetch output is primarily on stderr.
|
|
100
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
101
|
-
logger.debug(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
|
|
102
|
-
if (stderr) {
|
|
103
|
-
logger.debug(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
|
|
104
|
-
}
|
|
105
|
-
// Analyze stderr for success/summary
|
|
106
|
-
const message = "Fetch successful.";
|
|
107
|
-
const summary = stderr.trim() || "No changes detected.";
|
|
108
|
-
logger.info(message, {
|
|
109
|
-
...context,
|
|
110
|
-
operation,
|
|
111
|
-
path: targetPath,
|
|
112
|
-
summary,
|
|
113
|
-
});
|
|
114
|
-
return { success: true, message, summary };
|
|
35
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
36
|
+
const args = ["-C", targetPath, "fetch"];
|
|
37
|
+
if (params.prune)
|
|
38
|
+
args.push("--prune");
|
|
39
|
+
if (params.tags)
|
|
40
|
+
args.push("--tags");
|
|
41
|
+
if (params.all) {
|
|
42
|
+
args.push("--all");
|
|
115
43
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
...context,
|
|
119
|
-
operation,
|
|
120
|
-
path: targetPath,
|
|
121
|
-
error: error.message,
|
|
122
|
-
stderr: error.stderr,
|
|
123
|
-
stdout: error.stdout,
|
|
124
|
-
});
|
|
125
|
-
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
126
|
-
// Handle specific error cases
|
|
127
|
-
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
128
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
129
|
-
}
|
|
130
|
-
if (errorMessage.includes("resolve host") ||
|
|
131
|
-
errorMessage.includes("Could not read from remote repository") ||
|
|
132
|
-
errorMessage.includes("Connection timed out")) {
|
|
133
|
-
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
134
|
-
}
|
|
135
|
-
if (errorMessage.includes("fatal: ") &&
|
|
136
|
-
errorMessage.includes("couldn't find remote ref")) {
|
|
137
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
138
|
-
}
|
|
139
|
-
if (errorMessage.includes("Authentication failed") ||
|
|
140
|
-
errorMessage.includes("Permission denied")) {
|
|
141
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
142
|
-
}
|
|
143
|
-
if (errorMessage.includes("does not appear to be a git repository")) {
|
|
144
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || "default"}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
145
|
-
}
|
|
146
|
-
// Generic internal error for other failures
|
|
147
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git fetch for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
44
|
+
else if (params.remote) {
|
|
45
|
+
args.push(params.remote);
|
|
148
46
|
}
|
|
47
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
48
|
+
const { stderr } = await execFileAsync("git", args);
|
|
49
|
+
const message = stderr.trim() || "Fetch successful.";
|
|
50
|
+
return { success: true, message };
|
|
149
51
|
}
|
|
@@ -1,76 +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 { fetchGitRemote, GitFetchInputSchema, } from "./logic.js";
|
|
9
|
-
let _getWorkingDirectory;
|
|
10
|
-
let _getSessionId;
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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_fetch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitFetch/registration
|
|
16
4
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
_getSessionId = getSidFn;
|
|
20
|
-
logger.info("State accessors initialized for git_fetch tool registration.");
|
|
21
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { fetchGitRemote, GitFetchInputSchema, GitFetchOutputSchema, } from "./logic.js";
|
|
22
7
|
const TOOL_NAME = "git_fetch";
|
|
23
|
-
const TOOL_DESCRIPTION = "Downloads objects and refs from one or more
|
|
8
|
+
const TOOL_DESCRIPTION = "Downloads objects and refs from one or more repositories. Can fetch specific remotes or all, prune stale branches, and fetch tags.";
|
|
24
9
|
/**
|
|
25
|
-
* Registers the git_fetch tool with the MCP server.
|
|
26
|
-
*
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
10
|
+
* Registers the git_fetch 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.
|
|
29
14
|
*/
|
|
30
|
-
export async
|
|
31
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error("State accessors for git_fetch must be initialized before registration.");
|
|
33
|
-
}
|
|
15
|
+
export const registerGitFetchTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
34
16
|
const operation = "registerGitFetchTool";
|
|
35
17
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
server.registerTool(TOOL_NAME, {
|
|
19
|
+
title: "Git Fetch",
|
|
20
|
+
description: TOOL_DESCRIPTION,
|
|
21
|
+
inputSchema: GitFetchInputSchema.shape,
|
|
22
|
+
outputSchema: GitFetchOutputSchema.shape,
|
|
23
|
+
annotations: {
|
|
24
|
+
readOnlyHint: false, // Modifies local refs
|
|
25
|
+
destructiveHint: false,
|
|
26
|
+
idempotentHint: false,
|
|
27
|
+
openWorldHint: true, // Interacts with remote repositories
|
|
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 fetchGitRemote(params, {
|
|
37
|
+
...handlerContext,
|
|
38
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
39
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const logicContext = {
|
|
49
|
-
...requestContext,
|
|
50
|
-
sessionId: sessionId,
|
|
51
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
40
|
+
return {
|
|
41
|
+
structuredContent: result,
|
|
42
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
52
43
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Stringify the entire GitFetchResult object
|
|
61
|
-
text: JSON.stringify(fetchResult, null, 2), // Pretty-print JSON
|
|
62
|
-
contentType: "application/json",
|
|
63
|
-
};
|
|
64
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully: ${fetchResult.message}`, logicContext);
|
|
65
|
-
// Success is determined by the logic function and included in the result object
|
|
66
|
-
return { content: [resultContent] };
|
|
67
|
-
}, {
|
|
68
|
-
operation: toolOperation,
|
|
69
|
-
context: logicContext,
|
|
70
|
-
input: validatedArgs,
|
|
71
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
|
|
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,
|
|
72
51
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 gitInit tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitInit/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitInitTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitInitInput, GitInitResult } from './logic.js';
|
|
5
|
+
export { registerGitInitTool } from "./registration.js";
|
|
@@ -1,175 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_init tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitInit/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import fs from "fs/promises";
|
|
3
7
|
import path from "path";
|
|
4
8
|
import { promisify } from "util";
|
|
5
9
|
import { z } from "zod";
|
|
6
|
-
|
|
7
|
-
import { logger } from "../../../utils/index.js";
|
|
8
|
-
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
9
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
10
|
-
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
11
|
-
import { sanitization } from "../../../utils/index.js";
|
|
10
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
12
11
|
const execFileAsync = promisify(execFile);
|
|
13
|
-
//
|
|
12
|
+
// 1. DEFINE the Zod input schema.
|
|
14
13
|
export const GitInitInputSchema = z.object({
|
|
15
|
-
path: z
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.boolean()
|
|
27
|
-
.default(false)
|
|
28
|
-
.describe("Create a bare repository (no working directory)."),
|
|
29
|
-
quiet: z
|
|
30
|
-
.boolean()
|
|
31
|
-
.default(false)
|
|
32
|
-
.describe("Only print error and warning messages; all other output will be suppressed."),
|
|
14
|
+
path: z.string().default(".").describe("Path where the new Git repository should be initialized."),
|
|
15
|
+
initialBranch: z.string().optional().describe("The name for the initial branch (e.g., 'main')."),
|
|
16
|
+
bare: z.boolean().default(false).describe("Create a bare repository with no working directory."),
|
|
17
|
+
quiet: z.boolean().default(false).describe("Suppress all output except for errors and warnings."),
|
|
18
|
+
});
|
|
19
|
+
// 2. DEFINE the Zod response schema.
|
|
20
|
+
export const GitInitOutputSchema = z.object({
|
|
21
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
22
|
+
message: z.string().describe("A summary message of the result."),
|
|
23
|
+
path: z.string().describe("The path where the repository was initialized."),
|
|
24
|
+
gitDirExists: z.boolean().describe("Confirms the .git directory was created."),
|
|
33
25
|
});
|
|
34
26
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param {GitInitInput} input - The validated input object.
|
|
38
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
39
|
-
* @returns {Promise<GitInitResult>} A promise that resolves with the structured init result.
|
|
40
|
-
* @throws {McpError} Throws an McpError if path 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.
|
|
41
29
|
*/
|
|
42
|
-
export async function gitInitLogic(
|
|
30
|
+
export async function gitInitLogic(params, context) {
|
|
43
31
|
const operation = "gitInitLogic";
|
|
44
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await fs.access(parentDir, fs.constants.W_OK); // Check write access in parent
|
|
61
|
-
}
|
|
62
|
-
catch (accessError) {
|
|
63
|
-
logger.error(`Parent directory check failed for ${targetPath}`, {
|
|
64
|
-
...context,
|
|
65
|
-
operation,
|
|
66
|
-
error: accessError.message,
|
|
67
|
-
});
|
|
68
|
-
if (accessError.code === "ENOENT") {
|
|
69
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Parent directory does not exist: ${parentDir}`, { context, operation });
|
|
70
|
-
}
|
|
71
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot access parent directory: ${parentDir}. Error: ${accessError.message}`, { context, operation });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
logger.error("Path validation or sanitization failed", {
|
|
76
|
-
...context,
|
|
77
|
-
operation,
|
|
78
|
-
error,
|
|
79
|
-
});
|
|
80
|
-
if (error instanceof McpError)
|
|
81
|
-
throw error;
|
|
82
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
// Construct the git init command
|
|
86
|
-
const args = ["init"];
|
|
87
|
-
if (input.quiet) {
|
|
88
|
-
args.push("--quiet");
|
|
89
|
-
}
|
|
90
|
-
if (input.bare) {
|
|
91
|
-
args.push("--bare");
|
|
92
|
-
}
|
|
93
|
-
// Determine the initial branch name, defaulting to 'main' if not provided
|
|
94
|
-
const branchNameToUse = input.initialBranch || "main";
|
|
95
|
-
args.push("-b", branchNameToUse);
|
|
96
|
-
// Add the target directory path at the end
|
|
97
|
-
args.push(targetPath);
|
|
98
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
99
|
-
...context,
|
|
100
|
-
operation,
|
|
101
|
-
});
|
|
102
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
103
|
-
if (stderr && !input.quiet) {
|
|
104
|
-
// Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
|
|
105
|
-
logger.warning(`Git init command produced stderr`, {
|
|
106
|
-
...context,
|
|
107
|
-
operation,
|
|
108
|
-
stderr,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
if (stdout && !input.quiet) {
|
|
112
|
-
// Log stdout at debug level for cleaner info logs
|
|
113
|
-
logger.debug(`Git init command produced stdout`, {
|
|
114
|
-
...context,
|
|
115
|
-
operation,
|
|
116
|
-
stdout,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
// Verify .git directory exists (or equivalent for bare repo)
|
|
120
|
-
const gitDirPath = input.bare ? targetPath : path.join(targetPath, ".git");
|
|
121
|
-
let gitDirExists = false;
|
|
122
|
-
try {
|
|
123
|
-
await fs.access(gitDirPath);
|
|
124
|
-
gitDirExists = true;
|
|
125
|
-
}
|
|
126
|
-
catch (e) {
|
|
127
|
-
logger.warning(`Could not verify existence of ${gitDirPath} after git init`, { ...context, operation });
|
|
128
|
-
}
|
|
129
|
-
const successMessage = `Successfully initialized Git repository in ${targetPath}`;
|
|
130
|
-
logger.info(successMessage, {
|
|
131
|
-
...context,
|
|
132
|
-
operation,
|
|
133
|
-
path: targetPath,
|
|
134
|
-
bare: input.bare,
|
|
135
|
-
initialBranch: input.initialBranch || "default",
|
|
136
|
-
});
|
|
137
|
-
return {
|
|
138
|
-
success: true,
|
|
139
|
-
message: stdout.trim() || successMessage, // Return stdout to user if available
|
|
140
|
-
path: targetPath,
|
|
141
|
-
gitDirExists: gitDirExists,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
const errorMessage = error.stderr || error.message || "";
|
|
146
|
-
logger.error(`Failed to execute git init command`, {
|
|
147
|
-
...context,
|
|
148
|
-
operation,
|
|
149
|
-
path: targetPath,
|
|
150
|
-
error: errorMessage,
|
|
151
|
-
stderr: error.stderr,
|
|
152
|
-
stdout: error.stdout,
|
|
153
|
-
});
|
|
154
|
-
// Handle specific error cases
|
|
155
|
-
if (errorMessage.toLowerCase().includes("already exists") &&
|
|
156
|
-
errorMessage.toLowerCase().includes("git repository")) {
|
|
157
|
-
// Reinitializing is often okay, treat as success but mention it.
|
|
158
|
-
logger.info(`Repository already exists, reinitialized: ${targetPath}`, {
|
|
159
|
-
...context,
|
|
160
|
-
operation,
|
|
161
|
-
});
|
|
162
|
-
return {
|
|
163
|
-
success: true, // Treat reinitialization as success
|
|
164
|
-
message: `Reinitialized existing Git repository in ${targetPath}`,
|
|
165
|
-
path: targetPath,
|
|
166
|
-
gitDirExists: true, // Assume it exists if reinit message appears
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
if (errorMessage.toLowerCase().includes("permission denied")) {
|
|
170
|
-
throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
171
|
-
}
|
|
172
|
-
// Generic internal error for other failures
|
|
173
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
32
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
33
|
+
const targetPath = sanitization.sanitizePath(params.path, { allowAbsolute: true }).sanitizedPath;
|
|
34
|
+
const parentDir = path.dirname(targetPath);
|
|
35
|
+
await fs.access(parentDir, fs.constants.W_OK);
|
|
36
|
+
const args = ["init"];
|
|
37
|
+
if (params.quiet)
|
|
38
|
+
args.push("--quiet");
|
|
39
|
+
if (params.bare)
|
|
40
|
+
args.push("--bare");
|
|
41
|
+
args.push(`--initial-branch=${params.initialBranch || 'main'}`);
|
|
42
|
+
args.push(targetPath);
|
|
43
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
44
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
45
|
+
// Re-initializing is not an error, so we check for it and return a success response.
|
|
46
|
+
if ((stderr || stdout).toLowerCase().includes("reinitialized existing git repository")) {
|
|
47
|
+
return { success: true, message: `Reinitialized existing Git repository in ${targetPath}`, path: targetPath, gitDirExists: true };
|
|
174
48
|
}
|
|
49
|
+
const gitDirPath = params.bare ? targetPath : path.join(targetPath, ".git");
|
|
50
|
+
const gitDirExists = await fs.access(gitDirPath).then(() => true).catch(() => false);
|
|
51
|
+
const successMessage = stdout.trim() || `Successfully initialized Git repository in ${targetPath}`;
|
|
52
|
+
return { success: true, message: successMessage, path: targetPath, gitDirExists };
|
|
175
53
|
}
|