c118996746
- 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
313 lines
14 KiB
Swift
313 lines
14 KiB
Swift
// ProjectTools.swift
|
|
// CxSwiftAgent — Project Management MCP Tools
|
|
//
|
|
// 5 tools: project_detect, project_run, project_deps, project_config, project_create
|
|
|
|
import Foundation
|
|
|
|
enum ProjectTools {
|
|
static func register(on server: MCPServer, config: AgentConfig, memory: AgentMemory) {
|
|
|
|
// ── project_detect ────────────────────────────────────────────
|
|
server.registerTool(
|
|
"project_detect",
|
|
description: "Detect project type, language, build system, and key files.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
|
let fm = FileManager.default
|
|
|
|
var info: [String: Any] = ["path": root]
|
|
var languages: [String] = []
|
|
var buildSystems: [String] = []
|
|
var keyFiles: [String] = []
|
|
|
|
let detectors: [(String, String, String)] = [
|
|
("Package.swift", "swift", "spm"),
|
|
("Makefile", "", "make"),
|
|
("CMakeLists.txt", "c++", "cmake"),
|
|
("package.json", "javascript", "npm"),
|
|
("composer.json", "php", "composer"),
|
|
("Cargo.toml", "rust", "cargo"),
|
|
("go.mod", "go", "go-modules"),
|
|
("pyproject.toml", "python", "pip"),
|
|
("requirements.txt", "python", "pip"),
|
|
("Gemfile", "ruby", "bundler"),
|
|
("build.gradle", "java", "gradle"),
|
|
("pom.xml", "java", "maven"),
|
|
("cpanfile", "perl", "cpanm"),
|
|
]
|
|
|
|
for (file, lang, build) in detectors {
|
|
let path = (root as NSString).appendingPathComponent(file)
|
|
if fm.fileExists(atPath: path) {
|
|
keyFiles.append(file)
|
|
if !lang.isEmpty && !languages.contains(lang) { languages.append(lang) }
|
|
if !buildSystems.contains(build) { buildSystems.append(build) }
|
|
}
|
|
}
|
|
|
|
// Xcode detection
|
|
if let items = try? fm.contentsOfDirectory(atPath: root) {
|
|
for item in items {
|
|
if item.hasSuffix(".xcodeproj") || item.hasSuffix(".xcworkspace") {
|
|
keyFiles.append(item)
|
|
if !languages.contains("swift") { languages.append("swift") }
|
|
if !buildSystems.contains("xcode") { buildSystems.append("xcode") }
|
|
}
|
|
}
|
|
}
|
|
|
|
info["languages"] = languages
|
|
info["build_systems"] = buildSystems
|
|
info["key_files"] = keyFiles
|
|
info["type"] = languages.first ?? "unknown"
|
|
|
|
return ok(JSON.serialize(info, pretty: true))
|
|
}
|
|
|
|
// ── project_run ───────────────────────────────────────────────
|
|
server.registerTool(
|
|
"project_run",
|
|
description: "Run project build/test/lint commands based on detected project type.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["action"],
|
|
"properties": [
|
|
"action": ["type": "string", "description": "Action: build, test, lint, run, clean."],
|
|
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
|
let action = args["action"] as? String ?? "build"
|
|
let fm = FileManager.default
|
|
|
|
// Detect project type and run appropriate command
|
|
if fm.fileExists(atPath: (root as NSString).appendingPathComponent("Package.swift")) {
|
|
switch action {
|
|
case "build": return runCmd("swift", ["build"], in: root)
|
|
case "test": return runCmd("swift", ["test"], in: root)
|
|
case "clean": return runCmd("swift", ["package", "clean"], in: root)
|
|
case "run": return runCmd("swift", ["run"], in: root)
|
|
default: return err("Unknown action: \(action)")
|
|
}
|
|
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("package.json")) {
|
|
switch action {
|
|
case "build": return runCmd("npm", ["run", "build"], in: root)
|
|
case "test": return runCmd("npm", ["test"], in: root)
|
|
case "lint": return runCmd("npm", ["run", "lint"], in: root)
|
|
case "run": return runCmd("npm", ["start"], in: root)
|
|
default: return err("Unknown action: \(action)")
|
|
}
|
|
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("Makefile")) {
|
|
switch action {
|
|
case "build": return runCmd("make", [], in: root)
|
|
case "test": return runCmd("make", ["test"], in: root)
|
|
case "clean": return runCmd("make", ["clean"], in: root)
|
|
default: return err("Unknown action: \(action)")
|
|
}
|
|
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("composer.json")) {
|
|
switch action {
|
|
case "test": return runCmd("vendor/bin/phpunit", [], in: root)
|
|
case "lint": return runCmd("vendor/bin/phpcs", ["src/"], in: root)
|
|
default: return err("Unknown action: \(action)")
|
|
}
|
|
}
|
|
|
|
// Check for Xcode project
|
|
let xcodeProjects = (try? fm.contentsOfDirectory(atPath: root))?.filter { $0.hasSuffix(".xcodeproj") } ?? []
|
|
if let xcodeProj = xcodeProjects.first {
|
|
let fullProj = (root as NSString).appendingPathComponent(xcodeProj)
|
|
switch action {
|
|
case "build": return runCmd("/usr/bin/xcodebuild", ["build", "-project", fullProj, "-quiet"], in: root)
|
|
case "test": return runCmd("/usr/bin/xcodebuild", ["test", "-project", fullProj, "-quiet"], in: root)
|
|
case "clean": return runCmd("/usr/bin/xcodebuild", ["clean", "-project", fullProj, "-quiet"], in: root)
|
|
case "run": return runCmd("/usr/bin/xcodebuild", ["build", "-project", fullProj, "-quiet"], in: root)
|
|
default: return err("Unknown action: \(action)")
|
|
}
|
|
}
|
|
|
|
return err("Cannot detect project type at \(root)")
|
|
}
|
|
|
|
// ── project_deps ──────────────────────────────────────────────
|
|
server.registerTool(
|
|
"project_deps",
|
|
description: "List project dependencies from manifest files.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
|
let fm = FileManager.default
|
|
|
|
// Try Package.swift
|
|
let spmPath = (root as NSString).appendingPathComponent("Package.swift")
|
|
if fm.fileExists(atPath: spmPath) {
|
|
return runCmd("swift", ["package", "show-dependencies"], in: root)
|
|
}
|
|
|
|
// Try package.json
|
|
let npmPath = (root as NSString).appendingPathComponent("package.json")
|
|
if fm.fileExists(atPath: npmPath) {
|
|
if let content = try? String(contentsOfFile: npmPath, encoding: .utf8) {
|
|
return ok(content)
|
|
}
|
|
}
|
|
|
|
// Try composer.json
|
|
let composerPath = (root as NSString).appendingPathComponent("composer.json")
|
|
if fm.fileExists(atPath: composerPath) {
|
|
if let content = try? String(contentsOfFile: composerPath, encoding: .utf8) {
|
|
return ok(content)
|
|
}
|
|
}
|
|
|
|
return err("No dependency manifest found")
|
|
}
|
|
|
|
// ── project_config ────────────────────────────────────────────
|
|
server.registerTool(
|
|
"project_config",
|
|
description: "Read or list project configuration files.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
|
"file": ["type": "string", "description": "Specific config file to read."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
|
let fm = FileManager.default
|
|
|
|
if let file = args["file"] as? String {
|
|
let filePath = (root as NSString).appendingPathComponent(file)
|
|
guard let content = try? String(contentsOfFile: filePath, encoding: .utf8) else {
|
|
return err("Cannot read \(file)")
|
|
}
|
|
return ok(content)
|
|
}
|
|
|
|
// List config files
|
|
let configFiles = [
|
|
".env", ".env.example", ".editorconfig", ".gitignore",
|
|
".swiftlint.yml", ".swift-format", "tsconfig.json",
|
|
".eslintrc.json", "phpunit.xml", "pyproject.toml",
|
|
"Dockerfile", "docker-compose.yml", ".github/workflows",
|
|
]
|
|
|
|
var found: [String] = []
|
|
for file in configFiles {
|
|
let path = (root as NSString).appendingPathComponent(file)
|
|
if fm.fileExists(atPath: path) {
|
|
found.append(file)
|
|
}
|
|
}
|
|
|
|
return ok(found.isEmpty ? "No config files found" : "Config files:\n" + found.map { " \($0)" }.joined(separator: "\n"))
|
|
}
|
|
|
|
// ── project_create ────────────────────────────────────────────
|
|
server.registerTool(
|
|
"project_create",
|
|
description: "Create a new project from template (Swift Package, Node, etc.).",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["name", "type"],
|
|
"properties": [
|
|
"name": ["type": "string", "description": "Project name."],
|
|
"type": ["type": "string", "description": "Project type: swift-package, swift-executable, node, python."],
|
|
"path": ["type": "string", "description": "Parent directory (default: workspace root)."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(destructiveHint: true)
|
|
) { args in
|
|
if config.sandboxMode { return err("Project creation disabled in sandbox mode") }
|
|
|
|
guard let name = args["name"] as? String, !name.isEmpty else {
|
|
return err("Missing project name")
|
|
}
|
|
let projectType = args["type"] as? String ?? "swift-executable"
|
|
let parent = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
|
let projectPath = (parent as NSString).appendingPathComponent(name)
|
|
|
|
let fm = FileManager.default
|
|
try? fm.createDirectory(atPath: projectPath, withIntermediateDirectories: true)
|
|
|
|
switch projectType {
|
|
case "swift-package", "swift-executable":
|
|
let pkgType = projectType == "swift-package" ? "library" : "executable"
|
|
return runCmd("swift", ["package", "init", "--type", pkgType, "--name", name], in: projectPath)
|
|
|
|
case "node":
|
|
return runCmd("npm", ["init", "-y"], in: projectPath)
|
|
|
|
default:
|
|
return err("Unknown project type: \(projectType). Supported: swift-package, swift-executable, node, python")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private static func runCmd(_ executable: String, _ args: [String], in directory: String) -> [[String: Any]] {
|
|
let process = Process()
|
|
let exePath: String
|
|
if executable.hasPrefix("/") || executable.hasPrefix(".") {
|
|
exePath = executable
|
|
} else if executable.contains("/") {
|
|
exePath = (directory as NSString).appendingPathComponent(executable)
|
|
} else {
|
|
exePath = "/usr/bin/\(executable)"
|
|
}
|
|
|
|
process.executableURL = URL(fileURLWithPath: exePath)
|
|
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) ?? ""
|
|
|
|
if process.terminationStatus != 0 {
|
|
return err("Exit \(process.terminationStatus): \(errOutput.isEmpty ? output : errOutput)")
|
|
}
|
|
return ok(output.isEmpty ? "(success)" : output)
|
|
} 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)"]]
|
|
}
|
|
}
|