@cyanheads/git-mcp-server 2.0.2 → 2.0.3
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 +45 -85
- package/dist/config/index.js +16 -18
- package/dist/index.js +80 -30
- package/dist/mcp-server/server.js +247 -523
- package/dist/mcp-server/tools/gitAdd/logic.js +9 -6
- package/dist/mcp-server/tools/gitAdd/registration.js +7 -4
- package/dist/mcp-server/tools/gitBranch/logic.js +23 -12
- package/dist/mcp-server/tools/gitBranch/registration.js +8 -5
- package/dist/mcp-server/tools/gitCheckout/logic.js +92 -44
- package/dist/mcp-server/tools/gitCheckout/registration.js +8 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +10 -7
- package/dist/mcp-server/tools/gitCherryPick/registration.js +8 -5
- package/dist/mcp-server/tools/gitClean/logic.js +9 -6
- package/dist/mcp-server/tools/gitClean/registration.js +8 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
- package/dist/mcp-server/tools/gitClone/logic.js +8 -5
- package/dist/mcp-server/tools/gitClone/registration.js +7 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +98 -20
- package/dist/mcp-server/tools/gitCommit/registration.js +22 -15
- package/dist/mcp-server/tools/gitDiff/logic.js +9 -6
- package/dist/mcp-server/tools/gitDiff/registration.js +8 -5
- package/dist/mcp-server/tools/gitFetch/logic.js +10 -7
- package/dist/mcp-server/tools/gitFetch/registration.js +8 -5
- package/dist/mcp-server/tools/gitInit/index.js +2 -2
- package/dist/mcp-server/tools/gitInit/logic.js +9 -6
- package/dist/mcp-server/tools/gitInit/registration.js +66 -12
- package/dist/mcp-server/tools/gitLog/logic.js +53 -16
- package/dist/mcp-server/tools/gitLog/registration.js +8 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +9 -6
- package/dist/mcp-server/tools/gitMerge/registration.js +8 -5
- package/dist/mcp-server/tools/gitPull/logic.js +11 -8
- package/dist/mcp-server/tools/gitPull/registration.js +7 -4
- package/dist/mcp-server/tools/gitPush/logic.js +12 -9
- package/dist/mcp-server/tools/gitPush/registration.js +7 -4
- package/dist/mcp-server/tools/gitRebase/logic.js +9 -6
- package/dist/mcp-server/tools/gitRebase/registration.js +8 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +4 -5
- package/dist/mcp-server/tools/gitRemote/registration.js +2 -4
- package/dist/mcp-server/tools/gitReset/logic.js +5 -6
- package/dist/mcp-server/tools/gitReset/registration.js +2 -4
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
- package/dist/mcp-server/tools/gitShow/logic.js +5 -6
- package/dist/mcp-server/tools/gitShow/registration.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +5 -6
- package/dist/mcp-server/tools/gitStash/registration.js +3 -5
- package/dist/mcp-server/tools/gitStatus/logic.js +5 -6
- package/dist/mcp-server/tools/gitStatus/registration.js +2 -4
- package/dist/mcp-server/tools/gitTag/logic.js +3 -4
- package/dist/mcp-server/tools/gitTag/registration.js +2 -4
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.js +432 -0
- package/dist/mcp-server/transports/stdioTransport.js +87 -0
- package/dist/types-global/errors.js +2 -2
- package/dist/utils/index.js +12 -11
- package/dist/utils/{errorHandler.js → internal/errorHandler.js} +18 -8
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.js +254 -0
- package/dist/utils/{requestContext.js → internal/requestContext.js} +2 -3
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/{tokenCounter.js → metrics/tokenCounter.js} +3 -3
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/{jsonParser.js → parsing/jsonParser.js} +3 -2
- package/dist/utils/{idGenerator.js → security/idGenerator.js} +4 -5
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/{rateLimiter.js → security/rateLimiter.js} +7 -10
- package/dist/utils/{sanitization.js → security/sanitization.js} +4 -3
- package/package.json +12 -9
- package/dist/types-global/mcp.js +0 -59
- package/dist/types-global/tool.js +0 -1
- package/dist/utils/logger.js +0 -266
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
|
|
5
|
-
|
|
6
|
-
import {
|
|
4
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
5
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
6
|
+
import { logger } from '../../../utils/index.js';
|
|
7
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
8
|
+
import { sanitization } from '../../../utils/index.js';
|
|
9
|
+
// Import config to check signing flag
|
|
10
|
+
import { config } from '../../../config/index.js';
|
|
7
11
|
const execAsync = promisify(exec);
|
|
8
12
|
// Define the input schema for the git_commit tool using Zod
|
|
9
13
|
export const GitCommitInputSchema = z.object({
|
|
10
|
-
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the
|
|
14
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
11
15
|
message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
|
|
12
16
|
author: z.object({
|
|
13
17
|
name: z.string().describe('Author name for the commit'),
|
|
@@ -15,6 +19,7 @@ export const GitCommitInputSchema = z.object({
|
|
|
15
19
|
}).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
|
|
16
20
|
allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
|
|
17
21
|
amend: z.boolean().default(false).describe('Amend the previous commit instead of creating a new one'),
|
|
22
|
+
forceUnsignedOnFailure: z.boolean().default(false).describe('If true and signing is enabled but fails, attempt the commit without signing instead of failing.'),
|
|
18
23
|
});
|
|
19
24
|
/**
|
|
20
25
|
* Executes the 'git commit' command and returns structured JSON output.
|
|
@@ -56,8 +61,14 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
56
61
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
57
62
|
}
|
|
58
63
|
try {
|
|
64
|
+
// Escape message for shell safety
|
|
65
|
+
const escapeShellArg = (arg) => {
|
|
66
|
+
// Escape backslashes first, then other special chars
|
|
67
|
+
return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
68
|
+
};
|
|
69
|
+
const escapedMessage = escapeShellArg(input.message);
|
|
59
70
|
// Construct the git commit command using the resolved targetPath
|
|
60
|
-
let command = `git -C "${targetPath}" commit -m "${
|
|
71
|
+
let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
61
72
|
if (input.allowEmpty) {
|
|
62
73
|
command += ' --allow-empty';
|
|
63
74
|
}
|
|
@@ -65,17 +76,84 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
65
76
|
command += ' --amend --no-edit';
|
|
66
77
|
}
|
|
67
78
|
if (input.author) {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
// Escape author details as well
|
|
80
|
+
const escapedAuthorName = escapeShellArg(input.author.name);
|
|
81
|
+
const escapedAuthorEmail = escapeShellArg(input.author.email); // Email typically safe, but escape anyway
|
|
82
|
+
// Use -c flags to override author for this commit, using the already escaped message
|
|
83
|
+
command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
84
|
+
}
|
|
85
|
+
// Append common flags (ensure they are appended to the potentially modified command from author block)
|
|
86
|
+
if (input.allowEmpty && !command.includes(' --allow-empty'))
|
|
87
|
+
command += ' --allow-empty';
|
|
88
|
+
if (input.amend && !command.includes(' --amend'))
|
|
89
|
+
command += ' --amend --no-edit'; // Avoid double adding if author block modified command
|
|
90
|
+
// Append signing flag if configured via GIT_SIGN_COMMITS env var
|
|
91
|
+
if (config.gitSignCommits) {
|
|
92
|
+
command += ' -S'; // Add signing flag (-S)
|
|
93
|
+
logger.info('Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.', { ...context, operation });
|
|
94
|
+
}
|
|
95
|
+
logger.debug(`Executing initial command attempt: ${command}`, { ...context, operation });
|
|
96
|
+
let stdout;
|
|
97
|
+
let stderr;
|
|
98
|
+
let commitResult;
|
|
99
|
+
try {
|
|
100
|
+
// Initial attempt (potentially with -S flag)
|
|
101
|
+
const execResult = await execAsync(command);
|
|
102
|
+
stdout = execResult.stdout;
|
|
103
|
+
stderr = execResult.stderr;
|
|
74
104
|
}
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
catch (error) {
|
|
106
|
+
const initialErrorMessage = error.stderr || error.message || '';
|
|
107
|
+
const isSigningError = initialErrorMessage.includes('gpg failed to sign') || initialErrorMessage.includes('signing failed');
|
|
108
|
+
if (isSigningError && input.forceUnsignedOnFailure) {
|
|
109
|
+
logger.warning('Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.', { ...context, operation, initialError: initialErrorMessage });
|
|
110
|
+
// Construct command *without* -S flag, using escaped message/author
|
|
111
|
+
const escapeShellArg = (arg) => {
|
|
112
|
+
return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
113
|
+
};
|
|
114
|
+
const escapedMessage = escapeShellArg(input.message);
|
|
115
|
+
let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
116
|
+
if (input.allowEmpty)
|
|
117
|
+
unsignedCommand += ' --allow-empty';
|
|
118
|
+
if (input.amend)
|
|
119
|
+
unsignedCommand += ' --amend --no-edit';
|
|
120
|
+
if (input.author) {
|
|
121
|
+
const escapedAuthorName = escapeShellArg(input.author.name);
|
|
122
|
+
const escapedAuthorEmail = escapeShellArg(input.author.email);
|
|
123
|
+
unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
124
|
+
// Re-append common flags if author block overwrote command
|
|
125
|
+
if (input.allowEmpty && !unsignedCommand.includes(' --allow-empty'))
|
|
126
|
+
unsignedCommand += ' --allow-empty';
|
|
127
|
+
if (input.amend && !unsignedCommand.includes(' --amend'))
|
|
128
|
+
unsignedCommand += ' --amend --no-edit';
|
|
129
|
+
}
|
|
130
|
+
logger.debug(`Executing unsigned fallback command: ${unsignedCommand}`, { ...context, operation });
|
|
131
|
+
try {
|
|
132
|
+
// Retry commit without signing
|
|
133
|
+
const fallbackResult = await execAsync(unsignedCommand);
|
|
134
|
+
stdout = fallbackResult.stdout;
|
|
135
|
+
stderr = fallbackResult.stderr;
|
|
136
|
+
// Add a note to the status message indicating signing was skipped
|
|
137
|
+
commitResult = {
|
|
138
|
+
success: true,
|
|
139
|
+
statusMessage: `Commit successful (unsigned, signing failed): ${stdout.trim()}`, // Default message, hash parsed below
|
|
140
|
+
commitHash: undefined // Will be parsed below
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (fallbackError) {
|
|
144
|
+
// If the unsigned commit *also* fails, re-throw that error
|
|
145
|
+
logger.error('Unsigned fallback commit attempt also failed.', { ...context, operation, fallbackError: fallbackError.message, stderr: fallbackError.stderr });
|
|
146
|
+
throw fallbackError; // Re-throw the error from the unsigned attempt
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// If it wasn't a signing error, or forceUnsignedOnFailure is false, re-throw the original error
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Process result (either from initial attempt or fallback)
|
|
77
155
|
// Check stderr first for common non-error messages
|
|
78
|
-
if (stderr) {
|
|
156
|
+
if (stderr && !commitResult) { // Don't overwrite fallback message if stderr also exists
|
|
79
157
|
if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
|
|
80
158
|
const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
|
|
81
159
|
logger.info(msg, { ...context, operation, path: targetPath });
|
|
@@ -95,18 +173,18 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
95
173
|
// Fallback parsing if needed, or rely on success message
|
|
96
174
|
logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
|
|
97
175
|
}
|
|
98
|
-
// Use statusMessage
|
|
99
|
-
const
|
|
176
|
+
// Use statusMessage, potentially using the one set during fallback
|
|
177
|
+
const finalStatusMsg = commitResult?.statusMessage || (commitHash
|
|
100
178
|
? `Commit successful: ${commitHash}`
|
|
101
|
-
: `Commit successful (stdout: ${stdout.trim()})
|
|
102
|
-
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash });
|
|
179
|
+
: `Commit successful (stdout: ${stdout.trim()})`);
|
|
180
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash, signed: !commitResult }); // Log if it was signed (not fallback)
|
|
103
181
|
return {
|
|
104
182
|
success: true,
|
|
105
|
-
statusMessage:
|
|
183
|
+
statusMessage: finalStatusMsg, // Use potentially modified message
|
|
106
184
|
commitHash: commitHash
|
|
107
185
|
};
|
|
108
186
|
}
|
|
109
|
-
catch (error) {
|
|
187
|
+
catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
|
|
110
188
|
logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
|
|
111
189
|
const errorMessage = error.stderr || error.message || '';
|
|
112
190
|
// Handle specific error cases first
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
+
import { ErrorHandler } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
+
import { logger } from '../../../utils/index.js';
|
|
5
|
+
// Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
|
|
6
|
+
import { requestContextService } from '../../../utils/index.js';
|
|
4
7
|
// Import the result type along with the function and input schema
|
|
5
|
-
import { BaseErrorCode } from '../../../types-global/errors.js'; //
|
|
8
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
6
9
|
import { commitGitChanges, GitCommitInputSchema } from './logic.js';
|
|
7
10
|
let _getWorkingDirectory;
|
|
8
11
|
let _getSessionId;
|
|
@@ -26,28 +29,32 @@ Write clear, concise commit messages using the Conventional Commits format: \`ty
|
|
|
26
29
|
- \`(scope)\`: Optional context (e.g., \`auth\`, \`ui\`, filename).
|
|
27
30
|
- \`subject\`: Imperative, present tense description (e.g., "add login button", not "added login button").
|
|
28
31
|
|
|
32
|
+
I want to understand what you did and why. Use the body for detailed explanations, if necessary.
|
|
33
|
+
|
|
29
34
|
**Example Commit Message:**
|
|
30
35
|
\`\`\`
|
|
31
36
|
feat(auth): implement password reset endpoint
|
|
32
37
|
|
|
33
|
-
Adds the /api/auth/reset-password endpoint to allow users
|
|
34
|
-
|
|
35
|
-
validation and rate limiting.
|
|
38
|
+
- Adds the /api/auth/reset-password endpoint to allow users to reset their password via an email link.
|
|
39
|
+
- Includes input validation and rate limiting.
|
|
36
40
|
|
|
37
41
|
Closes #123 (if applicable).
|
|
38
42
|
\`\`\`
|
|
39
43
|
|
|
40
44
|
**Best Practice:** Commit related changes together in logical units. If you've modified multiple files for a single feature or fix, stage and commit them together with a message that describes the overall change.
|
|
41
45
|
|
|
42
|
-
**Path Handling:** If the 'path' parameter is omitted or set to '.', the tool uses the working directory set by 'git_set_working_dir'. Providing a full, absolute path overrides this default and ensures explicitness
|
|
46
|
+
**Path Handling:** If the 'path' parameter is omitted or set to '.', the tool uses the working directory set by 'git_set_working_dir'. Providing a full, absolute path overrides this default and ensures explicitness.
|
|
47
|
+
|
|
48
|
+
**Commit Signing:** If the server is configured with the \`GIT_SIGN_COMMITS=true\` environment variable, this tool adds the \`-S\` flag to the \`git commit\` command, requesting a signature. Signing requires proper GPG or SSH key setup and Git configuration on the server machine.
|
|
49
|
+
- **Fallback:** If signing is enabled but fails (e.g., key not found, agent issue), the commit will **fail by default**. However, if the optional \`forceUnsignedOnFailure: true\` parameter is provided in the tool call, the tool will attempt the commit again *without* the \`-S\` flag, resulting in an unsigned commit.`;
|
|
43
50
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
* Registers the git_commit tool with the MCP server.
|
|
52
|
+
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
53
|
+
*
|
|
54
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
57
|
+
*/
|
|
51
58
|
export const registerGitCommitTool = async (server) => {
|
|
52
59
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
53
60
|
throw new Error('State accessors for git_commit must be initialized before registration.');
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
1
|
import { exec } from 'child_process';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
+
import { logger } from '../../../utils/index.js';
|
|
6
|
+
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
8
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
+
import { sanitization } from '../../../utils/index.js';
|
|
7
10
|
const execAsync = promisify(exec);
|
|
8
11
|
// Define the base input schema without refinement
|
|
9
12
|
const GitDiffInputBaseSchema = z.object({
|
|
10
|
-
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session
|
|
13
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
11
14
|
commit1: z.string().optional().describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
|
|
12
15
|
commit2: z.string().optional().describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
|
|
13
16
|
staged: z.boolean().optional().default(false).describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
+
import { ErrorHandler } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
+
import { logger } from '../../../utils/index.js';
|
|
5
|
+
// Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
+
import { requestContextService } from '../../../utils/index.js';
|
|
4
7
|
// Import the shape and the final schema/types
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
9
|
+
import { diffGitChanges, GitDiffInputShape } from './logic.js';
|
|
7
10
|
let _getWorkingDirectory;
|
|
8
11
|
let _getSessionId;
|
|
9
12
|
/**
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
1
|
import { exec } from 'child_process';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
+
import { logger } from '../../../utils/index.js';
|
|
6
|
+
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
8
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
+
import { sanitization } from '../../../utils/index.js';
|
|
7
10
|
const execAsync = promisify(exec);
|
|
8
11
|
// Define the input schema for the git_fetch tool using Zod
|
|
9
12
|
export const GitFetchInputSchema = z.object({
|
|
10
|
-
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session
|
|
13
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
11
14
|
remote: z.string().optional().describe("The remote repository to fetch from (e.g., 'origin'). If omitted, fetches from 'origin' or the default configured remote."),
|
|
12
15
|
prune: z.boolean().optional().default(false).describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
|
|
13
16
|
tags: z.boolean().optional().default(false).describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
|
|
@@ -99,7 +102,7 @@ export async function fetchGitRemote(input, context) {
|
|
|
99
102
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
100
103
|
}
|
|
101
104
|
if (errorMessage.includes('resolve host') || errorMessage.includes('Could not read from remote repository') || errorMessage.includes('Connection timed out')) {
|
|
102
|
-
throw new McpError(BaseErrorCode.
|
|
105
|
+
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
103
106
|
}
|
|
104
107
|
if (errorMessage.includes('fatal: ') && errorMessage.includes('couldn\'t find remote ref')) {
|
|
105
108
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
+
import { ErrorHandler } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
+
import { logger } from '../../../utils/index.js';
|
|
5
|
+
// Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from '../../../utils/index.js';
|
|
8
|
+
import { fetchGitRemote, GitFetchInputSchema } from './logic.js';
|
|
6
9
|
let _getWorkingDirectory;
|
|
7
10
|
let _getSessionId;
|
|
8
11
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the git_init tool.
|
|
3
|
-
* Exports the registration function.
|
|
3
|
+
* Exports the registration function and the state accessor initializer.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitInitTool } from './registration.js';
|
|
5
|
+
export { registerGitInitTool, initializeGitInitStateAccessors } from './registration.js';
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitInitInput, GitInitResult } from './logic.js';
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
1
|
import { exec } from 'child_process';
|
|
4
2
|
import fs from 'fs/promises';
|
|
5
3
|
import path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
7
|
+
import { logger } from '../../../utils/index.js';
|
|
8
|
+
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
9
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
10
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
11
|
+
import { sanitization } from '../../../utils/index.js';
|
|
9
12
|
const execAsync = promisify(exec);
|
|
10
13
|
// Define the input schema for the git_init tool using Zod
|
|
11
14
|
export const GitInitInputSchema = z.object({
|
|
12
|
-
path: z.string().min(1).describe("
|
|
15
|
+
path: z.string().min(1).optional().default('.').describe("Path where the new Git repository should be initialized. Can be relative or absolute. If relative or '.', it resolves against the directory set via `git_set_working_dir` for the session. If absolute, it's used directly. If omitted, defaults to '.' (resolved against session git working directory)."),
|
|
13
16
|
initialBranch: z.string().optional().describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."),
|
|
14
17
|
bare: z.boolean().default(false).describe("Create a bare repository (no working directory)."),
|
|
15
18
|
quiet: z.boolean().default(false).describe("Only print error and warning messages; all other output will be suppressed."),
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
|
|
4
|
+
import { ErrorHandler, logger, requestContextService, sanitization } from '../../../utils/index.js';
|
|
4
5
|
import { GitInitInputSchema, gitInitLogic } from './logic.js';
|
|
5
|
-
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
6
|
const TOOL_NAME = 'git_init';
|
|
7
|
-
const TOOL_DESCRIPTION = 'Initializes a new Git repository at the specified
|
|
7
|
+
const TOOL_DESCRIPTION = 'Initializes a new Git repository at the specified path. If path is relative or omitted, it resolves against the session working directory (if you have set the git_working_dir). Can optionally set the initial branch name and create a bare repository.';
|
|
8
|
+
const RegistrationSchema = GitInitInputSchema.extend({
|
|
9
|
+
path: z.string().min(1).optional().default('.'),
|
|
10
|
+
}).shape;
|
|
11
|
+
// --- Module-level State Accessors ---
|
|
12
|
+
// These will be populated by the initialize function called from server.ts
|
|
13
|
+
let _getWorkingDirectory = () => undefined;
|
|
14
|
+
let _getSessionIdFromContext = () => undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Initializes state accessor functions for the git_init tool.
|
|
17
|
+
* This function is called by the main server setup to provide the tool
|
|
18
|
+
* with a way to access session-specific state (like the working directory)
|
|
19
|
+
* without needing direct access to the server or transport layer internals.
|
|
20
|
+
*
|
|
21
|
+
* @param getWorkingDirectory - Function to retrieve the working directory for a given session ID.
|
|
22
|
+
* @param getSessionIdFromContext - Function to extract the session ID from a tool's execution context.
|
|
23
|
+
*/
|
|
24
|
+
export function initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext) {
|
|
25
|
+
_getWorkingDirectory = getWorkingDirectory;
|
|
26
|
+
_getSessionIdFromContext = getSessionIdFromContext;
|
|
27
|
+
logger.debug(`State accessors initialized for ${TOOL_NAME}`);
|
|
28
|
+
}
|
|
8
29
|
/**
|
|
9
30
|
* Registers the git_init tool with the MCP server.
|
|
10
31
|
*
|
|
@@ -16,15 +37,48 @@ export const registerGitInitTool = async (server) => {
|
|
|
16
37
|
const operation = 'registerGitInitTool';
|
|
17
38
|
const context = requestContextService.createRequestContext({ operation });
|
|
18
39
|
await ErrorHandler.tryCatch(async () => {
|
|
19
|
-
server.tool(TOOL_NAME, TOOL_DESCRIPTION,
|
|
20
|
-
async (validatedArgs, callContext) => {
|
|
40
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, RegistrationSchema, async (validatedArgs, callContext) => {
|
|
21
41
|
const toolOperation = 'tool:git_init';
|
|
22
42
|
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
43
|
+
// Use the initialized accessor to get the session ID
|
|
44
|
+
const sessionId = _getSessionIdFromContext(requestContext); // Pass the created context
|
|
45
|
+
if (!sessionId && !path.isAbsolute(validatedArgs.path)) {
|
|
46
|
+
// If path is relative, we NEED a session ID to resolve against a potential working dir
|
|
47
|
+
logger.error('Session ID is missing in context, cannot resolve relative path', requestContext);
|
|
48
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Session context is unavailable for relative path resolution.', { context: requestContext, operation: toolOperation });
|
|
49
|
+
}
|
|
23
50
|
logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
|
|
24
51
|
return await ErrorHandler.tryCatch(async () => {
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
52
|
+
// Use the initialized accessor to get the working directory
|
|
53
|
+
const sessionWorkingDirectory = _getWorkingDirectory(sessionId);
|
|
54
|
+
const inputPath = validatedArgs.path;
|
|
55
|
+
let resolvedPath;
|
|
56
|
+
try {
|
|
57
|
+
if (path.isAbsolute(inputPath)) {
|
|
58
|
+
resolvedPath = sanitization.sanitizePath(inputPath);
|
|
59
|
+
logger.debug(`Using absolute path: ${resolvedPath}`, requestContext);
|
|
60
|
+
}
|
|
61
|
+
else if (sessionWorkingDirectory) {
|
|
62
|
+
resolvedPath = sanitization.sanitizePath(path.resolve(sessionWorkingDirectory, inputPath));
|
|
63
|
+
logger.debug(`Resolved relative path '${inputPath}' to absolute path: ${resolvedPath} using session CWD`, requestContext);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// This case should now only be hit if the path is relative AND there's no session CWD set.
|
|
67
|
+
logger.error(`Relative path '${inputPath}' provided but no session working directory is set.`, requestContext);
|
|
68
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Relative path '${inputPath}' provided but no session working directory is set. Please provide an absolute path or set a working directory using git_set_working_dir.`, { context: requestContext, operation: toolOperation });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logger.error('Path resolution or sanitization failed', { ...requestContext, operation: toolOperation, error });
|
|
73
|
+
if (error instanceof McpError)
|
|
74
|
+
throw error;
|
|
75
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path processing: ${error instanceof Error ? error.message : String(error)}`, { context: requestContext, operation: toolOperation, originalError: error });
|
|
76
|
+
}
|
|
77
|
+
const logicArgs = {
|
|
78
|
+
...validatedArgs,
|
|
79
|
+
path: resolvedPath,
|
|
80
|
+
};
|
|
81
|
+
const initResult = await gitInitLogic(logicArgs, requestContext);
|
|
28
82
|
const resultContent = {
|
|
29
83
|
type: 'text',
|
|
30
84
|
text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
|
|
@@ -36,9 +90,9 @@ export const registerGitInitTool = async (server) => {
|
|
|
36
90
|
operation: toolOperation,
|
|
37
91
|
context: requestContext,
|
|
38
92
|
input: validatedArgs,
|
|
39
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
93
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
40
94
|
});
|
|
41
95
|
});
|
|
42
96
|
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
43
|
-
}, { operation, context, critical: true });
|
|
97
|
+
}, { operation, context, critical: true });
|
|
44
98
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
1
|
import { exec } from 'child_process';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
+
import { logger } from '../../../utils/index.js';
|
|
6
|
+
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
8
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
+
import { sanitization } from '../../../utils/index.js';
|
|
7
10
|
const execAsync = promisify(exec);
|
|
8
11
|
// Define the structure for a single commit entry
|
|
9
12
|
export const CommitEntrySchema = z.object({
|
|
@@ -16,13 +19,14 @@ export const CommitEntrySchema = z.object({
|
|
|
16
19
|
});
|
|
17
20
|
// Define the input schema for the git_log tool using Zod
|
|
18
21
|
export const GitLogInputSchema = z.object({
|
|
19
|
-
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session
|
|
22
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
20
23
|
maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
|
|
21
24
|
author: z.string().optional().describe("Limit commits to those matching the specified author pattern."),
|
|
22
25
|
since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
|
|
23
26
|
until: z.string().optional().describe("Show commits older than a specific date."),
|
|
24
|
-
branchOrFile: z.string().optional().describe("Show logs for a specific branch, tag, or file path."),
|
|
25
|
-
|
|
27
|
+
branchOrFile: z.string().optional().describe("Show logs for a specific branch (e.g., 'main'), tag, or file path (e.g., 'src/utils/logger.ts')."),
|
|
28
|
+
showSignature: z.boolean().optional().default(false).describe("Show signature verification status for commits. Returns raw output instead of parsed JSON."),
|
|
29
|
+
// Note: We use a fixed pretty format for reliable parsing unless showSignature is true.
|
|
26
30
|
});
|
|
27
31
|
// Delimiters for parsing the custom format
|
|
28
32
|
const FIELD_SEP = '\x1f'; // Unit Separator
|
|
@@ -62,11 +66,29 @@ export async function logGitHistory(input, context) {
|
|
|
62
66
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
63
67
|
}
|
|
64
68
|
try {
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
command
|
|
69
|
+
let command;
|
|
70
|
+
let isRawOutput = false; // Flag to indicate if we should parse or return raw
|
|
71
|
+
if (input.showSignature) {
|
|
72
|
+
isRawOutput = true;
|
|
73
|
+
command = `git -C "${targetPath}" log --show-signature`;
|
|
74
|
+
logger.info('Show signature requested, returning raw output.', { ...context, operation });
|
|
75
|
+
// Append other filters if provided
|
|
76
|
+
if (input.maxCount)
|
|
77
|
+
command += ` -n ${input.maxCount}`;
|
|
78
|
+
if (input.author)
|
|
79
|
+
command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g, '')}"`;
|
|
80
|
+
if (input.since)
|
|
81
|
+
command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g, '')}"`;
|
|
82
|
+
if (input.until)
|
|
83
|
+
command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g, '')}"`;
|
|
84
|
+
if (input.branchOrFile)
|
|
85
|
+
command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g, '')}`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Construct the git log command with the fixed format for parsing
|
|
89
|
+
command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
|
|
90
|
+
if (input.maxCount)
|
|
91
|
+
command += ` -n ${input.maxCount}`;
|
|
70
92
|
}
|
|
71
93
|
if (input.author) {
|
|
72
94
|
// Basic sanitization for author string
|
|
@@ -90,13 +112,28 @@ export async function logGitHistory(input, context) {
|
|
|
90
112
|
const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 10 }); // 10MB buffer
|
|
91
113
|
if (stderr) {
|
|
92
114
|
// Log stderr as warning, as git log might sometimes use it for non-fatal info
|
|
93
|
-
|
|
115
|
+
// Exception: If showing signature, stderr about allowedSignersFile is expected, treat as info
|
|
116
|
+
if (isRawOutput && stderr.includes('allowedSignersFile needs to be configured')) {
|
|
117
|
+
logger.info(`Git log stderr (signature verification note): ${stderr.trim()}`, { ...context, operation });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.warning(`Git log stderr: ${stderr.trim()}`, { ...context, operation });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// If raw output was requested, return it directly
|
|
124
|
+
if (isRawOutput) {
|
|
125
|
+
const message = `Raw log output (showSignature=true):\n${stdout}`;
|
|
126
|
+
logger.info(`${operation} completed successfully (raw output).`, { ...context, operation, path: targetPath });
|
|
127
|
+
return { success: true, commits: [], message: message };
|
|
94
128
|
}
|
|
95
|
-
//
|
|
129
|
+
// Otherwise, parse the structured output
|
|
96
130
|
const commits = [];
|
|
97
131
|
const commitRecords = stdout.split(RECORD_SEP).filter(record => record.trim() !== ''); // Split records and remove empty ones
|
|
98
132
|
for (const record of commitRecords) {
|
|
99
|
-
const
|
|
133
|
+
const trimmedRecord = record.trim(); // Trim leading/trailing whitespace (like newlines)
|
|
134
|
+
if (!trimmedRecord)
|
|
135
|
+
continue; // Skip empty records after trimming
|
|
136
|
+
const fields = trimmedRecord.split(FIELD_SEP); // Split the trimmed record
|
|
100
137
|
if (fields.length >= 5) { // Need at least hash, name, email, timestamp, subject
|
|
101
138
|
try {
|
|
102
139
|
const commitEntry = {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
+
import { ErrorHandler } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
+
import { logger } from '../../../utils/index.js';
|
|
5
|
+
// Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from '../../../utils/index.js';
|
|
4
8
|
import { GitLogInputSchema, logGitHistory } from './logic.js';
|
|
5
|
-
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
9
|
let _getWorkingDirectory;
|
|
7
10
|
let _getSessionId;
|
|
8
11
|
/**
|
|
@@ -17,7 +20,7 @@ export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
|
|
|
17
20
|
logger.info('State accessors initialized for git_log tool registration.');
|
|
18
21
|
}
|
|
19
22
|
const TOOL_NAME = 'git_log';
|
|
20
|
-
const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a list of commit objects.";
|
|
23
|
+
const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a list of commit objects by default. Can optionally show signature verification status (`showSignature: true`), which returns raw text output instead of JSON.";
|
|
21
24
|
/**
|
|
22
25
|
* Registers the git_log tool with the MCP server.
|
|
23
26
|
*
|