@cyanheads/git-mcp-server 2.1.0 → 2.1.2
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 +8 -11
- package/dist/config/index.js +7 -7
- package/dist/index.js +35 -21
- package/dist/mcp-server/server.js +72 -56
- package/dist/mcp-server/tools/gitAdd/index.js +1 -1
- package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
- package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
- package/dist/mcp-server/tools/gitBranch/index.js +1 -1
- package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
- package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
- package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
- package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
- package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
- package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
- package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
- package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
- package/dist/mcp-server/tools/gitClean/index.js +1 -1
- package/dist/mcp-server/tools/gitClean/logic.js +93 -41
- package/dist/mcp-server/tools/gitClean/registration.js +19 -16
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
- package/dist/mcp-server/tools/gitClone/index.js +1 -1
- package/dist/mcp-server/tools/gitClone/logic.js +89 -30
- package/dist/mcp-server/tools/gitClone/registration.js +15 -12
- package/dist/mcp-server/tools/gitCommit/index.js +1 -1
- package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
- package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
- package/dist/mcp-server/tools/gitDiff/index.js +1 -1
- package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
- package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
- package/dist/mcp-server/tools/gitFetch/index.js +1 -1
- package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
- package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
- package/dist/mcp-server/tools/gitInit/index.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +88 -34
- package/dist/mcp-server/tools/gitInit/registration.js +32 -18
- package/dist/mcp-server/tools/gitLog/index.js +1 -1
- package/dist/mcp-server/tools/gitLog/logic.js +133 -47
- package/dist/mcp-server/tools/gitLog/registration.js +16 -14
- package/dist/mcp-server/tools/gitMerge/index.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
- package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
- package/dist/mcp-server/tools/gitPull/index.js +1 -1
- package/dist/mcp-server/tools/gitPull/logic.js +90 -69
- package/dist/mcp-server/tools/gitPull/registration.js +16 -14
- package/dist/mcp-server/tools/gitPush/index.js +1 -1
- package/dist/mcp-server/tools/gitPush/logic.js +116 -100
- package/dist/mcp-server/tools/gitPush/registration.js +16 -14
- package/dist/mcp-server/tools/gitRebase/index.js +1 -1
- package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
- package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
- package/dist/mcp-server/tools/gitRemote/index.js +1 -1
- package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
- package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
- package/dist/mcp-server/tools/gitReset/index.js +1 -1
- package/dist/mcp-server/tools/gitReset/logic.js +65 -37
- package/dist/mcp-server/tools/gitReset/registration.js +14 -12
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
- package/dist/mcp-server/tools/gitShow/index.js +1 -1
- package/dist/mcp-server/tools/gitShow/logic.js +78 -35
- package/dist/mcp-server/tools/gitShow/registration.js +17 -12
- package/dist/mcp-server/tools/gitStash/index.js +1 -1
- package/dist/mcp-server/tools/gitStash/logic.js +143 -58
- package/dist/mcp-server/tools/gitStash/registration.js +19 -12
- package/dist/mcp-server/tools/gitStatus/index.js +1 -1
- package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
- package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
- package/dist/mcp-server/tools/gitTag/index.js +1 -1
- package/dist/mcp-server/tools/gitTag/logic.js +124 -51
- package/dist/mcp-server/tools/gitTag/registration.js +14 -11
- package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
- package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
- package/dist/mcp-server/transports/httpTransport.js +187 -79
- package/dist/mcp-server/transports/stdioTransport.js +14 -8
- package/dist/types-global/errors.js +9 -4
- package/dist/utils/index.js +4 -4
- package/dist/utils/internal/errorHandler.js +62 -40
- package/dist/utils/internal/index.js +3 -3
- package/dist/utils/internal/logger.js +97 -54
- package/dist/utils/internal/requestContext.js +7 -5
- package/dist/utils/metrics/index.js +1 -1
- package/dist/utils/metrics/tokenCounter.js +18 -14
- package/dist/utils/parsing/dateParser.js +5 -5
- package/dist/utils/parsing/index.js +2 -2
- package/dist/utils/parsing/jsonParser.js +20 -11
- package/dist/utils/security/idGenerator.js +8 -10
- package/dist/utils/security/index.js +3 -3
- package/dist/utils/security/rateLimiter.js +16 -14
- package/dist/utils/security/sanitization.js +139 -82
- package/package.json +45 -23
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
7
|
-
import { logger } from
|
|
7
|
+
import { logger } from "../../../utils/index.js";
|
|
8
8
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
9
|
-
import { BaseErrorCode, McpError } from
|
|
9
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
10
10
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
11
|
-
import { sanitization } from
|
|
11
|
+
import { sanitization } from "../../../utils/index.js";
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
// Define the input schema for the git_init tool using Zod
|
|
14
14
|
export const GitInitInputSchema = z.object({
|
|
15
|
-
path: z
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
path: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.optional()
|
|
19
|
+
.default(".")
|
|
20
|
+
.describe("Path where the new Git repository should be initialized. Can be relative or absolute. If relative or '.', it resolves against the directory set via `git_set_working_dir` for the session. If absolute, it's used directly. If omitted, defaults to '.' (resolved against session git working directory)."),
|
|
21
|
+
initialBranch: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."),
|
|
25
|
+
bare: z
|
|
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."),
|
|
19
33
|
});
|
|
20
34
|
/**
|
|
21
35
|
* Executes the 'git init' command to initialize a new Git repository.
|
|
@@ -26,13 +40,19 @@ export const GitInitInputSchema = z.object({
|
|
|
26
40
|
* @throws {McpError} Throws an McpError if path validation fails or the git command fails unexpectedly.
|
|
27
41
|
*/
|
|
28
42
|
export async function gitInitLogic(input, context) {
|
|
29
|
-
const operation =
|
|
43
|
+
const operation = "gitInitLogic";
|
|
30
44
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
31
45
|
let targetPath;
|
|
32
46
|
try {
|
|
33
47
|
// Sanitize the provided absolute path
|
|
34
|
-
targetPath = sanitization.sanitizePath(input.path, {
|
|
35
|
-
|
|
48
|
+
targetPath = sanitization.sanitizePath(input.path, {
|
|
49
|
+
allowAbsolute: true,
|
|
50
|
+
}).sanitizedPath;
|
|
51
|
+
logger.debug("Sanitized path", {
|
|
52
|
+
...context,
|
|
53
|
+
operation,
|
|
54
|
+
sanitizedPath: targetPath,
|
|
55
|
+
});
|
|
36
56
|
// Ensure the target directory exists before trying to init inside it
|
|
37
57
|
// git init creates the directory if it doesn't exist, but we might want to ensure the parent exists
|
|
38
58
|
const parentDir = path.dirname(targetPath);
|
|
@@ -40,15 +60,23 @@ export async function gitInitLogic(input, context) {
|
|
|
40
60
|
await fs.access(parentDir, fs.constants.W_OK); // Check write access in parent
|
|
41
61
|
}
|
|
42
62
|
catch (accessError) {
|
|
43
|
-
logger.error(`Parent directory check failed for ${targetPath}`, {
|
|
44
|
-
|
|
63
|
+
logger.error(`Parent directory check failed for ${targetPath}`, {
|
|
64
|
+
...context,
|
|
65
|
+
operation,
|
|
66
|
+
error: accessError.message,
|
|
67
|
+
});
|
|
68
|
+
if (accessError.code === "ENOENT") {
|
|
45
69
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Parent directory does not exist: ${parentDir}`, { context, operation });
|
|
46
70
|
}
|
|
47
71
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot access parent directory: ${parentDir}. Error: ${accessError.message}`, { context, operation });
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
catch (error) {
|
|
51
|
-
logger.error(
|
|
75
|
+
logger.error("Path validation or sanitization failed", {
|
|
76
|
+
...context,
|
|
77
|
+
operation,
|
|
78
|
+
error,
|
|
79
|
+
});
|
|
52
80
|
if (error instanceof McpError)
|
|
53
81
|
throw error;
|
|
54
82
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -57,13 +85,13 @@ export async function gitInitLogic(input, context) {
|
|
|
57
85
|
// Construct the git init command
|
|
58
86
|
let command = `git init`;
|
|
59
87
|
if (input.quiet) {
|
|
60
|
-
command +=
|
|
88
|
+
command += " --quiet";
|
|
61
89
|
}
|
|
62
90
|
if (input.bare) {
|
|
63
|
-
command +=
|
|
91
|
+
command += " --bare";
|
|
64
92
|
}
|
|
65
93
|
// Determine the initial branch name, defaulting to 'main' if not provided
|
|
66
|
-
const branchNameToUse = input.initialBranch ||
|
|
94
|
+
const branchNameToUse = input.initialBranch || "main";
|
|
67
95
|
command += ` -b "${branchNameToUse.replace(/"/g, '\\"')}"`;
|
|
68
96
|
// Add the target directory path at the end
|
|
69
97
|
command += ` "${targetPath}"`;
|
|
@@ -71,13 +99,22 @@ export async function gitInitLogic(input, context) {
|
|
|
71
99
|
const { stdout, stderr } = await execAsync(command);
|
|
72
100
|
if (stderr && !input.quiet) {
|
|
73
101
|
// Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
|
|
74
|
-
logger.warning(`Git init command produced stderr`, {
|
|
102
|
+
logger.warning(`Git init command produced stderr`, {
|
|
103
|
+
...context,
|
|
104
|
+
operation,
|
|
105
|
+
stderr,
|
|
106
|
+
});
|
|
75
107
|
}
|
|
76
108
|
if (stdout && !input.quiet) {
|
|
77
|
-
|
|
109
|
+
// Log stdout at debug level for cleaner info logs
|
|
110
|
+
logger.debug(`Git init command produced stdout`, {
|
|
111
|
+
...context,
|
|
112
|
+
operation,
|
|
113
|
+
stdout,
|
|
114
|
+
});
|
|
78
115
|
}
|
|
79
116
|
// Verify .git directory exists (or equivalent for bare repo)
|
|
80
|
-
const gitDirPath = input.bare ? targetPath : path.join(targetPath,
|
|
117
|
+
const gitDirPath = input.bare ? targetPath : path.join(targetPath, ".git");
|
|
81
118
|
let gitDirExists = false;
|
|
82
119
|
try {
|
|
83
120
|
await fs.access(gitDirPath);
|
|
@@ -86,30 +123,47 @@ export async function gitInitLogic(input, context) {
|
|
|
86
123
|
catch (e) {
|
|
87
124
|
logger.warning(`Could not verify existence of ${gitDirPath} after git init`, { ...context, operation });
|
|
88
125
|
}
|
|
89
|
-
const successMessage =
|
|
90
|
-
logger.info(
|
|
126
|
+
const successMessage = `Successfully initialized Git repository in ${targetPath}`;
|
|
127
|
+
logger.info(successMessage, {
|
|
128
|
+
...context,
|
|
129
|
+
operation,
|
|
130
|
+
path: targetPath,
|
|
131
|
+
bare: input.bare,
|
|
132
|
+
initialBranch: input.initialBranch || "default",
|
|
133
|
+
});
|
|
91
134
|
return {
|
|
92
135
|
success: true,
|
|
93
|
-
message: successMessage,
|
|
136
|
+
message: stdout.trim() || successMessage, // Return stdout to user if available
|
|
94
137
|
path: targetPath,
|
|
95
|
-
gitDirExists: gitDirExists
|
|
138
|
+
gitDirExists: gitDirExists,
|
|
96
139
|
};
|
|
97
140
|
}
|
|
98
141
|
catch (error) {
|
|
99
|
-
const errorMessage = error.stderr || error.message ||
|
|
100
|
-
logger.error(`Failed to execute git init command`, {
|
|
142
|
+
const errorMessage = error.stderr || error.message || "";
|
|
143
|
+
logger.error(`Failed to execute git init command`, {
|
|
144
|
+
...context,
|
|
145
|
+
operation,
|
|
146
|
+
path: targetPath,
|
|
147
|
+
error: errorMessage,
|
|
148
|
+
stderr: error.stderr,
|
|
149
|
+
stdout: error.stdout,
|
|
150
|
+
});
|
|
101
151
|
// Handle specific error cases
|
|
102
|
-
if (errorMessage.toLowerCase().includes(
|
|
152
|
+
if (errorMessage.toLowerCase().includes("already exists") &&
|
|
153
|
+
errorMessage.toLowerCase().includes("git repository")) {
|
|
103
154
|
// Reinitializing is often okay, treat as success but mention it.
|
|
104
|
-
logger.info(`Repository already exists, reinitialized: ${targetPath}`, {
|
|
155
|
+
logger.info(`Repository already exists, reinitialized: ${targetPath}`, {
|
|
156
|
+
...context,
|
|
157
|
+
operation,
|
|
158
|
+
});
|
|
105
159
|
return {
|
|
106
160
|
success: true, // Treat reinitialization as success
|
|
107
161
|
message: `Reinitialized existing Git repository in ${targetPath}`,
|
|
108
162
|
path: targetPath,
|
|
109
|
-
gitDirExists: true // Assume it exists if reinit message appears
|
|
163
|
+
gitDirExists: true, // Assume it exists if reinit message appears
|
|
110
164
|
};
|
|
111
165
|
}
|
|
112
|
-
if (errorMessage.toLowerCase().includes(
|
|
166
|
+
if (errorMessage.toLowerCase().includes("permission denied")) {
|
|
113
167
|
throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
114
168
|
}
|
|
115
169
|
// Generic internal error for other failures
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { z } from
|
|
3
|
-
import { BaseErrorCode, McpError } from
|
|
4
|
-
import { ErrorHandler, logger, requestContextService, sanitization } from
|
|
5
|
-
import { GitInitInputSchema, gitInitLogic } from
|
|
6
|
-
const TOOL_NAME =
|
|
7
|
-
const TOOL_DESCRIPTION =
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
4
|
+
import { ErrorHandler, logger, requestContextService, sanitization, } from "../../../utils/index.js";
|
|
5
|
+
import { GitInitInputSchema, gitInitLogic, } from "./logic.js";
|
|
6
|
+
const TOOL_NAME = "git_init";
|
|
7
|
+
const TOOL_DESCRIPTION = "Initializes a new Git repository at the specified path. If path is relative or omitted, it resolves against the session working directory (if you have set the git_working_dir). Can optionally set the initial branch name and create a bare repository.";
|
|
8
8
|
const RegistrationSchema = GitInitInputSchema.extend({
|
|
9
|
-
path: z.string().min(1).optional().default(
|
|
9
|
+
path: z.string().min(1).optional().default("."),
|
|
10
10
|
}).shape;
|
|
11
11
|
// --- Module-level State Accessors ---
|
|
12
12
|
// These will be populated by the initialize function called from server.ts
|
|
@@ -34,18 +34,22 @@ export function initializeGitInitStateAccessors(getWorkingDirectory, getSessionI
|
|
|
34
34
|
* @throws {Error} If registration fails.
|
|
35
35
|
*/
|
|
36
36
|
export const registerGitInitTool = async (server) => {
|
|
37
|
-
const operation =
|
|
37
|
+
const operation = "registerGitInitTool";
|
|
38
38
|
const context = requestContextService.createRequestContext({ operation });
|
|
39
39
|
await ErrorHandler.tryCatch(async () => {
|
|
40
40
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, RegistrationSchema, async (validatedArgs, callContext) => {
|
|
41
|
-
|
|
42
|
-
const
|
|
41
|
+
// Removed explicit type for callContext
|
|
42
|
+
const toolOperation = "tool:git_init";
|
|
43
|
+
const requestContext = requestContextService.createRequestContext({
|
|
44
|
+
operation: toolOperation,
|
|
45
|
+
parentContext: callContext,
|
|
46
|
+
});
|
|
43
47
|
// Use the initialized accessor to get the session ID
|
|
44
48
|
const sessionId = _getSessionIdFromContext(requestContext); // Pass the created context
|
|
45
49
|
if (!sessionId && !path.isAbsolute(validatedArgs.path)) {
|
|
46
50
|
// If path is relative, we NEED a session ID to resolve against a potential working dir
|
|
47
|
-
logger.error(
|
|
48
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR,
|
|
51
|
+
logger.error("Session ID is missing in context, cannot resolve relative path", requestContext);
|
|
52
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Session context is unavailable for relative path resolution.", { context: requestContext, operation: toolOperation });
|
|
49
53
|
}
|
|
50
54
|
logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
|
|
51
55
|
return await ErrorHandler.tryCatch(async () => {
|
|
@@ -55,7 +59,9 @@ export const registerGitInitTool = async (server) => {
|
|
|
55
59
|
let resolvedPath;
|
|
56
60
|
try {
|
|
57
61
|
if (path.isAbsolute(inputPath)) {
|
|
58
|
-
resolvedPath = sanitization.sanitizePath(inputPath, {
|
|
62
|
+
resolvedPath = sanitization.sanitizePath(inputPath, {
|
|
63
|
+
allowAbsolute: true,
|
|
64
|
+
}).sanitizedPath;
|
|
59
65
|
logger.debug(`Using absolute path: ${resolvedPath}`, requestContext);
|
|
60
66
|
}
|
|
61
67
|
else if (sessionWorkingDirectory) {
|
|
@@ -69,10 +75,18 @@ export const registerGitInitTool = async (server) => {
|
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
catch (error) {
|
|
72
|
-
logger.error(
|
|
78
|
+
logger.error("Path resolution or sanitization failed", {
|
|
79
|
+
...requestContext,
|
|
80
|
+
operation: toolOperation,
|
|
81
|
+
error,
|
|
82
|
+
});
|
|
73
83
|
if (error instanceof McpError)
|
|
74
84
|
throw error;
|
|
75
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path processing: ${error instanceof Error ? error.message : String(error)}`, {
|
|
85
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path processing: ${error instanceof Error ? error.message : String(error)}`, {
|
|
86
|
+
context: requestContext,
|
|
87
|
+
operation: toolOperation,
|
|
88
|
+
originalError: error,
|
|
89
|
+
});
|
|
76
90
|
}
|
|
77
91
|
const logicArgs = {
|
|
78
92
|
...validatedArgs,
|
|
@@ -80,9 +94,9 @@ export const registerGitInitTool = async (server) => {
|
|
|
80
94
|
};
|
|
81
95
|
const initResult = await gitInitLogic(logicArgs, requestContext);
|
|
82
96
|
const resultContent = {
|
|
83
|
-
type:
|
|
97
|
+
type: "text",
|
|
84
98
|
text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
|
|
85
|
-
contentType:
|
|
99
|
+
contentType: "application/json",
|
|
86
100
|
};
|
|
87
101
|
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
|
|
88
102
|
return { content: [resultContent] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitLog tool.
|
|
3
3
|
*/
|
|
4
|
-
export { registerGitLogTool, initializeGitLogStateAccessors } from
|
|
4
|
+
export { registerGitLogTool, initializeGitLogStateAccessors, } from "./registration.js";
|
|
5
5
|
// Export types if needed elsewhere, e.g.:
|
|
6
6
|
// export type { GitLogInput, GitLogResult } from './logic.js';
|
|
@@ -1,36 +1,66 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
7
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import { sanitization } from
|
|
9
|
+
import { sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
// Define the structure for a single commit entry
|
|
12
12
|
export const CommitEntrySchema = z.object({
|
|
13
13
|
hash: z.string().describe("Full commit hash"),
|
|
14
14
|
authorName: z.string().describe("Author's name"),
|
|
15
15
|
authorEmail: z.string().email().describe("Author's email"),
|
|
16
|
-
timestamp: z
|
|
16
|
+
timestamp: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.positive()
|
|
20
|
+
.describe("Commit timestamp (Unix epoch seconds)"),
|
|
17
21
|
subject: z.string().describe("Commit subject line"),
|
|
18
22
|
body: z.string().optional().describe("Commit body (optional)"),
|
|
19
23
|
});
|
|
20
24
|
// Define the input schema for the git_log tool using Zod
|
|
21
25
|
export const GitLogInputSchema = z.object({
|
|
22
|
-
path: z
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
path: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.optional()
|
|
30
|
+
.default(".")
|
|
31
|
+
.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."),
|
|
32
|
+
maxCount: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.positive()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Limit the number of commits to output."),
|
|
38
|
+
author: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Limit commits to those matching the specified author pattern."),
|
|
42
|
+
since: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
|
|
46
|
+
until: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Show commits older than a specific date."),
|
|
50
|
+
branchOrFile: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Show logs for a specific branch (e.g., 'main'), tag, or file path (e.g., 'src/utils/logger.ts')."),
|
|
54
|
+
showSignature: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.optional()
|
|
57
|
+
.default(false)
|
|
58
|
+
.describe("Show signature verification status for commits. Returns raw output instead of parsed JSON."),
|
|
29
59
|
// Note: We use a fixed pretty format for reliable parsing unless showSignature is true.
|
|
30
60
|
});
|
|
31
61
|
// Delimiters for parsing the custom format
|
|
32
|
-
const FIELD_SEP =
|
|
33
|
-
const RECORD_SEP =
|
|
62
|
+
const FIELD_SEP = "\x1f"; // Unit Separator
|
|
63
|
+
const RECORD_SEP = "\x1e"; // Record Separator
|
|
34
64
|
const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`; // %H=hash, %an=author name, %ae=author email, %at=timestamp, %s=subject, %b=body
|
|
35
65
|
/**
|
|
36
66
|
* Executes the 'git log' command with a specific format and returns structured JSON output.
|
|
@@ -41,12 +71,13 @@ const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_
|
|
|
41
71
|
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
42
72
|
*/
|
|
43
73
|
export async function logGitHistory(input, context) {
|
|
44
|
-
|
|
74
|
+
// Return type updated to the union
|
|
75
|
+
const operation = "logGitHistory";
|
|
45
76
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
46
77
|
let targetPath;
|
|
47
78
|
try {
|
|
48
79
|
// Resolve and sanitize the target path
|
|
49
|
-
if (input.path && input.path !==
|
|
80
|
+
if (input.path && input.path !== ".") {
|
|
50
81
|
targetPath = input.path;
|
|
51
82
|
}
|
|
52
83
|
else {
|
|
@@ -56,11 +87,21 @@ export async function logGitHistory(input, context) {
|
|
|
56
87
|
}
|
|
57
88
|
targetPath = workingDir;
|
|
58
89
|
}
|
|
59
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
60
|
-
|
|
90
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
91
|
+
allowAbsolute: true,
|
|
92
|
+
}).sanitizedPath;
|
|
93
|
+
logger.debug("Sanitized path", {
|
|
94
|
+
...context,
|
|
95
|
+
operation,
|
|
96
|
+
sanitizedPath: targetPath,
|
|
97
|
+
});
|
|
61
98
|
}
|
|
62
99
|
catch (error) {
|
|
63
|
-
logger.error(
|
|
100
|
+
logger.error("Path resolution or sanitization failed", {
|
|
101
|
+
...context,
|
|
102
|
+
operation,
|
|
103
|
+
error,
|
|
104
|
+
});
|
|
64
105
|
if (error instanceof McpError)
|
|
65
106
|
throw error;
|
|
66
107
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -71,18 +112,21 @@ export async function logGitHistory(input, context) {
|
|
|
71
112
|
if (input.showSignature) {
|
|
72
113
|
isRawOutput = true;
|
|
73
114
|
command = `git -C "${targetPath}" log --show-signature`;
|
|
74
|
-
logger.info(
|
|
115
|
+
logger.info("Show signature requested, returning raw output.", {
|
|
116
|
+
...context,
|
|
117
|
+
operation,
|
|
118
|
+
});
|
|
75
119
|
// Append other filters if provided
|
|
76
120
|
if (input.maxCount)
|
|
77
121
|
command += ` -n ${input.maxCount}`;
|
|
78
122
|
if (input.author)
|
|
79
|
-
command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g,
|
|
123
|
+
command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g, "")}"`;
|
|
80
124
|
if (input.since)
|
|
81
|
-
command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g,
|
|
125
|
+
command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g, "")}"`;
|
|
82
126
|
if (input.until)
|
|
83
|
-
command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g,
|
|
127
|
+
command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g, "")}"`;
|
|
84
128
|
if (input.branchOrFile)
|
|
85
|
-
command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g,
|
|
129
|
+
command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g, "")}`;
|
|
86
130
|
}
|
|
87
131
|
else {
|
|
88
132
|
// Construct the git log command with the fixed format for parsing
|
|
@@ -92,50 +136,63 @@ export async function logGitHistory(input, context) {
|
|
|
92
136
|
}
|
|
93
137
|
if (input.author) {
|
|
94
138
|
// Basic sanitization for author string
|
|
95
|
-
const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g,
|
|
139
|
+
const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g, "");
|
|
96
140
|
command += ` --author="${safeAuthor}"`;
|
|
97
141
|
}
|
|
98
142
|
if (input.since) {
|
|
99
|
-
const safeSince = input.since.replace(/[`"$&;*()|<>]/g,
|
|
143
|
+
const safeSince = input.since.replace(/[`"$&;*()|<>]/g, "");
|
|
100
144
|
command += ` --since="${safeSince}"`;
|
|
101
145
|
}
|
|
102
146
|
if (input.until) {
|
|
103
|
-
const safeUntil = input.until.replace(/[`"$&;*()|<>]/g,
|
|
147
|
+
const safeUntil = input.until.replace(/[`"$&;*()|<>]/g, "");
|
|
104
148
|
command += ` --until="${safeUntil}"`;
|
|
105
149
|
}
|
|
106
150
|
if (input.branchOrFile) {
|
|
107
|
-
const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g,
|
|
151
|
+
const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g, "");
|
|
108
152
|
command += ` ${safeBranchOrFile}`; // Add branch or file path at the end
|
|
109
153
|
}
|
|
110
154
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
111
155
|
// Increase maxBuffer if logs can be large
|
|
112
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
156
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
157
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
158
|
+
}); // 10MB buffer
|
|
113
159
|
if (stderr) {
|
|
114
160
|
// Log stderr as warning, as git log might sometimes use it for non-fatal info
|
|
115
161
|
// Exception: If showing signature, stderr about allowedSignersFile is expected, treat as info
|
|
116
|
-
if (isRawOutput &&
|
|
162
|
+
if (isRawOutput &&
|
|
163
|
+
stderr.includes("allowedSignersFile needs to be configured")) {
|
|
117
164
|
logger.info(`Git log stderr (signature verification note): ${stderr.trim()}`, { ...context, operation });
|
|
118
165
|
}
|
|
119
166
|
else {
|
|
120
|
-
logger.warning(`Git log stderr: ${stderr.trim()}`, {
|
|
167
|
+
logger.warning(`Git log stderr: ${stderr.trim()}`, {
|
|
168
|
+
...context,
|
|
169
|
+
operation,
|
|
170
|
+
});
|
|
121
171
|
}
|
|
122
172
|
}
|
|
123
173
|
// If raw output was requested, return it directly in the message field, omitting commits
|
|
124
174
|
if (isRawOutput) {
|
|
125
175
|
const message = `Raw log output (showSignature=true):\n${stdout}`;
|
|
126
|
-
logger.info(`${operation} completed successfully (raw output).`, {
|
|
176
|
+
logger.info(`${operation} completed successfully (raw output).`, {
|
|
177
|
+
...context,
|
|
178
|
+
operation,
|
|
179
|
+
path: targetPath,
|
|
180
|
+
});
|
|
127
181
|
// Return without the 'commits' or 'groupedCommits' field
|
|
128
182
|
return { success: true, message: message };
|
|
129
183
|
}
|
|
130
184
|
// --- Parse the structured output into a flat list first ---
|
|
131
185
|
const flatCommits = [];
|
|
132
|
-
const commitRecords = stdout
|
|
186
|
+
const commitRecords = stdout
|
|
187
|
+
.split(RECORD_SEP)
|
|
188
|
+
.filter((record) => record.trim() !== ""); // Split records and remove empty ones
|
|
133
189
|
for (const record of commitRecords) {
|
|
134
190
|
const trimmedRecord = record.trim(); // Trim leading/trailing whitespace (like newlines)
|
|
135
191
|
if (!trimmedRecord)
|
|
136
192
|
continue; // Skip empty records after trimming
|
|
137
193
|
const fields = trimmedRecord.split(FIELD_SEP); // Split the trimmed record
|
|
138
|
-
if (fields.length >= 5) {
|
|
194
|
+
if (fields.length >= 5) {
|
|
195
|
+
// Need at least hash, name, email, timestamp, subject
|
|
139
196
|
try {
|
|
140
197
|
const commitEntry = {
|
|
141
198
|
hash: fields[0],
|
|
@@ -150,7 +207,13 @@ export async function logGitHistory(input, context) {
|
|
|
150
207
|
flatCommits.push(commitEntry);
|
|
151
208
|
}
|
|
152
209
|
catch (parseError) {
|
|
153
|
-
logger.warning(`Failed to parse commit record field`, {
|
|
210
|
+
logger.warning(`Failed to parse commit record field`, {
|
|
211
|
+
...context,
|
|
212
|
+
operation,
|
|
213
|
+
fieldIndex: fields.findIndex((_, i) => i > 5),
|
|
214
|
+
recordFragment: record.substring(0, 100),
|
|
215
|
+
parseError,
|
|
216
|
+
});
|
|
154
217
|
// Decide whether to skip the commit or throw an error
|
|
155
218
|
}
|
|
156
219
|
}
|
|
@@ -182,28 +245,51 @@ export async function logGitHistory(input, context) {
|
|
|
182
245
|
const groupedCommits = Array.from(groupedCommitsMap.values());
|
|
183
246
|
// --- Prepare final result ---
|
|
184
247
|
const commitCount = flatCommits.length;
|
|
185
|
-
const message = commitCount > 0
|
|
186
|
-
|
|
248
|
+
const message = commitCount > 0
|
|
249
|
+
? `${commitCount} commit(s) found.`
|
|
250
|
+
: "No commits found matching criteria.";
|
|
251
|
+
logger.info(message, {
|
|
252
|
+
...context,
|
|
253
|
+
operation,
|
|
254
|
+
path: targetPath,
|
|
255
|
+
commitCount: commitCount,
|
|
256
|
+
authorGroupCount: groupedCommits.length,
|
|
257
|
+
});
|
|
187
258
|
return { success: true, groupedCommits, message }; // Return the grouped structure
|
|
188
259
|
}
|
|
189
260
|
catch (error) {
|
|
190
|
-
logger.error(`Failed to execute git log command`, {
|
|
191
|
-
|
|
261
|
+
logger.error(`Failed to execute git log command`, {
|
|
262
|
+
...context,
|
|
263
|
+
operation,
|
|
264
|
+
path: targetPath,
|
|
265
|
+
error: error.message,
|
|
266
|
+
stderr: error.stderr,
|
|
267
|
+
stdout: error.stdout,
|
|
268
|
+
});
|
|
269
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
192
270
|
// Handle specific error cases
|
|
193
|
-
if (errorMessage.toLowerCase().includes(
|
|
271
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
194
272
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
195
273
|
}
|
|
196
|
-
if (errorMessage.includes(
|
|
274
|
+
if (errorMessage.includes("fatal: bad revision")) {
|
|
197
275
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
198
276
|
}
|
|
199
|
-
if (errorMessage.includes(
|
|
277
|
+
if (errorMessage.includes("fatal: ambiguous argument")) {
|
|
200
278
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided (e.g., branch/tag/file conflict): '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
201
279
|
}
|
|
202
280
|
// Check if it's just that no commits were found
|
|
203
|
-
if (errorMessage.includes(
|
|
204
|
-
logger.info(
|
|
281
|
+
if (errorMessage.includes("does not have any commits yet")) {
|
|
282
|
+
logger.info("Repository has no commits yet.", {
|
|
283
|
+
...context,
|
|
284
|
+
operation,
|
|
285
|
+
path: targetPath,
|
|
286
|
+
});
|
|
205
287
|
// Return the grouped structure even for no commits
|
|
206
|
-
return {
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
groupedCommits: [],
|
|
291
|
+
message: "Repository has no commits yet.",
|
|
292
|
+
};
|
|
207
293
|
}
|
|
208
294
|
// Generic internal error for other failures
|
|
209
295
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git log for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|