@cyanheads/git-mcp-server 1.2.4 → 2.0.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 +172 -285
- package/dist/config/index.js +69 -0
- package/dist/index.js +135 -0
- package/dist/mcp-server/server.js +572 -0
- package/dist/mcp-server/tools/gitAdd/index.js +7 -0
- package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
- package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
- package/dist/mcp-server/tools/gitBranch/index.js +7 -0
- package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
- package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
- package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
- package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
- package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
- package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
- package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
- package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
- package/dist/mcp-server/tools/gitClean/index.js +7 -0
- package/dist/mcp-server/tools/gitClean/logic.js +110 -0
- package/dist/mcp-server/tools/gitClean/registration.js +98 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
- package/dist/mcp-server/tools/gitClone/index.js +7 -0
- package/dist/mcp-server/tools/gitClone/logic.js +136 -0
- package/dist/mcp-server/tools/gitClone/registration.js +44 -0
- package/dist/mcp-server/tools/gitCommit/index.js +7 -0
- package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
- package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
- package/dist/mcp-server/tools/gitDiff/index.js +6 -0
- package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
- package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
- package/dist/mcp-server/tools/gitFetch/index.js +6 -0
- package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
- package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
- package/dist/mcp-server/tools/gitInit/index.js +7 -0
- package/dist/mcp-server/tools/gitInit/logic.js +117 -0
- package/dist/mcp-server/tools/gitInit/registration.js +44 -0
- package/dist/mcp-server/tools/gitLog/index.js +6 -0
- package/dist/mcp-server/tools/gitLog/logic.js +148 -0
- package/dist/mcp-server/tools/gitLog/registration.js +71 -0
- package/dist/mcp-server/tools/gitMerge/index.js +7 -0
- package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
- package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
- package/dist/mcp-server/tools/gitPull/index.js +6 -0
- package/dist/mcp-server/tools/gitPull/logic.js +144 -0
- package/dist/mcp-server/tools/gitPull/registration.js +81 -0
- package/dist/mcp-server/tools/gitPush/index.js +6 -0
- package/dist/mcp-server/tools/gitPush/logic.js +188 -0
- package/dist/mcp-server/tools/gitPush/registration.js +81 -0
- package/dist/mcp-server/tools/gitRebase/index.js +7 -0
- package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
- package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
- package/dist/mcp-server/tools/gitRemote/index.js +7 -0
- package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
- package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
- package/dist/mcp-server/tools/gitReset/index.js +6 -0
- package/dist/mcp-server/tools/gitReset/logic.js +116 -0
- package/dist/mcp-server/tools/gitReset/registration.js +71 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
- package/dist/mcp-server/tools/gitShow/index.js +7 -0
- package/dist/mcp-server/tools/gitShow/logic.js +99 -0
- package/dist/mcp-server/tools/gitShow/registration.js +83 -0
- package/dist/mcp-server/tools/gitStash/index.js +7 -0
- package/dist/mcp-server/tools/gitStash/logic.js +161 -0
- package/dist/mcp-server/tools/gitStash/registration.js +84 -0
- package/dist/mcp-server/tools/gitStatus/index.js +7 -0
- package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
- package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
- package/dist/mcp-server/tools/gitTag/index.js +7 -0
- package/dist/mcp-server/tools/gitTag/logic.js +142 -0
- package/dist/mcp-server/tools/gitTag/registration.js +84 -0
- package/dist/types-global/errors.js +68 -0
- package/dist/types-global/mcp.js +59 -0
- package/dist/types-global/tool.js +1 -0
- package/dist/utils/errorHandler.js +237 -0
- package/dist/utils/idGenerator.js +148 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/jsonParser.js +78 -0
- package/dist/utils/logger.js +266 -0
- package/dist/utils/rateLimiter.js +177 -0
- package/dist/utils/requestContext.js +49 -0
- package/dist/utils/sanitization.js +371 -0
- package/dist/utils/tokenCounter.js +124 -0
- package/package.json +62 -17
- package/build/index.js +0 -54
- package/build/resources/descriptors.js +0 -77
- package/build/resources/diff.js +0 -241
- package/build/resources/file.js +0 -222
- package/build/resources/history.js +0 -242
- package/build/resources/index.js +0 -99
- package/build/resources/repository.js +0 -286
- package/build/server.js +0 -120
- package/build/services/error-service.js +0 -73
- package/build/services/git-service.js +0 -965
- package/build/tools/advanced.js +0 -526
- package/build/tools/branch.js +0 -296
- package/build/tools/index.js +0 -29
- package/build/tools/remote.js +0 -279
- package/build/tools/repository.js +0 -170
- package/build/tools/workdir.js +0 -445
- package/build/types/git.js +0 -7
- package/build/utils/global-settings.js +0 -64
- package/build/utils/validation.js +0 -108
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Define the input schema for the git_show tool using Zod
|
|
9
|
+
// No refinements needed here, so we don't need a separate BaseSchema
|
|
10
|
+
export const GitShowInputSchema = z.object({
|
|
11
|
+
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. If omitted, defaults to the path set by `git_set_working_dir` for the current session, or the server's CWD if no session path is set."),
|
|
12
|
+
ref: z.string().min(1).describe("The object reference (commit hash, tag name, branch name, HEAD, etc.) to show."),
|
|
13
|
+
filePath: z.string().optional().describe("Optional specific file path within the ref to show (e.g., show a file's content at a specific commit). If provided, use the format '<ref>:<filePath>'."),
|
|
14
|
+
// format: z.string().optional().describe("Optional format string for the output"), // Consider adding later
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Executes the 'git show' command for a given reference and optional file path.
|
|
18
|
+
*
|
|
19
|
+
* @param {GitShowInput} input - The validated input object.
|
|
20
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
21
|
+
* @returns {Promise<GitShowResult>} A promise that resolves with the structured result.
|
|
22
|
+
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
23
|
+
*/
|
|
24
|
+
export async function gitShowLogic(input, context) {
|
|
25
|
+
const operation = 'gitShowLogic';
|
|
26
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
27
|
+
let targetPath;
|
|
28
|
+
try {
|
|
29
|
+
// Resolve and sanitize the target path
|
|
30
|
+
const workingDir = context.getWorkingDirectory();
|
|
31
|
+
targetPath = (input.path && input.path !== '.')
|
|
32
|
+
? input.path
|
|
33
|
+
: workingDir ?? '.';
|
|
34
|
+
if (targetPath === '.' && !workingDir) {
|
|
35
|
+
logger.warning("Executing git show in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
36
|
+
targetPath = process.cwd();
|
|
37
|
+
}
|
|
38
|
+
else if (targetPath === '.' && workingDir) {
|
|
39
|
+
targetPath = workingDir;
|
|
40
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
44
|
+
}
|
|
45
|
+
targetPath = sanitization.sanitizePath(targetPath);
|
|
46
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
50
|
+
if (error instanceof McpError)
|
|
51
|
+
throw error;
|
|
52
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
53
|
+
}
|
|
54
|
+
// Validate ref format (simple validation)
|
|
55
|
+
if (!/^[a-zA-Z0-9_./~^:-]+$/.test(input.ref)) { // Allow ':' for filePath combination
|
|
56
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid reference format: ${input.ref}`, { context, operation });
|
|
57
|
+
}
|
|
58
|
+
// Validate filePath format if provided (basic path chars)
|
|
59
|
+
if (input.filePath && !/^[a-zA-Z0-9_./-]+$/.test(input.filePath)) {
|
|
60
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid file path format: ${input.filePath}`, { context, operation });
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
// Construct the refspec, combining ref and filePath if needed
|
|
64
|
+
const refSpec = input.filePath ? `${input.ref}:"${input.filePath}"` : `"${input.ref}"`;
|
|
65
|
+
// Construct the command
|
|
66
|
+
const command = `git -C "${targetPath}" show ${refSpec}`;
|
|
67
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
68
|
+
// Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
|
|
69
|
+
// We primarily care about stdout for the content. Errors usually have non-zero exit code.
|
|
70
|
+
const { stdout, stderr } = await execAsync(command);
|
|
71
|
+
if (stderr) {
|
|
72
|
+
// Log stderr as debug info, as it might contain commit details etc.
|
|
73
|
+
logger.debug(`Git show command produced stderr (may be informational)`, { ...context, operation, stderr });
|
|
74
|
+
}
|
|
75
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, refSpec });
|
|
76
|
+
return { success: true, content: stdout }; // Return raw stdout content
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const errorMessage = error.stderr || error.message || '';
|
|
80
|
+
logger.error(`Failed to execute git show command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
81
|
+
// Specific error handling
|
|
82
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
83
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
84
|
+
}
|
|
85
|
+
if (/unknown revision or path not in the working tree/i.test(errorMessage)) {
|
|
86
|
+
const target = input.filePath ? `${input.ref}:${input.filePath}` : input.ref;
|
|
87
|
+
return { success: false, message: `Failed to show: Reference or pathspec '${target}' not found.`, error: errorMessage };
|
|
88
|
+
}
|
|
89
|
+
if (/ambiguous argument/i.test(errorMessage)) {
|
|
90
|
+
return { success: false, message: `Failed to show: Reference '${input.ref}' is ambiguous. Provide a more specific reference.`, error: errorMessage };
|
|
91
|
+
}
|
|
92
|
+
// Return structured failure for other git errors
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
message: `Git show failed for path: ${targetPath}, ref: ${input.ref}.`,
|
|
96
|
+
error: errorMessage
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { logger } from '../../../utils/logger.js';
|
|
2
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
// Import the schema and types
|
|
5
|
+
import { gitShowLogic, GitShowInputSchema } from './logic.js';
|
|
6
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
7
|
+
let _getWorkingDirectory;
|
|
8
|
+
let _getSessionId;
|
|
9
|
+
/**
|
|
10
|
+
* Initializes the state accessors needed by the git_show tool registration.
|
|
11
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
12
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
13
|
+
*/
|
|
14
|
+
export function initializeGitShowStateAccessors(getWdFn, getSidFn) {
|
|
15
|
+
_getWorkingDirectory = getWdFn;
|
|
16
|
+
_getSessionId = getSidFn;
|
|
17
|
+
logger.info('State accessors initialized for git_show tool registration.');
|
|
18
|
+
}
|
|
19
|
+
const TOOL_NAME = 'git_show';
|
|
20
|
+
const TOOL_DESCRIPTION = 'Shows information about Git objects (commits, tags, blobs, trees) based on a reference. Can optionally show the content of a specific file at that reference. Returns the raw output.';
|
|
21
|
+
/**
|
|
22
|
+
* Registers the git_show tool with the MCP server.
|
|
23
|
+
*
|
|
24
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
27
|
+
*/
|
|
28
|
+
export const registerGitShowTool = async (server) => {
|
|
29
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
30
|
+
throw new Error('State accessors for git_show must be initialized before registration.');
|
|
31
|
+
}
|
|
32
|
+
const operation = 'registerGitShowTool';
|
|
33
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
34
|
+
await ErrorHandler.tryCatch(async () => {
|
|
35
|
+
// Register the tool using the schema's shape (no refinements here)
|
|
36
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitShowInputSchema.shape, // Use the shape directly
|
|
37
|
+
// Let TypeScript infer handler argument types.
|
|
38
|
+
async (validatedArgs, callContext) => {
|
|
39
|
+
// Cast validatedArgs to the specific input type for use within the handler
|
|
40
|
+
const toolInput = validatedArgs;
|
|
41
|
+
const toolOperation = `tool:${TOOL_NAME}`;
|
|
42
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
43
|
+
const sessionId = _getSessionId(requestContext);
|
|
44
|
+
const getWorkingDirectoryForSession = () => {
|
|
45
|
+
return _getWorkingDirectory(sessionId);
|
|
46
|
+
};
|
|
47
|
+
const logicContext = {
|
|
48
|
+
...requestContext,
|
|
49
|
+
sessionId: sessionId,
|
|
50
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
51
|
+
};
|
|
52
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
53
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
54
|
+
// Call the core logic function which returns a GitShowResult object
|
|
55
|
+
const showResult = await gitShowLogic(toolInput, logicContext);
|
|
56
|
+
// Format the result within TextContent
|
|
57
|
+
const resultContent = {
|
|
58
|
+
type: 'text',
|
|
59
|
+
// Return raw content on success, or error message on failure
|
|
60
|
+
text: showResult.success ? showResult.content : `Error: ${showResult.message}${showResult.error ? `\nDetails: ${showResult.error}` : ''}`,
|
|
61
|
+
// Use plain text content type, unless we decide to return JSON later
|
|
62
|
+
contentType: 'text/plain',
|
|
63
|
+
};
|
|
64
|
+
// Log based on the success flag in the result
|
|
65
|
+
if (showResult.success) {
|
|
66
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully`, logicContext);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Log specific failure message from the result
|
|
70
|
+
logger.warning(`Tool ${TOOL_NAME} failed: ${showResult.message}`, { ...logicContext, errorDetails: showResult.error });
|
|
71
|
+
}
|
|
72
|
+
// Return the result, whether success or structured failure
|
|
73
|
+
return { content: [resultContent] };
|
|
74
|
+
}, {
|
|
75
|
+
operation: toolOperation,
|
|
76
|
+
context: logicContext,
|
|
77
|
+
input: validatedArgs, // Log the raw validated args
|
|
78
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
82
|
+
}, { operation, context, critical: true }); // Mark registration as critical
|
|
83
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the git_stash tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitStashTool, initializeGitStashStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitStashInput, GitStashResult } from './logic.js';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Define the BASE input schema for the git_stash tool using Zod
|
|
9
|
+
export const GitStashBaseSchema = z.object({
|
|
10
|
+
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. If omitted, defaults to the path set by `git_set_working_dir` for the current session, or the server's CWD if no session path is set."),
|
|
11
|
+
mode: z.enum(['list', 'apply', 'pop', 'drop', 'save']).describe("The stash operation to perform: 'list', 'apply', 'pop', 'drop', 'save'."),
|
|
12
|
+
stashRef: z.string().optional().describe("Stash reference (e.g., 'stash@{1}'). Required for 'apply', 'pop', 'drop' modes."),
|
|
13
|
+
message: z.string().optional().describe("Optional descriptive message used only for 'save' mode."),
|
|
14
|
+
// includeUntracked: z.boolean().default(false).describe("Include untracked files in 'save' mode (-u)"), // Consider adding later
|
|
15
|
+
// keepIndex: z.boolean().default(false).describe("Keep staged changes in 'save' mode (--keep-index)"), // Consider adding later
|
|
16
|
+
});
|
|
17
|
+
// Apply refinements and export the FINAL schema for validation within the handler
|
|
18
|
+
export const GitStashInputSchema = GitStashBaseSchema.refine(data => !(['apply', 'pop', 'drop'].includes(data.mode) && !data.stashRef), {
|
|
19
|
+
message: "A 'stashRef' (e.g., 'stash@{0}') is required for 'apply', 'pop', and 'drop' modes.",
|
|
20
|
+
path: ["stashRef"], // Point error to the stashRef field
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Executes git stash commands based on the specified mode.
|
|
24
|
+
*
|
|
25
|
+
* @param {GitStashInput} input - The validated input object (validated against GitStashInputSchema).
|
|
26
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
27
|
+
* @returns {Promise<GitStashResult>} A promise that resolves with the structured result.
|
|
28
|
+
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
29
|
+
*/
|
|
30
|
+
export async function gitStashLogic(input, context) {
|
|
31
|
+
const operation = `gitStashLogic:${input.mode}`;
|
|
32
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
33
|
+
let targetPath;
|
|
34
|
+
try {
|
|
35
|
+
// Resolve and sanitize the target path
|
|
36
|
+
const workingDir = context.getWorkingDirectory();
|
|
37
|
+
targetPath = (input.path && input.path !== '.')
|
|
38
|
+
? input.path
|
|
39
|
+
: workingDir ?? '.';
|
|
40
|
+
if (targetPath === '.' && !workingDir) {
|
|
41
|
+
logger.warning("Executing git stash in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
42
|
+
targetPath = process.cwd();
|
|
43
|
+
}
|
|
44
|
+
else if (targetPath === '.' && workingDir) {
|
|
45
|
+
targetPath = workingDir;
|
|
46
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
50
|
+
}
|
|
51
|
+
targetPath = sanitization.sanitizePath(targetPath);
|
|
52
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
56
|
+
if (error instanceof McpError)
|
|
57
|
+
throw error;
|
|
58
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
59
|
+
}
|
|
60
|
+
// Validate stashRef format if provided (simple validation)
|
|
61
|
+
if (input.stashRef && !/^stash@\{[0-9]+\}$/.test(input.stashRef)) {
|
|
62
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid stash reference format: ${input.stashRef}. Expected format: stash@{n}`, { context, operation });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
let command;
|
|
66
|
+
let result;
|
|
67
|
+
switch (input.mode) {
|
|
68
|
+
case 'list':
|
|
69
|
+
command = `git -C "${targetPath}" stash list`;
|
|
70
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
71
|
+
const { stdout: listStdout } = await execAsync(command);
|
|
72
|
+
const stashes = listStdout.trim().split('\n')
|
|
73
|
+
.filter(line => line)
|
|
74
|
+
.map(line => {
|
|
75
|
+
// Improved regex to handle different stash list formats
|
|
76
|
+
const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
|
|
77
|
+
return match
|
|
78
|
+
? { ref: match[1], branch: match[3] || 'unknown', description: match[4] }
|
|
79
|
+
: { ref: 'unknown', branch: 'unknown', description: line }; // Fallback parsing
|
|
80
|
+
});
|
|
81
|
+
result = { success: true, mode: 'list', stashes };
|
|
82
|
+
break;
|
|
83
|
+
case 'apply':
|
|
84
|
+
case 'pop':
|
|
85
|
+
// stashRef is validated by Zod refine
|
|
86
|
+
const stashRefApplyPop = input.stashRef;
|
|
87
|
+
command = `git -C "${targetPath}" stash ${input.mode} ${stashRefApplyPop}`;
|
|
88
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
89
|
+
try {
|
|
90
|
+
const { stdout, stderr } = await execAsync(command);
|
|
91
|
+
// Check stdout/stderr for conflict messages, although exit code 0 usually means success
|
|
92
|
+
const conflicts = /conflict/i.test(stdout) || /conflict/i.test(stderr);
|
|
93
|
+
const message = conflicts
|
|
94
|
+
? `Stash ${input.mode} resulted in conflicts that need manual resolution.`
|
|
95
|
+
: `Stash ${stashRefApplyPop} ${input.mode === 'apply' ? 'applied' : 'popped'} successfully.`;
|
|
96
|
+
logger.info(message, { ...context, operation, path: targetPath, conflicts });
|
|
97
|
+
result = { success: true, mode: input.mode, message, conflicts };
|
|
98
|
+
}
|
|
99
|
+
catch (applyError) {
|
|
100
|
+
const applyErrorMessage = applyError.stderr || applyError.message || '';
|
|
101
|
+
if (/conflict/i.test(applyErrorMessage)) {
|
|
102
|
+
logger.warning(`Stash ${input.mode} failed due to conflicts.`, { ...context, operation, path: targetPath, error: applyErrorMessage });
|
|
103
|
+
// Return failure but indicate conflicts
|
|
104
|
+
return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash ${stashRefApplyPop} due to conflicts. Resolve conflicts manually.`, error: applyErrorMessage, conflicts: true };
|
|
105
|
+
}
|
|
106
|
+
// Rethrow other errors
|
|
107
|
+
throw applyError;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'drop':
|
|
111
|
+
// stashRef is validated by Zod refine
|
|
112
|
+
const stashRefDrop = input.stashRef;
|
|
113
|
+
command = `git -C "${targetPath}" stash drop ${stashRefDrop}`;
|
|
114
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
115
|
+
await execAsync(command);
|
|
116
|
+
result = { success: true, mode: 'drop', message: `Dropped ${stashRefDrop} successfully.`, stashRef: stashRefDrop };
|
|
117
|
+
break;
|
|
118
|
+
case 'save':
|
|
119
|
+
command = `git -C "${targetPath}" stash save`;
|
|
120
|
+
if (input.message) {
|
|
121
|
+
// Ensure message is properly quoted for the shell
|
|
122
|
+
command += ` "${input.message.replace(/"/g, '\\"')}"`;
|
|
123
|
+
}
|
|
124
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
125
|
+
const { stdout: saveStdout } = await execAsync(command);
|
|
126
|
+
const stashCreated = !/no local changes to save/i.test(saveStdout);
|
|
127
|
+
const saveMessage = stashCreated
|
|
128
|
+
? `Changes stashed successfully.` + (input.message ? ` Message: "${input.message}"` : '')
|
|
129
|
+
: "No local changes to save.";
|
|
130
|
+
result = { success: true, mode: 'save', message: saveMessage, stashCreated };
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
// Should not happen due to Zod validation
|
|
134
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
135
|
+
}
|
|
136
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const errorMessage = error.stderr || error.message || '';
|
|
141
|
+
logger.error(`Failed to execute git stash command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
142
|
+
// Specific error handling
|
|
143
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
144
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
145
|
+
}
|
|
146
|
+
if ((input.mode === 'apply' || input.mode === 'pop' || input.mode === 'drop') && /no such stash/i.test(errorMessage)) {
|
|
147
|
+
return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash: Stash '${input.stashRef}' not found.`, error: errorMessage };
|
|
148
|
+
}
|
|
149
|
+
if ((input.mode === 'apply' || input.mode === 'pop') && /conflict/i.test(errorMessage)) {
|
|
150
|
+
// This case might be caught above, but double-check here
|
|
151
|
+
return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash '${input.stashRef}' due to conflicts. Resolve conflicts manually.`, error: errorMessage, conflicts: true };
|
|
152
|
+
}
|
|
153
|
+
// Return structured failure for other git errors
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
mode: input.mode,
|
|
157
|
+
message: `Git stash ${input.mode} failed for path: ${targetPath}.`,
|
|
158
|
+
error: errorMessage
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { logger } from '../../../utils/logger.js';
|
|
2
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
// Import the final schema and types for handler logic
|
|
5
|
+
// Import the BASE schema separately for registration shape
|
|
6
|
+
import { gitStashLogic, GitStashBaseSchema } from './logic.js';
|
|
7
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
8
|
+
let _getWorkingDirectory;
|
|
9
|
+
let _getSessionId;
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the state accessors needed by the git_stash tool registration.
|
|
12
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
13
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
14
|
+
*/
|
|
15
|
+
export function initializeGitStashStateAccessors(getWdFn, getSidFn) {
|
|
16
|
+
_getWorkingDirectory = getWdFn;
|
|
17
|
+
_getSessionId = getSidFn;
|
|
18
|
+
logger.info('State accessors initialized for git_stash tool registration.');
|
|
19
|
+
}
|
|
20
|
+
const TOOL_NAME = 'git_stash';
|
|
21
|
+
const TOOL_DESCRIPTION = 'Manages stashed changes in the working directory. Supports listing stashes, applying/popping specific stashes (with conflict detection), dropping stashes, and saving current changes to a new stash with an optional message. Returns results as a JSON object.';
|
|
22
|
+
/**
|
|
23
|
+
* Registers the git_stash tool with the MCP server.
|
|
24
|
+
*
|
|
25
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
28
|
+
*/
|
|
29
|
+
export const registerGitStashTool = async (server) => {
|
|
30
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
31
|
+
throw new Error('State accessors for git_stash must be initialized before registration.');
|
|
32
|
+
}
|
|
33
|
+
const operation = 'registerGitStashTool';
|
|
34
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
35
|
+
await ErrorHandler.tryCatch(async () => {
|
|
36
|
+
// Register the tool using the *base* schema's shape for definition
|
|
37
|
+
server.tool(// Use BASE schema shape here
|
|
38
|
+
TOOL_NAME, TOOL_DESCRIPTION, GitStashBaseSchema.shape, // Use the shape from the BASE schema
|
|
39
|
+
// Let TypeScript infer handler argument types.
|
|
40
|
+
// The SDK validates against the full GitStashInputSchema before calling this.
|
|
41
|
+
async (validatedArgs, callContext) => {
|
|
42
|
+
// Cast validatedArgs to the specific input type for use within the handler
|
|
43
|
+
const toolInput = validatedArgs;
|
|
44
|
+
const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`; // Include mode in operation
|
|
45
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
46
|
+
const sessionId = _getSessionId(requestContext);
|
|
47
|
+
const getWorkingDirectoryForSession = () => {
|
|
48
|
+
return _getWorkingDirectory(sessionId);
|
|
49
|
+
};
|
|
50
|
+
const logicContext = {
|
|
51
|
+
...requestContext,
|
|
52
|
+
sessionId: sessionId,
|
|
53
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
54
|
+
};
|
|
55
|
+
logger.info(`Executing tool: ${TOOL_NAME} (mode: ${toolInput.mode})`, logicContext);
|
|
56
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
57
|
+
// Call the core logic function which returns a GitStashResult object
|
|
58
|
+
const stashResult = await gitStashLogic(toolInput, logicContext);
|
|
59
|
+
// Format the result as a JSON string within TextContent
|
|
60
|
+
const resultContent = {
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: JSON.stringify(stashResult, null, 2), // Pretty-print JSON
|
|
63
|
+
contentType: 'application/json',
|
|
64
|
+
};
|
|
65
|
+
// Log based on the success flag in the result
|
|
66
|
+
if (stashResult.success) {
|
|
67
|
+
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Log specific failure message from the result
|
|
71
|
+
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${stashResult.message}`, { ...logicContext, errorDetails: stashResult.error, conflicts: stashResult.conflicts });
|
|
72
|
+
}
|
|
73
|
+
// Return the result, whether success or structured failure
|
|
74
|
+
return { content: [resultContent] };
|
|
75
|
+
}, {
|
|
76
|
+
operation: toolOperation,
|
|
77
|
+
context: logicContext,
|
|
78
|
+
input: validatedArgs, // Log the raw validated args
|
|
79
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
83
|
+
}, { operation, context, critical: true }); // Mark registration as critical
|
|
84
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the gitStatus tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitStatusTool, initializeGitStatusStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitStatusInput, GitStatusResult } from './logic.js';
|