@cyanheads/git-mcp-server 2.0.1 → 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 +55 -89
- package/{build → dist}/config/index.js +16 -18
- package/{build → dist}/index.js +80 -30
- package/dist/mcp-server/server.js +296 -0
- package/{build → dist}/mcp-server/tools/gitAdd/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitAdd/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitBranch/logic.js +23 -12
- package/{build → dist}/mcp-server/tools/gitBranch/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitCheckout/logic.js +92 -44
- package/{build → dist}/mcp-server/tools/gitCheckout/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitCherryPick/logic.js +10 -7
- package/{build → dist}/mcp-server/tools/gitCherryPick/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClean/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitClean/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
- package/{build → dist}/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitClone/logic.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClone/registration.js +7 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +207 -0
- package/{build → dist}/mcp-server/tools/gitCommit/registration.js +22 -15
- package/{build → dist}/mcp-server/tools/gitDiff/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitDiff/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitFetch/logic.js +10 -7
- package/{build → dist}/mcp-server/tools/gitFetch/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitInit/index.js +2 -2
- package/{build → dist}/mcp-server/tools/gitInit/logic.js +9 -6
- package/dist/mcp-server/tools/gitInit/registration.js +98 -0
- package/{build → dist}/mcp-server/tools/gitLog/logic.js +53 -16
- package/{build → dist}/mcp-server/tools/gitLog/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitMerge/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitMerge/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitPull/logic.js +11 -8
- package/{build → dist}/mcp-server/tools/gitPull/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitPush/logic.js +12 -9
- package/{build → dist}/mcp-server/tools/gitPush/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitRebase/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitRebase/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitRemote/logic.js +4 -5
- package/{build → dist}/mcp-server/tools/gitRemote/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitReset/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitReset/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
- package/{build → dist}/mcp-server/tools/gitShow/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitShow/registration.js +3 -5
- package/{build → dist}/mcp-server/tools/gitStash/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitStash/registration.js +3 -5
- package/{build → dist}/mcp-server/tools/gitStatus/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitStatus/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitTag/logic.js +3 -4
- package/{build → 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/{build → dist}/types-global/errors.js +2 -2
- package/dist/utils/index.js +12 -0
- package/{build/utils → dist/utils/internal}/errorHandler.js +18 -8
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.js +254 -0
- package/{build/utils → dist/utils/internal}/requestContext.js +2 -3
- package/dist/utils/metrics/index.js +1 -0
- package/{build/utils → dist/utils/metrics}/tokenCounter.js +3 -3
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.js +2 -0
- package/{build/utils → dist/utils/parsing}/jsonParser.js +3 -2
- package/{build/utils → dist/utils/security}/idGenerator.js +4 -5
- package/dist/utils/security/index.js +3 -0
- package/{build/utils → dist/utils/security}/rateLimiter.js +7 -10
- package/{build/utils → dist/utils/security}/sanitization.js +4 -3
- package/package.json +20 -16
- package/build/mcp-server/server.js +0 -572
- package/build/mcp-server/tools/gitCommit/logic.js +0 -129
- package/build/mcp-server/tools/gitInit/registration.js +0 -44
- package/build/types-global/mcp.js +0 -59
- package/build/types-global/tool.js +0 -1
- package/build/utils/index.js +0 -11
- package/build/utils/logger.js +0 -266
- /package/{build → dist}/mcp-server/tools/gitAdd/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitBranch/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCheckout/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCherryPick/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClean/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClearWorkingDir/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClone/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCommit/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitDiff/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitFetch/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitLog/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitMerge/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitPull/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitPush/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitRebase/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitRemote/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitReset/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitSetWorkingDir/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitShow/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitStash/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitStatus/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitTag/index.js +0 -0
|
@@ -1,16 +1,19 @@
|
|
|
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_cherry-pick tool using Zod
|
|
9
12
|
export const GitCherryPickInputSchema = z.object({
|
|
10
|
-
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository.
|
|
13
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
11
14
|
commitRef: z.string().min(1).describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
|
|
12
15
|
mainline: z.number().int().min(1).optional().describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
|
|
13
|
-
strategy: z.enum(['recursive', 'resolve', 'ours', 'theirs', 'octopus', 'subtree']).optional().describe("Specifies
|
|
16
|
+
strategy: z.enum(['recursive', 'resolve', 'ours', 'theirs', 'octopus', 'subtree']).optional().describe("Specifies a merge strategy *option* (passed via -X)."),
|
|
14
17
|
noCommit: z.boolean().default(false).describe("Apply the changes but do not create a commit."),
|
|
15
18
|
signoff: z.boolean().default(false).describe("Add a Signed-off-by line to the commit message."),
|
|
16
19
|
// Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
2
|
+
import { logger } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
+
import { ErrorHandler } from '../../../utils/index.js';
|
|
5
|
+
// Import utils from barrel (requestContextService 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 { GitCherryPickInputSchema, gitCherryPickLogic } from './logic.js';
|
|
6
9
|
let _getWorkingDirectory;
|
|
7
10
|
let _getSessionId;
|
|
8
11
|
/**
|
|
@@ -1,14 +1,17 @@
|
|
|
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_clean tool using Zod
|
|
9
12
|
// No refinements needed here, but the 'force' check is critical in the logic
|
|
10
13
|
export const GitCleanInputSchema = z.object({
|
|
11
|
-
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository.
|
|
14
|
+
path: z.string().min(1).optional().default('.').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."),
|
|
12
15
|
force: z.boolean().describe("REQUIRED confirmation to run the command. Must be explicitly set to true to perform the clean operation. If false or omitted, the command will not run."),
|
|
13
16
|
dryRun: z.boolean().default(false).describe("Show what would be deleted without actually deleting (-n flag)."),
|
|
14
17
|
directories: z.boolean().default(false).describe("Remove untracked directories in addition to files (-d flag)."),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
2
|
+
import { logger } from '../../../utils/index.js';
|
|
3
|
+
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
+
import { ErrorHandler } 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 schema and types
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
9
|
+
import { GitCleanInputSchema, gitCleanLogic } from './logic.js';
|
|
7
10
|
let _getWorkingDirectory;
|
|
8
11
|
let _getSessionId;
|
|
9
12
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
3
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
4
|
+
import { logger } from '../../../utils/index.js';
|
|
4
5
|
// Define the Zod schema for input validation (no arguments needed)
|
|
5
6
|
export const GitClearWorkingDirInputSchema = z.object({});
|
|
6
7
|
/**
|
|
@@ -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 { GitClearWorkingDirInputSchema, gitClearWorkingDirLogic } from './logic.js';
|
|
5
|
-
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
9
|
let _clearWorkingDirectory;
|
|
7
10
|
let _getSessionId;
|
|
8
11
|
/**
|
|
@@ -1,10 +1,13 @@
|
|
|
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
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
6
|
+
import { logger } from '../../../utils/index.js';
|
|
7
|
+
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
8
|
+
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
9
|
+
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
10
|
+
import { sanitization } from '../../../utils/index.js';
|
|
8
11
|
const execAsync = promisify(exec);
|
|
9
12
|
// Define the input schema for the git_clone tool using Zod
|
|
10
13
|
export const GitCloneInputSchema = z.object({
|
|
@@ -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 { GitCloneInputSchema, gitCloneLogic } from './logic.js';
|
|
5
|
-
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
8
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
|
|
6
9
|
const TOOL_NAME = 'git_clone';
|
|
7
10
|
const TOOL_DESCRIPTION = 'Clones a Git repository from a given URL into a specified absolute directory path. Supports cloning specific branches and setting clone depth.';
|
|
8
11
|
/**
|
|
@@ -0,0 +1,207 @@
|
|
|
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'; // 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';
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
// Define the input schema for the git_commit tool using Zod
|
|
13
|
+
export const GitCommitInputSchema = z.object({
|
|
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."),
|
|
15
|
+
message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
|
|
16
|
+
author: z.object({
|
|
17
|
+
name: z.string().describe('Author name for the commit'),
|
|
18
|
+
email: z.string().email().describe('Author email for the commit'),
|
|
19
|
+
}).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
|
|
20
|
+
allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
|
|
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.'),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Executes the 'git commit' command and returns structured JSON output.
|
|
26
|
+
*
|
|
27
|
+
* @param {GitCommitInput} input - The validated input object.
|
|
28
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
29
|
+
* @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
|
|
30
|
+
* @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
|
|
31
|
+
*/
|
|
32
|
+
export async function commitGitChanges(input, context // Add getter to context
|
|
33
|
+
) {
|
|
34
|
+
const operation = 'commitGitChanges';
|
|
35
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
36
|
+
let targetPath;
|
|
37
|
+
try {
|
|
38
|
+
// Resolve the target path
|
|
39
|
+
if (input.path && input.path !== '.') {
|
|
40
|
+
targetPath = input.path;
|
|
41
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const workingDir = context.getWorkingDirectory();
|
|
45
|
+
if (!workingDir) {
|
|
46
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
47
|
+
}
|
|
48
|
+
targetPath = workingDir;
|
|
49
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
50
|
+
}
|
|
51
|
+
// Sanitize the resolved path
|
|
52
|
+
const sanitizedPath = sanitization.sanitizePath(targetPath);
|
|
53
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath });
|
|
54
|
+
targetPath = sanitizedPath; // Use the sanitized path going forward
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
58
|
+
if (error instanceof McpError) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
62
|
+
}
|
|
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);
|
|
70
|
+
// Construct the git commit command using the resolved targetPath
|
|
71
|
+
let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
72
|
+
if (input.allowEmpty) {
|
|
73
|
+
command += ' --allow-empty';
|
|
74
|
+
}
|
|
75
|
+
if (input.amend) {
|
|
76
|
+
command += ' --amend --no-edit';
|
|
77
|
+
}
|
|
78
|
+
if (input.author) {
|
|
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;
|
|
104
|
+
}
|
|
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)
|
|
155
|
+
// Check stderr first for common non-error messages
|
|
156
|
+
if (stderr && !commitResult) { // Don't overwrite fallback message if stderr also exists
|
|
157
|
+
if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
|
|
158
|
+
const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
|
|
159
|
+
logger.info(msg, { ...context, operation, path: targetPath });
|
|
160
|
+
// Use statusMessage
|
|
161
|
+
return { success: true, statusMessage: msg, nothingToCommit: true };
|
|
162
|
+
}
|
|
163
|
+
// Log other stderr as warning but continue, as commit might still succeed
|
|
164
|
+
logger.warning(`Git commit command produced stderr`, { ...context, operation, stderr });
|
|
165
|
+
}
|
|
166
|
+
// Extract commit hash (more robustly)
|
|
167
|
+
let commitHash = undefined;
|
|
168
|
+
const hashMatch = stdout.match(/([a-f0-9]{7,40})/); // Look for typical short or long hash
|
|
169
|
+
if (hashMatch) {
|
|
170
|
+
commitHash = hashMatch[1];
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Fallback parsing if needed, or rely on success message
|
|
174
|
+
logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
|
|
175
|
+
}
|
|
176
|
+
// Use statusMessage, potentially using the one set during fallback
|
|
177
|
+
const finalStatusMsg = commitResult?.statusMessage || (commitHash
|
|
178
|
+
? `Commit successful: ${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)
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
statusMessage: finalStatusMsg, // Use potentially modified message
|
|
184
|
+
commitHash: commitHash
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
|
|
188
|
+
logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
|
|
189
|
+
const errorMessage = error.stderr || error.message || '';
|
|
190
|
+
// Handle specific error cases first
|
|
191
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
192
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
193
|
+
}
|
|
194
|
+
if (errorMessage.includes('nothing to commit') || errorMessage.includes('no changes added to commit')) {
|
|
195
|
+
// This might happen if git exits with error despite these messages
|
|
196
|
+
const msg = errorMessage.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
|
|
197
|
+
logger.info(msg + ' (caught as error)', { ...context, operation, path: targetPath, errorMessage });
|
|
198
|
+
// Return success=false but indicate the reason using statusMessage
|
|
199
|
+
return { success: false, statusMessage: msg, nothingToCommit: true };
|
|
200
|
+
}
|
|
201
|
+
if (errorMessage.includes('conflicts')) {
|
|
202
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
|
|
203
|
+
}
|
|
204
|
+
// Generic internal error for other failures
|
|
205
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to commit changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -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."),
|