@nvhien20vn/git-mcp-server
Version:
118 lines (117 loc) • 6.82 kB
JavaScript
import { exec } from 'child_process';
import fs from 'fs/promises';
import { promisify } from 'util';
import { z } from 'zod';
import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Direct import for types-global
import { logger, sanitization } from '../../../utils/index.js'; // RequestContext (./utils/internal/requestContext.js), logger (./utils/internal/logger.js), sanitization (./utils/security/sanitization.js)
const execAsync = promisify(exec);
// Define the Zod schema for input validation
export const GitSetWorkingDirInputSchema = z.object({
path: z.string().min(1, "Path cannot be empty.").describe("The absolute path to set as the default working directory for the current session. Set this before using other git_* tools."),
validateGitRepo: z.boolean().default(true).describe("Whether to validate that the path is a Git repository"),
initializeIfNotPresent: z.boolean().optional().default(false).describe("If true and the directory is not a Git repository, attempt to initialize it with 'git init'.")
});
/**
* Logic for the git_set_working_dir tool.
* Sets a global working directory path for the current session.
* Validates the path and optionally checks if it's a Git repository.
*
* @param {GitSetWorkingDirInput} input - The validated input arguments.
* @param {RequestContext} context - The request context, potentially containing session ID.
* @returns {Promise<GitSetWorkingDirResult>} The result of the operation.
* @throws {McpError} Throws McpError for validation failures or operational errors.
*/
export async function gitSetWorkingDirLogic(input, context // Assuming context provides session info and setter
) {
const operation = 'gitSetWorkingDirLogic';
logger.info('Executing git_set_working_dir logic', { ...context, operation, inputPath: input.path });
let sanitizedPath;
try {
// Sanitize the path. Must explicitly allow absolute paths for this tool.
// It normalizes and checks for traversal issues.
sanitizedPath = sanitization.sanitizePath(input.path, { allowAbsolute: true }).sanitizedPath;
logger.debug(`Sanitized path: ${sanitizedPath}`, { ...context, operation });
}
catch (error) {
logger.error('Path sanitization failed', error, { ...context, operation });
if (error instanceof McpError)
throw error;
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path provided: ${error.message}`, { context, operation });
}
// Check if the directory exists
try {
const stats = await fs.stat(sanitizedPath);
if (!stats.isDirectory()) {
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a directory: ${sanitizedPath}`, { context, operation });
}
}
catch (error) {
if (error.code === 'ENOENT') {
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Directory does not exist: ${sanitizedPath}`, { context, operation });
}
logger.error('Failed to stat directory', error, { ...context, operation, path: sanitizedPath });
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to access path: ${error.message}`, { context, operation });
}
let isGitRepo = false;
let initializedRepo = false;
try {
const { stdout } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: sanitizedPath });
if (stdout.trim() === 'true') {
isGitRepo = true;
logger.debug('Path is already a Git repository', { ...context, operation, path: sanitizedPath });
}
}
catch (error) {
logger.debug('Path is not a Git repository (rev-parse failed or returned non-true)', { ...context, operation, path: sanitizedPath, error: error.message });
isGitRepo = false;
}
if (!isGitRepo && input.initializeIfNotPresent) {
logger.info(`Path is not a Git repository. Attempting to initialize (initializeIfNotPresent=true) with initial branch 'main'.`, { ...context, operation, path: sanitizedPath });
try {
await execAsync('git init --initial-branch=main', { cwd: sanitizedPath });
initializedRepo = true;
isGitRepo = true; // Now it is a git repo
logger.info('Successfully initialized Git repository with initial branch "main".', { ...context, operation, path: sanitizedPath });
}
catch (initError) {
logger.error('Failed to initialize Git repository', initError, { ...context, operation, path: sanitizedPath });
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize Git repository at ${sanitizedPath}: ${initError.message}`, { context, operation });
}
}
// After potential initialization, if validateGitRepo is true, it must now be a Git repo.
if (input.validateGitRepo && !isGitRepo) {
logger.warning('Path is not a valid Git repository and initialization was not performed or failed.', { ...context, operation, path: sanitizedPath });
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a valid Git repository: ${sanitizedPath}.`, { context, operation });
}
// --- Update Session State ---
// This part needs access to the session state mechanism defined in server.ts
// We assume the context provides a way to set the working directory for the current session.
try {
context.setWorkingDirectory(sanitizedPath);
logger.info(`Working directory set for session ${context.sessionId || 'stdio'} to: ${sanitizedPath}`, { ...context, operation });
}
catch (error) {
logger.error('Failed to set working directory in session state', error, { ...context, operation });
// This indicates an internal logic error in how state is passed/managed.
throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Failed to update session state.', { context, operation });
}
let message = `Working directory set to: ${sanitizedPath}`;
if (initializedRepo) {
message += ' (New Git repository initialized).';
}
else if (isGitRepo && input.validateGitRepo) { // Only state "Existing" if validation was on and it passed
message += ' (Existing Git repository).';
}
else if (isGitRepo && !input.validateGitRepo) { // It is a git repo, but we weren't asked to validate it
message += ' (Is a Git repository, validation skipped).';
}
else if (!isGitRepo && !input.validateGitRepo && !input.initializeIfNotPresent) { // Not a git repo, validation off, no init request
message += ' (Not a Git repository, validation skipped, no initialization requested).';
}
return {
success: true,
message: message,
path: sanitizedPath,
initialized: initializedRepo,
};
}