@nvhien20vn/git-mcp-server
Version:
99 lines (98 loc) • 6.18 kB
JavaScript
import { exec } from 'child_process';
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'; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
const execAsync = promisify(exec);
// Define the input schema for the git_show tool using Zod
// No refinements needed here, so we don't need a separate BaseSchema
export const GitShowInputSchema = z.object({
path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
ref: z.string().min(1).describe("The object reference (commit hash, tag name, branch name, HEAD, etc.) to show."),
filePath: z.string().optional().describe("Optional specific file path within the ref to show (e.g., show a file's content at a specific commit). If provided, use the format '<ref>:<filePath>'."),
// format: z.string().optional().describe("Optional format string for the output"), // Consider adding later
});
/**
* Executes the 'git show' command for a given reference and optional file path.
*
* @param {GitShowInput} input - The validated input object.
* @param {RequestContext} context - The request context for logging and error handling.
* @returns {Promise<GitShowResult>} A promise that resolves with the structured result.
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
*/
export async function gitShowLogic(input, context) {
const operation = 'gitShowLogic';
logger.debug(`Executing ${operation}`, { ...context, input });
let targetPath;
try {
// Resolve and sanitize the target path
const workingDir = context.getWorkingDirectory();
targetPath = (input.path && input.path !== '.')
? input.path
: workingDir ?? '.';
if (targetPath === '.' && !workingDir) {
logger.warning("Executing git show in server's CWD as no path provided and no session WD set.", { ...context, operation });
targetPath = process.cwd();
}
else if (targetPath === '.' && workingDir) {
targetPath = workingDir;
logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
}
else {
logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
}
targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
}
catch (error) {
logger.error('Path resolution or sanitization failed', { ...context, operation, error });
if (error instanceof McpError)
throw error;
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
}
// Validate ref format (simple validation)
if (!/^[a-zA-Z0-9_./~^:-]+$/.test(input.ref)) { // Allow ':' for filePath combination
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid reference format: ${input.ref}`, { context, operation });
}
// Validate filePath format if provided (basic path chars)
if (input.filePath && !/^[a-zA-Z0-9_./-]+$/.test(input.filePath)) {
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid file path format: ${input.filePath}`, { context, operation });
}
try {
// Construct the refspec, combining ref and filePath if needed
const refSpec = input.filePath ? `${input.ref}:"${input.filePath}"` : `"${input.ref}"`;
// Construct the command
const command = `git -C "${targetPath}" show ${refSpec}`;
logger.debug(`Executing command: ${command}`, { ...context, operation });
// Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
// We primarily care about stdout for the content. Errors usually have non-zero exit code.
const { stdout, stderr } = await execAsync(command);
if (stderr) {
// Log stderr as debug info, as it might contain commit details etc.
logger.debug(`Git show command produced stderr (may be informational)`, { ...context, operation, stderr });
}
logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, refSpec });
return { success: true, content: stdout }; // Return raw stdout content
}
catch (error) {
const errorMessage = error.stderr || error.message || '';
logger.error(`Failed to execute git show command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
// Specific error handling
if (errorMessage.toLowerCase().includes('not a git repository')) {
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
}
if (/unknown revision or path not in the working tree/i.test(errorMessage)) {
const target = input.filePath ? `${input.ref}:${input.filePath}` : input.ref;
return { success: false, message: `Failed to show: Reference or pathspec '${target}' not found.`, error: errorMessage };
}
if (/ambiguous argument/i.test(errorMessage)) {
return { success: false, message: `Failed to show: Reference '${input.ref}' is ambiguous. Provide a more specific reference.`, error: errorMessage };
}
// Return structured failure for other git errors
return {
success: false,
message: `Git show failed for path: ${targetPath}, ref: ${input.ref}.`,
error: errorMessage
};
}
}