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
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// 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)"]]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user