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