2026-10-17 22:43:38 -07:00
|
|
|
import AppKit
|
|
|
|
|
|
|
|
|
|
struct ToolError: Error {
|
|
|
|
|
let message: String
|
|
|
|
|
|
|
|
|
|
init(_ message: String) {
|
|
|
|
|
self.message = message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum Content {
|
|
|
|
|
case text(String)
|
|
|
|
|
case png(Data)
|
|
|
|
|
|
|
|
|
|
var json: [String: Any] {
|
|
|
|
|
switch self {
|
|
|
|
|
case .text(let s):
|
|
|
|
|
return ["type": "text", "text": s]
|
|
|
|
|
case .png(let d):
|
|
|
|
|
return ["type": "image", "data": d.base64EncodedString(), "mimeType": "image/png"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Tool {
|
|
|
|
|
let name: String
|
|
|
|
|
let description: String
|
|
|
|
|
let schema: String
|
2026-10-17 23:03:33 -07:00
|
|
|
let run: ([String: Any]) async throws -> [Content]
|
2026-10-17 22:43:38 -07:00
|
|
|
}
|
|
|
|
|
|
2026-10-17 23:03:33 -07:00
|
|
|
let stdoutLock = NSLock()
|
|
|
|
|
|
2026-10-18 00:39:04 -07:00
|
|
|
let mediaDir: String? = {
|
|
|
|
|
let args = CommandLine.arguments
|
|
|
|
|
guard let i = args.firstIndex(of: "--media-dir"), i + 1 < args.count else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return args[i + 1]
|
|
|
|
|
}()
|
|
|
|
|
|
2026-10-17 22:43:38 -07:00
|
|
|
@main
|
|
|
|
|
struct Server {
|
|
|
|
|
static func main() {
|
|
|
|
|
_ = NSApplication.shared
|
2026-10-18 00:05:26 -07:00
|
|
|
NSApp.setActivationPolicy(.prohibited)
|
2026-10-17 23:03:33 -07:00
|
|
|
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), 3)
|
2026-10-17 22:43:38 -07:00
|
|
|
Thread.detachNewThread {
|
2026-10-17 23:03:33 -07:00
|
|
|
let inFlight = DispatchGroup()
|
2026-10-17 22:43:38 -07:00
|
|
|
while let line = readLine() {
|
2026-10-17 23:03:33 -07:00
|
|
|
inFlight.enter()
|
|
|
|
|
Task.detached {
|
2026-10-17 22:43:38 -07:00
|
|
|
await handle(line)
|
2026-10-17 23:03:33 -07:00
|
|
|
inFlight.leave()
|
2026-10-17 22:43:38 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-17 23:03:33 -07:00
|
|
|
inFlight.wait()
|
2026-10-17 22:43:38 -07:00
|
|
|
exit(0)
|
|
|
|
|
}
|
|
|
|
|
RunLoop.main.run()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func handle(_ line: String) async {
|
|
|
|
|
guard let data = line.data(using: .utf8),
|
|
|
|
|
let req = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
|
|
|
let id = req["id"]
|
|
|
|
|
else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var resp: [String: Any] = ["jsonrpc": "2.0", "id": id]
|
|
|
|
|
let params = req["params"] as? [String: Any] ?? [:]
|
|
|
|
|
switch req["method"] as? String ?? "" {
|
|
|
|
|
case "initialize":
|
|
|
|
|
resp["result"] = [
|
|
|
|
|
"protocolVersion": params["protocolVersion"] as? String ?? "2025-06-18",
|
|
|
|
|
"capabilities": ["tools": [:]],
|
|
|
|
|
"serverInfo": ["name": "hands", "version": "0.0.0"],
|
|
|
|
|
]
|
|
|
|
|
case "ping":
|
|
|
|
|
resp["result"] = [:] as [String: Any]
|
|
|
|
|
case "tools/list":
|
|
|
|
|
resp["result"] = ["tools": tools.map { t in
|
|
|
|
|
["name": t.name, "description": t.description, "inputSchema": try! JSONSerialization.jsonObject(with: Data(t.schema.utf8))]
|
|
|
|
|
}]
|
|
|
|
|
case "tools/call":
|
|
|
|
|
resp["result"] = await call(params)
|
|
|
|
|
default:
|
|
|
|
|
resp["error"] = ["code": -32601, "message": "method not found: \(req["method"] ?? "")"]
|
|
|
|
|
}
|
2026-10-17 23:03:33 -07:00
|
|
|
var out = try! JSONSerialization.data(withJSONObject: resp, options: .withoutEscapingSlashes)
|
|
|
|
|
out.append(Data("\n".utf8))
|
|
|
|
|
stdoutLock.withLock {
|
|
|
|
|
FileHandle.standardOutput.write(out)
|
|
|
|
|
}
|
2026-10-17 22:43:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func call(_ params: [String: Any]) async -> [String: Any] {
|
|
|
|
|
let name = params["name"] as? String ?? ""
|
|
|
|
|
let args = params["arguments"] as? [String: Any] ?? [:]
|
|
|
|
|
guard let tool = tools.first(where: { $0.name == name }) else {
|
|
|
|
|
return ["content": [Content.text("unknown tool: \(name)").json], "isError": true]
|
|
|
|
|
}
|
|
|
|
|
do {
|
2026-10-18 21:23:26 -07:00
|
|
|
let content = try await withDisplayAwake { try await tool.run(args) }
|
2026-10-17 22:43:38 -07:00
|
|
|
return ["content": content.map { $0.json }]
|
|
|
|
|
} catch let e as ToolError {
|
|
|
|
|
return ["content": [Content.text(e.message).json], "isError": true]
|
|
|
|
|
} catch {
|
|
|
|
|
return ["content": [Content.text("\(error)").json], "isError": true]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|