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