@cyanheads/git-mcp-server 2.1.8 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +44 -143
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +50 -171
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +46 -152
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +75 -257
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +48 -146
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +73 -181
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +37 -121
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +33 -122
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +70 -214
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +66 -188
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +8 -8
|
@@ -1,89 +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 final schema and types for handler logic
|
|
4
|
-
// Import the BASE schema separately for registration shape
|
|
5
|
-
import { GitStashBaseSchema, gitStashLogic, } from "./logic.js";
|
|
6
|
-
let _getWorkingDirectory;
|
|
7
|
-
let _getSessionId;
|
|
8
1
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
11
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_stash tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitStash/registration
|
|
12
4
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger.info("State accessors initialized for git_stash tool registration.");
|
|
17
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { gitStashLogic, GitStashOutputSchema, GitStashBaseSchema, } from "./logic.js";
|
|
18
8
|
const TOOL_NAME = "git_stash";
|
|
19
9
|
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
10
|
/**
|
|
21
|
-
* Registers the git_stash tool with the MCP server.
|
|
22
|
-
*
|
|
23
|
-
* @param
|
|
24
|
-
* @
|
|
25
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
11
|
+
* Registers the git_stash tool with the MCP server instance.
|
|
12
|
+
* @param server The MCP server instance.
|
|
13
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
14
|
+
* @param getSessionId Function to get the session ID from context.
|
|
26
15
|
*/
|
|
27
|
-
export const registerGitStashTool = async (server) => {
|
|
28
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
-
throw new Error("State accessors for git_stash must be initialized before registration.");
|
|
30
|
-
}
|
|
16
|
+
export const registerGitStashTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
31
17
|
const operation = "registerGitStashTool";
|
|
32
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Stash",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitStashBaseSchema.shape,
|
|
23
|
+
outputSchema: GitStashOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: true, // Can drop stashes or cause conflicts
|
|
27
|
+
idempotentHint: false,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async (params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
toolName: TOOL_NAME,
|
|
33
|
+
parentContext: callContext,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const sessionId = getSessionId(handlerContext);
|
|
37
|
+
const result = await gitStashLogic(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
46
40
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
const logicContext = {
|
|
52
|
-
...requestContext,
|
|
53
|
-
sessionId: sessionId,
|
|
54
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
55
44
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
text: JSON.stringify(stashResult, null, 2), // Pretty-print JSON
|
|
64
|
-
contentType: "application/json",
|
|
65
|
-
};
|
|
66
|
-
// Log based on the success flag in the result
|
|
67
|
-
if (stashResult.success) {
|
|
68
|
-
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
// Log specific failure message from the result
|
|
72
|
-
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${stashResult.message}`, {
|
|
73
|
-
...logicContext,
|
|
74
|
-
errorDetails: stashResult.error,
|
|
75
|
-
conflicts: stashResult.conflicts,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
// Return the result, whether success or structured failure
|
|
79
|
-
return { content: [resultContent] };
|
|
80
|
-
}, {
|
|
81
|
-
operation: toolOperation,
|
|
82
|
-
context: logicContext,
|
|
83
|
-
input: validatedArgs, // Log the raw validated args
|
|
84
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
48
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
49
|
+
operation: `tool:${TOOL_NAME}`,
|
|
50
|
+
context: handlerContext,
|
|
51
|
+
input: params,
|
|
85
52
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
53
|
+
const mcpError = handledError instanceof McpError
|
|
54
|
+
? handledError
|
|
55
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
56
|
+
return {
|
|
57
|
+
isError: true,
|
|
58
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
59
|
+
structuredContent: mcpError.details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
89
64
|
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitStatus tool.
|
|
3
|
-
*
|
|
3
|
+
* @module src/mcp-server/tools/gitStatus/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitStatusTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitStatusInput, GitStatusResult } from './logic.js';
|
|
5
|
+
export { registerGitStatusTool } from "./registration.js";
|
|
@@ -1,267 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_status tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitStatus/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 GitStatusInputSchema = z.object({
|
|
9
|
-
path: z
|
|
10
|
-
.string()
|
|
11
|
-
.min(1)
|
|
12
|
-
.optional()
|
|
13
|
-
.default(".")
|
|
14
|
-
.describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
15
14
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
// 2. DEFINE the Zod response schema.
|
|
16
|
+
const ChangesSchema = z.object({
|
|
17
|
+
Added: z.array(z.string()).optional(),
|
|
18
|
+
Modified: z.array(z.string()).optional(),
|
|
19
|
+
Deleted: z.array(z.string()).optional(),
|
|
20
|
+
Renamed: z.array(z.string()).optional(),
|
|
21
|
+
Copied: z.array(z.string()).optional(),
|
|
22
|
+
TypeChanged: z.array(z.string()).optional(),
|
|
23
|
+
});
|
|
24
|
+
export const GitStatusOutputSchema = z.object({
|
|
25
|
+
current_branch: z.string().nullable().describe("The current branch, or null for detached HEAD."),
|
|
26
|
+
staged_changes: ChangesSchema.describe("Changes staged for the next commit."),
|
|
27
|
+
unstaged_changes: ChangesSchema.describe("Changes not staged for commit."),
|
|
28
|
+
untracked_files: z.array(z.string()).describe("Files not tracked by Git."),
|
|
29
|
+
conflicted_files: z.array(z.string()).describe("Files with merge conflicts."),
|
|
30
|
+
is_clean: z.boolean().describe("True if there are no pending changes."),
|
|
31
|
+
});
|
|
32
|
+
function parseGitStatus(porcelainOutput) {
|
|
33
|
+
const lines = porcelainOutput.trim().split("\n").filter(Boolean);
|
|
25
34
|
const result = {
|
|
26
35
|
current_branch: null,
|
|
27
36
|
staged_changes: {},
|
|
28
37
|
unstaged_changes: {},
|
|
29
38
|
untracked_files: [],
|
|
30
39
|
conflicted_files: [],
|
|
31
|
-
is_clean: true,
|
|
40
|
+
is_clean: true,
|
|
32
41
|
};
|
|
33
|
-
if (lines.length
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
if (lines[0].startsWith("## ")) {
|
|
42
|
+
if (lines.length > 0 && lines[0].startsWith("## ")) {
|
|
37
43
|
const branchLine = lines.shift();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const detachedMatch = branchLine.match(/^## HEAD \(no branch\)/);
|
|
41
|
-
if (standardBranchMatch) {
|
|
42
|
-
result.current_branch = standardBranchMatch[1];
|
|
43
|
-
}
|
|
44
|
-
else if (noCommitsMatch) {
|
|
45
|
-
result.current_branch = `${noCommitsMatch[1]} (no commits yet)`;
|
|
46
|
-
}
|
|
47
|
-
else if (detachedMatch) {
|
|
48
|
-
result.current_branch = "HEAD (detached)";
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
logger.warning("Could not parse branch information from line:", {
|
|
52
|
-
branchLine,
|
|
53
|
-
});
|
|
54
|
-
result.current_branch = "(unknown)";
|
|
55
|
-
}
|
|
44
|
+
const branchMatch = branchLine.match(/^## (.*?)(?:\.\.\..*)?$/);
|
|
45
|
+
result.current_branch = branchMatch ? branchMatch[1] : "HEAD (detached)";
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
continue;
|
|
60
|
-
result.is_clean = false; // Any line indicates non-clean state
|
|
47
|
+
lines.forEach(line => {
|
|
48
|
+
result.is_clean = false;
|
|
61
49
|
const xy = line.substring(0, 2);
|
|
62
50
|
const file = line.substring(3);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
if (xy === "??") {
|
|
51
|
+
const staged = xy[0];
|
|
52
|
+
const unstaged = xy[1];
|
|
53
|
+
if (xy === '??') {
|
|
67
54
|
result.untracked_files.push(file);
|
|
68
|
-
continue;
|
|
69
55
|
}
|
|
70
|
-
|
|
71
|
-
// DD = both deleted, AU = added by us, UD = deleted by them, UA = added by them, DU = deleted by us
|
|
72
|
-
// AA = both added, UU = both modified
|
|
73
|
-
if (stagedStatusChar === "U" ||
|
|
74
|
-
unstagedStatusChar === "U" ||
|
|
75
|
-
(stagedStatusChar === "D" && unstagedStatusChar === "D") ||
|
|
76
|
-
(stagedStatusChar === "A" && unstagedStatusChar === "A")) {
|
|
56
|
+
else if (staged === 'U' || unstaged === 'U' || (staged === 'A' && unstaged === 'A') || (staged === 'D' && unstaged === 'D')) {
|
|
77
57
|
result.conflicted_files.push(file);
|
|
78
|
-
continue; // Conflicted files are handled separately and not in staged/unstaged
|
|
79
58
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (statusDesc) {
|
|
104
|
-
if (!result.staged_changes[statusDesc]) {
|
|
105
|
-
result.staged_changes[statusDesc] = [];
|
|
59
|
+
else {
|
|
60
|
+
const mapStatus = (char, changeSet) => {
|
|
61
|
+
let statusKey;
|
|
62
|
+
switch (char) {
|
|
63
|
+
case 'M':
|
|
64
|
+
statusKey = 'Modified';
|
|
65
|
+
break;
|
|
66
|
+
case 'A':
|
|
67
|
+
statusKey = 'Added';
|
|
68
|
+
break;
|
|
69
|
+
case 'D':
|
|
70
|
+
statusKey = 'Deleted';
|
|
71
|
+
break;
|
|
72
|
+
case 'R':
|
|
73
|
+
statusKey = 'Renamed';
|
|
74
|
+
break;
|
|
75
|
+
case 'C':
|
|
76
|
+
statusKey = 'Copied';
|
|
77
|
+
break;
|
|
78
|
+
case 'T':
|
|
79
|
+
statusKey = 'TypeChanged';
|
|
80
|
+
break;
|
|
81
|
+
default: return;
|
|
106
82
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
// Handle unstaged changes (worktree status)
|
|
111
|
-
if (unstagedStatusChar !== " " && unstagedStatusChar !== "?") {
|
|
112
|
-
let statusDesc = undefined;
|
|
113
|
-
switch (unstagedStatusChar) {
|
|
114
|
-
case "M":
|
|
115
|
-
statusDesc = "Modified";
|
|
116
|
-
break;
|
|
117
|
-
case "D":
|
|
118
|
-
statusDesc = "Deleted";
|
|
119
|
-
break;
|
|
120
|
-
case "T":
|
|
121
|
-
statusDesc = "TypeChanged";
|
|
122
|
-
break;
|
|
123
|
-
// 'A' (Added but not committed) is handled by '??' (untracked)
|
|
124
|
-
// 'R' and 'C' in worktree without being staged are complex, often appear as deleted + untracked
|
|
125
|
-
}
|
|
126
|
-
if (statusDesc) {
|
|
127
|
-
if (!result.unstaged_changes[statusDesc]) {
|
|
128
|
-
result.unstaged_changes[statusDesc] = [];
|
|
83
|
+
if (!changeSet[statusKey]) {
|
|
84
|
+
changeSet[statusKey] = [];
|
|
129
85
|
}
|
|
130
|
-
|
|
131
|
-
}
|
|
86
|
+
changeSet[statusKey].push(file);
|
|
87
|
+
};
|
|
88
|
+
mapStatus(staged, result.staged_changes);
|
|
89
|
+
mapStatus(unstaged, result.unstaged_changes);
|
|
132
90
|
}
|
|
133
|
-
}
|
|
134
|
-
result.is_clean =
|
|
135
|
-
Object.keys(result.staged_changes).length === 0 &&
|
|
136
|
-
Object.keys(result.unstaged_changes).length === 0 &&
|
|
137
|
-
result.untracked_files.length === 0 &&
|
|
138
|
-
result.conflicted_files.length === 0;
|
|
91
|
+
});
|
|
139
92
|
return result;
|
|
140
93
|
}
|
|
141
94
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* @param {GitStatusInput} input - The validated input object containing the repository path.
|
|
145
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
146
|
-
* @returns {Promise<GitStatusResult>} A promise that resolves with the structured git status.
|
|
147
|
-
* @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails.
|
|
95
|
+
* 4. IMPLEMENT the core logic function.
|
|
96
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
148
97
|
*/
|
|
149
|
-
export async function getGitStatus(
|
|
98
|
+
export async function getGitStatus(params, context) {
|
|
150
99
|
const operation = "getGitStatus";
|
|
151
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (input.path && input.path !== ".") {
|
|
156
|
-
// Use the provided path directly
|
|
157
|
-
targetPath = input.path;
|
|
158
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
159
|
-
...context,
|
|
160
|
-
operation,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// Path is '.' or undefined, try to get the session's working directory
|
|
165
|
-
const workingDir = context.getWorkingDirectory();
|
|
166
|
-
if (!workingDir) {
|
|
167
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
168
|
-
}
|
|
169
|
-
targetPath = workingDir;
|
|
170
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
171
|
-
...context,
|
|
172
|
-
operation,
|
|
173
|
-
sessionId: context.sessionId,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
// Sanitize the resolved path
|
|
177
|
-
const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
|
|
178
|
-
allowAbsolute: true,
|
|
179
|
-
});
|
|
180
|
-
logger.debug("Sanitized path", {
|
|
181
|
-
...context,
|
|
182
|
-
operation,
|
|
183
|
-
sanitizedPathInfo,
|
|
184
|
-
});
|
|
185
|
-
targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
logger.error("Path resolution or sanitization failed", {
|
|
189
|
-
...context,
|
|
190
|
-
operation,
|
|
191
|
-
error,
|
|
192
|
-
});
|
|
193
|
-
if (error instanceof McpError) {
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
100
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
101
|
+
const workingDir = context.getWorkingDirectory();
|
|
102
|
+
if (params.path === "." && !workingDir) {
|
|
103
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
197
104
|
}
|
|
105
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
106
|
+
const args = ["-C", targetPath, "status", "--porcelain=v1", "-b"];
|
|
198
107
|
try {
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
...context,
|
|
203
|
-
operation,
|
|
204
|
-
});
|
|
205
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
206
|
-
if (stderr) {
|
|
207
|
-
// Log stderr as warning but proceed to parse stdout
|
|
208
|
-
logger.warning(`Git status command produced stderr (may be informational)`, { ...context, operation, stderr });
|
|
209
|
-
}
|
|
210
|
-
logger.debug(`${operation} command executed, parsing output...`, {
|
|
211
|
-
...context,
|
|
212
|
-
operation,
|
|
213
|
-
path: targetPath,
|
|
214
|
-
});
|
|
215
|
-
// Parse the porcelain output
|
|
216
|
-
const structuredResult = parseGitStatusPorcelainV1(stdout);
|
|
217
|
-
// If parsing resulted in clean state but no branch, re-check branch explicitly
|
|
218
|
-
// This handles the case of an empty repo after init but before first commit
|
|
219
|
-
if (structuredResult.is_clean && !structuredResult.current_branch) {
|
|
220
|
-
try {
|
|
221
|
-
const branchArgs = [
|
|
222
|
-
"-C",
|
|
223
|
-
targetPath,
|
|
224
|
-
"rev-parse",
|
|
225
|
-
"--abbrev-ref",
|
|
226
|
-
"HEAD",
|
|
227
|
-
];
|
|
228
|
-
const { stdout: branchStdout } = await execFileAsync("git", branchArgs);
|
|
229
|
-
const currentBranchName = branchStdout.trim(); // Renamed variable for clarity
|
|
230
|
-
if (currentBranchName && currentBranchName !== "HEAD") {
|
|
231
|
-
structuredResult.current_branch = currentBranchName;
|
|
232
|
-
}
|
|
233
|
-
else if (currentBranchName === "HEAD" &&
|
|
234
|
-
!structuredResult.current_branch) {
|
|
235
|
-
// If rev-parse returns HEAD and we still don't have a branch (e.g. detached from no-commits branch)
|
|
236
|
-
structuredResult.current_branch = "HEAD (detached)";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (branchError) {
|
|
240
|
-
// Ignore error if rev-parse fails (e.g., still no commits)
|
|
241
|
-
logger.debug("Could not determine branch via rev-parse, likely no commits yet.", { ...context, operation, branchError });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
logger.info("git status parsed successfully", {
|
|
245
|
-
...context,
|
|
246
|
-
operation,
|
|
247
|
-
path: targetPath,
|
|
248
|
-
isClean: structuredResult.is_clean,
|
|
249
|
-
currentBranch: structuredResult.current_branch,
|
|
250
|
-
});
|
|
251
|
-
return structuredResult; // Return the structured JSON object
|
|
108
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
109
|
+
const { stdout } = await execFileAsync("git", args);
|
|
110
|
+
return parseGitStatus(stdout);
|
|
252
111
|
}
|
|
253
112
|
catch (error) {
|
|
254
|
-
logger.error(`Failed to execute or parse git status command`, {
|
|
255
|
-
...context,
|
|
256
|
-
operation,
|
|
257
|
-
path: targetPath,
|
|
258
|
-
error: error.message,
|
|
259
|
-
stderr: error.stderr,
|
|
260
|
-
});
|
|
261
113
|
const errorMessage = error.stderr || error.message || "";
|
|
114
|
+
logger.error(`Failed to execute git status command`, { ...context, operation, errorMessage });
|
|
262
115
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
263
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
116
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
264
117
|
}
|
|
265
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `
|
|
118
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git status failed: ${errorMessage}`);
|
|
266
119
|
}
|
|
267
120
|
}
|
|
@@ -1,78 +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 result type along with the function and input schema
|
|
4
|
-
import { getGitStatus, GitStatusInputSchema, } from "./logic.js";
|
|
5
|
-
let _getWorkingDirectory;
|
|
6
|
-
let _getSessionId;
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @param getWdFn - Function to get the working directory for a session.
|
|
11
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_status tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitStatus/registration
|
|
12
4
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger.info("State accessors initialized for git_status tool registration.");
|
|
17
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { getGitStatus, GitStatusInputSchema, GitStatusOutputSchema, } from "./logic.js";
|
|
18
8
|
const TOOL_NAME = "git_status";
|
|
19
9
|
const TOOL_DESCRIPTION = "Retrieves the status of a Git repository. Returns a JSON object detailing the current branch, cleanliness, and changes. Staged and unstaged changes are grouped by status (e.g., Added, Modified), alongside lists of untracked and conflicted files.";
|
|
20
10
|
/**
|
|
21
|
-
* Registers the git_status tool with the MCP server.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @param
|
|
25
|
-
* @returns {Promise<void>}
|
|
26
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
11
|
+
* Registers the git_status tool with the MCP server instance.
|
|
12
|
+
* @param server The MCP server instance.
|
|
13
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
14
|
+
* @param getSessionId Function to get the session ID from context.
|
|
27
15
|
*/
|
|
28
|
-
export const registerGitStatusTool = async (server) => {
|
|
29
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
30
|
-
throw new Error("State accessors for git_status must be initialized before registration.");
|
|
31
|
-
}
|
|
16
|
+
export const registerGitStatusTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
32
17
|
const operation = "registerGitStatusTool";
|
|
33
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Status",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitStatusInputSchema.shape,
|
|
23
|
+
outputSchema: GitStatusOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
idempotentHint: true,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async (params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
toolName: TOOL_NAME,
|
|
33
|
+
parentContext: callContext,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const sessionId = getSessionId(handlerContext);
|
|
37
|
+
const result = await getGitStatus(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
42
40
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const getWorkingDirectoryForSession = () => {
|
|
47
|
-
return _getWorkingDirectory(sessionId);
|
|
48
|
-
};
|
|
49
|
-
// Enhance context for the logic function
|
|
50
|
-
const logicContext = {
|
|
51
|
-
...requestContext,
|
|
52
|
-
sessionId: sessionId,
|
|
53
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
54
44
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
type: "text",
|
|
63
|
-
// Stringify the JSON object for the response content
|
|
64
|
-
text: JSON.stringify(statusResult, null, 2), // Pretty-print JSON
|
|
65
|
-
contentType: "application/json", // Specify content type
|
|
66
|
-
};
|
|
67
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
68
|
-
return { content: [resultContent] }; // isError defaults to false
|
|
69
|
-
}, {
|
|
70
|
-
operation: toolOperation,
|
|
71
|
-
context: logicContext,
|
|
72
|
-
input: validatedArgs, // Log sanitized input
|
|
73
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code if logic fails unexpectedly
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
48
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
49
|
+
operation: `tool:${TOOL_NAME}`,
|
|
50
|
+
context: handlerContext,
|
|
51
|
+
input: params,
|
|
74
52
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
const mcpError = handledError instanceof McpError
|
|
54
|
+
? handledError
|
|
55
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
56
|
+
return {
|
|
57
|
+
isError: true,
|
|
58
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
59
|
+
structuredContent: mcpError.details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
78
64
|
};
|