@cyanheads/git-mcp-server 1.2.4 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -285
- package/dist/config/index.js +69 -0
- package/dist/index.js +135 -0
- package/dist/mcp-server/server.js +572 -0
- package/dist/mcp-server/tools/gitAdd/index.js +7 -0
- package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
- package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
- package/dist/mcp-server/tools/gitBranch/index.js +7 -0
- package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
- package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
- package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
- package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
- package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
- package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
- package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
- package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
- package/dist/mcp-server/tools/gitClean/index.js +7 -0
- package/dist/mcp-server/tools/gitClean/logic.js +110 -0
- package/dist/mcp-server/tools/gitClean/registration.js +98 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
- package/dist/mcp-server/tools/gitClone/index.js +7 -0
- package/dist/mcp-server/tools/gitClone/logic.js +136 -0
- package/dist/mcp-server/tools/gitClone/registration.js +44 -0
- package/dist/mcp-server/tools/gitCommit/index.js +7 -0
- package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
- package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
- package/dist/mcp-server/tools/gitDiff/index.js +6 -0
- package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
- package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
- package/dist/mcp-server/tools/gitFetch/index.js +6 -0
- package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
- package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
- package/dist/mcp-server/tools/gitInit/index.js +7 -0
- package/dist/mcp-server/tools/gitInit/logic.js +117 -0
- package/dist/mcp-server/tools/gitInit/registration.js +44 -0
- package/dist/mcp-server/tools/gitLog/index.js +6 -0
- package/dist/mcp-server/tools/gitLog/logic.js +148 -0
- package/dist/mcp-server/tools/gitLog/registration.js +71 -0
- package/dist/mcp-server/tools/gitMerge/index.js +7 -0
- package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
- package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
- package/dist/mcp-server/tools/gitPull/index.js +6 -0
- package/dist/mcp-server/tools/gitPull/logic.js +144 -0
- package/dist/mcp-server/tools/gitPull/registration.js +81 -0
- package/dist/mcp-server/tools/gitPush/index.js +6 -0
- package/dist/mcp-server/tools/gitPush/logic.js +188 -0
- package/dist/mcp-server/tools/gitPush/registration.js +81 -0
- package/dist/mcp-server/tools/gitRebase/index.js +7 -0
- package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
- package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
- package/dist/mcp-server/tools/gitRemote/index.js +7 -0
- package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
- package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
- package/dist/mcp-server/tools/gitReset/index.js +6 -0
- package/dist/mcp-server/tools/gitReset/logic.js +116 -0
- package/dist/mcp-server/tools/gitReset/registration.js +71 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
- package/dist/mcp-server/tools/gitShow/index.js +7 -0
- package/dist/mcp-server/tools/gitShow/logic.js +99 -0
- package/dist/mcp-server/tools/gitShow/registration.js +83 -0
- package/dist/mcp-server/tools/gitStash/index.js +7 -0
- package/dist/mcp-server/tools/gitStash/logic.js +161 -0
- package/dist/mcp-server/tools/gitStash/registration.js +84 -0
- package/dist/mcp-server/tools/gitStatus/index.js +7 -0
- package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
- package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
- package/dist/mcp-server/tools/gitTag/index.js +7 -0
- package/dist/mcp-server/tools/gitTag/logic.js +142 -0
- package/dist/mcp-server/tools/gitTag/registration.js +84 -0
- package/dist/types-global/errors.js +68 -0
- package/dist/types-global/mcp.js +59 -0
- package/dist/types-global/tool.js +1 -0
- package/dist/utils/errorHandler.js +237 -0
- package/dist/utils/idGenerator.js +148 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/jsonParser.js +78 -0
- package/dist/utils/logger.js +266 -0
- package/dist/utils/rateLimiter.js +177 -0
- package/dist/utils/requestContext.js +49 -0
- package/dist/utils/sanitization.js +371 -0
- package/dist/utils/tokenCounter.js +124 -0
- package/package.json +62 -17
- package/build/index.js +0 -54
- package/build/resources/descriptors.js +0 -77
- package/build/resources/diff.js +0 -241
- package/build/resources/file.js +0 -222
- package/build/resources/history.js +0 -242
- package/build/resources/index.js +0 -99
- package/build/resources/repository.js +0 -286
- package/build/server.js +0 -120
- package/build/services/error-service.js +0 -73
- package/build/services/git-service.js +0 -965
- package/build/tools/advanced.js +0 -526
- package/build/tools/branch.js +0 -296
- package/build/tools/index.js +0 -29
- package/build/tools/remote.js +0 -279
- package/build/tools/repository.js +0 -170
- package/build/tools/workdir.js +0 -445
- package/build/types/git.js +0 -7
- package/build/utils/global-settings.js +0 -64
- package/build/utils/validation.js +0 -108
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Define the input schema for the git_fetch tool using Zod
|
|
9
|
+
export const GitFetchInputSchema = z.object({
|
|
10
|
+
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
|
|
11
|
+
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
|
+
prune: z.boolean().optional().default(false).describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
|
|
13
|
+
tags: z.boolean().optional().default(false).describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
|
|
14
|
+
all: z.boolean().optional().default(false).describe("Fetch all remotes."),
|
|
15
|
+
// Add options like --depth, specific refspecs if needed
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Executes the 'git fetch' command and returns structured JSON output.
|
|
19
|
+
*
|
|
20
|
+
* @param {GitFetchInput} input - The validated input object.
|
|
21
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
22
|
+
* @returns {Promise<GitFetchResult>} A promise that resolves with the structured fetch result.
|
|
23
|
+
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchGitRemote(input, context) {
|
|
26
|
+
const operation = 'fetchGitRemote';
|
|
27
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
28
|
+
let targetPath;
|
|
29
|
+
try {
|
|
30
|
+
// Resolve and sanitize the target path
|
|
31
|
+
if (input.path && input.path !== '.') {
|
|
32
|
+
targetPath = input.path;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const workingDir = context.getWorkingDirectory();
|
|
36
|
+
if (!workingDir) {
|
|
37
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
38
|
+
}
|
|
39
|
+
targetPath = workingDir;
|
|
40
|
+
}
|
|
41
|
+
targetPath = sanitization.sanitizePath(targetPath);
|
|
42
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
46
|
+
if (error instanceof McpError)
|
|
47
|
+
throw error;
|
|
48
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
49
|
+
}
|
|
50
|
+
// Basic sanitization for remote name
|
|
51
|
+
const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, '');
|
|
52
|
+
try {
|
|
53
|
+
// Construct the git fetch command
|
|
54
|
+
let command = `git -C "${targetPath}" fetch`;
|
|
55
|
+
if (input.prune) {
|
|
56
|
+
command += ' --prune';
|
|
57
|
+
}
|
|
58
|
+
if (input.tags) {
|
|
59
|
+
command += ' --tags';
|
|
60
|
+
}
|
|
61
|
+
if (input.all) {
|
|
62
|
+
command += ' --all';
|
|
63
|
+
}
|
|
64
|
+
else if (safeRemote) {
|
|
65
|
+
command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
|
|
66
|
+
}
|
|
67
|
+
// If neither 'all' nor 'remote' is specified, git fetch defaults to 'origin' or configured upstream.
|
|
68
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
69
|
+
// Execute command. Fetch output is primarily on stderr.
|
|
70
|
+
const { stdout, stderr } = await execAsync(command);
|
|
71
|
+
logger.info(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
|
|
72
|
+
logger.info(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
|
|
73
|
+
// Analyze stderr for success/summary
|
|
74
|
+
let message = stderr.trim() || 'Fetch command executed.'; // Use stderr as the primary message
|
|
75
|
+
let summary = undefined;
|
|
76
|
+
// Check for common patterns in stderr
|
|
77
|
+
if (stderr.includes('Updating') || stderr.includes('->') || stderr.includes('new tag') || stderr.includes('new branch')) {
|
|
78
|
+
message = 'Fetch successful.';
|
|
79
|
+
summary = stderr.trim(); // Use the full stderr as summary
|
|
80
|
+
}
|
|
81
|
+
else if (stderr.trim() === '') {
|
|
82
|
+
// Sometimes fetch completes successfully with no output if nothing changed
|
|
83
|
+
message = 'Fetch successful (no changes detected).';
|
|
84
|
+
}
|
|
85
|
+
else if (message.includes('fatal:')) {
|
|
86
|
+
// Should be caught by catch block, but double-check
|
|
87
|
+
logger.error(`Git fetch command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
|
|
88
|
+
// Re-throw as an internal error if not caught below
|
|
89
|
+
throw new Error(`Fetch failed: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
|
|
92
|
+
return { success: true, message, summary };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger.error(`Failed to execute git fetch command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
|
|
96
|
+
const errorMessage = error.stderr || error.stdout || error.message || '';
|
|
97
|
+
// Handle specific error cases
|
|
98
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
99
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
100
|
+
}
|
|
101
|
+
if (errorMessage.includes('resolve host') || errorMessage.includes('Could not read from remote repository') || errorMessage.includes('Connection timed out')) {
|
|
102
|
+
throw new McpError(BaseErrorCode.NETWORK_ERROR, `Failed to connect to remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
103
|
+
}
|
|
104
|
+
if (errorMessage.includes('fatal: ') && errorMessage.includes('couldn\'t find remote ref')) {
|
|
105
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
106
|
+
}
|
|
107
|
+
if (errorMessage.includes('Authentication failed') || errorMessage.includes('Permission denied')) {
|
|
108
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
109
|
+
}
|
|
110
|
+
if (errorMessage.includes('does not appear to be a git repository')) {
|
|
111
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || 'default'}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
112
|
+
}
|
|
113
|
+
// Generic internal error for other failures
|
|
114
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git fetch for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
import { GitFetchInputSchema, fetchGitRemote } from './logic.js';
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
let _getWorkingDirectory;
|
|
7
|
+
let _getSessionId;
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the state accessors needed by the tool registration.
|
|
10
|
+
* This should be called once during server setup.
|
|
11
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
12
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
13
|
+
*/
|
|
14
|
+
export function initializeGitFetchStateAccessors(getWdFn, getSidFn) {
|
|
15
|
+
_getWorkingDirectory = getWdFn;
|
|
16
|
+
_getSessionId = getSidFn;
|
|
17
|
+
logger.info('State accessors initialized for git_fetch tool registration.');
|
|
18
|
+
}
|
|
19
|
+
const TOOL_NAME = 'git_fetch';
|
|
20
|
+
const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repositories. Can fetch specific remotes or all, prune stale branches, and fetch tags.";
|
|
21
|
+
/**
|
|
22
|
+
* Registers the git_fetch tool with the MCP server.
|
|
23
|
+
*
|
|
24
|
+
* @param {McpServer} server - The MCP server instance.
|
|
25
|
+
* @throws {Error} If state accessors are not initialized.
|
|
26
|
+
*/
|
|
27
|
+
export async function registerGitFetchTool(server) {
|
|
28
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
+
throw new Error('State accessors for git_fetch must be initialized before registration.');
|
|
30
|
+
}
|
|
31
|
+
const operation = 'registerGitFetchTool';
|
|
32
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
33
|
+
await ErrorHandler.tryCatch(async () => {
|
|
34
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitFetchInputSchema.shape, // Provide the Zod schema shape
|
|
35
|
+
async (validatedArgs, callContext) => {
|
|
36
|
+
const toolOperation = 'tool:git_fetch';
|
|
37
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
38
|
+
const sessionId = _getSessionId(requestContext);
|
|
39
|
+
const getWorkingDirectoryForSession = () => {
|
|
40
|
+
return _getWorkingDirectory(sessionId);
|
|
41
|
+
};
|
|
42
|
+
const logicContext = {
|
|
43
|
+
...requestContext,
|
|
44
|
+
sessionId: sessionId,
|
|
45
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
46
|
+
};
|
|
47
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
48
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
49
|
+
// Call the core logic function
|
|
50
|
+
const fetchResult = await fetchGitRemote(validatedArgs, logicContext);
|
|
51
|
+
// Format the result as a JSON string within TextContent
|
|
52
|
+
const resultContent = {
|
|
53
|
+
type: 'text',
|
|
54
|
+
// Stringify the entire GitFetchResult object
|
|
55
|
+
text: JSON.stringify(fetchResult, null, 2), // Pretty-print JSON
|
|
56
|
+
contentType: 'application/json',
|
|
57
|
+
};
|
|
58
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully: ${fetchResult.message}`, logicContext);
|
|
59
|
+
// Success is determined by the logic function and included in the result object
|
|
60
|
+
return { content: [resultContent] };
|
|
61
|
+
}, {
|
|
62
|
+
operation: toolOperation,
|
|
63
|
+
context: logicContext,
|
|
64
|
+
input: validatedArgs,
|
|
65
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
69
|
+
}, { operation, context, critical: true });
|
|
70
|
+
}
|
|
71
|
+
;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the git_init tool.
|
|
3
|
+
* Exports the registration function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitInitTool } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitInitInput, GitInitResult } from './logic.js';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { logger } from '../../../utils/logger.js';
|
|
7
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
8
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
// Define the input schema for the git_init tool using Zod
|
|
11
|
+
export const GitInitInputSchema = z.object({
|
|
12
|
+
path: z.string().min(1).describe("The absolute path where the new Git repository should be initialized."),
|
|
13
|
+
initialBranch: z.string().optional().describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."),
|
|
14
|
+
bare: z.boolean().default(false).describe("Create a bare repository (no working directory)."),
|
|
15
|
+
quiet: z.boolean().default(false).describe("Only print error and warning messages; all other output will be suppressed."),
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Executes the 'git init' command to initialize a new Git repository.
|
|
19
|
+
*
|
|
20
|
+
* @param {GitInitInput} input - The validated input object.
|
|
21
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
22
|
+
* @returns {Promise<GitInitResult>} A promise that resolves with the structured init result.
|
|
23
|
+
* @throws {McpError} Throws an McpError if path validation fails or the git command fails unexpectedly.
|
|
24
|
+
*/
|
|
25
|
+
export async function gitInitLogic(input, context) {
|
|
26
|
+
const operation = 'gitInitLogic';
|
|
27
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
28
|
+
let targetPath;
|
|
29
|
+
try {
|
|
30
|
+
// Sanitize the provided absolute path
|
|
31
|
+
targetPath = sanitization.sanitizePath(input.path);
|
|
32
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
33
|
+
// Ensure the target directory exists before trying to init inside it
|
|
34
|
+
// git init creates the directory if it doesn't exist, but we might want to ensure the parent exists
|
|
35
|
+
const parentDir = path.dirname(targetPath);
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(parentDir, fs.constants.W_OK); // Check write access in parent
|
|
38
|
+
}
|
|
39
|
+
catch (accessError) {
|
|
40
|
+
logger.error(`Parent directory check failed for ${targetPath}`, { ...context, operation, error: accessError.message });
|
|
41
|
+
if (accessError.code === 'ENOENT') {
|
|
42
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Parent directory does not exist: ${parentDir}`, { context, operation });
|
|
43
|
+
}
|
|
44
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot access parent directory: ${parentDir}. Error: ${accessError.message}`, { context, operation });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
logger.error('Path validation or sanitization failed', { ...context, operation, error });
|
|
49
|
+
if (error instanceof McpError)
|
|
50
|
+
throw error;
|
|
51
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Construct the git init command
|
|
55
|
+
let command = `git init`;
|
|
56
|
+
if (input.quiet) {
|
|
57
|
+
command += ' --quiet';
|
|
58
|
+
}
|
|
59
|
+
if (input.bare) {
|
|
60
|
+
command += ' --bare';
|
|
61
|
+
}
|
|
62
|
+
if (input.initialBranch) {
|
|
63
|
+
// Use -b for modern Git versions, older might need --initial-branch=
|
|
64
|
+
// Sticking with -b as it's common now. Add quotes around branch name.
|
|
65
|
+
command += ` -b "${input.initialBranch.replace(/"/g, '\\"')}"`;
|
|
66
|
+
}
|
|
67
|
+
// Add the target directory path at the end
|
|
68
|
+
command += ` "${targetPath}"`;
|
|
69
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
70
|
+
const { stdout, stderr } = await execAsync(command);
|
|
71
|
+
if (stderr && !input.quiet) {
|
|
72
|
+
// Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
|
|
73
|
+
logger.warning(`Git init command produced stderr`, { ...context, operation, stderr });
|
|
74
|
+
}
|
|
75
|
+
if (stdout && !input.quiet) {
|
|
76
|
+
logger.info(`Git init command produced stdout`, { ...context, operation, stdout });
|
|
77
|
+
}
|
|
78
|
+
// Verify .git directory exists (or equivalent for bare repo)
|
|
79
|
+
const gitDirPath = input.bare ? targetPath : path.join(targetPath, '.git');
|
|
80
|
+
let gitDirExists = false;
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(gitDirPath);
|
|
83
|
+
gitDirExists = true;
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
logger.warning(`Could not verify existence of ${gitDirPath} after git init`, { ...context, operation });
|
|
87
|
+
}
|
|
88
|
+
const successMessage = stdout.trim() || `Initialized empty Git repository in ${targetPath}`;
|
|
89
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
message: successMessage,
|
|
93
|
+
path: targetPath,
|
|
94
|
+
gitDirExists: gitDirExists
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errorMessage = error.stderr || error.message || '';
|
|
99
|
+
logger.error(`Failed to execute git init command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
100
|
+
// Handle specific error cases
|
|
101
|
+
if (errorMessage.toLowerCase().includes('already exists') && errorMessage.toLowerCase().includes('git repository')) {
|
|
102
|
+
// Reinitializing is often okay, treat as success but mention it.
|
|
103
|
+
logger.info(`Repository already exists, reinitialized: ${targetPath}`, { ...context, operation });
|
|
104
|
+
return {
|
|
105
|
+
success: true, // Treat reinitialization as success
|
|
106
|
+
message: `Reinitialized existing Git repository in ${targetPath}`,
|
|
107
|
+
path: targetPath,
|
|
108
|
+
gitDirExists: true // Assume it exists if reinit message appears
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (errorMessage.toLowerCase().includes('permission denied')) {
|
|
112
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
113
|
+
}
|
|
114
|
+
// Generic internal error for other failures
|
|
115
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
import { GitInitInputSchema, gitInitLogic } from './logic.js';
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
const TOOL_NAME = 'git_init';
|
|
7
|
+
const TOOL_DESCRIPTION = 'Initializes a new Git repository at the specified absolute path. Can optionally set the initial branch name and create a bare repository.';
|
|
8
|
+
/**
|
|
9
|
+
* Registers the git_init tool with the MCP server.
|
|
10
|
+
*
|
|
11
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
* @throws {Error} If registration fails.
|
|
14
|
+
*/
|
|
15
|
+
export const registerGitInitTool = async (server) => {
|
|
16
|
+
const operation = 'registerGitInitTool';
|
|
17
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
18
|
+
await ErrorHandler.tryCatch(async () => {
|
|
19
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitInitInputSchema.shape, // Provide the Zod schema shape
|
|
20
|
+
async (validatedArgs, callContext) => {
|
|
21
|
+
const toolOperation = 'tool:git_init';
|
|
22
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
23
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
|
|
24
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
25
|
+
// Call the core logic function
|
|
26
|
+
const initResult = await gitInitLogic(validatedArgs, requestContext);
|
|
27
|
+
// Format the result as a JSON string within TextContent
|
|
28
|
+
const resultContent = {
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
|
|
31
|
+
contentType: 'application/json',
|
|
32
|
+
};
|
|
33
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
|
|
34
|
+
return { content: [resultContent] };
|
|
35
|
+
}, {
|
|
36
|
+
operation: toolOperation,
|
|
37
|
+
context: requestContext,
|
|
38
|
+
input: validatedArgs,
|
|
39
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
43
|
+
}, { operation, context, critical: true }); // Mark registration as critical
|
|
44
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Define the structure for a single commit entry
|
|
9
|
+
export const CommitEntrySchema = z.object({
|
|
10
|
+
hash: z.string().describe("Full commit hash"),
|
|
11
|
+
authorName: z.string().describe("Author's name"),
|
|
12
|
+
authorEmail: z.string().email().describe("Author's email"),
|
|
13
|
+
timestamp: z.number().int().positive().describe("Commit timestamp (Unix epoch seconds)"),
|
|
14
|
+
subject: z.string().describe("Commit subject line"),
|
|
15
|
+
body: z.string().optional().describe("Commit body (optional)"),
|
|
16
|
+
});
|
|
17
|
+
// Define the input schema for the git_log tool using Zod
|
|
18
|
+
export const GitLogInputSchema = z.object({
|
|
19
|
+
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
|
|
20
|
+
maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
|
|
21
|
+
author: z.string().optional().describe("Limit commits to those matching the specified author pattern."),
|
|
22
|
+
since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
|
|
23
|
+
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
|
+
// Note: We use a fixed pretty format for reliable parsing. Custom formats are not directly supported via input.
|
|
26
|
+
});
|
|
27
|
+
// Delimiters for parsing the custom format
|
|
28
|
+
const FIELD_SEP = '\x1f'; // Unit Separator
|
|
29
|
+
const RECORD_SEP = '\x1e'; // Record Separator
|
|
30
|
+
const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`; // %H=hash, %an=author name, %ae=author email, %at=timestamp, %s=subject, %b=body
|
|
31
|
+
/**
|
|
32
|
+
* Executes the 'git log' command with a specific format and returns structured JSON output.
|
|
33
|
+
*
|
|
34
|
+
* @param {GitLogInput} input - The validated input object.
|
|
35
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
36
|
+
* @returns {Promise<GitLogResult>} A promise that resolves with the structured log result.
|
|
37
|
+
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
38
|
+
*/
|
|
39
|
+
export async function logGitHistory(input, context) {
|
|
40
|
+
const operation = 'logGitHistory';
|
|
41
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
42
|
+
let targetPath;
|
|
43
|
+
try {
|
|
44
|
+
// Resolve and sanitize the target path
|
|
45
|
+
if (input.path && input.path !== '.') {
|
|
46
|
+
targetPath = input.path;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const workingDir = context.getWorkingDirectory();
|
|
50
|
+
if (!workingDir) {
|
|
51
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
52
|
+
}
|
|
53
|
+
targetPath = workingDir;
|
|
54
|
+
}
|
|
55
|
+
targetPath = sanitization.sanitizePath(targetPath);
|
|
56
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
60
|
+
if (error instanceof McpError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// Construct the git log command
|
|
66
|
+
// Use a specific format for reliable parsing
|
|
67
|
+
let command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
|
|
68
|
+
if (input.maxCount) {
|
|
69
|
+
command += ` -n ${input.maxCount}`;
|
|
70
|
+
}
|
|
71
|
+
if (input.author) {
|
|
72
|
+
// Basic sanitization for author string
|
|
73
|
+
const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g, '');
|
|
74
|
+
command += ` --author="${safeAuthor}"`;
|
|
75
|
+
}
|
|
76
|
+
if (input.since) {
|
|
77
|
+
const safeSince = input.since.replace(/[`"$&;*()|<>]/g, '');
|
|
78
|
+
command += ` --since="${safeSince}"`;
|
|
79
|
+
}
|
|
80
|
+
if (input.until) {
|
|
81
|
+
const safeUntil = input.until.replace(/[`"$&;*()|<>]/g, '');
|
|
82
|
+
command += ` --until="${safeUntil}"`;
|
|
83
|
+
}
|
|
84
|
+
if (input.branchOrFile) {
|
|
85
|
+
const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g, '');
|
|
86
|
+
command += ` ${safeBranchOrFile}`; // Add branch or file path at the end
|
|
87
|
+
}
|
|
88
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
89
|
+
// Increase maxBuffer if logs can be large
|
|
90
|
+
const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 10 }); // 10MB buffer
|
|
91
|
+
if (stderr) {
|
|
92
|
+
// Log stderr as warning, as git log might sometimes use it for non-fatal info
|
|
93
|
+
logger.warning(`Git log stderr: ${stderr}`, { ...context, operation });
|
|
94
|
+
}
|
|
95
|
+
// Parse the output
|
|
96
|
+
const commits = [];
|
|
97
|
+
const commitRecords = stdout.split(RECORD_SEP).filter(record => record.trim() !== ''); // Split records and remove empty ones
|
|
98
|
+
for (const record of commitRecords) {
|
|
99
|
+
const fields = record.split(FIELD_SEP);
|
|
100
|
+
if (fields.length >= 5) { // Need at least hash, name, email, timestamp, subject
|
|
101
|
+
try {
|
|
102
|
+
const commitEntry = {
|
|
103
|
+
hash: fields[0],
|
|
104
|
+
authorName: fields[1],
|
|
105
|
+
authorEmail: fields[2],
|
|
106
|
+
timestamp: parseInt(fields[3], 10), // Unix timestamp
|
|
107
|
+
subject: fields[4],
|
|
108
|
+
body: fields[5] || undefined, // Body might be empty
|
|
109
|
+
};
|
|
110
|
+
// Validate parsed entry (optional but recommended)
|
|
111
|
+
CommitEntrySchema.parse(commitEntry);
|
|
112
|
+
commits.push(commitEntry);
|
|
113
|
+
}
|
|
114
|
+
catch (parseError) {
|
|
115
|
+
logger.warning(`Failed to parse commit record field`, { ...context, operation, fieldIndex: fields.findIndex((_, i) => i > 5), recordFragment: record.substring(0, 100), parseError });
|
|
116
|
+
// Decide whether to skip the commit or throw an error
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.warning(`Skipping commit record due to unexpected number of fields (${fields.length})`, { ...context, operation, recordFragment: record.substring(0, 100) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const message = commits.length > 0 ? `${commits.length} commit(s) found.` : 'No commits found matching criteria.';
|
|
124
|
+
logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath, commitCount: commits.length });
|
|
125
|
+
return { success: true, commits, message };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger.error(`Failed to execute git log command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
|
|
129
|
+
const errorMessage = error.stderr || error.stdout || error.message || '';
|
|
130
|
+
// Handle specific error cases
|
|
131
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
132
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
133
|
+
}
|
|
134
|
+
if (errorMessage.includes('fatal: bad revision')) {
|
|
135
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
136
|
+
}
|
|
137
|
+
if (errorMessage.includes('fatal: ambiguous argument')) {
|
|
138
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided (e.g., branch/tag/file conflict): '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
139
|
+
}
|
|
140
|
+
// Check if it's just that no commits were found (which git might treat as an error in some cases, though usually not for log)
|
|
141
|
+
if (errorMessage.includes('does not have any commits yet')) {
|
|
142
|
+
logger.info('Repository has no commits yet.', { ...context, operation, path: targetPath });
|
|
143
|
+
return { success: true, commits: [], message: 'Repository has no commits yet.' };
|
|
144
|
+
}
|
|
145
|
+
// Generic internal error for other failures
|
|
146
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git log for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
import { GitLogInputSchema, logGitHistory } from './logic.js';
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
let _getWorkingDirectory;
|
|
7
|
+
let _getSessionId;
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the state accessors needed by the tool registration.
|
|
10
|
+
* This should be called once during server setup.
|
|
11
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
12
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
13
|
+
*/
|
|
14
|
+
export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
|
|
15
|
+
_getWorkingDirectory = getWdFn;
|
|
16
|
+
_getSessionId = getSidFn;
|
|
17
|
+
logger.info('State accessors initialized for git_log tool registration.');
|
|
18
|
+
}
|
|
19
|
+
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.";
|
|
21
|
+
/**
|
|
22
|
+
* Registers the git_log tool with the MCP server.
|
|
23
|
+
*
|
|
24
|
+
* @param {McpServer} server - The MCP server instance.
|
|
25
|
+
* @throws {Error} If state accessors are not initialized.
|
|
26
|
+
*/
|
|
27
|
+
export async function registerGitLogTool(server) {
|
|
28
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
+
throw new Error('State accessors for git_log must be initialized before registration.');
|
|
30
|
+
}
|
|
31
|
+
const operation = 'registerGitLogTool';
|
|
32
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
33
|
+
await ErrorHandler.tryCatch(async () => {
|
|
34
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitLogInputSchema.shape, // Provide the Zod schema shape
|
|
35
|
+
async (validatedArgs, callContext) => {
|
|
36
|
+
const toolOperation = 'tool:git_log';
|
|
37
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
38
|
+
const sessionId = _getSessionId(requestContext);
|
|
39
|
+
const getWorkingDirectoryForSession = () => {
|
|
40
|
+
return _getWorkingDirectory(sessionId);
|
|
41
|
+
};
|
|
42
|
+
const logicContext = {
|
|
43
|
+
...requestContext,
|
|
44
|
+
sessionId: sessionId,
|
|
45
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
46
|
+
};
|
|
47
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
48
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
49
|
+
// Call the core logic function
|
|
50
|
+
const logResult = await logGitHistory(validatedArgs, logicContext);
|
|
51
|
+
// Format the result (array of commits) as a JSON string within TextContent
|
|
52
|
+
const resultContent = {
|
|
53
|
+
type: 'text',
|
|
54
|
+
// Stringify the entire GitLogResult object which includes the commits array and success flag
|
|
55
|
+
text: JSON.stringify(logResult, null, 2), // Pretty-print JSON
|
|
56
|
+
contentType: 'application/json',
|
|
57
|
+
};
|
|
58
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
59
|
+
// Success is determined by the logic function and included in the result object
|
|
60
|
+
return { content: [resultContent] };
|
|
61
|
+
}, {
|
|
62
|
+
operation: toolOperation,
|
|
63
|
+
context: logicContext,
|
|
64
|
+
input: validatedArgs,
|
|
65
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
69
|
+
}, { operation, context, critical: true });
|
|
70
|
+
}
|
|
71
|
+
;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the git_merge tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitMergeTool, initializeGitMergeStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitMergeInput, GitMergeResult } from './logic.js';
|