Skip to main content
Glama
mattyatea

Git Conflict MCP

by mattyatea

list_conflicts

Identify files with Git merge conflicts, display conflict types, and provide resolution suggestions to streamline conflict resolution workflows.

Instructions

List files with git conflicts including conflict types. Returns a map of ID to file info with conflict type and suggested resolution. (Rate limit: 2 calls per minute). IMPORTANT: You must run init_project before using this tool.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pageNoPage number (1-based). Default is 1.
extensionNoFilter by file extension (e.g. 'ts', '.ts').
pathNoFilter by file path (substring match).
showTypesNoShow detailed conflict types and resolution suggestions. Default is true.

Implementation Reference

  • The core handler function for the 'list_conflicts' tool. It handles rate limiting, fetches conflicted files, applies pagination and filters, generates IDs and suggestions, excludes pending resolves, and returns a JSON map of ID to file info.
    async ({ page, extension, path, showTypes }) => {
        if (!rateLimiter.check("list_conflicts", 2, 60 * 1000)) {
            return { content: [{ type: "text", text: "Please fix the conflicted files that have already been provided." }], isError: true };
        }
    
        const pageNum = page || 1;
        const pageSize = 20;
        const includeTypes = showTypes !== false; // Default to true
    
        try {
            const allConflicts = await getConflictedFilesWithStatus();
    
            // Assign consistent IDs based on file path
            const conflictsWithIds = allConflicts.map((conflict) => ({
                ...conflict,
                id: generateId(conflict.file)
            }));
    
            // Fetch pending resolves (local or external)
            const pendingResolves = await getPendingResolves();
            const pendingPaths = new Set(pendingResolves.map(p => p.filePath));
    
            // Filter out conflicts that are already in the pending list
            let filteredConflicts = conflictsWithIds.filter(c => !pendingPaths.has(c.file));
    
            // Apply filters
            if (extension) {
                const ext = extension.startsWith('.') ? extension : `.${extension}`;
                filteredConflicts = filteredConflicts.filter(c => c.file.endsWith(ext));
            }
    
            if (path) {
                filteredConflicts = filteredConflicts.filter(c => c.file.includes(path));
            }
    
            const start = (pageNum - 1) * pageSize;
            const end = start + pageSize;
            const slice = filteredConflicts.slice(start, end);
    
            const result: Record<string, any> = {};
    
            if (includeTypes) {
                // Include detailed information with conflict types
                slice.forEach((item) => {
                    const suggestion = getResolutionSuggestion(item.status);
                    const rejectionReason = state.getRejection(item.file);
    
                    result[item.id] = {
                        file: item.file,
                        conflictType: item.conflictType,
                        fileSize: item.fileSize !== undefined ? `${item.fileSize} bytes` : "N/A (file deleted)",
                        suggestion: suggestion,
                        ...(rejectionReason ? { previousRejectionReason: rejectionReason } : {})
                    };
                });
            } else {
                // Simple format: just ID to file path
                slice.forEach((item) => {
                    result[item.id] = item.file;
                });
            }
    
            if (filteredConflicts.length > end) {
                result["isMoreConflict"] = "true";
            }
    
            return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
        } catch (e: any) {
            return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
        }
    }
  • Key helper function that retrieves detailed conflict information (status, type, size) by running 'git status --porcelain' and parsing the output. Used by the handler.
    export async function getConflictedFilesWithStatus(): Promise<ConflictInfo[]> {
      try {
        const output = await runGit(["status", "--porcelain"]);
        const lines = output.split("\n").map(s => s.trim()).filter(s => s.length > 0);
    
        const conflicts: ConflictInfo[] = [];
        const projectPath = state.getProjectPath();
        if (!projectPath) {
          throw new Error("Project not initialized. Call init_project first.");
        }
    
        for (const line of lines) {
          // Format: XY filename
          // X = index status, Y = working tree status
          const status = line.substring(0, 2);
          const file = line.substring(3);
    
          // Only include unmerged files (conflicts)
          if (["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(status)) {
            let conflictType: string;
            switch (status) {
              case "DD":
                conflictType = "both deleted";
                break;
              case "AU":
                conflictType = "added by us";
                break;
              case "UD":
                conflictType = "deleted by them";
                break;
              case "UA":
                conflictType = "added by them";
                break;
              case "DU":
                conflictType = "deleted by us";
                break;
              case "AA":
                conflictType = "both added";
                break;
              case "UU":
                conflictType = "both modified";
                break;
              default:
                conflictType = "unknown";
            }
    
            // Get file size if file exists
            let fileSize: number | undefined;
            try {
              const filePath = join(projectPath, file);
              const stats = await stat(filePath);
              fileSize = stats.size;
            } catch (e) {
              // File doesn't exist (deleted), fileSize remains undefined
              fileSize = undefined;
            }
    
            conflicts.push({ file, status, conflictType, fileSize });
          }
        }
    
        return conflicts.sort((a, b) => a.file.localeCompare(b.file));
      } catch (e) {
        throw e;
      }
    }
  • Zod input schema defining optional parameters for pagination, filtering by extension/path, and showing types/suggestions.
    inputSchema: z.object({
        page: z.number().optional().describe("Page number (1-based). Default is 1."),
        extension: z.string().optional().describe("Filter by file extension (e.g. 'ts', '.ts')."),
        path: z.string().optional().describe("Filter by file path (substring match)."),
        showTypes: z.boolean().optional().describe("Show detailed conflict types and resolution suggestions. Default is true."),
    }),
  • Function that registers the 'list_conflicts' tool with the MCP server, including name, description, schema, and handler.
    export function registerListConflicts(server: McpServer) {
        server.registerTool(
            "list_conflicts",
            {
                description: "List files with git conflicts including conflict types. Returns a map of ID to file info with conflict type and suggested resolution. (Rate limit: 2 calls per minute). IMPORTANT: You must run init_project before using this tool.",
                inputSchema: z.object({
                    page: z.number().optional().describe("Page number (1-based). Default is 1."),
                    extension: z.string().optional().describe("Filter by file extension (e.g. 'ts', '.ts')."),
                    path: z.string().optional().describe("Filter by file path (substring match)."),
                    showTypes: z.boolean().optional().describe("Show detailed conflict types and resolution suggestions. Default is true."),
                }),
            },
            async ({ page, extension, path, showTypes }) => {
                if (!rateLimiter.check("list_conflicts", 2, 60 * 1000)) {
                    return { content: [{ type: "text", text: "Please fix the conflicted files that have already been provided." }], isError: true };
                }
    
                const pageNum = page || 1;
                const pageSize = 20;
                const includeTypes = showTypes !== false; // Default to true
    
                try {
                    const allConflicts = await getConflictedFilesWithStatus();
    
                    // Assign consistent IDs based on file path
                    const conflictsWithIds = allConflicts.map((conflict) => ({
                        ...conflict,
                        id: generateId(conflict.file)
                    }));
    
                    // Fetch pending resolves (local or external)
                    const pendingResolves = await getPendingResolves();
                    const pendingPaths = new Set(pendingResolves.map(p => p.filePath));
    
                    // Filter out conflicts that are already in the pending list
                    let filteredConflicts = conflictsWithIds.filter(c => !pendingPaths.has(c.file));
    
                    // Apply filters
                    if (extension) {
                        const ext = extension.startsWith('.') ? extension : `.${extension}`;
                        filteredConflicts = filteredConflicts.filter(c => c.file.endsWith(ext));
                    }
    
                    if (path) {
                        filteredConflicts = filteredConflicts.filter(c => c.file.includes(path));
                    }
    
                    const start = (pageNum - 1) * pageSize;
                    const end = start + pageSize;
                    const slice = filteredConflicts.slice(start, end);
    
                    const result: Record<string, any> = {};
    
                    if (includeTypes) {
                        // Include detailed information with conflict types
                        slice.forEach((item) => {
                            const suggestion = getResolutionSuggestion(item.status);
                            const rejectionReason = state.getRejection(item.file);
    
                            result[item.id] = {
                                file: item.file,
                                conflictType: item.conflictType,
                                fileSize: item.fileSize !== undefined ? `${item.fileSize} bytes` : "N/A (file deleted)",
                                suggestion: suggestion,
                                ...(rejectionReason ? { previousRejectionReason: rejectionReason } : {})
                            };
                        });
                    } else {
                        // Simple format: just ID to file path
                        slice.forEach((item) => {
                            result[item.id] = item.file;
                        });
                    }
    
                    if (filteredConflicts.length > end) {
                        result["isMoreConflict"] = "true";
                    }
    
                    return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
                } catch (e: any) {
                    return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
                }
            }
        );
    }
  • Invocation of registerListConflicts within the main registerTools function to include the tool in the server.
    registerListConflicts(server);

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/mattyatea/git-conflict-mcp'

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