@cyanheads/git-mcp-server 2.1.8 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
- package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +45 -154
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +47 -187
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +40 -162
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +71 -266
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +39 -156
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +65 -192
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +33 -133
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +28 -133
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +59 -219
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +57 -198
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +8 -8
|
@@ -1,144 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_show tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitShow/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
//
|
|
8
|
-
// No refinements needed here, so we don't need a separate BaseSchema
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
9
12
|
export const GitShowInputSchema = z.object({
|
|
10
|
-
path: z
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.min(1)
|
|
19
|
-
.describe("The object reference (commit hash, tag name, branch name, HEAD, etc.) to show."),
|
|
20
|
-
filePath: z
|
|
21
|
-
.string()
|
|
22
|
-
.optional()
|
|
23
|
-
.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>'."),
|
|
24
|
-
// format: z.string().optional().describe("Optional format string for the output"), // Consider adding later
|
|
13
|
+
path: z.string().default(".").describe("Path to the local Git repository."),
|
|
14
|
+
ref: z.string().min(1).describe("The object reference (commit hash, tag, branch, etc.) to show."),
|
|
15
|
+
filePath: z.string().optional().describe("Optional specific file path within the ref to show."),
|
|
16
|
+
});
|
|
17
|
+
// 2. DEFINE the Zod response schema.
|
|
18
|
+
export const GitShowOutputSchema = z.object({
|
|
19
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
20
|
+
content: z.string().describe("Raw output from the git show command."),
|
|
25
21
|
});
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @param {GitShowInput} input - The validated input object.
|
|
30
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
31
|
-
* @returns {Promise<GitShowResult>} A promise that resolves with the structured result.
|
|
32
|
-
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
23
|
+
* 4. IMPLEMENT the core logic function.
|
|
24
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
33
25
|
*/
|
|
34
|
-
export async function gitShowLogic(
|
|
26
|
+
export async function gitShowLogic(params, context) {
|
|
35
27
|
const operation = "gitShowLogic";
|
|
36
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const workingDir = context.getWorkingDirectory();
|
|
41
|
-
targetPath =
|
|
42
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
43
|
-
if (targetPath === "." && !workingDir) {
|
|
44
|
-
logger.warning("Executing git show in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
45
|
-
targetPath = process.cwd();
|
|
46
|
-
}
|
|
47
|
-
else if (targetPath === "." && workingDir) {
|
|
48
|
-
targetPath = workingDir;
|
|
49
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
50
|
-
...context,
|
|
51
|
-
operation,
|
|
52
|
-
sessionId: context.sessionId,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
57
|
-
...context,
|
|
58
|
-
operation,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
62
|
-
allowAbsolute: true,
|
|
63
|
-
}).sanitizedPath;
|
|
64
|
-
logger.debug("Sanitized path", {
|
|
65
|
-
...context,
|
|
66
|
-
operation,
|
|
67
|
-
sanitizedPath: targetPath,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
logger.error("Path resolution or sanitization failed", {
|
|
72
|
-
...context,
|
|
73
|
-
operation,
|
|
74
|
-
error,
|
|
75
|
-
});
|
|
76
|
-
if (error instanceof McpError)
|
|
77
|
-
throw error;
|
|
78
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
79
|
-
}
|
|
80
|
-
// Validate ref format (simple validation)
|
|
81
|
-
if (!/^[a-zA-Z0-9_./~^:-]+$/.test(input.ref)) {
|
|
82
|
-
// Allow ':' for filePath combination
|
|
83
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid reference format: ${input.ref}`, { context, operation });
|
|
84
|
-
}
|
|
85
|
-
// Validate filePath format if provided (basic path chars)
|
|
86
|
-
if (input.filePath && !/^[a-zA-Z0-9_./-]+$/.test(input.filePath)) {
|
|
87
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid file path format: ${input.filePath}`, { context, operation });
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
// Construct the refspec, combining ref and filePath if needed
|
|
91
|
-
const refSpec = input.filePath
|
|
92
|
-
? `${input.ref}:${input.filePath}`
|
|
93
|
-
: input.ref;
|
|
94
|
-
// Construct the command
|
|
95
|
-
const args = ["-C", targetPath, "show", refSpec];
|
|
96
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
97
|
-
...context,
|
|
98
|
-
operation,
|
|
99
|
-
});
|
|
100
|
-
// Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
|
|
101
|
-
// We primarily care about stdout for the content. Errors usually have non-zero exit code.
|
|
102
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
103
|
-
if (stderr) {
|
|
104
|
-
// Log stderr as debug info, as it might contain commit details etc.
|
|
105
|
-
logger.debug(`Git show command produced stderr (may be informational)`, {
|
|
106
|
-
...context,
|
|
107
|
-
operation,
|
|
108
|
-
stderr,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
logger.info(`git show executed successfully for ref: ${refSpec}`, {
|
|
112
|
-
...context,
|
|
113
|
-
operation,
|
|
114
|
-
path: targetPath,
|
|
115
|
-
});
|
|
116
|
-
return { success: true, content: stdout }; // Return raw stdout content
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
const errorMessage = error.stderr || error.message || "";
|
|
120
|
-
logger.error(`Failed to execute git show command`, {
|
|
121
|
-
...context,
|
|
122
|
-
operation,
|
|
123
|
-
path: targetPath,
|
|
124
|
-
error: errorMessage,
|
|
125
|
-
stderr: error.stderr,
|
|
126
|
-
stdout: error.stdout,
|
|
127
|
-
});
|
|
128
|
-
// Specific error handling
|
|
129
|
-
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
130
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
131
|
-
}
|
|
132
|
-
if (/unknown revision or path not in the working tree/i.test(errorMessage)) {
|
|
133
|
-
const target = input.filePath
|
|
134
|
-
? `${input.ref}:${input.filePath}`
|
|
135
|
-
: input.ref;
|
|
136
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to show: Reference or pathspec '${target}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
137
|
-
}
|
|
138
|
-
if (/ambiguous argument/i.test(errorMessage)) {
|
|
139
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to show: Reference '${input.ref}' is ambiguous. Provide a more specific reference. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
140
|
-
}
|
|
141
|
-
// Throw a generic McpError for other failures
|
|
142
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git show failed for path: ${targetPath}, ref: ${input.ref}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
28
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
29
|
+
const workingDir = context.getWorkingDirectory();
|
|
30
|
+
if (params.path === "." && !workingDir) {
|
|
31
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
143
32
|
}
|
|
33
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
34
|
+
const refSpec = params.filePath ? `${params.ref}:${params.filePath}` : params.ref;
|
|
35
|
+
const args = ["-C", targetPath, "show", refSpec];
|
|
36
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
37
|
+
const { stdout } = await execFileAsync("git", args);
|
|
38
|
+
return { success: true, content: stdout };
|
|
144
39
|
}
|
|
@@ -1,86 +1,64 @@
|
|
|
1
|
-
import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
2
|
-
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), ErrorHandler (./utils/internal/errorHandler.js), requestContextService (./utils/internal/requestContext.js)
|
|
3
|
-
// Import the schema and types
|
|
4
|
-
import { GitShowInputSchema, gitShowLogic, } from "./logic.js";
|
|
5
|
-
let _getWorkingDirectory;
|
|
6
|
-
let _getSessionId;
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @
|
|
10
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_show tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitShow/registration
|
|
11
4
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
_getSessionId = getSidFn;
|
|
15
|
-
logger.info("State accessors initialized for git_show tool registration.");
|
|
16
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { gitShowLogic, GitShowInputSchema, GitShowOutputSchema, } from "./logic.js";
|
|
17
7
|
const TOOL_NAME = "git_show";
|
|
18
8
|
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.";
|
|
19
9
|
/**
|
|
20
|
-
* Registers the git_show tool with the MCP server.
|
|
21
|
-
*
|
|
22
|
-
* @param
|
|
23
|
-
* @
|
|
24
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
10
|
+
* Registers the git_show tool with the MCP server instance.
|
|
11
|
+
* @param server The MCP server instance.
|
|
12
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
13
|
+
* @param getSessionId Function to get the session ID from context.
|
|
25
14
|
*/
|
|
26
|
-
export const registerGitShowTool = async (server) => {
|
|
27
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
28
|
-
throw new Error("State accessors for git_show must be initialized before registration.");
|
|
29
|
-
}
|
|
15
|
+
export const registerGitShowTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
30
16
|
const operation = "registerGitShowTool";
|
|
31
17
|
const context = requestContextService.createRequestContext({ operation });
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
server.registerTool(TOOL_NAME, {
|
|
19
|
+
title: "Git Show",
|
|
20
|
+
description: TOOL_DESCRIPTION,
|
|
21
|
+
inputSchema: GitShowInputSchema.shape,
|
|
22
|
+
outputSchema: GitShowOutputSchema.shape,
|
|
23
|
+
annotations: {
|
|
24
|
+
readOnlyHint: true,
|
|
25
|
+
destructiveHint: false,
|
|
26
|
+
idempotentHint: true,
|
|
27
|
+
openWorldHint: false,
|
|
28
|
+
},
|
|
29
|
+
}, async (params, callContext) => {
|
|
30
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
31
|
+
toolName: TOOL_NAME,
|
|
32
|
+
parentContext: callContext,
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const sessionId = getSessionId(handlerContext);
|
|
36
|
+
const result = await gitShowLogic(params, {
|
|
37
|
+
...handlerContext,
|
|
38
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
39
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const logicContext = {
|
|
49
|
-
...requestContext,
|
|
50
|
-
sessionId: sessionId,
|
|
51
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
40
|
+
return {
|
|
41
|
+
structuredContent: result,
|
|
42
|
+
content: [{ type: "text", text: result.content, contentType: "text/plain; charset=utf-8" }],
|
|
52
43
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Return raw content on success, or error message on failure
|
|
61
|
-
text: showResult.success
|
|
62
|
-
? showResult.content
|
|
63
|
-
: `Error: ${showResult.message}${showResult.error ? `\nDetails: ${showResult.error}` : ""}`,
|
|
64
|
-
// Use plain text content type, unless we decide to return JSON later
|
|
65
|
-
contentType: "text/plain",
|
|
66
|
-
};
|
|
67
|
-
// Log based on the success flag in the result
|
|
68
|
-
if (showResult.success) {
|
|
69
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully`, logicContext);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
// Log specific failure message from the result
|
|
73
|
-
logger.warning(`Tool ${TOOL_NAME} failed: ${showResult.message}`, { ...logicContext, errorDetails: showResult.error });
|
|
74
|
-
}
|
|
75
|
-
// Return the result, whether success or structured failure
|
|
76
|
-
return { content: [resultContent] };
|
|
77
|
-
}, {
|
|
78
|
-
operation: toolOperation,
|
|
79
|
-
context: logicContext,
|
|
80
|
-
input: validatedArgs, // Log the raw validated args
|
|
81
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
47
|
+
const mcpError = ErrorHandler.handleError(error, {
|
|
48
|
+
operation: `tool:${TOOL_NAME}`,
|
|
49
|
+
context: handlerContext,
|
|
50
|
+
input: params,
|
|
82
51
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
52
|
+
return {
|
|
53
|
+
isError: true,
|
|
54
|
+
content: [{ type: "text", text: `Error: ${mcpError.message}` }],
|
|
55
|
+
structuredContent: {
|
|
56
|
+
code: mcpError.code,
|
|
57
|
+
message: mcpError.message,
|
|
58
|
+
details: mcpError.details,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
86
64
|
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitStash tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitStash/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitStashTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitStashInput, GitStashResult } from './logic.js';
|
|
5
|
+
export { registerGitStashTool } from "./registration.js";
|
|
@@ -1,244 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_stash tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitStash/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
8
12
|
export const GitStashBaseSchema = z.object({
|
|
9
|
-
path: z
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.default(".")
|
|
14
|
-
.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."),
|
|
15
|
-
mode: z
|
|
16
|
-
.enum(["list", "apply", "pop", "drop", "save"])
|
|
17
|
-
.describe("The stash operation to perform: 'list', 'apply', 'pop', 'drop', 'save'."),
|
|
18
|
-
stashRef: z
|
|
19
|
-
.string()
|
|
20
|
-
.optional()
|
|
21
|
-
.describe("Stash reference (e.g., 'stash@{1}'). Required for 'apply', 'pop', 'drop' modes."),
|
|
22
|
-
message: z
|
|
23
|
-
.string()
|
|
24
|
-
.optional()
|
|
25
|
-
.describe("Optional descriptive message used only for 'save' mode."),
|
|
26
|
-
// includeUntracked: z.boolean().default(false).describe("Include untracked files in 'save' mode (-u)"), // Consider adding later
|
|
27
|
-
// keepIndex: z.boolean().default(false).describe("Keep staged changes in 'save' mode (--keep-index)"), // Consider adding later
|
|
13
|
+
path: z.string().default(".").describe("Path to the local Git repository."),
|
|
14
|
+
mode: z.enum(["list", "apply", "pop", "drop", "save"]).describe("The stash operation to perform."),
|
|
15
|
+
stashRef: z.string().optional().describe("Stash reference (e.g., 'stash@{1}')."),
|
|
16
|
+
message: z.string().optional().describe("Optional descriptive message for 'save' mode."),
|
|
28
17
|
});
|
|
29
|
-
// Apply refinements and export the FINAL schema for validation within the handler
|
|
30
18
|
export const GitStashInputSchema = GitStashBaseSchema.refine((data) => !(["apply", "pop", "drop"].includes(data.mode) && !data.stashRef), {
|
|
31
|
-
message: "A 'stashRef'
|
|
32
|
-
path: ["stashRef"],
|
|
19
|
+
message: "A 'stashRef' is required for 'apply', 'pop', and 'drop' modes.",
|
|
20
|
+
path: ["stashRef"],
|
|
21
|
+
});
|
|
22
|
+
// 2. DEFINE the Zod response schema.
|
|
23
|
+
const StashInfoSchema = z.object({
|
|
24
|
+
ref: z.string(),
|
|
25
|
+
branch: z.string(),
|
|
26
|
+
description: z.string(),
|
|
27
|
+
});
|
|
28
|
+
export const GitStashOutputSchema = z.object({
|
|
29
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
30
|
+
mode: z.string().describe("The mode of operation that was performed."),
|
|
31
|
+
message: z.string().optional().describe("A summary message of the result."),
|
|
32
|
+
stashes: z.array(StashInfoSchema).optional().describe("A list of stashes for the 'list' mode."),
|
|
33
|
+
conflicts: z.boolean().optional().describe("Indicates if a merge conflict occurred."),
|
|
34
|
+
stashCreated: z.boolean().optional().describe("Indicates if a stash was created."),
|
|
33
35
|
});
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param {GitStashInput} input - The validated input object (validated against GitStashInputSchema).
|
|
38
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
39
|
-
* @returns {Promise<GitStashResult>} A promise that resolves with the structured result.
|
|
40
|
-
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
37
|
+
* 4. IMPLEMENT the core logic function.
|
|
38
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
41
39
|
*/
|
|
42
|
-
export async function gitStashLogic(
|
|
43
|
-
const operation = `gitStashLogic:${
|
|
44
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const workingDir = context.getWorkingDirectory();
|
|
49
|
-
targetPath =
|
|
50
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
51
|
-
if (targetPath === "." && !workingDir) {
|
|
52
|
-
logger.warning("Executing git stash in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
53
|
-
targetPath = process.cwd();
|
|
54
|
-
}
|
|
55
|
-
else if (targetPath === "." && workingDir) {
|
|
56
|
-
targetPath = workingDir;
|
|
57
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
58
|
-
...context,
|
|
59
|
-
operation,
|
|
60
|
-
sessionId: context.sessionId,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
65
|
-
...context,
|
|
66
|
-
operation,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
70
|
-
allowAbsolute: true,
|
|
71
|
-
}).sanitizedPath;
|
|
72
|
-
logger.debug("Sanitized path", {
|
|
73
|
-
...context,
|
|
74
|
-
operation,
|
|
75
|
-
sanitizedPath: targetPath,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
logger.error("Path resolution or sanitization failed", {
|
|
80
|
-
...context,
|
|
81
|
-
operation,
|
|
82
|
-
error,
|
|
83
|
-
});
|
|
84
|
-
if (error instanceof McpError)
|
|
85
|
-
throw error;
|
|
86
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
87
|
-
}
|
|
88
|
-
// Validate stashRef format if provided (simple validation)
|
|
89
|
-
if (input.stashRef && !/^stash@\{[0-9]+\}$/.test(input.stashRef)) {
|
|
90
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid stash reference format: ${input.stashRef}. Expected format: stash@{n}`, { context, operation });
|
|
40
|
+
export async function gitStashLogic(params, context) {
|
|
41
|
+
const operation = `gitStashLogic:${params.mode}`;
|
|
42
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
43
|
+
const workingDir = context.getWorkingDirectory();
|
|
44
|
+
if (params.path === "." && !workingDir) {
|
|
45
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
91
46
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
switch (
|
|
47
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
48
|
+
const buildArgs = () => {
|
|
49
|
+
const baseArgs = ["-C", targetPath, "stash", params.mode];
|
|
50
|
+
switch (params.mode) {
|
|
96
51
|
case "list":
|
|
97
|
-
|
|
98
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
99
|
-
...context,
|
|
100
|
-
operation,
|
|
101
|
-
});
|
|
102
|
-
const { stdout: listStdout } = await execFileAsync("git", args);
|
|
103
|
-
const stashes = listStdout
|
|
104
|
-
.trim()
|
|
105
|
-
.split("\n")
|
|
106
|
-
.filter((line) => line)
|
|
107
|
-
.map((line) => {
|
|
108
|
-
// Improved regex to handle different stash list formats
|
|
109
|
-
const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
|
|
110
|
-
return match
|
|
111
|
-
? {
|
|
112
|
-
ref: match[1],
|
|
113
|
-
branch: match[3] || "unknown",
|
|
114
|
-
description: match[4],
|
|
115
|
-
}
|
|
116
|
-
: { ref: "unknown", branch: "unknown", description: line }; // Fallback parsing
|
|
117
|
-
});
|
|
118
|
-
result = { success: true, mode: "list", stashes };
|
|
52
|
+
// No extra args needed
|
|
119
53
|
break;
|
|
120
54
|
case "apply":
|
|
121
55
|
case "pop":
|
|
122
|
-
// stashRef is validated by Zod refine
|
|
123
|
-
const stashRefApplyPop = input.stashRef;
|
|
124
|
-
args = ["-C", targetPath, "stash", input.mode, stashRefApplyPop];
|
|
125
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
126
|
-
...context,
|
|
127
|
-
operation,
|
|
128
|
-
});
|
|
129
|
-
try {
|
|
130
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
131
|
-
// Check stdout/stderr for conflict messages, although exit code 0 usually means success
|
|
132
|
-
const conflicts = /conflict/i.test(stdout) || /conflict/i.test(stderr);
|
|
133
|
-
const message = conflicts
|
|
134
|
-
? `Stash ${input.mode} resulted in conflicts that need manual resolution.`
|
|
135
|
-
: `Stash ${stashRefApplyPop} ${input.mode === "apply" ? "applied" : "popped"} successfully.`;
|
|
136
|
-
logger.info(message, {
|
|
137
|
-
...context,
|
|
138
|
-
operation,
|
|
139
|
-
path: targetPath,
|
|
140
|
-
conflicts,
|
|
141
|
-
});
|
|
142
|
-
result = { success: true, mode: input.mode, message, conflicts };
|
|
143
|
-
}
|
|
144
|
-
catch (applyError) {
|
|
145
|
-
const applyErrorMessage = applyError.stderr || applyError.message || "";
|
|
146
|
-
if (/conflict/i.test(applyErrorMessage)) {
|
|
147
|
-
logger.warning(`Stash ${input.mode} failed due to conflicts.`, {
|
|
148
|
-
...context,
|
|
149
|
-
operation,
|
|
150
|
-
path: targetPath,
|
|
151
|
-
error: applyErrorMessage,
|
|
152
|
-
});
|
|
153
|
-
// Return failure but indicate conflicts
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
mode: input.mode,
|
|
157
|
-
message: `Failed to ${input.mode} stash ${stashRefApplyPop} due to conflicts. Resolve conflicts manually.`,
|
|
158
|
-
error: applyErrorMessage,
|
|
159
|
-
conflicts: true,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
// Rethrow other errors
|
|
163
|
-
throw applyError;
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
56
|
case "drop":
|
|
167
|
-
|
|
168
|
-
const stashRefDrop = input.stashRef;
|
|
169
|
-
args = ["-C", targetPath, "stash", "drop", stashRefDrop];
|
|
170
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
171
|
-
...context,
|
|
172
|
-
operation,
|
|
173
|
-
});
|
|
174
|
-
await execFileAsync("git", args);
|
|
175
|
-
result = {
|
|
176
|
-
success: true,
|
|
177
|
-
mode: "drop",
|
|
178
|
-
message: `Dropped ${stashRefDrop} successfully.`,
|
|
179
|
-
stashRef: stashRefDrop,
|
|
180
|
-
};
|
|
57
|
+
baseArgs.push(params.stashRef);
|
|
181
58
|
break;
|
|
182
59
|
case "save":
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
args.push(input.message);
|
|
60
|
+
if (params.message) {
|
|
61
|
+
baseArgs.push(params.message);
|
|
186
62
|
}
|
|
187
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
188
|
-
...context,
|
|
189
|
-
operation,
|
|
190
|
-
});
|
|
191
|
-
const { stdout: saveStdout } = await execFileAsync("git", args);
|
|
192
|
-
const stashCreated = !/no local changes to save/i.test(saveStdout);
|
|
193
|
-
const saveMessage = stashCreated
|
|
194
|
-
? `Changes stashed successfully.` +
|
|
195
|
-
(input.message ? ` Message: "${input.message}"` : "")
|
|
196
|
-
: "No local changes to save.";
|
|
197
|
-
result = {
|
|
198
|
-
success: true,
|
|
199
|
-
mode: "save",
|
|
200
|
-
message: saveMessage,
|
|
201
|
-
stashCreated,
|
|
202
|
-
};
|
|
203
63
|
break;
|
|
204
|
-
default:
|
|
205
|
-
// Should not happen due to Zod validation
|
|
206
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
207
64
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
65
|
+
return baseArgs;
|
|
66
|
+
};
|
|
67
|
+
const args = buildArgs();
|
|
68
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
69
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
70
|
+
if (params.mode === 'list') {
|
|
71
|
+
const stashes = stdout.trim().split("\n").filter(Boolean).map(line => {
|
|
72
|
+
const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
|
|
73
|
+
return match ? { ref: match[1], branch: match[3] || "unknown", description: match[4] } : { ref: "unknown", branch: "unknown", description: line };
|
|
213
74
|
});
|
|
214
|
-
return
|
|
75
|
+
return { success: true, mode: params.mode, stashes };
|
|
215
76
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
path: targetPath,
|
|
222
|
-
error: errorMessage,
|
|
223
|
-
stderr: error.stderr,
|
|
224
|
-
stdout: error.stdout,
|
|
225
|
-
});
|
|
226
|
-
// Specific error handling
|
|
227
|
-
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
228
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
229
|
-
}
|
|
230
|
-
if ((input.mode === "apply" ||
|
|
231
|
-
input.mode === "pop" ||
|
|
232
|
-
input.mode === "drop") &&
|
|
233
|
-
/no such stash/i.test(errorMessage)) {
|
|
234
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to ${input.mode} stash: Stash '${input.stashRef}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
235
|
-
}
|
|
236
|
-
if ((input.mode === "apply" || input.mode === "pop") &&
|
|
237
|
-
/conflict/i.test(errorMessage)) {
|
|
238
|
-
// This case might be caught above, but double-check here
|
|
239
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Failed to ${input.mode} stash '${input.stashRef}' due to conflicts. Resolve conflicts manually. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
240
|
-
}
|
|
241
|
-
// Throw a generic McpError for other failures
|
|
242
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git stash ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
77
|
+
const output = stdout + stderr;
|
|
78
|
+
const conflicts = /conflict/i.test(output);
|
|
79
|
+
if (params.mode === 'save') {
|
|
80
|
+
const stashCreated = !/no local changes to save/i.test(output);
|
|
81
|
+
return { success: true, mode: params.mode, message: stashCreated ? "Changes stashed." : "No local changes to save.", stashCreated };
|
|
243
82
|
}
|
|
83
|
+
return { success: true, mode: params.mode, message: `${params.mode} operation successful.`, conflicts };
|
|
244
84
|
}
|