Skip to main content
Glama
blade47

ShadowGit MCP Server

by blade47

git_command

Execute read-only git commands on ShadowGit repositories for debugging and code analysis. Access git history safely without write permissions.

Instructions

Execute a read-only git command on a ShadowGit repository. Only safe, read-only commands are allowed.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
repoYesRepository name (use list_repos to see available repositories)
commandYesGit command to execute (e.g., "log -10", "diff HEAD~1", "status")

Implementation Reference

  • The GitHandler class provides the core implementation of the git_command tool. It validates input arguments, resolves repository paths, executes git commands via GitExecutor, and formats responses including workflow reminders for certain commands.
    export class GitHandler { constructor( private repositoryManager: RepositoryManager, private gitExecutor: GitExecutor ) {} /** * Validate git command arguments */ private isGitCommandArgs(args: unknown): args is GitCommandArgs { return ( typeof args === 'object' && args !== null && 'repo' in args && 'command' in args && typeof (args as GitCommandArgs).repo === 'string' && typeof (args as GitCommandArgs).command === 'string' ); } /** * Handle git_command tool execution */ async handle(args: unknown): Promise<MCPToolResponse> { if (!this.isGitCommandArgs(args)) { return createErrorResponse( "Error: Both 'repo' and 'command' parameters are required.", `Example usage: git_command({repo: "my-project", command: "log --oneline -10"}) git_command({repo: "my-project", command: "diff HEAD~1"}) Use list_repos() to see available repositories.` ); } const repoPath = this.repositoryManager.resolveRepoPath(args.repo); if (!repoPath) { const repos = this.repositoryManager.getRepositories(); return createRepoNotFoundResponse(args.repo, repos); } const output = await this.gitExecutor.execute(args.command, repoPath); // Add workflow reminder for common commands that suggest changes are being planned // Show workflow hints unless disabled const showHints = process.env.SHADOWGIT_HINTS !== '0'; const reminderCommands = ['diff', 'status', 'log', 'blame']; const needsReminder = showHints && reminderCommands.some(cmd => args.command.toLowerCase().includes(cmd)); if (needsReminder) { return createTextResponse( `${output} ${'='.repeat(50)} 📝 **Planning to Make Changes?** ${'='.repeat(50)} **Required Workflow:** 1️⃣ \`start_session({repo: "${args.repo}", description: "your task"})\` 2️⃣ Make your changes 3️⃣ \`checkpoint({repo: "${args.repo}", title: "commit message"})\` 4️⃣ \`end_session({sessionId: "...", commitHash: "..."})\` 💡 **NEXT STEP:** Call \`start_session()\` before editing any files!` ); } return createTextResponse(output); } }
  • Registration of the git_command tool in the MCP server's listTools handler, including name, description, and input schema definition.
    name: 'git_command', description: 'Execute a read-only git command on a ShadowGit repository. Only safe, read-only commands are allowed.', inputSchema: { type: 'object', properties: { repo: { type: 'string', description: 'Repository name (use list_repos to see available repositories)', }, command: { type: 'string', description: 'Git command to execute (e.g., "log -10", "diff HEAD~1", "status")', }, }, required: ['repo', 'command'], }, },
  • Dispatch to GitHandler in the MCP server's callTool request handler for git_command execution.
    return await this.gitHandler.handle(args);
  • TypeScript interface defining the input arguments for the git_command tool.
    export interface GitCommandArgs { repo: string; command: string; }
  • GitExecutor class provides secure git command execution used by the git_command handler, with safety checks, whitelisting, and sandboxing.
    async execute( command: string | string[], repoPath: string, isInternal = false, additionalEnv?: NodeJS.ProcessEnv ): Promise<string> { // Parse command into arguments let args: string[]; if (Array.isArray(command)) { // Array-based command (safer for internal use) args = command; } else { // String command - check length only for external calls if (!isInternal && command.length > MAX_COMMAND_LENGTH) { return `Error: Command too long (max ${MAX_COMMAND_LENGTH} characters).`; } // Remove control characters const sanitizedCommand = command.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // Simple argument parsing that handles quotes and all whitespace args = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < sanitizedCommand.length; i++) { const char = sanitizedCommand[i]; const nextChar = sanitizedCommand[i + 1]; if (!inQuotes && (char === '"' || char === "'")) { inQuotes = true; quoteChar = char; } else if (inQuotes && char === '\\' && nextChar === quoteChar) { // Handle escaped quote current += quoteChar; i++; // Skip the quote } else if (inQuotes && char === quoteChar) { inQuotes = false; quoteChar = ''; } else if (!inQuotes && /\s/.test(char)) { // Split on any whitespace (space, tab, etc.) if (current) { args.push(current); current = ''; } } else { current += char; } } if (current) { args.push(current); } } if (args.length === 0) { return 'Error: No command provided.'; } const gitCommand = args[0]; // Safety check 1: ALWAYS block dangerous arguments for (const arg of args) { if (isDangerousArg(arg)) { return 'Error: Command contains potentially dangerous arguments.'; } } // Safety check 2: Only check command whitelist for external calls if (!isInternal && !SAFE_COMMANDS.has(gitCommand)) { return `Error: Command '${gitCommand}' is not allowed. Only read-only commands are permitted. Allowed commands: ${Array.from(SAFE_COMMANDS).join(', ')}`; } // Safety check 3: Ensure we're operating on a .shadowgit.git repository const gitDir = path.join(repoPath, SHADOWGIT_DIR); if (!fs.existsSync(gitDir)) { return `Error: Not a ShadowGit repository. The .shadowgit.git directory was not found at ${gitDir}`; } log('debug', `Executing git ${gitCommand} in ${repoPath}`); try { const output = execFileSync('git', [ `--git-dir=${gitDir}`, `--work-tree=${repoPath}`, ...args ], { cwd: repoPath, encoding: 'utf-8', timeout: TIMEOUT_MS, maxBuffer: MAX_BUFFER_SIZE, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', // Disable interactive prompts GIT_SSH_COMMAND: 'ssh -o BatchMode=yes', // Disable SSH prompts GIT_PAGER: 'cat', // Disable pager PAGER: 'cat', // Fallback pager disable ...additionalEnv } }); return output || '(empty output)'; } catch (error: unknown) { if (error && typeof error === 'object') { const execError = error as any; // Check for timeout if (execError.code === 'ETIMEDOUT' || execError.signal === 'SIGTERM') { return `Error: Command timed out after ${TIMEOUT_MS}ms.`; } // Check for detailed error info (has stderr/stdout or status code) if ('stderr' in execError || 'stdout' in execError || 'status' in execError) { const stderr = execError.stderr?.toString() || ''; const stdout = execError.stdout?.toString() || ''; const message = execError.message || 'Unknown error'; return `Error executing git command: ${message} ${stderr ? `\nError output:\n${stderr}` : ''} ${stdout ? `\nPartial output:\n${stdout}` : ''}`; } } return `Error: ${error}`; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/blade47/shadowgit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server