@cyanheads/git-mcp-server 2.0.11 → 2.0.14
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 +33 -29
- package/dist/mcp-server/server.js +29 -23
- package/dist/mcp-server/tools/gitDiff/logic.js +85 -20
- package/dist/mcp-server/tools/gitDiff/registration.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +43 -16
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/index.js +7 -0
- package/dist/mcp-server/tools/gitWorktree/logic.js +239 -0
- package/dist/mcp-server/tools/gitWorktree/registration.js +67 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +3 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +29 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +42 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +127 -105
- package/dist/mcp-server/transports/httpTransport.js +14 -0
- package/package.json +33 -21
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { logger, sanitization } from '../../../utils/index.js';
|
|
5
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
// Define the BASE input schema for the git_worktree tool using Zod
|
|
8
|
+
export const GitWorktreeBaseSchema = z.object({
|
|
9
|
+
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
10
|
+
mode: z.enum(['list', 'add', 'remove', 'move', 'prune']).describe("The worktree operation to perform: 'list', 'add', 'remove', 'move', 'prune'."),
|
|
11
|
+
// Common optional path for operations
|
|
12
|
+
worktreePath: z.string().min(1).optional().describe("Path of the worktree. Required for 'add', 'remove', 'move' modes."),
|
|
13
|
+
// 'add' mode specific
|
|
14
|
+
commitish: z.string().min(1).optional().describe("Branch or commit to checkout in the new worktree. Used only in 'add' mode. Defaults to HEAD."),
|
|
15
|
+
newBranch: z.string().min(1).optional().describe("Create a new branch in the worktree. Used only in 'add' mode."),
|
|
16
|
+
force: z.boolean().default(false).describe("Force the operation (e.g., for 'add' if branch exists, or 'remove' if uncommitted changes)."),
|
|
17
|
+
detach: z.boolean().default(false).describe("Detach HEAD in the new worktree. Used only in 'add' mode."),
|
|
18
|
+
// 'move' mode specific
|
|
19
|
+
newPath: z.string().min(1).optional().describe("The new path for the worktree. Required for 'move' mode."),
|
|
20
|
+
// 'prune' mode specific
|
|
21
|
+
verbose: z.boolean().default(false).describe("Provide more detailed output. Used in 'list' and 'prune' modes."),
|
|
22
|
+
dryRun: z.boolean().default(false).describe("Show what would be done without actually doing it. Used in 'prune' mode."),
|
|
23
|
+
expire: z.string().min(1).optional().describe("Prune entries older than this time (e.g., '1.month.ago'). Used in 'prune' mode."),
|
|
24
|
+
});
|
|
25
|
+
// Apply refinements and export the FINAL schema
|
|
26
|
+
export const GitWorktreeInputSchema = GitWorktreeBaseSchema.refine(data => !(data.mode === 'add' && !data.worktreePath), {
|
|
27
|
+
message: "A 'worktreePath' is required for 'add' mode.", path: ["worktreePath"],
|
|
28
|
+
}).refine(data => !(data.mode === 'remove' && !data.worktreePath), {
|
|
29
|
+
message: "A 'worktreePath' is required for 'remove' mode.", path: ["worktreePath"],
|
|
30
|
+
}).refine(data => !(data.mode === 'move' && (!data.worktreePath || !data.newPath)), {
|
|
31
|
+
message: "Both 'worktreePath' (old path) and 'newPath' are required for 'move' mode.", path: ["worktreePath", "newPath"],
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Parses the output of `git worktree list --porcelain`.
|
|
35
|
+
*/
|
|
36
|
+
function parsePorcelainWorktreeList(stdout) {
|
|
37
|
+
const worktrees = [];
|
|
38
|
+
const entries = stdout.trim().split('\n\n'); // Entries are separated by double newlines
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const lines = entry.trim().split('\n');
|
|
41
|
+
let path = '';
|
|
42
|
+
let head = '';
|
|
43
|
+
let branch;
|
|
44
|
+
let isBare = false;
|
|
45
|
+
let isLocked = false;
|
|
46
|
+
let isPrunable = false;
|
|
47
|
+
let prunableReason;
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (line.startsWith('worktree ')) {
|
|
50
|
+
path = line.substring('worktree '.length);
|
|
51
|
+
}
|
|
52
|
+
else if (line.startsWith('HEAD ')) {
|
|
53
|
+
head = line.substring('HEAD '.length);
|
|
54
|
+
}
|
|
55
|
+
else if (line.startsWith('branch ')) {
|
|
56
|
+
branch = line.substring('branch '.length);
|
|
57
|
+
}
|
|
58
|
+
else if (line.startsWith('bare')) {
|
|
59
|
+
isBare = true;
|
|
60
|
+
}
|
|
61
|
+
else if (line.startsWith('locked')) {
|
|
62
|
+
isLocked = true;
|
|
63
|
+
const reasonMatch = line.match(/locked(?: (.+))?/);
|
|
64
|
+
if (reasonMatch && reasonMatch[1]) {
|
|
65
|
+
prunableReason = reasonMatch[1]; // Using prunableReason for lock reason too
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (line.startsWith('prunable')) {
|
|
69
|
+
isPrunable = true;
|
|
70
|
+
const reasonMatch = line.match(/prunable(?: (.+))?/);
|
|
71
|
+
if (reasonMatch && reasonMatch[1]) {
|
|
72
|
+
prunableReason = reasonMatch[1];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (path) { // Only add if a path was found
|
|
77
|
+
worktrees.push({ path, head, branch, isBare, isLocked, isPrunable, prunableReason });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return worktrees;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Executes git worktree commands.
|
|
84
|
+
*/
|
|
85
|
+
export async function gitWorktreeLogic(input, context) {
|
|
86
|
+
const operation = `gitWorktreeLogic:${input.mode}`;
|
|
87
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
88
|
+
let targetPath;
|
|
89
|
+
try {
|
|
90
|
+
const workingDir = context.getWorkingDirectory();
|
|
91
|
+
targetPath = (input.path && input.path !== '.')
|
|
92
|
+
? input.path
|
|
93
|
+
: workingDir ?? '.';
|
|
94
|
+
if (targetPath === '.' && !workingDir) {
|
|
95
|
+
logger.warning("Executing git worktree in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
96
|
+
targetPath = process.cwd();
|
|
97
|
+
}
|
|
98
|
+
else if (targetPath === '.' && workingDir) {
|
|
99
|
+
targetPath = workingDir;
|
|
100
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
104
|
+
}
|
|
105
|
+
targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
109
|
+
if (error instanceof McpError)
|
|
110
|
+
throw error;
|
|
111
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
let command = `git -C "${targetPath}" worktree `;
|
|
115
|
+
let result;
|
|
116
|
+
switch (input.mode) {
|
|
117
|
+
case 'list':
|
|
118
|
+
command += 'list';
|
|
119
|
+
if (input.verbose)
|
|
120
|
+
command += ' --porcelain'; // Use porcelain for structured output
|
|
121
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
122
|
+
const { stdout: listStdout } = await execAsync(command);
|
|
123
|
+
if (input.verbose) {
|
|
124
|
+
const worktrees = parsePorcelainWorktreeList(listStdout);
|
|
125
|
+
result = { success: true, mode: 'list', worktrees };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Simple list output parsing (less structured)
|
|
129
|
+
const worktrees = listStdout.trim().split('\n').map(line => {
|
|
130
|
+
const parts = line.split(/\s+/);
|
|
131
|
+
return {
|
|
132
|
+
path: parts[0],
|
|
133
|
+
head: parts[1],
|
|
134
|
+
branch: parts[2]?.replace(/[\[\]]/g, ''), // Remove brackets from branch name
|
|
135
|
+
isBare: false, // Cannot determine from simple list
|
|
136
|
+
isLocked: false, // Cannot determine
|
|
137
|
+
isPrunable: false // Cannot determine
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
result = { success: true, mode: 'list', worktrees };
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case 'add':
|
|
144
|
+
// worktreePath is guaranteed by refine
|
|
145
|
+
const sanitizedWorktreePathAdd = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
146
|
+
command += `add `;
|
|
147
|
+
if (input.force)
|
|
148
|
+
command += '--force ';
|
|
149
|
+
if (input.detach)
|
|
150
|
+
command += '--detach ';
|
|
151
|
+
if (input.newBranch)
|
|
152
|
+
command += `-b "${input.newBranch}" `;
|
|
153
|
+
command += `"${sanitizedWorktreePathAdd}"`;
|
|
154
|
+
if (input.commitish)
|
|
155
|
+
command += ` "${input.commitish}"`;
|
|
156
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
157
|
+
await execAsync(command);
|
|
158
|
+
// To get the HEAD of the new worktree, we might need another command or parse output if available
|
|
159
|
+
// For simplicity, we'll report success. A more robust solution might `git -C new_worktree_path rev-parse HEAD`
|
|
160
|
+
result = {
|
|
161
|
+
success: true,
|
|
162
|
+
mode: 'add',
|
|
163
|
+
worktreePath: sanitizedWorktreePathAdd,
|
|
164
|
+
branch: input.newBranch,
|
|
165
|
+
head: 'HEAD', // Placeholder, actual SHA would require another call
|
|
166
|
+
message: `Worktree '${sanitizedWorktreePathAdd}' added successfully.`
|
|
167
|
+
};
|
|
168
|
+
break;
|
|
169
|
+
case 'remove':
|
|
170
|
+
// worktreePath is guaranteed by refine
|
|
171
|
+
const sanitizedWorktreePathRemove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
172
|
+
command += `remove `;
|
|
173
|
+
if (input.force)
|
|
174
|
+
command += '--force ';
|
|
175
|
+
command += `"${sanitizedWorktreePathRemove}"`;
|
|
176
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
177
|
+
const { stdout: removeStdout } = await execAsync(command);
|
|
178
|
+
result = { success: true, mode: 'remove', worktreePath: sanitizedWorktreePathRemove, message: removeStdout.trim() || `Worktree '${sanitizedWorktreePathRemove}' removed successfully.` };
|
|
179
|
+
break;
|
|
180
|
+
case 'move':
|
|
181
|
+
// worktreePath and newPath are guaranteed by refine
|
|
182
|
+
const sanitizedOldPathMove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
183
|
+
const sanitizedNewPathMove = sanitization.sanitizePath(input.newPath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
|
|
184
|
+
command += `move "${sanitizedOldPathMove}" "${sanitizedNewPathMove}"`;
|
|
185
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
186
|
+
await execAsync(command);
|
|
187
|
+
result = { success: true, mode: 'move', oldPath: sanitizedOldPathMove, newPath: sanitizedNewPathMove, message: `Worktree moved from '${sanitizedOldPathMove}' to '${sanitizedNewPathMove}' successfully.` };
|
|
188
|
+
break;
|
|
189
|
+
case 'prune':
|
|
190
|
+
command += 'prune ';
|
|
191
|
+
if (input.dryRun)
|
|
192
|
+
command += '--dry-run ';
|
|
193
|
+
if (input.verbose)
|
|
194
|
+
command += '--verbose ';
|
|
195
|
+
if (input.expire)
|
|
196
|
+
command += `--expire "${input.expire}" `;
|
|
197
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
198
|
+
const { stdout: pruneStdout, stderr: pruneStderr } = await execAsync(command);
|
|
199
|
+
// Prune often outputs to stderr even on success for verbose/dry-run
|
|
200
|
+
const pruneMessage = (pruneStdout.trim() || pruneStderr.trim()) || 'Worktree prune operation completed.';
|
|
201
|
+
result = { success: true, mode: 'prune', message: pruneMessage };
|
|
202
|
+
if (input.verbose && pruneStdout.trim()) {
|
|
203
|
+
// Attempt to parse verbose output if needed, for now just return raw message
|
|
204
|
+
// result.prunedItems = ...
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
209
|
+
}
|
|
210
|
+
logger.info(`${operation} executed successfully`, { ...context, path: targetPath });
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const errorMessage = error.stderr || error.stdout || (error.message || '');
|
|
215
|
+
logger.error(`Failed to execute git worktree command`, { ...context, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
216
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
217
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
218
|
+
}
|
|
219
|
+
// Add more specific error handling based on `git worktree` messages
|
|
220
|
+
if (input.mode === 'add' && errorMessage.includes('already exists')) {
|
|
221
|
+
return { success: false, mode: 'add', message: `Failed to add worktree: Path '${input.worktreePath}' already exists or is a worktree.`, error: errorMessage };
|
|
222
|
+
}
|
|
223
|
+
if (input.mode === 'add' && errorMessage.includes('is a submodule')) {
|
|
224
|
+
return { success: false, mode: 'add', message: `Failed to add worktree: Path '${input.worktreePath}' is a submodule.`, error: errorMessage };
|
|
225
|
+
}
|
|
226
|
+
if (input.mode === 'remove' && errorMessage.includes('cannot remove the current worktree')) {
|
|
227
|
+
return { success: false, mode: 'remove', message: `Failed to remove worktree: Cannot remove the current worktree.`, error: errorMessage };
|
|
228
|
+
}
|
|
229
|
+
if (input.mode === 'remove' && errorMessage.includes('has unclean changes')) {
|
|
230
|
+
return { success: false, mode: 'remove', message: `Failed to remove worktree: '${input.worktreePath}' has uncommitted changes. Use force=true to remove.`, error: errorMessage };
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
mode: input.mode,
|
|
235
|
+
message: `Git worktree ${input.mode} failed for path: ${targetPath}.`,
|
|
236
|
+
error: errorMessage
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { logger, ErrorHandler, requestContextService } from '../../../utils/index.js';
|
|
2
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
3
|
+
import { GitWorktreeBaseSchema, gitWorktreeLogic } from './logic.js';
|
|
4
|
+
let _getWorkingDirectory;
|
|
5
|
+
let _getSessionId;
|
|
6
|
+
/**
|
|
7
|
+
* Initializes the state accessors needed by the git_worktree tool registration.
|
|
8
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
9
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
10
|
+
*/
|
|
11
|
+
export function initializeGitWorktreeStateAccessors(getWdFn, getSidFn) {
|
|
12
|
+
_getWorkingDirectory = getWdFn;
|
|
13
|
+
_getSessionId = getSidFn;
|
|
14
|
+
logger.info('State accessors initialized for git_worktree tool registration.');
|
|
15
|
+
}
|
|
16
|
+
const TOOL_NAME = 'git_worktree';
|
|
17
|
+
const TOOL_DESCRIPTION = 'Manages Git worktrees. Supports listing, adding, removing, moving, and pruning worktrees. Returns results as a JSON object.';
|
|
18
|
+
/**
|
|
19
|
+
* Registers the git_worktree tool with the MCP server.
|
|
20
|
+
*
|
|
21
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
24
|
+
*/
|
|
25
|
+
export const registerGitWorktreeTool = async (server) => {
|
|
26
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
27
|
+
throw new Error('State accessors for git_worktree must be initialized before registration.');
|
|
28
|
+
}
|
|
29
|
+
const operation = 'registerGitWorktreeTool';
|
|
30
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
31
|
+
await ErrorHandler.tryCatch(async () => {
|
|
32
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitWorktreeBaseSchema.shape, async (validatedArgs, callContext) => {
|
|
33
|
+
const toolInput = validatedArgs; // Cast for use
|
|
34
|
+
const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`;
|
|
35
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
36
|
+
const sessionId = _getSessionId(requestContext);
|
|
37
|
+
const getWorkingDirectoryForSession = () => _getWorkingDirectory(sessionId);
|
|
38
|
+
const logicContext = {
|
|
39
|
+
...requestContext,
|
|
40
|
+
sessionId: sessionId,
|
|
41
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
42
|
+
};
|
|
43
|
+
logger.info(`Executing tool: ${TOOL_NAME} (mode: ${toolInput.mode})`, logicContext);
|
|
44
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
45
|
+
const worktreeResult = await gitWorktreeLogic(toolInput, logicContext);
|
|
46
|
+
const resultContent = {
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: JSON.stringify(worktreeResult, null, 2), // Pretty-print JSON
|
|
49
|
+
contentType: 'application/json',
|
|
50
|
+
};
|
|
51
|
+
if (worktreeResult.success) {
|
|
52
|
+
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${worktreeResult.message}`, { ...logicContext, errorDetails: worktreeResult.error });
|
|
56
|
+
}
|
|
57
|
+
return { content: [resultContent] };
|
|
58
|
+
}, {
|
|
59
|
+
operation: toolOperation,
|
|
60
|
+
context: logicContext,
|
|
61
|
+
input: validatedArgs,
|
|
62
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
66
|
+
}, { operation, context, critical: true });
|
|
67
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Define the input schema
|
|
3
|
+
export const GitWrapupInstructionsInputSchema = z.object({
|
|
4
|
+
acknowledgement: z.enum(['Y', 'y', 'Yes', 'yes'], {
|
|
5
|
+
required_error: 'Acknowledgement is required.',
|
|
6
|
+
description: 'Acknowledgement that you have permission (implicit allowed, explicit preferred) from the user to initiate this tool. Must be "Y" or "Yes" (case-insensitive).',
|
|
7
|
+
}),
|
|
8
|
+
updateAgentMetaFiles: z.enum(['Y', 'y', 'Yes', 'yes']).optional().describe("If set to 'Y' or 'Yes', include an extra instruction to review and update agent-specific meta files like .clinerules or claude.md if present. Only use this if the user explicitly requested it."),
|
|
9
|
+
});
|
|
10
|
+
// The predefined instructions string.
|
|
11
|
+
const WRAPUP_INSTRUCTIONS = `Initiate our standard git wrapup workflow. (1) First, review all changes to our repo using the git_diff tool to understand the precise nature and rationale behind each change (what changed and why did it change?). (2) For substantial code updates, review and update the README to ensure it is up to date with our current codebase (make a note to the user of any discrepancies you noticed, gathered from everything you've seen of our codebase). (3) Then, update the CHANGELOG with concise, descriptive entries detailing all modifications, clearly indicating their purpose (e.g., bug fix, feature implementation, refactoring). (4) Finally, proceed to commit all changes; group these changes into logical, atomic commits, each accompanied by a clear and descriptive message adhering to Conventional Commits standards (e.g. "docs(readme): updated readme to include xyz."). Note the 'git_commit' tool allows you to also stage the files while commiting. Ensure commit messages accurately convey the scope and impact of the changes, incorporating specific metrics or identifiers where applicable. Be sure to set 'git_set_working_dir' if not already set.`;
|
|
12
|
+
/**
|
|
13
|
+
* Core logic for the git_wrapup_instructions tool.
|
|
14
|
+
* This tool simply returns a predefined set of instructions, potentially augmented.
|
|
15
|
+
*
|
|
16
|
+
* @param {GitWrapupInstructionsInput} input - The validated input, may contain 'updateAgentMetaFiles'.
|
|
17
|
+
* @param {RequestContext} _context - The request context (included for consistency, not used in this simple logic).
|
|
18
|
+
* @returns {Promise<GitWrapupInstructionsResult>} A promise that resolves with the wrap-up instructions.
|
|
19
|
+
*/
|
|
20
|
+
export async function getWrapupInstructions(input, _context // Included for structural consistency, not used by this simple tool
|
|
21
|
+
) {
|
|
22
|
+
let finalInstructions = WRAPUP_INSTRUCTIONS;
|
|
23
|
+
if (input.updateAgentMetaFiles && ['Y', 'y', 'Yes', 'yes'].includes(input.updateAgentMetaFiles)) {
|
|
24
|
+
finalInstructions += ` Extra request: review and update if needed the .clinerules and claude.md files if present.`;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
instructions: finalInstructions,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
2
|
+
import { ErrorHandler, logger, requestContextService } from '../../../utils/index.js';
|
|
3
|
+
import { getWrapupInstructions, GitWrapupInstructionsInputSchema, } from './logic.js';
|
|
4
|
+
const TOOL_NAME = 'git_wrapup_instructions';
|
|
5
|
+
const TOOL_DESCRIPTION = 'Provides a standard Git wrap-up workflow. This involves reviewing changes with `git_diff`, updating documentation (README, CHANGELOG), and making logical, descriptive commits using the `git_commit` tool.';
|
|
6
|
+
/**
|
|
7
|
+
* Registers the git_wrapup_instructions tool with the MCP server.
|
|
8
|
+
*
|
|
9
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
* @throws {Error} If registration fails.
|
|
12
|
+
*/
|
|
13
|
+
export const registerGitWrapupInstructionsTool = async (server) => {
|
|
14
|
+
const operation = 'registerGitWrapupInstructionsTool';
|
|
15
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
16
|
+
await ErrorHandler.tryCatch(async () => {
|
|
17
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitWrapupInstructionsInputSchema.shape, // Empty schema shape
|
|
18
|
+
async (validatedArgs, callContext) => {
|
|
19
|
+
const toolOperation = 'tool:git_wrapup_instructions';
|
|
20
|
+
// Pass callContext as parentContext for consistent context chaining
|
|
21
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
22
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
|
|
23
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
24
|
+
const result = await getWrapupInstructions(validatedArgs, requestContext // Pass the created requestContext
|
|
25
|
+
);
|
|
26
|
+
const resultContent = {
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify(result, null, 2),
|
|
29
|
+
contentType: 'application/json',
|
|
30
|
+
};
|
|
31
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
|
|
32
|
+
return { content: [resultContent] };
|
|
33
|
+
}, {
|
|
34
|
+
operation: toolOperation,
|
|
35
|
+
context: requestContext,
|
|
36
|
+
input: validatedArgs,
|
|
37
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
41
|
+
}, { operation, context, critical: true });
|
|
42
|
+
};
|