@cyanheads/git-mcp-server 2.1.8 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- 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 -122
- 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 +55 -162
- 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 +44 -143
- 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 +19 -26
- 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 +50 -171
- 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 +90 -295
- 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 +78 -254
- 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 +47 -129
- 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 +46 -152
- 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 +75 -257
- 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 +52 -179
- 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 +48 -146
- 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 +73 -181
- 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 +73 -202
- 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 +85 -193
- 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 +37 -121
- 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 +45 -133
- 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 +33 -122
- 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 +70 -214
- 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 +82 -229
- 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 +66 -188
- 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 +112 -322
- 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 +26 -38
- 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,284 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_log tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitLog/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
|
-
//
|
|
12
|
-
export const
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
|
+
export const GitLogInputSchema = z.object({
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
|
|
15
|
+
author: z.string().optional().describe("Limit commits to those by a specific author."),
|
|
16
|
+
since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago')."),
|
|
17
|
+
until: z.string().optional().describe("Show commits older than a specific date."),
|
|
18
|
+
branchOrFile: z.string().optional().describe("Show logs for a specific branch, tag, or file path."),
|
|
19
|
+
showSignature: z.boolean().default(false).describe("Show signature verification status for commits."),
|
|
20
|
+
});
|
|
21
|
+
// 2. DEFINE the Zod response schema.
|
|
22
|
+
const CommitEntrySchema = z.object({
|
|
13
23
|
hash: z.string().describe("Full commit hash"),
|
|
14
24
|
authorName: z.string().describe("Author's name"),
|
|
15
25
|
authorEmail: z.string().email().describe("Author's email"),
|
|
16
|
-
timestamp: z
|
|
17
|
-
.number()
|
|
18
|
-
.int()
|
|
19
|
-
.positive()
|
|
20
|
-
.describe("Commit timestamp (Unix epoch seconds)"),
|
|
26
|
+
timestamp: z.number().int().positive().describe("Commit timestamp (Unix epoch seconds)"),
|
|
21
27
|
subject: z.string().describe("Commit subject line"),
|
|
22
|
-
body: z.string().optional().describe("Commit body
|
|
28
|
+
body: z.string().optional().describe("Commit body"),
|
|
23
29
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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."),
|
|
59
|
-
// Note: We use a fixed pretty format for reliable parsing unless showSignature is true.
|
|
30
|
+
export const GitLogOutputSchema = z.object({
|
|
31
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
32
|
+
message: z.string().describe("A summary message of the result."),
|
|
33
|
+
commits: z.array(CommitEntrySchema).optional().describe("A list of commits."),
|
|
34
|
+
rawOutput: z.string().optional().describe("Raw output from the git log command, used when showSignature is true."),
|
|
60
35
|
});
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
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
|
|
36
|
+
const FIELD_SEP = "\x1f";
|
|
37
|
+
const RECORD_SEP = "\x1e";
|
|
38
|
+
const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`;
|
|
65
39
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* @param {GitLogInput} input - The validated input object.
|
|
69
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
70
|
-
* @returns {Promise<GitLogResult>} A promise that resolves with the structured log result (either flat or grouped).
|
|
71
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
40
|
+
* 4. IMPLEMENT the core logic function.
|
|
41
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
72
42
|
*/
|
|
73
|
-
export async function logGitHistory(
|
|
74
|
-
// Return type updated to the union
|
|
43
|
+
export async function logGitHistory(params, context) {
|
|
75
44
|
const operation = "logGitHistory";
|
|
76
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (input.path && input.path !== ".") {
|
|
81
|
-
targetPath = input.path;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
const workingDir = context.getWorkingDirectory();
|
|
85
|
-
if (!workingDir) {
|
|
86
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
87
|
-
}
|
|
88
|
-
targetPath = workingDir;
|
|
89
|
-
}
|
|
90
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
91
|
-
allowAbsolute: true,
|
|
92
|
-
}).sanitizedPath;
|
|
93
|
-
logger.debug("Sanitized path", {
|
|
94
|
-
...context,
|
|
95
|
-
operation,
|
|
96
|
-
sanitizedPath: targetPath,
|
|
97
|
-
});
|
|
45
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
46
|
+
const workingDir = context.getWorkingDirectory();
|
|
47
|
+
if (params.path === "." && !workingDir) {
|
|
48
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
98
49
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
throw error;
|
|
107
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
50
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
51
|
+
const args = ["-C", targetPath, "log"];
|
|
52
|
+
if (params.showSignature) {
|
|
53
|
+
args.push("--show-signature");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
args.push(GIT_LOG_FORMAT);
|
|
108
57
|
}
|
|
58
|
+
if (params.maxCount)
|
|
59
|
+
args.push(`-n${params.maxCount}`);
|
|
60
|
+
if (params.author)
|
|
61
|
+
args.push(`--author=${params.author}`);
|
|
62
|
+
if (params.since)
|
|
63
|
+
args.push(`--since=${params.since}`);
|
|
64
|
+
if (params.until)
|
|
65
|
+
args.push(`--until=${params.until}`);
|
|
66
|
+
if (params.branchOrFile)
|
|
67
|
+
args.push(params.branchOrFile);
|
|
109
68
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (input.author) {
|
|
127
|
-
args.push(`--author=${input.author}`);
|
|
128
|
-
}
|
|
129
|
-
if (input.since) {
|
|
130
|
-
args.push(`--since=${input.since}`);
|
|
131
|
-
}
|
|
132
|
-
if (input.until) {
|
|
133
|
-
args.push(`--until=${input.until}`);
|
|
134
|
-
}
|
|
135
|
-
if (input.branchOrFile) {
|
|
136
|
-
args.push(input.branchOrFile);
|
|
137
|
-
}
|
|
138
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
139
|
-
...context,
|
|
140
|
-
operation,
|
|
141
|
-
});
|
|
142
|
-
// Increase maxBuffer if logs can be large
|
|
143
|
-
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
144
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
145
|
-
}); // 10MB buffer
|
|
146
|
-
if (stderr) {
|
|
147
|
-
// Log stderr as warning, as git log might sometimes use it for non-fatal info
|
|
148
|
-
// Exception: If showing signature, stderr about allowedSignersFile is expected, treat as info
|
|
149
|
-
if (isRawOutput &&
|
|
150
|
-
stderr.includes("allowedSignersFile needs to be configured")) {
|
|
151
|
-
logger.info(`Git log stderr (signature verification note): ${stderr.trim()}`, { ...context, operation });
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
logger.warning(`Git log stderr: ${stderr.trim()}`, {
|
|
155
|
-
...context,
|
|
156
|
-
operation,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// If raw output was requested, return it directly in the message field, omitting commits
|
|
161
|
-
if (isRawOutput) {
|
|
162
|
-
const message = `Raw log output (showSignature=true):\n${stdout}`;
|
|
163
|
-
logger.info(`${operation} completed successfully (raw output).`, {
|
|
164
|
-
...context,
|
|
165
|
-
operation,
|
|
166
|
-
path: targetPath,
|
|
167
|
-
});
|
|
168
|
-
// Return without the 'commits' or 'groupedCommits' field
|
|
169
|
-
return { success: true, message: message };
|
|
170
|
-
}
|
|
171
|
-
// --- Parse the structured output into a flat list first ---
|
|
172
|
-
const flatCommits = [];
|
|
173
|
-
const commitRecords = stdout
|
|
174
|
-
.split(RECORD_SEP)
|
|
175
|
-
.filter((record) => record.trim() !== ""); // Split records and remove empty ones
|
|
176
|
-
for (const record of commitRecords) {
|
|
177
|
-
const trimmedRecord = record.trim(); // Trim leading/trailing whitespace (like newlines)
|
|
178
|
-
if (!trimmedRecord)
|
|
179
|
-
continue; // Skip empty records after trimming
|
|
180
|
-
const fields = trimmedRecord.split(FIELD_SEP); // Split the trimmed record
|
|
181
|
-
if (fields.length >= 5) {
|
|
182
|
-
// Need at least hash, name, email, timestamp, subject
|
|
183
|
-
try {
|
|
184
|
-
const commitEntry = {
|
|
185
|
-
hash: fields[0],
|
|
186
|
-
authorName: fields[1],
|
|
187
|
-
authorEmail: fields[2],
|
|
188
|
-
timestamp: parseInt(fields[3], 10), // Unix timestamp
|
|
189
|
-
subject: fields[4],
|
|
190
|
-
body: fields[5] || undefined, // Body might be empty
|
|
191
|
-
};
|
|
192
|
-
// Validate parsed entry
|
|
193
|
-
CommitEntrySchema.parse(commitEntry);
|
|
194
|
-
flatCommits.push(commitEntry);
|
|
195
|
-
}
|
|
196
|
-
catch (parseError) {
|
|
197
|
-
logger.warning(`Failed to parse commit record field`, {
|
|
198
|
-
...context,
|
|
199
|
-
operation,
|
|
200
|
-
fieldIndex: fields.findIndex((_, i) => i > 5),
|
|
201
|
-
recordFragment: record.substring(0, 100),
|
|
202
|
-
parseError,
|
|
203
|
-
});
|
|
204
|
-
// Decide whether to skip the commit or throw an error
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
logger.warning(`Skipping commit record due to unexpected number of fields (${fields.length})`, { ...context, operation, recordFragment: record.substring(0, 100) });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// --- Group the flat list by author ---
|
|
212
|
-
const groupedCommitsMap = new Map();
|
|
213
|
-
for (const commit of flatCommits) {
|
|
214
|
-
const authorKey = `${commit.authorName} <${commit.authorEmail}>`;
|
|
215
|
-
const groupedInfo = {
|
|
216
|
-
hash: commit.hash,
|
|
217
|
-
timestamp: commit.timestamp,
|
|
218
|
-
subject: commit.subject,
|
|
219
|
-
body: commit.body,
|
|
69
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
70
|
+
const { stdout } = await execFileAsync("git", args, { maxBuffer: 1024 * 1024 * 10 });
|
|
71
|
+
if (params.showSignature) {
|
|
72
|
+
return { success: true, message: "Raw log output with signature status.", rawOutput: stdout };
|
|
73
|
+
}
|
|
74
|
+
const commitRecords = stdout.split(RECORD_SEP).filter(r => r.trim());
|
|
75
|
+
const commits = commitRecords.map(record => {
|
|
76
|
+
const fields = record.trim().split(FIELD_SEP);
|
|
77
|
+
return {
|
|
78
|
+
hash: fields[0],
|
|
79
|
+
authorName: fields[1],
|
|
80
|
+
authorEmail: fields[2],
|
|
81
|
+
timestamp: parseInt(fields[3], 10),
|
|
82
|
+
subject: fields[4],
|
|
83
|
+
body: fields[5] || undefined,
|
|
220
84
|
};
|
|
221
|
-
if (groupedCommitsMap.has(authorKey)) {
|
|
222
|
-
groupedCommitsMap.get(authorKey).commits.push(groupedInfo);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
groupedCommitsMap.set(authorKey, {
|
|
226
|
-
authorName: commit.authorName,
|
|
227
|
-
authorEmail: commit.authorEmail,
|
|
228
|
-
commits: [groupedInfo],
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const groupedCommits = Array.from(groupedCommitsMap.values());
|
|
233
|
-
// --- Prepare final result ---
|
|
234
|
-
const commitCount = flatCommits.length;
|
|
235
|
-
const message = commitCount > 0
|
|
236
|
-
? `${commitCount} commit(s) found.`
|
|
237
|
-
: "No commits found matching criteria.";
|
|
238
|
-
logger.info(message, {
|
|
239
|
-
...context,
|
|
240
|
-
operation,
|
|
241
|
-
path: targetPath,
|
|
242
|
-
commitCount: commitCount,
|
|
243
|
-
authorGroupCount: groupedCommits.length,
|
|
244
85
|
});
|
|
245
|
-
return { success: true,
|
|
86
|
+
return { success: true, message: `Found ${commits.length} commit(s).`, commits };
|
|
246
87
|
}
|
|
247
88
|
catch (error) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
operation,
|
|
251
|
-
path: targetPath,
|
|
252
|
-
error: error.message,
|
|
253
|
-
stderr: error.stderr,
|
|
254
|
-
stdout: error.stdout,
|
|
255
|
-
});
|
|
256
|
-
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
257
|
-
// Handle specific error cases
|
|
89
|
+
const errorMessage = error.stderr || error.message || "";
|
|
90
|
+
logger.error(`Failed to execute git log command`, { ...context, operation, errorMessage });
|
|
258
91
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
259
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
92
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
260
93
|
}
|
|
261
|
-
if (errorMessage.includes("
|
|
262
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${
|
|
94
|
+
if (errorMessage.includes("bad revision")) {
|
|
95
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${params.branchOrFile}'.`);
|
|
263
96
|
}
|
|
264
|
-
if (errorMessage.includes("fatal: ambiguous argument")) {
|
|
265
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided (e.g., branch/tag/file conflict): '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
266
|
-
}
|
|
267
|
-
// Check if it's just that no commits were found
|
|
268
97
|
if (errorMessage.includes("does not have any commits yet")) {
|
|
269
|
-
|
|
270
|
-
...context,
|
|
271
|
-
operation,
|
|
272
|
-
path: targetPath,
|
|
273
|
-
});
|
|
274
|
-
// Return the grouped structure even for no commits
|
|
275
|
-
return {
|
|
276
|
-
success: true,
|
|
277
|
-
groupedCommits: [],
|
|
278
|
-
message: "Repository has no commits yet.",
|
|
279
|
-
};
|
|
98
|
+
return { success: true, message: "Repository has no commits yet.", commits: [] };
|
|
280
99
|
}
|
|
281
|
-
|
|
282
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git log for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
100
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git log failed: ${errorMessage}`);
|
|
283
101
|
}
|
|
284
102
|
}
|
|
@@ -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 { GitLogInputSchema, logGitHistory, } 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_log tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitLog/registration
|
|
16
4
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger.info("State accessors initialized for git_log tool registration.");
|
|
21
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { logGitHistory, GitLogInputSchema, GitLogOutputSchema, } from "./logic.js";
|
|
22
8
|
const TOOL_NAME = "git_log";
|
|
23
|
-
const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a JSON object containing a list of commit objects (`commits` array) by default. If `showSignature: true` is used, it returns a JSON object where the `commits` array is empty and the raw signature verification output is included in the `
|
|
9
|
+
const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a JSON object containing a list of commit objects (`commits` array) by default. If `showSignature: true` is used, it returns a JSON object where the `commits` array is empty and the raw signature verification output is included in the `rawOutput` field.";
|
|
24
10
|
/**
|
|
25
|
-
* Registers the git_log tool with the MCP server.
|
|
26
|
-
*
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
11
|
+
* Registers the git_log tool with the MCP server instance.
|
|
12
|
+
* @param server The MCP server instance.
|
|
13
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
14
|
+
* @param getSessionId Function to get the session ID from context.
|
|
29
15
|
*/
|
|
30
|
-
export async
|
|
31
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error("State accessors for git_log must be initialized before registration.");
|
|
33
|
-
}
|
|
16
|
+
export const registerGitLogTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
34
17
|
const operation = "registerGitLogTool";
|
|
35
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Log",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitLogInputSchema.shape,
|
|
23
|
+
outputSchema: GitLogOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
idempotentHint: true,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async (params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
toolName: TOOL_NAME,
|
|
33
|
+
parentContext: callContext,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const sessionId = getSessionId(handlerContext);
|
|
37
|
+
const result = await logGitHistory(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
40
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const logicContext = {
|
|
49
|
-
...requestContext,
|
|
50
|
-
sessionId: sessionId,
|
|
51
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
52
44
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Stringify the entire GitLogResult object which includes the commits array and success flag
|
|
61
|
-
text: JSON.stringify(logResult, null, 2), // Pretty-print JSON
|
|
62
|
-
contentType: "application/json",
|
|
63
|
-
};
|
|
64
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, 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
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
48
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
49
|
+
operation: `tool:${TOOL_NAME}`,
|
|
50
|
+
context: handlerContext,
|
|
51
|
+
input: params,
|
|
72
52
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
53
|
+
const mcpError = handledError instanceof McpError
|
|
54
|
+
? handledError
|
|
55
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
56
|
+
return {
|
|
57
|
+
isError: true,
|
|
58
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
59
|
+
structuredContent: mcpError.details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
64
|
+
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitMerge tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitMerge/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitMergeTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitMergeInput, GitMergeResult } from './logic.js';
|
|
5
|
+
export { registerGitMergeTool } from "./registration.js";
|