Skip to main content
Glama

MCP Xcode

by Stefan-Nitu
xcbeautify-parser.ts7.98 kB
/** * Unified parser for xcbeautify output * * This replaces all the complex parsers and handlers since all our output * goes through xcbeautify which already formats it nicely. * * xcbeautify output format: * - ❌ for errors * - ⚠️ for warnings * - ✔ for test passes * - ✖ for test failures */ import { createModuleLogger } from '../../logger.js'; const logger = createModuleLogger('XcbeautifyParser'); export interface Issue { type: 'error' | 'warning'; file?: string; line?: number; column?: number; message: string; rawLine: string; } export interface Test { name: string; passed: boolean; duration?: number; failureReason?: string; } export interface XcbeautifyOutput { errors: Issue[]; warnings: Issue[]; tests: Test[]; buildSucceeded: boolean; testsPassed: boolean; totalTests: number; failedTests: number; } /** * Parse a line with error or warning from xcbeautify */ function parseErrorLine(line: string, isError: boolean): Issue { // Remove the emoji prefix (❌ or ⚠️) and any color codes const cleanLine = line .replace(/^[❌⚠]\s*/, '') // Character class with individual emojis .replace(/^️\s*/, '') // Remove any lingering emoji variation selectors .replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI color codes // Try to extract file:line:column information // Format: /path/to/file.swift:10:15: error message const fileMatch = cleanLine.match(/^([^:]+):(\d+):(\d+):\s*(.*)$/); if (fileMatch) { const [, file, lineStr, columnStr, message] = fileMatch; return { type: isError ? 'error' : 'warning', file, line: parseInt(lineStr, 10), column: parseInt(columnStr, 10), message, rawLine: line }; } // No file information return { type: isError ? 'error' : 'warning', message: cleanLine, rawLine: line }; } /** * Parse test results from xcbeautify output */ function parseTestLine(line: string): Test | null { // Remove color codes const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ''); // Test passed: ✔ testName (0.123 seconds) const passMatch = cleanLine.match(/✔\s+(\w+)\s*\(([0-9.]+)\s+seconds?\)/); if (passMatch) { return { name: passMatch[1], passed: true, duration: parseFloat(passMatch[2]) }; } // Test failed: ✖ testName, failure reason const failMatch = cleanLine.match(/✖\s+(\w+)(?:,\s*(.*))?/); if (failMatch) { return { name: failMatch[1], passed: false, failureReason: failMatch[2] || 'Test failed' }; } // XCTest format: Test Case '-[ClassName testName]' passed/failed (X.XXX seconds) const xcTestMatch = cleanLine.match(/Test Case\s+'-\[(\w+)\s+(\w+)\]'\s+(passed|failed)\s*\(([0-9.]+)\s+seconds\)/); if (xcTestMatch) { return { name: `${xcTestMatch[1]}.${xcTestMatch[2]}`, passed: xcTestMatch[3] === 'passed', duration: parseFloat(xcTestMatch[4]) }; } return null; } /** * Main parser for xcbeautify output */ export function parseXcbeautifyOutput(output: string): XcbeautifyOutput { const lines = output.split('\n'); // Use Maps to deduplicate errors/warnings (for multi-architecture builds) const errorMap = new Map<string, Issue>(); const warningMap = new Map<string, Issue>(); let buildSucceeded = true; let testsPassed = true; let totalTests = 0; let failedTests = 0; const tests: Test[] = []; for (const line of lines) { if (!line.trim()) continue; // Skip xcbeautify header if (line.includes('xcbeautify') || line.startsWith('---') || line.startsWith('Version:')) { continue; } // Parse errors (❌) if (line.includes('❌')) { const error = parseErrorLine(line, true); const key = `${error.file}:${error.line}:${error.column}:${error.message}`; errorMap.set(key, error); buildSucceeded = false; } // Parse warnings (⚠️) else if (line.includes('⚠️')) { const warning = parseErrorLine(line, false); const key = `${warning.file}:${warning.line}:${warning.column}:${warning.message}`; warningMap.set(key, warning); } // Parse test results (✔ or ✖) else if (line.includes('✔') || line.includes('✖')) { const test = parseTestLine(line); if (test) { tests.push(test); totalTests++; if (!test.passed) { failedTests++; testsPassed = false; } } } // Check for build/test failure indicators else if (line.includes('** BUILD FAILED **') || line.includes('BUILD FAILED')) { buildSucceeded = false; } else if (line.includes('** TEST FAILED **') || line.includes('TEST FAILED')) { testsPassed = false; } // Parse test summary: "Executed X tests, with Y failures" else if (line.includes('Executed') && line.includes('test')) { const summaryMatch = line.match(/Executed\s+(\d+)\s+tests?,\s+with\s+(\d+)\s+failures?/); if (summaryMatch) { totalTests = parseInt(summaryMatch[1], 10); failedTests = parseInt(summaryMatch[2], 10); testsPassed = failedTests === 0; } } } // Convert Maps to arrays const result: XcbeautifyOutput = { errors: Array.from(errorMap.values()), warnings: Array.from(warningMap.values()), tests, buildSucceeded, testsPassed, totalTests, failedTests }; // Log summary logger.debug({ errors: result.errors.length, warnings: result.warnings.length, tests: result.tests.length, buildSucceeded: result.buildSucceeded, testsPassed: result.testsPassed }, 'Parsed xcbeautify output'); return result; } /** * Format parsed output for display */ export function formatParsedOutput(parsed: XcbeautifyOutput): string { const lines: string[] = []; // Build status if (!parsed.buildSucceeded) { lines.push(`❌ Build failed with ${parsed.errors.length} error${parsed.errors.length !== 1 ? 's' : ''}`); } else if (parsed.errors.length === 0 && parsed.warnings.length === 0) { lines.push('✅ Build succeeded'); } else { lines.push(`⚠️ Build succeeded with ${parsed.warnings.length} warning${parsed.warnings.length !== 1 ? 's' : ''}`); } // Errors if (parsed.errors.length > 0) { lines.push('\nErrors:'); for (const error of parsed.errors.slice(0, 10)) { // Show first 10 if (error.file) { lines.push(` ❌ ${error.file}:${error.line}:${error.column} - ${error.message}`); } else { lines.push(` ❌ ${error.message}`); } } if (parsed.errors.length > 10) { lines.push(` ... and ${parsed.errors.length - 10} more errors`); } } // Warnings (only show if no errors) if (parsed.errors.length === 0 && parsed.warnings.length > 0) { lines.push('\nWarnings:'); for (const warning of parsed.warnings.slice(0, 5)) { // Show first 5 if (warning.file) { lines.push(` ⚠️ ${warning.file}:${warning.line}:${warning.column} - ${warning.message}`); } else { lines.push(` ⚠️ ${warning.message}`); } } if (parsed.warnings.length > 5) { lines.push(` ... and ${parsed.warnings.length - 5} more warnings`); } } // Test results if (parsed.tests.length > 0) { lines.push('\nTest Results:'); if (parsed.testsPassed) { lines.push(` ✅ All ${parsed.totalTests} tests passed`); } else { lines.push(` ❌ ${parsed.failedTests} of ${parsed.totalTests} tests failed`); // Show failed tests const failedTests = parsed.tests.filter(t => !t.passed); for (const test of failedTests.slice(0, 5)) { lines.push(` ✖ ${test.name}: ${test.failureReason || 'Failed'}`); } if (failedTests.length > 5) { lines.push(` ... and ${failedTests.length - 5} more failures`); } } } return lines.join('\n'); }

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/Stefan-Nitu/mcp-xcode'

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