"""Parsing tools for xcsift MCP server.
These tools parse raw xcodebuild/swift output into structured formats.
"""
import asyncio
import json
from typing import Literal
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.session import ServerSession
from xcsift_mcp.xcsift_installer import ensure_xcsift
class BuildError(BaseModel):
"""A build error with location information."""
file: str = Field(description="Source file path")
line: int = Field(description="Line number")
message: str = Field(description="Error message")
class BuildWarning(BaseModel):
"""A build warning with location and type information."""
file: str = Field(description="Source file path")
line: int = Field(description="Line number")
message: str = Field(description="Warning message")
type: str = Field(description="Warning type: compile, runtime, or swiftui")
class FailedTest(BaseModel):
"""A test failure with details."""
test: str = Field(description="Test name")
message: str = Field(description="Failure message")
class LinkerError(BaseModel):
"""A linker error with symbol information."""
symbol: str = Field(description="Symbol name")
architecture: str = Field(description="Target architecture")
referenced_from: str = Field(description="Where the symbol is referenced from")
message: str = Field(description="Error message")
conflicting_files: list[str] = Field(
default_factory=list, description="Files with conflicting definitions"
)
class BuildSummary(BaseModel):
"""Summary of build results."""
errors: int = Field(description="Number of errors")
warnings: int = Field(description="Number of warnings")
failed_tests: int = Field(description="Number of failed tests")
linker_errors: int = Field(description="Number of linker errors")
passed_tests: int | None = Field(default=None, description="Number of passed tests")
build_time: str | None = Field(default=None, description="Build duration")
test_time: str | None = Field(default=None, description="Test duration")
coverage_percent: float | None = Field(default=None, description="Code coverage percentage")
class ParsedBuildOutput(BaseModel):
"""Complete parsed build output."""
status: str = Field(description="Build status: succeeded or failed")
summary: BuildSummary = Field(description="Build summary statistics")
errors: list[BuildError] = Field(default_factory=list, description="List of errors")
warnings: list[BuildWarning] = Field(default_factory=list, description="List of warnings")
failed_tests: list[FailedTest] = Field(
default_factory=list, description="List of failed tests"
)
linker_errors: list[LinkerError] = Field(
default_factory=list, description="List of linker errors"
)
async def _run_xcsift(
output: str,
format: str = "json",
warnings: bool = False,
coverage: bool = False,
xcsift_path: str | None = None,
) -> str:
"""Run xcsift on the given output.
Args:
output: Raw xcodebuild/swift output.
format: Output format (json or toon).
warnings: Include detailed warnings list.
coverage: Include coverage information.
xcsift_path: Path to xcsift binary.
Returns:
Parsed output from xcsift.
"""
if xcsift_path is None:
xcsift_path = await ensure_xcsift()
args = [xcsift_path, "--format", format]
if warnings:
args.append("--warnings")
if coverage:
args.append("--coverage")
proc = await asyncio.create_subprocess_exec(
*args,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate(input=output.encode())
if proc.returncode != 0:
error_msg = stderr.decode() if stderr else "Unknown error"
raise RuntimeError(f"xcsift failed: {error_msg}")
return stdout.decode()
def register_parse_tools(mcp: FastMCP) -> None:
"""Register parsing tools with the MCP server."""
@mcp.tool()
async def parse_xcodebuild_output(
output: str,
format: Literal["json", "toon"] = "json",
include_warnings: bool = False,
include_coverage: bool = False,
ctx: Context[ServerSession, None] | None = None,
) -> str:
"""Parse raw xcodebuild or swift build output into structured format.
This tool takes the raw output from xcodebuild or swift build/test commands
and parses it into a structured format (JSON or TOON) that is optimized
for consumption by AI coding agents.
Args:
output: Raw output from xcodebuild or swift build/test command.
Make sure to capture both stdout and stderr (use 2>&1).
format: Output format - "json" for standard JSON, "toon" for
Token-Oriented Object Notation (30-60% fewer tokens).
include_warnings: If True, include detailed list of warnings.
Otherwise only warning count is shown.
include_coverage: If True, include code coverage information
if available in the output.
Returns:
Parsed build output in the requested format containing:
- status: "succeeded" or "failed"
- summary: Error/warning counts, build times
- errors: List of compiler errors with file/line
- warnings: List of warnings (if include_warnings=True)
- failed_tests: List of failed tests
- linker_errors: List of linker errors
"""
if ctx:
await ctx.info(f"Parsing build output ({len(output)} chars) to {format} format")
xcsift_path = None
if ctx and ctx.request_context.lifespan_context:
xcsift_path = ctx.request_context.lifespan_context.xcsift_path
result = await _run_xcsift(
output=output,
format=format,
warnings=include_warnings,
coverage=include_coverage,
xcsift_path=xcsift_path,
)
return result
@mcp.tool()
async def extract_errors(
output: str,
ctx: Context[ServerSession, None] | None = None,
) -> list[BuildError]:
"""Extract only the errors from xcodebuild/swift output.
This is a convenience tool that parses the build output and returns
only the list of errors with file and line information.
Args:
output: Raw output from xcodebuild or swift build command.
Returns:
List of errors, each with file, line, and message.
"""
if ctx:
await ctx.info("Extracting errors from build output")
xcsift_path = None
if ctx and ctx.request_context.lifespan_context:
xcsift_path = ctx.request_context.lifespan_context.xcsift_path
result = await _run_xcsift(output=output, format="json", xcsift_path=xcsift_path)
parsed = json.loads(result)
errors = []
for error in parsed.get("errors", []):
errors.append(
BuildError(
file=error.get("file", ""),
line=error.get("line", 0),
message=error.get("message", ""),
)
)
return errors
@mcp.tool()
async def extract_warnings(
output: str,
ctx: Context[ServerSession, None] | None = None,
) -> list[BuildWarning]:
"""Extract only the warnings from xcodebuild/swift output.
This is a convenience tool that parses the build output and returns
only the list of warnings with file, line, type, and message.
Args:
output: Raw output from xcodebuild or swift build command.
Returns:
List of warnings, each with file, line, message, and type.
"""
if ctx:
await ctx.info("Extracting warnings from build output")
xcsift_path = None
if ctx and ctx.request_context.lifespan_context:
xcsift_path = ctx.request_context.lifespan_context.xcsift_path
result = await _run_xcsift(
output=output, format="json", warnings=True, xcsift_path=xcsift_path
)
parsed = json.loads(result)
warnings = []
for warning in parsed.get("warnings", []):
warnings.append(
BuildWarning(
file=warning.get("file", ""),
line=warning.get("line", 0),
message=warning.get("message", ""),
type=warning.get("type", "compile"),
)
)
return warnings
@mcp.tool()
async def extract_test_failures(
output: str,
ctx: Context[ServerSession, None] | None = None,
) -> list[FailedTest]:
"""Extract only the test failures from xcodebuild/swift test output.
This is a convenience tool that parses the test output and returns
only the list of failed tests with their failure messages.
Args:
output: Raw output from xcodebuild test or swift test command.
Returns:
List of test failures, each with test name and failure message.
"""
if ctx:
await ctx.info("Extracting test failures from output")
xcsift_path = None
if ctx and ctx.request_context.lifespan_context:
xcsift_path = ctx.request_context.lifespan_context.xcsift_path
result = await _run_xcsift(output=output, format="json", xcsift_path=xcsift_path)
parsed = json.loads(result)
failures = []
for failure in parsed.get("failed_tests", []):
failures.append(
FailedTest(
test=failure.get("test", ""),
message=failure.get("message", ""),
)
)
return failures
@mcp.tool()
async def get_build_summary(
output: str,
ctx: Context[ServerSession, None] | None = None,
) -> BuildSummary:
"""Get a summary of the build results.
This tool provides a quick overview of the build with counts of
errors, warnings, test failures, and timing information.
Args:
output: Raw output from xcodebuild or swift build/test command.
Returns:
BuildSummary with error/warning counts and timing info.
"""
if ctx:
await ctx.info("Getting build summary")
xcsift_path = None
if ctx and ctx.request_context.lifespan_context:
xcsift_path = ctx.request_context.lifespan_context.xcsift_path
result = await _run_xcsift(output=output, format="json", xcsift_path=xcsift_path)
parsed = json.loads(result)
summary = parsed.get("summary", {})
return BuildSummary(
errors=summary.get("errors", 0),
warnings=summary.get("warnings", 0),
failed_tests=summary.get("failed_tests", 0),
linker_errors=summary.get("linker_errors", 0),
passed_tests=summary.get("passed_tests"),
build_time=summary.get("build_time"),
test_time=summary.get("test_time"),
coverage_percent=summary.get("coverage_percent"),
)