@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,158 @@
|
|
|
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_remote tool using Zod
|
|
9
|
+
export const GitRemoteInputSchema = 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
|
+
mode: z.enum(['list', 'add', 'remove', 'show']).describe("Operation mode: 'list', 'add', 'remove', 'show'"),
|
|
12
|
+
name: z.string().min(1).optional().describe("Remote name (required for 'add', 'remove', 'show')"),
|
|
13
|
+
url: z.string().url().optional().describe("Remote URL (required for 'add')"),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Executes git remote commands based on the specified mode.
|
|
17
|
+
*
|
|
18
|
+
* @param {GitRemoteInput} input - The validated input object.
|
|
19
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
20
|
+
* @returns {Promise<GitRemoteResult>} A promise that resolves with the structured result.
|
|
21
|
+
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
22
|
+
*/
|
|
23
|
+
export async function gitRemoteLogic(input, context) {
|
|
24
|
+
const operation = `gitRemoteLogic:${input.mode}`;
|
|
25
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
26
|
+
let targetPath;
|
|
27
|
+
try {
|
|
28
|
+
// Resolve and sanitize the target path
|
|
29
|
+
const workingDir = context.getWorkingDirectory();
|
|
30
|
+
targetPath = (input.path && input.path !== '.')
|
|
31
|
+
? input.path
|
|
32
|
+
: workingDir ?? '.'; // Default to '.' if no working dir set and no path provided
|
|
33
|
+
if (targetPath === '.' && !workingDir) {
|
|
34
|
+
logger.warning("Executing git remote in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
35
|
+
// Allow execution in CWD but log it clearly. Consider if an error is more appropriate.
|
|
36
|
+
// For now, let's proceed but be aware.
|
|
37
|
+
targetPath = process.cwd(); // Use actual CWD if '.' was the default
|
|
38
|
+
}
|
|
39
|
+
else if (targetPath === '.' && workingDir) {
|
|
40
|
+
targetPath = workingDir;
|
|
41
|
+
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
|
|
45
|
+
}
|
|
46
|
+
targetPath = sanitization.sanitizePath(targetPath); // Sanitize the final resolved path
|
|
47
|
+
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
|
|
51
|
+
if (error instanceof McpError)
|
|
52
|
+
throw error;
|
|
53
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
let command;
|
|
57
|
+
let result;
|
|
58
|
+
switch (input.mode) {
|
|
59
|
+
case 'list':
|
|
60
|
+
command = `git -C "${targetPath}" remote -v`;
|
|
61
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
62
|
+
const { stdout: listStdout } = await execAsync(command);
|
|
63
|
+
const remotes = [];
|
|
64
|
+
const lines = listStdout.trim().split('\n');
|
|
65
|
+
const remoteMap = new Map();
|
|
66
|
+
lines.forEach(line => {
|
|
67
|
+
const parts = line.split(/\s+/);
|
|
68
|
+
if (parts.length >= 3) {
|
|
69
|
+
const name = parts[0];
|
|
70
|
+
const url = parts[1];
|
|
71
|
+
const type = parts[2].replace(/[()]/g, ''); // Remove parentheses around (fetch) or (push)
|
|
72
|
+
if (!remoteMap.has(name)) {
|
|
73
|
+
remoteMap.set(name, {});
|
|
74
|
+
}
|
|
75
|
+
if (type === 'fetch') {
|
|
76
|
+
remoteMap.get(name).fetchUrl = url;
|
|
77
|
+
}
|
|
78
|
+
else if (type === 'push') {
|
|
79
|
+
remoteMap.get(name).pushUrl = url;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
remoteMap.forEach((urls, name) => {
|
|
84
|
+
// Ensure both URLs are present, defaulting to fetch URL if push is missing (common case)
|
|
85
|
+
remotes.push({
|
|
86
|
+
name,
|
|
87
|
+
fetchUrl: urls.fetchUrl || 'N/A',
|
|
88
|
+
pushUrl: urls.pushUrl || urls.fetchUrl || 'N/A',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
result = { success: true, mode: 'list', remotes };
|
|
92
|
+
break;
|
|
93
|
+
case 'add':
|
|
94
|
+
if (!input.name || !input.url) {
|
|
95
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' and 'url' are required for 'add' mode.", { context, operation });
|
|
96
|
+
}
|
|
97
|
+
// Basic validation for remote name (avoiding shell injection characters)
|
|
98
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
99
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
100
|
+
}
|
|
101
|
+
command = `git -C "${targetPath}" remote add "${input.name}" "${input.url}"`;
|
|
102
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
103
|
+
await execAsync(command);
|
|
104
|
+
result = { success: true, mode: 'add', message: `Remote '${input.name}' added successfully.` };
|
|
105
|
+
break;
|
|
106
|
+
case 'remove':
|
|
107
|
+
if (!input.name) {
|
|
108
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'remove' mode.", { context, operation });
|
|
109
|
+
}
|
|
110
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
111
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
112
|
+
}
|
|
113
|
+
command = `git -C "${targetPath}" remote remove "${input.name}"`;
|
|
114
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
115
|
+
await execAsync(command);
|
|
116
|
+
result = { success: true, mode: 'remove', message: `Remote '${input.name}' removed successfully.` };
|
|
117
|
+
break;
|
|
118
|
+
case 'show':
|
|
119
|
+
if (!input.name) {
|
|
120
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'show' mode.", { context, operation });
|
|
121
|
+
}
|
|
122
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
123
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
124
|
+
}
|
|
125
|
+
command = `git -C "${targetPath}" remote show "${input.name}"`;
|
|
126
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
127
|
+
const { stdout: showStdout } = await execAsync(command);
|
|
128
|
+
result = { success: true, mode: 'show', details: showStdout.trim() };
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
// Should not happen due to Zod validation, but good practice
|
|
132
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
133
|
+
}
|
|
134
|
+
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const errorMessage = error.stderr || error.message || '';
|
|
139
|
+
logger.error(`Failed to execute git remote command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
|
|
140
|
+
// Specific error handling
|
|
141
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
142
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
143
|
+
}
|
|
144
|
+
if (input.mode === 'add' && errorMessage.toLowerCase().includes('already exists')) {
|
|
145
|
+
return { success: false, mode: 'add', message: `Failed to add remote: Remote '${input.name}' already exists.`, error: errorMessage };
|
|
146
|
+
}
|
|
147
|
+
if ((input.mode === 'remove' || input.mode === 'show') && errorMessage.toLowerCase().includes('no such remote')) {
|
|
148
|
+
return { success: false, mode: input.mode, message: `Failed to ${input.mode} remote: Remote '${input.name}' does not exist.`, error: errorMessage };
|
|
149
|
+
}
|
|
150
|
+
// Return structured failure for other git errors
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
mode: input.mode,
|
|
154
|
+
message: `Git remote ${input.mode} failed for path: ${targetPath}.`,
|
|
155
|
+
error: errorMessage
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
2
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
3
|
+
import { logger } from '../../../utils/logger.js';
|
|
4
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
5
|
+
import { GitRemoteInputSchema, gitRemoteLogic } from './logic.js';
|
|
6
|
+
let _getWorkingDirectory;
|
|
7
|
+
let _getSessionId;
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the state accessors needed by the git_remote tool registration.
|
|
10
|
+
* @param getWdFn - Function to get the working directory for a session.
|
|
11
|
+
* @param getSidFn - Function to get the session ID from context.
|
|
12
|
+
*/
|
|
13
|
+
export function initializeGitRemoteStateAccessors(getWdFn, getSidFn) {
|
|
14
|
+
_getWorkingDirectory = getWdFn;
|
|
15
|
+
_getSessionId = getSidFn;
|
|
16
|
+
logger.info('State accessors initialized for git_remote tool registration.');
|
|
17
|
+
}
|
|
18
|
+
const TOOL_NAME = 'git_remote';
|
|
19
|
+
const TOOL_DESCRIPTION = 'Manages remote repositories (list, add, remove, show).';
|
|
20
|
+
/**
|
|
21
|
+
* Registers the git_remote tool with the MCP server.
|
|
22
|
+
*
|
|
23
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
26
|
+
*/
|
|
27
|
+
export const registerGitRemoteTool = async (server) => {
|
|
28
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
+
throw new Error('State accessors for git_remote must be initialized before registration.');
|
|
30
|
+
}
|
|
31
|
+
const operation = 'registerGitRemoteTool';
|
|
32
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
33
|
+
await ErrorHandler.tryCatch(async () => {
|
|
34
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitRemoteInputSchema.shape, // Provide the Zod schema shape
|
|
35
|
+
async (validatedArgs, callContext) => {
|
|
36
|
+
const toolOperation = `tool:${TOOL_NAME}:${validatedArgs.mode}`; // Include mode in operation
|
|
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} (mode: ${validatedArgs.mode})`, logicContext);
|
|
48
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
49
|
+
// Call the core logic function which returns a GitRemoteResult object
|
|
50
|
+
const remoteResult = await gitRemoteLogic(validatedArgs, logicContext);
|
|
51
|
+
// Format the result as a JSON string within TextContent
|
|
52
|
+
const resultContent = {
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: JSON.stringify(remoteResult, null, 2), // Pretty-print JSON
|
|
55
|
+
contentType: 'application/json',
|
|
56
|
+
};
|
|
57
|
+
// Log based on the success flag in the result
|
|
58
|
+
if (remoteResult.success) {
|
|
59
|
+
logger.info(`Tool ${TOOL_NAME} (mode: ${validatedArgs.mode}) executed successfully, returning JSON`, logicContext);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Log specific failure message from the result
|
|
63
|
+
logger.warning(`Tool ${TOOL_NAME} (mode: ${validatedArgs.mode}) failed: ${remoteResult.message}`, { ...logicContext, errorDetails: remoteResult.error });
|
|
64
|
+
}
|
|
65
|
+
// Return the result, whether success or structured failure
|
|
66
|
+
return { content: [resultContent] };
|
|
67
|
+
}, {
|
|
68
|
+
operation: toolOperation,
|
|
69
|
+
context: logicContext,
|
|
70
|
+
input: validatedArgs,
|
|
71
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
75
|
+
}, { operation, context, critical: true }); // Mark registration as critical
|
|
76
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the gitReset tool.
|
|
3
|
+
*/
|
|
4
|
+
export { registerGitResetTool, initializeGitResetStateAccessors } from './registration.js';
|
|
5
|
+
// Export types if needed elsewhere, e.g.:
|
|
6
|
+
// export type { GitResetInput, GitResetResult } from './logic.js';
|
|
@@ -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 reset modes
|
|
9
|
+
const ResetModeEnum = z.enum(['soft', 'mixed', 'hard', 'merge', 'keep']);
|
|
10
|
+
// Define the input schema for the git_reset tool using Zod
|
|
11
|
+
export const GitResetInputSchema = z.object({
|
|
12
|
+
path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
|
|
13
|
+
mode: ResetModeEnum.optional().default('mixed').describe("Reset mode: 'soft' (reset HEAD only), 'mixed' (reset HEAD and index, default), 'hard' (reset HEAD, index, and working tree - USE WITH CAUTION), 'merge', 'keep'."),
|
|
14
|
+
commit: z.string().optional().describe("Commit, branch, or ref to reset to. Defaults to HEAD (useful for unstaging with 'mixed' mode)."),
|
|
15
|
+
// file: z.string().optional().describe("If specified, reset only this file in the index (unstaging). Mode must be 'mixed' or omitted."), // Git reset [<mode>] [<tree-ish>] [--] <paths>… is complex, handle separately if needed
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Executes the 'git reset' command and returns structured JSON output.
|
|
19
|
+
*
|
|
20
|
+
* @param {GitResetInput} input - The validated input object.
|
|
21
|
+
* @param {RequestContext} context - The request context for logging and error handling.
|
|
22
|
+
* @returns {Promise<GitResetResult>} A promise that resolves with the structured reset result.
|
|
23
|
+
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
24
|
+
*/
|
|
25
|
+
export async function resetGitState(input, context) {
|
|
26
|
+
const operation = 'resetGitState';
|
|
27
|
+
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
28
|
+
// Validate input combinations (e.g., file path usage) if refinement wasn't used
|
|
29
|
+
// if (input.file && input.mode && input.mode !== 'mixed') {
|
|
30
|
+
// throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Resetting specific files is only supported with 'mixed' mode (or default).", { context, operation });
|
|
31
|
+
// }
|
|
32
|
+
// if (input.file && input.commit) {
|
|
33
|
+
// throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Cannot specify both a commit and file paths for reset.", { context, operation });
|
|
34
|
+
// }
|
|
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 commit ref
|
|
58
|
+
const safeCommit = input.commit?.replace(/[`$&;*()|<>]/g, '');
|
|
59
|
+
try {
|
|
60
|
+
// Construct the git reset command
|
|
61
|
+
let command = `git -C "${targetPath}" reset`;
|
|
62
|
+
if (input.mode) {
|
|
63
|
+
command += ` --${input.mode}`;
|
|
64
|
+
}
|
|
65
|
+
if (safeCommit) {
|
|
66
|
+
command += ` ${safeCommit}`;
|
|
67
|
+
}
|
|
68
|
+
// Handling file paths requires careful command construction, often without a commit ref.
|
|
69
|
+
// Example: `git reset HEAD -- path/to/file` or `git reset -- path/to/file` (unstages)
|
|
70
|
+
// For simplicity, this initial version focuses on resetting the whole HEAD/index/tree.
|
|
71
|
+
// Add file path logic here if needed, adjusting command structure.
|
|
72
|
+
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
73
|
+
// Execute command. Reset output is often minimal on success, but stderr might indicate issues.
|
|
74
|
+
const { stdout, stderr } = await execAsync(command);
|
|
75
|
+
logger.info(`Git reset stdout: ${stdout}`, { ...context, operation });
|
|
76
|
+
if (stderr) {
|
|
77
|
+
// Log stderr as info, as it often contains the primary status message
|
|
78
|
+
logger.info(`Git reset stderr: ${stderr}`, { ...context, operation });
|
|
79
|
+
}
|
|
80
|
+
// Analyze output (primarily stderr for reset)
|
|
81
|
+
let message = stderr.trim() || stdout.trim() || `Reset successful (mode: ${input.mode || 'mixed'}).`; // Default success message
|
|
82
|
+
let changesSummary = undefined;
|
|
83
|
+
if (stderr.includes('Unstaged changes after reset')) {
|
|
84
|
+
message = `Reset successful (mode: ${input.mode || 'mixed'}).`;
|
|
85
|
+
changesSummary = stderr; // Include the list of unstaged changes
|
|
86
|
+
}
|
|
87
|
+
else if (stderr.match(/HEAD is now at [a-f0-9]+ /)) {
|
|
88
|
+
message = stderr.trim(); // Use the direct message from git
|
|
89
|
+
}
|
|
90
|
+
else if (!stderr && !stdout) {
|
|
91
|
+
// If no output, assume success but provide context
|
|
92
|
+
message = `Reset successful (mode: ${input.mode || 'mixed'}, commit: ${input.commit || 'HEAD'}). No specific output.`;
|
|
93
|
+
}
|
|
94
|
+
logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
|
|
95
|
+
return { success: true, message, changesSummary };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger.error(`Failed to execute git reset command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
|
|
99
|
+
const errorMessage = error.stderr || error.stdout || error.message || '';
|
|
100
|
+
// Handle specific error cases
|
|
101
|
+
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
102
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
103
|
+
}
|
|
104
|
+
if (errorMessage.includes('fatal: bad revision') || errorMessage.includes('unknown revision')) {
|
|
105
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference specified: '${input.commit}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
106
|
+
}
|
|
107
|
+
if (errorMessage.includes('Cannot reset paths') && errorMessage.includes('mode')) {
|
|
108
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode ('${input.mode}') used with file paths. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
109
|
+
}
|
|
110
|
+
if (errorMessage.includes('unmerged paths')) {
|
|
111
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Cannot reset due to unmerged files. Please resolve conflicts first. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
112
|
+
}
|
|
113
|
+
// Generic internal error for other failures
|
|
114
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git reset 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 { GitResetInputSchema, resetGitState } 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 initializeGitResetStateAccessors(getWdFn, getSidFn) {
|
|
15
|
+
_getWorkingDirectory = getWdFn;
|
|
16
|
+
_getSessionId = getSidFn;
|
|
17
|
+
logger.info('State accessors initialized for git_reset tool registration.');
|
|
18
|
+
}
|
|
19
|
+
const TOOL_NAME = 'git_reset';
|
|
20
|
+
const TOOL_DESCRIPTION = "Resets the current HEAD to a specified state. Supports different modes ('soft', 'mixed', 'hard', 'merge', 'keep') to control how the index and working tree are affected. Can reset to a specific commit. USE 'hard' MODE WITH EXTREME CAUTION as it discards local changes.";
|
|
21
|
+
/**
|
|
22
|
+
* Registers the git_reset 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 registerGitResetTool(server) {
|
|
28
|
+
if (!_getWorkingDirectory || !_getSessionId) {
|
|
29
|
+
throw new Error('State accessors for git_reset must be initialized before registration.');
|
|
30
|
+
}
|
|
31
|
+
const operation = 'registerGitResetTool';
|
|
32
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
33
|
+
await ErrorHandler.tryCatch(async () => {
|
|
34
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitResetInputSchema.shape, // Provide the Zod schema shape
|
|
35
|
+
async (validatedArgs, callContext) => {
|
|
36
|
+
const toolOperation = 'tool:git_reset';
|
|
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 resetResult = await resetGitState(validatedArgs, logicContext);
|
|
51
|
+
// Format the result as a JSON string within TextContent
|
|
52
|
+
const resultContent = {
|
|
53
|
+
type: 'text',
|
|
54
|
+
// Stringify the entire GitResetResult object
|
|
55
|
+
text: JSON.stringify(resetResult, null, 2), // Pretty-print JSON
|
|
56
|
+
contentType: 'application/json',
|
|
57
|
+
};
|
|
58
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully: ${resetResult.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_set_working_dir tool.
|
|
3
|
+
* Exports the registration function and potentially other related components.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitSetWorkingDirTool, initializeGitSetWorkingDirStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitSetWorkingDirInput, GitSetWorkingDirResult } from './logic.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
import { logger } from '../../../utils/logger.js';
|
|
7
|
+
import { sanitization } from '../../../utils/sanitization.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
// Define the Zod schema for input validation
|
|
10
|
+
export const GitSetWorkingDirInputSchema = z.object({
|
|
11
|
+
path: z.string().min(1, "Path cannot be empty.").describe("The absolute path to set as the default working directory for the current session."),
|
|
12
|
+
validateGitRepo: z.boolean().default(true).describe("Whether to validate that the path is a Git repository"),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Logic for the git_set_working_dir tool.
|
|
16
|
+
* Sets a global working directory path for the current session.
|
|
17
|
+
* Validates the path and optionally checks if it's a Git repository.
|
|
18
|
+
*
|
|
19
|
+
* @param {GitSetWorkingDirInput} input - The validated input arguments.
|
|
20
|
+
* @param {RequestContext} context - The request context, potentially containing session ID.
|
|
21
|
+
* @returns {Promise<GitSetWorkingDirResult>} The result of the operation.
|
|
22
|
+
* @throws {McpError} Throws McpError for validation failures or operational errors.
|
|
23
|
+
*/
|
|
24
|
+
export async function gitSetWorkingDirLogic(input, context // Assuming context provides session info and setter
|
|
25
|
+
) {
|
|
26
|
+
const operation = 'gitSetWorkingDirLogic';
|
|
27
|
+
logger.info('Executing git_set_working_dir logic', { ...context, operation, inputPath: input.path });
|
|
28
|
+
let sanitizedPath;
|
|
29
|
+
try {
|
|
30
|
+
// Sanitize the path. By default, sanitizePath allows absolute paths.
|
|
31
|
+
// It normalizes and checks for traversal issues.
|
|
32
|
+
sanitizedPath = sanitization.sanitizePath(input.path);
|
|
33
|
+
logger.debug(`Sanitized path: ${sanitizedPath}`, { ...context, operation });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error('Path sanitization failed', error, { ...context, operation });
|
|
37
|
+
if (error instanceof McpError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path provided: ${error.message}`, { context, operation });
|
|
40
|
+
}
|
|
41
|
+
// Check if the directory exists
|
|
42
|
+
try {
|
|
43
|
+
const stats = await fs.stat(sanitizedPath);
|
|
44
|
+
if (!stats.isDirectory()) {
|
|
45
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a directory: ${sanitizedPath}`, { context, operation });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') {
|
|
50
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Directory does not exist: ${sanitizedPath}`, { context, operation });
|
|
51
|
+
}
|
|
52
|
+
logger.error('Failed to stat directory', error, { ...context, operation, path: sanitizedPath });
|
|
53
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to access path: ${error.message}`, { context, operation });
|
|
54
|
+
}
|
|
55
|
+
// Optionally validate if it's a Git repository
|
|
56
|
+
if (input.validateGitRepo) {
|
|
57
|
+
logger.debug('Validating if path is a Git repository', { ...context, operation, path: sanitizedPath });
|
|
58
|
+
try {
|
|
59
|
+
// A common way to check is using 'git rev-parse --is-inside-work-tree'
|
|
60
|
+
// or checking for the existence of a .git directory/file.
|
|
61
|
+
// Using rev-parse is generally more robust.
|
|
62
|
+
const { stdout } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: sanitizedPath });
|
|
63
|
+
if (stdout.trim() !== 'true') {
|
|
64
|
+
// This case should ideally not happen if rev-parse succeeds, but good to check.
|
|
65
|
+
throw new Error('Not a Git repository (rev-parse returned non-true)');
|
|
66
|
+
}
|
|
67
|
+
logger.debug('Path validated as Git repository', { ...context, operation, path: sanitizedPath });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
logger.warning('Path is not a valid Git repository', { ...context, operation, path: sanitizedPath, error: error.message });
|
|
71
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a valid Git repository: ${sanitizedPath}. Error: ${error.message}`, { context, operation });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- Update Session State ---
|
|
75
|
+
// This part needs access to the session state mechanism defined in server.ts
|
|
76
|
+
// We assume the context provides a way to set the working directory for the current session.
|
|
77
|
+
try {
|
|
78
|
+
context.setWorkingDirectory(sanitizedPath);
|
|
79
|
+
logger.info(`Working directory set for session ${context.sessionId || 'stdio'} to: ${sanitizedPath}`, { ...context, operation });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logger.error('Failed to set working directory in session state', error, { ...context, operation });
|
|
83
|
+
// This indicates an internal logic error in how state is passed/managed.
|
|
84
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Failed to update session state.', { context, operation });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
message: `Working directory set to: ${sanitizedPath}`,
|
|
89
|
+
path: sanitizedPath,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ErrorHandler } from '../../../utils/errorHandler.js';
|
|
2
|
+
import { logger } from '../../../utils/logger.js';
|
|
3
|
+
import { requestContextService } from '../../../utils/requestContext.js';
|
|
4
|
+
import { GitSetWorkingDirInputSchema, gitSetWorkingDirLogic } from './logic.js';
|
|
5
|
+
import { BaseErrorCode } from '../../../types-global/errors.js';
|
|
6
|
+
let _setWorkingDirectory;
|
|
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 setFn - Function to set the working directory for a session.
|
|
12
|
+
* @param getFn - Function to get the session ID from context.
|
|
13
|
+
*/
|
|
14
|
+
export function initializeGitSetWorkingDirStateAccessors(setFn, getFn) {
|
|
15
|
+
_setWorkingDirectory = setFn;
|
|
16
|
+
_getSessionId = getFn;
|
|
17
|
+
logger.info('State accessors initialized for git_set_working_dir tool registration.');
|
|
18
|
+
}
|
|
19
|
+
const TOOL_NAME = 'git_set_working_dir';
|
|
20
|
+
const TOOL_DESCRIPTION = "Sets the default working directory for the current session. Subsequent Git tool calls within this session can use '.' for the `path` parameter, which will resolve to this directory. Optionally validates if the path is a Git repository (`validateGitRepo: true`). Returns the result as a JSON object. IMPORTANT: The provided path must be absolute.";
|
|
21
|
+
/**
|
|
22
|
+
* Registers the git_set_working_dir 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 registerGitSetWorkingDirTool(server) {
|
|
28
|
+
if (!_setWorkingDirectory || !_getSessionId) {
|
|
29
|
+
throw new Error('State accessors for git_set_working_dir must be initialized before registration.');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitSetWorkingDirInputSchema.shape, // Pass the shape for SDK validation
|
|
33
|
+
async (validatedArgs, callContext) => {
|
|
34
|
+
const operation = 'tool:git_set_working_dir';
|
|
35
|
+
// Create a request context, potentially inheriting from callContext if it provides relevant info
|
|
36
|
+
const requestContext = requestContextService.createRequestContext({ operation, parentContext: callContext });
|
|
37
|
+
// Get session ID using the accessor function
|
|
38
|
+
const sessionId = _getSessionId(requestContext); // Non-null assertion as we checked initialization
|
|
39
|
+
// Define the session-specific setter function
|
|
40
|
+
const setWorkingDirectoryForSession = (path) => {
|
|
41
|
+
_setWorkingDirectory(sessionId, path); // Non-null assertion
|
|
42
|
+
};
|
|
43
|
+
// Enhance context with session ID and the setter function
|
|
44
|
+
const logicContext = {
|
|
45
|
+
...requestContext,
|
|
46
|
+
sessionId: sessionId,
|
|
47
|
+
setWorkingDirectory: setWorkingDirectoryForSession,
|
|
48
|
+
};
|
|
49
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
50
|
+
// Call the core logic function with validated args and enhanced context
|
|
51
|
+
const result = await gitSetWorkingDirLogic(validatedArgs, logicContext);
|
|
52
|
+
// Format the successful result for the MCP client
|
|
53
|
+
const responseContent = {
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: JSON.stringify(result, null, 2), // Pretty-print JSON result
|
|
56
|
+
contentType: 'application/json',
|
|
57
|
+
};
|
|
58
|
+
logger.info(`Tool ${TOOL_NAME} executed successfully`, { ...logicContext, result });
|
|
59
|
+
return { content: [responseContent] };
|
|
60
|
+
}, {
|
|
61
|
+
operation,
|
|
62
|
+
context: logicContext,
|
|
63
|
+
input: validatedArgs, // Log sanitized input
|
|
64
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code if logic fails unexpectedly
|
|
65
|
+
// toolName: TOOL_NAME, // Removed as it's not part of ErrorHandlerOptions
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
logger.info(`Tool registered: ${TOOL_NAME}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.error(`Failed to register tool: ${TOOL_NAME}`, {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
74
|
+
});
|
|
75
|
+
// Propagate the error to prevent server startup if registration fails
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the git_show tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitShowTool, initializeGitShowStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitShowInput, GitShowResult } from './logic.js';
|