@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,136 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import { logger } from '../../../utils/logger.js';
|
|
6
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
7
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
// Define the input schema for the git_clone tool using Zod
|
|
10
|
+
export const GitCloneInputSchema = z.object({
|
|
11
|
+
repositoryUrl: z.string().url("Invalid repository URL format.").describe("The URL of the repository to clone (e.g., https://github.com/cyanheads/git-mcp-server, git@github.com:cyanheads/git-mcp-server.git)."),
|
|
12
|
+
targetPath: z.string().min(1).describe("The absolute path to the directory where the repository should be cloned."),
|
|
13
|
+
branch: z.string().optional().describe("Specify a specific branch to checkout after cloning."),
|
|
14
|
+
depth: z.number().int().positive().optional().describe("Create a shallow clone with a history truncated to the specified number of commits."),
|
|
15
|
+
// recursive: z.boolean().default(false).describe("After the clone is created, initialize all submodules within, using their default settings."), // Consider adding later
|
|
16
|
+
quiet: z.boolean().default(false).describe("Operate quietly. Progress is not reported to the standard error stream."),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Executes the 'git clone' command to clone a repository.
|
|
20
|
+
*
|
|
21
|
+
* @param {GitCloneInput} input - The validated input object.
|
|
22
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
23
|
+
* @returns {Promise<GitCloneResult>} A promise that resolves with the structured clone result.
|
|
24
|
+
* @throws {McpError} Throws an McpError if path/URL validation fails or the git command fails unexpectedly.
|
|
25
|
+
*/
|
|
26
|
+
export async function gitCloneLogic(input, context) {
|
|
27
|
+
const operation = 'gitCloneLogic';
|
|
28
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
29
|
+
let sanitizedTargetPath;
|
|
30
|
+
let sanitizedRepoUrl;
|
|
31
|
+
try {
|
|
32
|
+
// Sanitize the target path (must be absolute)
|
|
33
|
+
sanitizedTargetPath = sanitization.sanitizePath(input.targetPath);
|
|
34
|
+
logger.debug('Sanitized target path', { ...context, operation, sanitizedTargetPath });
|
|
35
|
+
// Basic sanitization/validation for URL (Zod already checks format)
|
|
36
|
+
// Further sanitization might be needed depending on how it's used in the shell command
|
|
37
|
+
// For now, rely on Zod's URL validation and careful command construction.
|
|
38
|
+
sanitizedRepoUrl = input.repositoryUrl; // Assume Zod validation is sufficient for now
|
|
39
|
+
logger.debug('Validated repository URL', { ...context, operation, sanitizedRepoUrl });
|
|
40
|
+
// Check if target directory already exists and is not empty
|
|
41
|
+
try {
|
|
42
|
+
const stats = await fs.stat(sanitizedTargetPath);
|
|
43
|
+
if (stats.isDirectory()) {
|
|
44
|
+
const files = await fs.readdir(sanitizedTargetPath);
|
|
45
|
+
if (files.length > 0) {
|
|
46
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target directory already exists and is not empty: ${sanitizedTargetPath}`, { context, operation });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target path exists but is not a directory: ${sanitizedTargetPath}`, { context, operation });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof McpError)
|
|
55
|
+
throw error; // Re-throw our specific validation errors
|
|
56
|
+
if (error.code !== 'ENOENT') {
|
|
57
|
+
// If error is not "does not exist", it's unexpected
|
|
58
|
+
logger.error(`Error checking target directory ${sanitizedTargetPath}`, { ...context, operation, error: error.message });
|
|
59
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to check target directory: ${error.message}`, { context, operation });
|
|
60
|
+
}
|
|
61
|
+
// ENOENT is expected - directory doesn't exist, which is fine for clone
|
|
62
|
+
logger.debug(`Target directory ${sanitizedTargetPath} does not exist, proceeding with clone.`, { ...context, operation });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
logger.error('Path/URL validation or sanitization failed', { ...context, operation, error });
|
|
67
|
+
if (error instanceof McpError)
|
|
68
|
+
throw error;
|
|
69
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid input: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Construct the git clone command
|
|
73
|
+
// Use placeholders and pass args safely if possible, but exec requires string command. Be careful with quoting.
|
|
74
|
+
let command = `git clone`;
|
|
75
|
+
if (input.quiet) {
|
|
76
|
+
command += ' --quiet';
|
|
77
|
+
}
|
|
78
|
+
if (input.branch) {
|
|
79
|
+
command += ` --branch "${input.branch.replace(/"/g, '\\"')}"`;
|
|
80
|
+
}
|
|
81
|
+
if (input.depth) {
|
|
82
|
+
command += ` --depth ${input.depth}`;
|
|
83
|
+
}
|
|
84
|
+
// Add repo URL and target path (ensure they are quoted)
|
|
85
|
+
command += ` "${sanitizedRepoUrl}" "${sanitizedTargetPath}"`;
|
|
86
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
87
|
+
// Increase timeout for clone operations as they can take time
|
|
88
|
+
const { stdout, stderr } = await execAsync(command, { timeout: 300000 }); // 5 minutes timeout
|
|
89
|
+
if (stderr && !input.quiet) {
|
|
90
|
+
// Stderr often contains progress info, log as info if quiet is false
|
|
91
|
+
logger.info(`Git clone command produced stderr (progress/info)`, { ...context, operation, stderr });
|
|
92
|
+
}
|
|
93
|
+
if (stdout && !input.quiet) {
|
|
94
|
+
logger.info(`Git clone command produced stdout`, { ...context, operation, stdout });
|
|
95
|
+
}
|
|
96
|
+
// Verify the target directory exists after clone
|
|
97
|
+
let repoDirExists = false;
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(sanitizedTargetPath);
|
|
100
|
+
repoDirExists = true;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
logger.error(`Could not verify existence of target directory ${sanitizedTargetPath} after git clone`, { ...context, operation });
|
|
104
|
+
// This indicates a potential failure despite exec not throwing
|
|
105
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Clone command finished but target directory ${sanitizedTargetPath} not found.`, { context, operation });
|
|
106
|
+
}
|
|
107
|
+
const successMessage = `Repository cloned successfully into ${sanitizedTargetPath}`;
|
|
108
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: sanitizedTargetPath });
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
message: successMessage,
|
|
112
|
+
path: sanitizedTargetPath,
|
|
113
|
+
repoDirExists: repoDirExists
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const errorMessage = error.stderr || error.message || '';
|
|
118
|
+
logger.error(`Failed to execute git clone command`, { ...context, operation, path: sanitizedTargetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
119
|
+
// Handle specific error cases
|
|
120
|
+
if (errorMessage.toLowerCase().includes('repository not found') || errorMessage.toLowerCase().includes('could not read from remote repository')) {
|
|
121
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Repository not found or access denied: ${sanitizedRepoUrl}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
122
|
+
}
|
|
123
|
+
if (errorMessage.toLowerCase().includes('already exists and is not an empty directory')) {
|
|
124
|
+
// This should have been caught by our pre-check, but handle defensively
|
|
125
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Target directory already exists and is not empty: ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
126
|
+
}
|
|
127
|
+
if (errorMessage.toLowerCase().includes('permission denied')) {
|
|
128
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied during clone operation for path: ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
129
|
+
}
|
|
130
|
+
if (errorMessage.toLowerCase().includes('timeout')) {
|
|
131
|
+
throw new McpError(BaseErrorCode.TIMEOUT, `Git clone operation timed out for repository: ${sanitizedRepoUrl}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
132
|
+
}
|
|
133
|
+
// Generic internal error for other failures
|
|
134
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to clone repository ${sanitizedRepoUrl} to ${sanitizedTargetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -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 { GitCloneInputSchema, gitCloneLogic } from './logic.js';
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
const TOOL_NAME = 'git_clone';
|
|
7
|
+
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
|
+
/**
|
|
9
|
+
* Registers the git_clone 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 registerGitCloneTool = async (server) => {
|
|
16
|
+
const operation = 'registerGitCloneTool';
|
|
17
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
18
|
+
await ErrorHandler.tryCatch(async () => {
|
|
19
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCloneInputSchema.shape, // Provide the Zod schema shape
|
|
20
|
+
async (validatedArgs, callContext) => {
|
|
21
|
+
const toolOperation = 'tool:git_clone';
|
|
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 cloneResult = await gitCloneLogic(validatedArgs, requestContext);
|
|
27
|
+
// Format the result as a JSON string within TextContent
|
|
28
|
+
const resultContent = {
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: JSON.stringify(cloneResult, 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, // Log sanitized input
|
|
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,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the gitCommit tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { initializeGitCommitStateAccessors, registerGitCommitTool } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitCommitInput, GitCommitResult } from './logic.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
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';
|
|
5
|
+
import { logger } from '../../../utils/logger.js';
|
|
6
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Define the input schema for the git_commit tool using Zod
|
|
9
|
+
export const GitCommitInputSchema = 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 via `git_set_working_dir`, otherwise defaults to the server's current working directory (`.`)."),
|
|
11
|
+
message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
|
|
12
|
+
author: z.object({
|
|
13
|
+
name: z.string().describe('Author name for the commit'),
|
|
14
|
+
email: z.string().email().describe('Author email for the commit'),
|
|
15
|
+
}).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
|
|
16
|
+
allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
|
|
17
|
+
amend: z.boolean().default(false).describe('Amend the previous commit instead of creating a new one'),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Executes the 'git commit' command and returns structured JSON output.
|
|
21
|
+
*
|
|
22
|
+
* @param {GitCommitInput} input - The validated input object.
|
|
23
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
24
|
+
* @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
|
|
25
|
+
* @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
|
|
26
|
+
*/
|
|
27
|
+
export async function commitGitChanges(input, context // Add getter to context
|
|
28
|
+
) {
|
|
29
|
+
const operation = 'commitGitChanges';
|
|
30
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
31
|
+
let targetPath;
|
|
32
|
+
try {
|
|
33
|
+
// Resolve the target path
|
|
34
|
+
if (input.path && input.path !== '.') {
|
|
35
|
+
targetPath = input.path;
|
|
36
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const workingDir = context.getWorkingDirectory();
|
|
40
|
+
if (!workingDir) {
|
|
41
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
42
|
+
}
|
|
43
|
+
targetPath = workingDir;
|
|
44
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
45
|
+
}
|
|
46
|
+
// Sanitize the resolved path
|
|
47
|
+
const sanitizedPath = sanitization.sanitizePath(targetPath);
|
|
48
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath });
|
|
49
|
+
targetPath = sanitizedPath; // Use the sanitized path going forward
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
53
|
+
if (error instanceof McpError) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// Construct the git commit command using the resolved targetPath
|
|
60
|
+
let command = `git -C "${targetPath}" commit -m "${input.message.replace(/"/g, '\\"')}"`; // Escape double quotes
|
|
61
|
+
if (input.allowEmpty) {
|
|
62
|
+
command += ' --allow-empty';
|
|
63
|
+
}
|
|
64
|
+
if (input.amend) {
|
|
65
|
+
command += ' --amend --no-edit';
|
|
66
|
+
}
|
|
67
|
+
if (input.author) {
|
|
68
|
+
// Ensure author details are properly escaped if needed, though exec usually handles this
|
|
69
|
+
command = `git -C "${targetPath}" -c user.name="${input.author.name.replace(/"/g, '\\"')}" -c user.email="${input.author.email.replace(/"/g, '\\"')}" commit -m "${input.message.replace(/"/g, '\\"')}"`;
|
|
70
|
+
if (input.allowEmpty)
|
|
71
|
+
command += ' --allow-empty';
|
|
72
|
+
if (input.amend)
|
|
73
|
+
command += ' --amend --no-edit';
|
|
74
|
+
}
|
|
75
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
76
|
+
const { stdout, stderr } = await execAsync(command);
|
|
77
|
+
// Check stderr first for common non-error messages
|
|
78
|
+
if (stderr) {
|
|
79
|
+
if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
|
|
80
|
+
const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
|
|
81
|
+
logger.info(msg, { ...context, operation, path: targetPath });
|
|
82
|
+
// Use statusMessage
|
|
83
|
+
return { success: true, statusMessage: msg, nothingToCommit: true };
|
|
84
|
+
}
|
|
85
|
+
// Log other stderr as warning but continue, as commit might still succeed
|
|
86
|
+
logger.warning(`Git commit command produced stderr`, { ...context, operation, stderr });
|
|
87
|
+
}
|
|
88
|
+
// Extract commit hash (more robustly)
|
|
89
|
+
let commitHash = undefined;
|
|
90
|
+
const hashMatch = stdout.match(/([a-f0-9]{7,40})/); // Look for typical short or long hash
|
|
91
|
+
if (hashMatch) {
|
|
92
|
+
commitHash = hashMatch[1];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Fallback parsing if needed, or rely on success message
|
|
96
|
+
logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
|
|
97
|
+
}
|
|
98
|
+
// Use statusMessage
|
|
99
|
+
const statusMsg = commitHash
|
|
100
|
+
? `Commit successful: ${commitHash}`
|
|
101
|
+
: `Commit successful (stdout: ${stdout.trim()})`;
|
|
102
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash });
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
statusMessage: statusMsg,
|
|
106
|
+
commitHash: commitHash
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
|
|
111
|
+
const errorMessage = error.stderr || error.message || '';
|
|
112
|
+
// Handle specific error cases first
|
|
113
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
114
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
115
|
+
}
|
|
116
|
+
if (errorMessage.includes('nothing to commit') || errorMessage.includes('no changes added to commit')) {
|
|
117
|
+
// This might happen if git exits with error despite these messages
|
|
118
|
+
const msg = errorMessage.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
|
|
119
|
+
logger.info(msg + ' (caught as error)', { ...context, operation, path: targetPath, errorMessage });
|
|
120
|
+
// Return success=false but indicate the reason using statusMessage
|
|
121
|
+
return { success: false, statusMessage: msg, nothingToCommit: true };
|
|
122
|
+
}
|
|
123
|
+
if (errorMessage.includes('conflicts')) {
|
|
124
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
|
|
125
|
+
}
|
|
126
|
+
// Generic internal error for other failures
|
|
127
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to commit changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
// Import the result type along with the function and input schema
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js'; // Import BaseErrorCode
|
|
6
|
+
import { commitGitChanges, GitCommitInputSchema } from './logic.js';
|
|
7
|
+
let _getWorkingDirectory;
|
|
8
|
+
let _getSessionId;
|
|
9
|
+
/**
|
|
10
|
+
* Initializes the state accessors needed by the tool registration.
|
|
11
|
+
* This should be called once during server setup.
|
|
12
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
13
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
14
|
+
*/
|
|
15
|
+
export function initializeGitCommitStateAccessors(getWdFn, getSidFn) {
|
|
16
|
+
_getWorkingDirectory = getWdFn;
|
|
17
|
+
_getSessionId = getSidFn;
|
|
18
|
+
logger.info('State accessors initialized for git_commit tool registration.');
|
|
19
|
+
}
|
|
20
|
+
const TOOL_NAME = 'git_commit';
|
|
21
|
+
const TOOL_DESCRIPTION = `Commits staged changes to the Git repository index with a descriptive message. Supports author override, amending, and empty commits. Returns a JSON result.
|
|
22
|
+
|
|
23
|
+
**Commit Message Guidance:**
|
|
24
|
+
Write clear, concise commit messages using the Conventional Commits format: \`type(scope): subject\`.
|
|
25
|
+
- \`type\`: feat, fix, docs, style, refactor, test, chore, etc.
|
|
26
|
+
- \`(scope)\`: Optional context (e.g., \`auth\`, \`ui\`, filename).
|
|
27
|
+
- \`subject\`: Imperative, present tense description (e.g., "add login button", not "added login button").
|
|
28
|
+
|
|
29
|
+
**Example Commit Message:**
|
|
30
|
+
\`\`\`
|
|
31
|
+
feat(auth): implement password reset endpoint
|
|
32
|
+
|
|
33
|
+
Adds the /api/auth/reset-password endpoint to allow users
|
|
34
|
+
to reset their password via an email link. Includes input
|
|
35
|
+
validation and rate limiting.
|
|
36
|
+
|
|
37
|
+
Closes #123 (if applicable).
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
**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
|
+
|
|
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.`;
|
|
43
|
+
/**
|
|
44
|
+
* Registers the git_commit tool with the MCP server.
|
|
45
|
+
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
46
|
+
*
|
|
47
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
50
|
+
*/
|
|
51
|
+
export const registerGitCommitTool = async (server) => {
|
|
52
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
53
|
+
throw new Error('State accessors for git_commit must be initialized before registration.');
|
|
54
|
+
}
|
|
55
|
+
const operation = 'registerGitCommitTool';
|
|
56
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
57
|
+
await ErrorHandler.tryCatch(async () => {
|
|
58
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCommitInputSchema.shape, // Provide the Zod schema shape
|
|
59
|
+
async (validatedArgs, callContext) => {
|
|
60
|
+
const toolOperation = 'tool:git_commit';
|
|
61
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
62
|
+
const sessionId = _getSessionId(requestContext);
|
|
63
|
+
const getWorkingDirectoryForSession = () => {
|
|
64
|
+
return _getWorkingDirectory(sessionId);
|
|
65
|
+
};
|
|
66
|
+
const logicContext = {
|
|
67
|
+
...requestContext,
|
|
68
|
+
sessionId: sessionId,
|
|
69
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
70
|
+
};
|
|
71
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
72
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
73
|
+
// Call the core logic function which now returns a GitCommitResult object
|
|
74
|
+
const commitResult = await commitGitChanges(validatedArgs, logicContext);
|
|
75
|
+
// Format the result as a JSON string within TextContent
|
|
76
|
+
const resultContent = {
|
|
77
|
+
type: 'text',
|
|
78
|
+
// Stringify the JSON object for the response content
|
|
79
|
+
text: JSON.stringify(commitResult, null, 2), // Pretty-print JSON
|
|
80
|
+
contentType: 'application/json',
|
|
81
|
+
};
|
|
82
|
+
// Log based on the success flag in the result
|
|
83
|
+
if (commitResult.success) {
|
|
84
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info(`Tool ${TOOL_NAME} completed with non-fatal condition (e.g., nothing to commit), returning JSON`, logicContext);
|
|
88
|
+
}
|
|
89
|
+
// Even if success is false (e.g., nothing to commit), it's not a tool execution error
|
|
90
|
+
return { content: [resultContent] };
|
|
91
|
+
}, {
|
|
92
|
+
operation: toolOperation,
|
|
93
|
+
context: logicContext,
|
|
94
|
+
input: validatedArgs,
|
|
95
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
99
|
+
}, { operation, context, critical: true });
|
|
100
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
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 base input schema without refinement
|
|
9
|
+
const GitDiffInputBaseSchema = 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
|
+
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
|
+
commit2: z.string().optional().describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
|
|
13
|
+
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."),
|
|
14
|
+
file: z.string().optional().describe("Limit the diff output to a specific file path."),
|
|
15
|
+
// Add options like --name-only, --stat, context lines (-U<n>) if needed
|
|
16
|
+
});
|
|
17
|
+
// Export the shape for registration
|
|
18
|
+
export const GitDiffInputShape = GitDiffInputBaseSchema.shape;
|
|
19
|
+
// Define the final schema with refinement for validation during execution
|
|
20
|
+
export const GitDiffInputSchema = GitDiffInputBaseSchema.refine(data => !(data.staged && (data.commit1 || data.commit2)), {
|
|
21
|
+
message: "Cannot use 'staged' option with specific commit references (commit1 or commit2).",
|
|
22
|
+
path: ["staged", "commit1", "commit2"], // Indicate related fields
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Executes the 'git diff' command and returns the diff output.
|
|
26
|
+
*
|
|
27
|
+
* @param {GitDiffInput} input - The validated input object.
|
|
28
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
29
|
+
* @returns {Promise<GitDiffResult>} A promise that resolves with the structured diff result.
|
|
30
|
+
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
31
|
+
*/
|
|
32
|
+
export async function diffGitChanges(input, context) {
|
|
33
|
+
const operation = 'diffGitChanges';
|
|
34
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
35
|
+
let targetPath;
|
|
36
|
+
try {
|
|
37
|
+
// Resolve and sanitize the target path
|
|
38
|
+
if (input.path && input.path !== '.') {
|
|
39
|
+
targetPath = input.path;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const workingDir = context.getWorkingDirectory();
|
|
43
|
+
if (!workingDir) {
|
|
44
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
45
|
+
}
|
|
46
|
+
targetPath = workingDir;
|
|
47
|
+
}
|
|
48
|
+
targetPath = sanitization.sanitizePath(targetPath);
|
|
49
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
53
|
+
if (error instanceof McpError)
|
|
54
|
+
throw error;
|
|
55
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
56
|
+
}
|
|
57
|
+
// Basic sanitization for refs and file path
|
|
58
|
+
const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, '');
|
|
59
|
+
const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, '');
|
|
60
|
+
const safeFile = input.file?.replace(/[`$&;*()|<>]/g, '');
|
|
61
|
+
try {
|
|
62
|
+
// Construct the git diff command
|
|
63
|
+
let command = `git -C "${targetPath}" diff`;
|
|
64
|
+
if (input.staged) {
|
|
65
|
+
command += ' --staged'; // Or --cached
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Add commit references if not doing staged diff
|
|
69
|
+
if (safeCommit1) {
|
|
70
|
+
command += ` ${safeCommit1}`;
|
|
71
|
+
}
|
|
72
|
+
if (safeCommit2) {
|
|
73
|
+
command += ` ${safeCommit2}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Add file path limiter if provided
|
|
77
|
+
if (safeFile) {
|
|
78
|
+
command += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
|
|
79
|
+
}
|
|
80
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
81
|
+
// Execute command. Diff output is primarily on stdout.
|
|
82
|
+
// Increase maxBuffer as diffs can be large.
|
|
83
|
+
const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 20 }); // 20MB buffer
|
|
84
|
+
if (stderr) {
|
|
85
|
+
// Log stderr as warning, as it might contain non-fatal info
|
|
86
|
+
logger.warning(`Git diff stderr: ${stderr}`, { ...context, operation });
|
|
87
|
+
}
|
|
88
|
+
const diffOutput = stdout;
|
|
89
|
+
const message = diffOutput.trim() === '' ? 'No changes found.' : 'Diff generated successfully.';
|
|
90
|
+
logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
|
|
91
|
+
return { success: true, diff: diffOutput, message };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error(`Failed to execute git diff command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
|
|
95
|
+
const errorMessage = error.stderr || error.stdout || error.message || '';
|
|
96
|
+
// Handle specific error cases
|
|
97
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
98
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
99
|
+
}
|
|
100
|
+
if (errorMessage.includes('fatal: bad object') || errorMessage.includes('unknown revision or path not in the working tree')) {
|
|
101
|
+
const invalidRef = input.commit1 || input.commit2 || input.file;
|
|
102
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified: '${invalidRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
103
|
+
}
|
|
104
|
+
if (errorMessage.includes('ambiguous argument')) {
|
|
105
|
+
const ambiguousArg = input.commit1 || input.commit2 || input.file;
|
|
106
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided: '${ambiguousArg}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
107
|
+
}
|
|
108
|
+
// If the command exits with an error but stdout has content, it might still be useful (e.g., diff with conflicts)
|
|
109
|
+
// However, standard 'git diff' usually exits 0 even with differences. Errors typically mean invalid input/repo state.
|
|
110
|
+
// We'll treat most exec errors as failures.
|
|
111
|
+
// Generic internal error for other failures
|
|
112
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git diff for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
// Import the shape and the final schema/types
|
|
5
|
+
import { GitDiffInputShape, diffGitChanges } from './logic.js';
|
|
6
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
7
|
+
let _getWorkingDirectory;
|
|
8
|
+
let _getSessionId;
|
|
9
|
+
/**
|
|
10
|
+
* Initializes the state accessors needed by the tool registration.
|
|
11
|
+
* This should be called once during server setup.
|
|
12
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
13
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
14
|
+
*/
|
|
15
|
+
export function initializeGitDiffStateAccessors(getWdFn, getSidFn) {
|
|
16
|
+
_getWorkingDirectory = getWdFn;
|
|
17
|
+
_getSessionId = getSidFn;
|
|
18
|
+
logger.info('State accessors initialized for git_diff tool registration.');
|
|
19
|
+
}
|
|
20
|
+
const TOOL_NAME = 'git_diff';
|
|
21
|
+
const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree, etc. Can show staged changes or diff specific files. Returns the diff output as plain text.";
|
|
22
|
+
/**
|
|
23
|
+
* Registers the git_diff tool with the MCP server.
|
|
24
|
+
*
|
|
25
|
+
* @param {McpServer} server - The MCP server instance.
|
|
26
|
+
* @throws {Error} If state accessors are not initialized.
|
|
27
|
+
*/
|
|
28
|
+
export async function registerGitDiffTool(server) {
|
|
29
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
30
|
+
throw new Error('State accessors for git_diff must be initialized before registration.');
|
|
31
|
+
}
|
|
32
|
+
const operation = 'registerGitDiffTool';
|
|
33
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
34
|
+
await ErrorHandler.tryCatch(async () => {
|
|
35
|
+
// Use the exported shape for registration
|
|
36
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitDiffInputShape, // Provide the Zod base schema shape
|
|
37
|
+
async (validatedArgs, callContext) => {
|
|
38
|
+
const toolOperation = 'tool:git_diff';
|
|
39
|
+
const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
|
|
40
|
+
const sessionId = _getSessionId(requestContext);
|
|
41
|
+
const getWorkingDirectoryForSession = () => {
|
|
42
|
+
return _getWorkingDirectory(sessionId);
|
|
43
|
+
};
|
|
44
|
+
const logicContext = {
|
|
45
|
+
...requestContext,
|
|
46
|
+
sessionId: sessionId,
|
|
47
|
+
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
48
|
+
};
|
|
49
|
+
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
50
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
51
|
+
// Call the core logic function
|
|
52
|
+
const diffResult = await diffGitChanges(validatedArgs, logicContext);
|
|
53
|
+
// Format the result (the diff string) as plain text within TextContent
|
|
54
|
+
const resultContent = {
|
|
55
|
+
type: 'text',
|
|
56
|
+
// Return the raw diff output directly
|
|
57
|
+
text: diffResult.diff,
|
|
58
|
+
// Indicate the content type is plain text diff
|
|
59
|
+
contentType: 'text/plain; charset=utf-8', // Or 'text/x-diff'
|
|
60
|
+
};
|
|
61
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully: ${diffResult.message}`, logicContext);
|
|
62
|
+
// Success is determined by the logic function
|
|
63
|
+
return { content: [resultContent] };
|
|
64
|
+
}, {
|
|
65
|
+
operation: toolOperation,
|
|
66
|
+
context: logicContext,
|
|
67
|
+
input: validatedArgs,
|
|
68
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
72
|
+
}, { operation, context, critical: true });
|
|
73
|
+
}
|
|
74
|
+
;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the gitFetch tool.
|
|
3
|
+
*/
|
|
4
|
+
export { registerGitFetchTool, initializeGitFetchStateAccessors } from './registration.js';
|
|
5
|
+
// Export types if needed elsewhere, e.g.:
|
|
6
|
+
// export type { GitFetchInput, GitFetchResult } from './logic.js';
|