mcpxcodebuild
by ShenghaiWang
- src
- mcpxcodebuild
from mcp.server.lowlevel import Server
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
ErrorData,
TextContent,
Tool,
Annotations,
Field,
Annotated,
INVALID_PARAMS,
)
from pydantic import BaseModel
import subprocess
import os, json
from mcp.shared.exceptions import McpError
def find_xcode_project():
for root, dirs, files in os.walk("."):
dirs.sort(reverse = True)
for dir in dirs:
if dir.endswith(".xcworkspace") or dir.endswith(".xcodeproj"):
return os.path.join(root, dir)
return None
def find_scheme(project_type: str, project_name: str) -> str:
schemes_result = subprocess.run(["xcodebuild",
"-list",
project_type,
project_name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False).stdout.decode("utf-8")
schemes_lines = schemes_result.splitlines()
schemes = []
in_schemes_section = False
for line in schemes_lines:
if "Schemes:" in line:
in_schemes_section = True
continue
if in_schemes_section:
scheme = line.strip()
if scheme:
schemes.append(scheme)
if schemes:
return schemes[0]
else:
return ""
def find_available_simulator() -> str:
devices_result = subprocess.run(["xcrun", "simctl", "list", "devices", "--json"], stdout=subprocess.PIPE, check=False)
devices_json = json.loads(devices_result.stdout.decode("utf-8"))
for runtime_id, devices in devices_json["devices"].items():
if "iOS" in runtime_id:
for device in devices:
if device["isAvailable"]:
return f'platform=iOS Simulator,name={device["name"]},OS={runtime_id.split(".")[-1].replace("iOS-", "").replace("-", ".")}'
return ""
class Folder(BaseModel):
"""Parameters"""
folder: Annotated[str, Field(description="The full path of the current folder that the iOS Xcode workspace/project sits")]
server = Server("build")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name = "build",
description = "Build the iOS Xcode workspace/project in the folder",
inputSchema = Folder.model_json_schema(),
),
Tool(
name="test",
description="Run test for the iOS Xcode workspace/project in the folder",
inputSchema=Folder.model_json_schema(),
)
]
@server.call_tool()
async def call_tool(name, arguments: dict) -> list[TextContent]:
try:
args = Folder(**arguments)
except ValueError as e:
raise McpError(ErrorData(code=INVALID_PARAMS, message=str(e)))
os.chdir(args.folder)
xcode_project_path = find_xcode_project()
project_name = os.path.basename(xcode_project_path)
project_type = ""
if xcode_project_path.endswith(".xcworkspace"):
project_type = "-workspace"
else:
project_type = "-project"
scheme = find_scheme(project_type, project_name)
destination = find_available_simulator()
command = ["xcodebuild",
project_type,
project_name,
"-scheme",
scheme,
"-destination",
destination]
if name == "test":
command.append("test")
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False).stdout
lines = result.decode("utf-8").splitlines()
error_lines = [line for line in lines if "error:" in line.lower()]
error_message = "\n".join(error_lines)
if not error_message:
error_message = "Successful"
return [
TextContent(type="text", text=f"Command: {' '.join(command)}"),
TextContent(type="text", text=f"{error_message}")
]
async def run():
options = server.create_initialization_options()
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
options,
raise_exceptions=True,
)
if __name__ == "__main__":
import asyncio
asyncio.run(run())