Files
CxIDE/Agent/Tools/XcodeTools.swift
T
cx-git-agent c118996746 feat: CxIDE v1 — native macOS SwiftUI IDE with agentic AI assistant
- SwiftUI macOS app with C++17 code analysis engine (ObjC++ bridge)
- Agentic AI loop: LLM plans → tool calls → execution → feedback loop
- 15 agent tools: file ops, terminal, git, xcode build, code intel
- 7 persistent terminal tools with background session management
- Chat sidebar with agent step rendering and auto-apply
- NVIDIA NIM API integration (Llama 3.3 70B default)
- OpenAI tool_calls format with prompt-based fallback
- Code editor with syntax highlighting and multi-tab support
- File tree, console view, terminal view
- Git integration and workspace management
2026-04-21 16:05:52 -05:00

293 lines
14 KiB
Swift

// XcodeTools.swift
// CxSwiftAgent Xcode Integration MCP Tools
//
// 10 tools: xcode_build, xcode_test, xcode_analyze, xcode_schemes,
// xcode_devices, xcode_swift_check, xcode_clean, xcode_archive,
// xcode_provisioning, xcode_swift_format
import Foundation
enum XcodeTools {
static func register(on server: MCPServer, config: AgentConfig, memory: AgentMemory) {
// xcode_build
server.registerTool(
"xcode_build",
description: "Build an Xcode project or Swift package.",
inputSchema: [
"type": "object",
"properties": [
"scheme": ["type": "string", "description": "Xcode scheme name."],
"project": ["type": "string", "description": "Project/workspace path."],
"configuration": ["type": "string", "description": "Build configuration (Debug/Release, default: Debug)."],
"destination": ["type": "string", "description": "Build destination."],
"swift_package": ["type": "boolean", "description": "Build as Swift Package (default: false)."],
] as [String: Any],
] as [String: Any]
) { args in
if args["swift_package"] as? Bool == true {
return runCommand("swift", ["build"], in: config.workspaceRoot)
}
let xcArgs = buildXcodebuildCmd(args, action: "build", config: config)
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_test
server.registerTool(
"xcode_test",
description: "Run tests for an Xcode project or Swift package.",
inputSchema: [
"type": "object",
"properties": [
"scheme": ["type": "string", "description": "Xcode scheme name."],
"project": ["type": "string", "description": "Project/workspace path."],
"test_plan": ["type": "string", "description": "Test plan name."],
"destination": ["type": "string", "description": "Test destination."],
"swift_package": ["type": "boolean", "description": "Test as Swift Package (default: false)."],
] as [String: Any],
] as [String: Any]
) { args in
if args["swift_package"] as? Bool == true {
return runCommand("swift", ["test"], in: config.workspaceRoot)
}
var xcArgs = buildXcodebuildCmd(args, action: "test", config: config)
if let plan = args["test_plan"] as? String {
xcArgs += ["-testPlan", plan]
}
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_analyze
server.registerTool(
"xcode_analyze",
description: "Run Xcode static analyzer.",
inputSchema: [
"type": "object",
"properties": [
"scheme": ["type": "string", "description": "Xcode scheme name."],
"project": ["type": "string", "description": "Project/workspace path."],
] as [String: Any],
] as [String: Any],
annotations: ToolAnnotations(readOnlyHint: true)
) { args in
let xcArgs = buildXcodebuildCmd(args, action: "analyze", config: config)
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_schemes
server.registerTool(
"xcode_schemes",
description: "List available Xcode schemes.",
inputSchema: [
"type": "object",
"properties": [
"project": ["type": "string", "description": "Project/workspace path."],
] as [String: Any],
] as [String: Any],
annotations: ToolAnnotations(readOnlyHint: true)
) { args in
var xcArgs = ["-list"]
if let project = args["project"] as? String {
if project.hasSuffix(".xcworkspace") {
xcArgs += ["-workspace", project]
} else {
xcArgs += ["-project", project]
}
}
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_devices
server.registerTool(
"xcode_devices",
description: "List available simulators and devices.",
inputSchema: ["type": "object", "properties": [:] as [String: Any]] as [String: Any],
annotations: ToolAnnotations(readOnlyHint: true)
) { _ in
return runCommand("xcrun", ["simctl", "list", "devices", "--json"], in: config.workspaceRoot)
}
// xcode_swift_check
server.registerTool(
"xcode_swift_check",
description: "Type-check Swift source file without building.",
inputSchema: [
"type": "object",
"required": ["path"],
"properties": [
"path": ["type": "string", "description": "Swift source file path."],
] as [String: Any],
] as [String: Any],
annotations: ToolAnnotations(readOnlyHint: true)
) { args in
guard let path = config.resolvePath(args["path"] as? String ?? "") else {
return err("Path not allowed")
}
return runCommand("swiftc", ["-typecheck", path], in: config.workspaceRoot)
}
// xcode_clean
server.registerTool(
"xcode_clean",
description: "Clean Xcode build products.",
inputSchema: [
"type": "object",
"properties": [
"scheme": ["type": "string", "description": "Xcode scheme name."],
"project": ["type": "string", "description": "Project/workspace path."],
"swift_package": ["type": "boolean", "description": "Clean Swift Package (default: false)."],
] as [String: Any],
] as [String: Any],
annotations: ToolAnnotations(destructiveHint: true)
) { args in
if args["swift_package"] as? Bool == true {
return runCommand("swift", ["package", "clean"], in: config.workspaceRoot)
}
let xcArgs = buildXcodebuildCmd(args, action: "clean", config: config)
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_archive
server.registerTool(
"xcode_archive",
description: "Create an Xcode archive for distribution.",
inputSchema: [
"type": "object",
"required": ["scheme"],
"properties": [
"scheme": ["type": "string", "description": "Xcode scheme name."],
"project": ["type": "string", "description": "Project/workspace path."],
"archive_path": ["type": "string", "description": "Output archive path."],
] as [String: Any],
] as [String: Any]
) { args in
if config.sandboxMode { return err("Archive disabled in sandbox mode") }
var xcArgs = buildXcodebuildCmd(args, action: "archive", config: config)
if let archivePath = args["archive_path"] as? String {
xcArgs += ["-archivePath", archivePath]
}
return runCommand(config.xcodePath, xcArgs, in: config.workspaceRoot)
}
// xcode_provisioning
server.registerTool(
"xcode_provisioning",
description: "List installed provisioning profiles.",
inputSchema: ["type": "object", "properties": [:] as [String: Any]] as [String: Any],
annotations: ToolAnnotations(readOnlyHint: true)
) { _ in
let profileDir = NSHomeDirectory() + "/Library/MobileDevice/Provisioning Profiles"
let fm = FileManager.default
guard let files = try? fm.contentsOfDirectory(atPath: profileDir) else {
return ok("No provisioning profiles directory found")
}
var output = "Provisioning Profiles:\n"
for file in files.sorted() where file.hasSuffix(".mobileprovision") {
output += " \(file)\n"
}
return ok(output)
}
// xcode_swift_format
server.registerTool(
"xcode_swift_format",
description: "Format Swift source file using swift-format.",
inputSchema: [
"type": "object",
"required": ["path"],
"properties": [
"path": ["type": "string", "description": "Swift source file path."],
"in_place": ["type": "boolean", "description": "Format in place (default: false)."],
] as [String: Any],
] as [String: Any]
) { args in
guard let path = config.resolvePath(args["path"] as? String ?? "") else {
return err("Path not allowed")
}
var formatArgs = [path]
if args["in_place"] as? Bool == true {
formatArgs.insert("-i", at: 0)
}
return runCommand("swift-format", formatArgs, in: config.workspaceRoot)
}
}
// MARK: - Helpers
private static func buildXcodebuildCmd(_ args: [String: Any], action: String, config: AgentConfig) -> [String] {
var xcArgs = [action]
if let scheme = args["scheme"] as? String { xcArgs += ["-scheme", scheme] }
if let project = args["project"] as? String {
if project.hasSuffix(".xcworkspace") {
xcArgs += ["-workspace", project]
} else {
xcArgs += ["-project", project]
}
} else {
// Auto-detect .xcodeproj or .xcworkspace in workspace root
let fm = FileManager.default
let root = config.workspaceRoot
if let contents = try? fm.contentsOfDirectory(atPath: root) {
if let ws = contents.first(where: { $0.hasSuffix(".xcworkspace") }) {
xcArgs += ["-workspace", (root as NSString).appendingPathComponent(ws)]
} else if let proj = contents.first(where: { $0.hasSuffix(".xcodeproj") }) {
xcArgs += ["-project", (root as NSString).appendingPathComponent(proj)]
}
}
}
if let configuration = args["configuration"] as? String {
xcArgs += ["-configuration", configuration]
}
if let destination = args["destination"] as? String {
xcArgs += ["-destination", destination]
}
return xcArgs
}
private static func runCommand(_ executable: String, _ args: [String], in directory: String) -> [[String: Any]] {
let process = Process()
process.executableURL = URL(fileURLWithPath: executable.hasPrefix("/") ? executable : "/usr/bin/\(executable)")
process.arguments = args
process.currentDirectoryURL = URL(fileURLWithPath: directory)
process.environment = ProcessInfo.processInfo.environment
let pipe = Pipe()
let errPipe = Pipe()
process.standardOutput = pipe
process.standardError = errPipe
do {
try process.run()
process.waitUntilExit()
let outData = pipe.fileHandleForReading.readDataToEndOfFile()
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outData, encoding: .utf8) ?? ""
let errOutput = String(data: errData, encoding: .utf8) ?? ""
let combined = output + (errOutput.isEmpty ? "" : "\n--- stderr ---\n\(errOutput)")
if process.terminationStatus != 0 {
return err("Exit code \(process.terminationStatus):\n\(combined)")
}
return ok(combined.isEmpty ? "(success, no output)" : combined)
} catch {
return err("Failed to run \(executable): \(error.localizedDescription)")
}
}
private static func ok(_ text: String) -> [[String: Any]] {
[["type": "text", "text": text]]
}
private static func err(_ message: String) -> [[String: Any]] {
[["type": "text", "text": "Error: \(message)"]]
}
}