desktop-pilot-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Package.swift +38 -0
- package/README.md +462 -0
- package/Sources/DesktopPilot/Core/AppRegistry.swift +102 -0
- package/Sources/DesktopPilot/Core/ElementStore.swift +59 -0
- package/Sources/DesktopPilot/Core/Router.swift +242 -0
- package/Sources/DesktopPilot/Core/Snapshot.swift +192 -0
- package/Sources/DesktopPilot/Layers/AccessibilityLayer.swift +190 -0
- package/Sources/DesktopPilot/Layers/AppleScriptLayer.swift +462 -0
- package/Sources/DesktopPilot/Layers/CGEventLayer.swift +318 -0
- package/Sources/DesktopPilot/Layers/LayerProtocol.swift +40 -0
- package/Sources/DesktopPilot/Layers/ScreenshotLayer.swift +122 -0
- package/Sources/DesktopPilot/MCP/Server.swift +536 -0
- package/Sources/DesktopPilot/MCP/Tools.swift +772 -0
- package/Sources/DesktopPilot/MCP/Types.swift +107 -0
- package/Sources/DesktopPilot/Platform/PlatformProtocol.swift +49 -0
- package/Sources/DesktopPilot/Platform/macOS/AXBridge.swift +232 -0
- package/Sources/DesktopPilot/Platform/macOS/Permissions.swift +34 -0
- package/Sources/DesktopPilot/Platform/macOS/SystemEvents.swift +323 -0
- package/Sources/DesktopPilot/main.swift +19 -0
- package/Sources/DesktopPilotCLI/main.swift +19 -0
- package/Tests/DesktopPilotTests/DesktopPilotTests.swift +290 -0
- package/bin/cli.js +61 -0
- package/package.json +52 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - JSON Value
|
|
4
|
+
|
|
5
|
+
/// A flexible JSON value type for handling arbitrary MCP params.
|
|
6
|
+
public enum JSONValue: Codable, Sendable, Equatable {
|
|
7
|
+
case string(String)
|
|
8
|
+
case number(Double)
|
|
9
|
+
case bool(Bool)
|
|
10
|
+
case null
|
|
11
|
+
case array([JSONValue])
|
|
12
|
+
case object([String: JSONValue])
|
|
13
|
+
|
|
14
|
+
public init(from decoder: Decoder) throws {
|
|
15
|
+
let container = try decoder.singleValueContainer()
|
|
16
|
+
|
|
17
|
+
if container.decodeNil() {
|
|
18
|
+
self = .null
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if let boolVal = try? container.decode(Bool.self) {
|
|
22
|
+
self = .bool(boolVal)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if let intVal = try? container.decode(Int.self) {
|
|
26
|
+
self = .number(Double(intVal))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if let doubleVal = try? container.decode(Double.self) {
|
|
30
|
+
self = .number(doubleVal)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if let strVal = try? container.decode(String.self) {
|
|
34
|
+
self = .string(strVal)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if let arrVal = try? container.decode([JSONValue].self) {
|
|
38
|
+
self = .array(arrVal)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if let objVal = try? container.decode([String: JSONValue].self) {
|
|
42
|
+
self = .object(objVal)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw DecodingError.typeMismatch(
|
|
47
|
+
JSONValue.self,
|
|
48
|
+
DecodingError.Context(
|
|
49
|
+
codingPath: decoder.codingPath,
|
|
50
|
+
debugDescription: "Cannot decode JSONValue"
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func encode(to encoder: Encoder) throws {
|
|
56
|
+
var container = encoder.singleValueContainer()
|
|
57
|
+
switch self {
|
|
58
|
+
case .string(let val):
|
|
59
|
+
try container.encode(val)
|
|
60
|
+
case .number(let val):
|
|
61
|
+
try container.encode(val)
|
|
62
|
+
case .bool(let val):
|
|
63
|
+
try container.encode(val)
|
|
64
|
+
case .null:
|
|
65
|
+
try container.encodeNil()
|
|
66
|
+
case .array(let val):
|
|
67
|
+
try container.encode(val)
|
|
68
|
+
case .object(let val):
|
|
69
|
+
try container.encode(val)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Extract a string value for the given key (object only).
|
|
74
|
+
public func stringValue(forKey key: String) -> String? {
|
|
75
|
+
guard case .object(let dict) = self,
|
|
76
|
+
case .string(let val) = dict[key] else {
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
return val
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Extract an integer value for the given key (object only).
|
|
83
|
+
public func intValue(forKey key: String) -> Int? {
|
|
84
|
+
guard case .object(let dict) = self,
|
|
85
|
+
case .number(let val) = dict[key] else {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
return Int(val)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Return the underlying dictionary if this is an object.
|
|
92
|
+
public var objectValue: [String: JSONValue]? {
|
|
93
|
+
guard case .object(let dict) = self else { return nil }
|
|
94
|
+
return dict
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - JSON-RPC Types
|
|
99
|
+
|
|
100
|
+
/// Flexible request ID that can be Int or String.
|
|
101
|
+
enum RequestID: Codable, Sendable {
|
|
102
|
+
case int(Int)
|
|
103
|
+
case string(String)
|
|
104
|
+
|
|
105
|
+
init(from decoder: Decoder) throws {
|
|
106
|
+
let container = try decoder.singleValueContainer()
|
|
107
|
+
if let intVal = try? container.decode(Int.self) {
|
|
108
|
+
self = .int(intVal)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if let strVal = try? container.decode(String.self) {
|
|
112
|
+
self = .string(strVal)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
throw DecodingError.typeMismatch(
|
|
116
|
+
RequestID.self,
|
|
117
|
+
DecodingError.Context(
|
|
118
|
+
codingPath: decoder.codingPath,
|
|
119
|
+
debugDescription: "Request ID must be Int or String"
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func encode(to encoder: Encoder) throws {
|
|
125
|
+
var container = encoder.singleValueContainer()
|
|
126
|
+
switch self {
|
|
127
|
+
case .int(let val):
|
|
128
|
+
try container.encode(val)
|
|
129
|
+
case .string(let val):
|
|
130
|
+
try container.encode(val)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
struct JSONRPCRequest: Codable, Sendable {
|
|
136
|
+
let jsonrpc: String
|
|
137
|
+
let id: RequestID?
|
|
138
|
+
let method: String
|
|
139
|
+
let params: JSONValue?
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
struct JSONRPCResponse: Codable, Sendable {
|
|
143
|
+
let jsonrpc: String
|
|
144
|
+
let id: RequestID?
|
|
145
|
+
let result: JSONValue?
|
|
146
|
+
let error: JSONRPCError?
|
|
147
|
+
|
|
148
|
+
init(id: RequestID?, result: JSONValue) {
|
|
149
|
+
self.jsonrpc = "2.0"
|
|
150
|
+
self.id = id
|
|
151
|
+
self.result = result
|
|
152
|
+
self.error = nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
init(id: RequestID?, error: JSONRPCError) {
|
|
156
|
+
self.jsonrpc = "2.0"
|
|
157
|
+
self.id = id
|
|
158
|
+
self.result = nil
|
|
159
|
+
self.error = error
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
struct JSONRPCError: Codable, Sendable {
|
|
164
|
+
let code: Int
|
|
165
|
+
let message: String
|
|
166
|
+
let data: JSONValue?
|
|
167
|
+
|
|
168
|
+
init(code: Int, message: String, data: JSONValue? = nil) {
|
|
169
|
+
self.code = code
|
|
170
|
+
self.message = message
|
|
171
|
+
self.data = data
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static func methodNotFound(_ method: String) -> JSONRPCError {
|
|
175
|
+
JSONRPCError(code: -32601, message: "Method not found: \(method)")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static func invalidParams(_ detail: String) -> JSONRPCError {
|
|
179
|
+
JSONRPCError(code: -32602, message: "Invalid params: \(detail)")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static func internalError(_ detail: String) -> JSONRPCError {
|
|
183
|
+
JSONRPCError(code: -32603, message: "Internal error: \(detail)")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static let parseError = JSONRPCError(code: -32700, message: "Parse error")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// MARK: - MCP Content Types
|
|
190
|
+
|
|
191
|
+
/// A content block returned by a tool call.
|
|
192
|
+
public struct MCPContent: Codable, Sendable {
|
|
193
|
+
public let type: String
|
|
194
|
+
public let text: String?
|
|
195
|
+
public let data: String?
|
|
196
|
+
public let mimeType: String?
|
|
197
|
+
|
|
198
|
+
public static func text(_ value: String) -> MCPContent {
|
|
199
|
+
MCPContent(type: "text", text: value, data: nil, mimeType: nil)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public static func image(base64 data: String, mimeType: String) -> MCPContent {
|
|
203
|
+
MCPContent(type: "image", text: nil, data: data, mimeType: mimeType)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// The result envelope for a tools/call response.
|
|
208
|
+
public struct MCPToolResult: Sendable {
|
|
209
|
+
public let content: [MCPContent]
|
|
210
|
+
public let isError: Bool
|
|
211
|
+
|
|
212
|
+
public init(content: [MCPContent], isError: Bool) {
|
|
213
|
+
self.content = content
|
|
214
|
+
self.isError = isError
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public static func success(_ text: String) -> MCPToolResult {
|
|
218
|
+
MCPToolResult(content: [.text(text)], isError: false)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public static func error(_ text: String) -> MCPToolResult {
|
|
222
|
+
MCPToolResult(content: [.text(text)], isError: true)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func toJSONValue() -> JSONValue {
|
|
226
|
+
let contentArray: [JSONValue] = content.map { item in
|
|
227
|
+
var dict: [String: JSONValue] = ["type": .string(item.type)]
|
|
228
|
+
if let text = item.text {
|
|
229
|
+
dict["text"] = .string(text)
|
|
230
|
+
}
|
|
231
|
+
if let data = item.data {
|
|
232
|
+
dict["data"] = .string(data)
|
|
233
|
+
}
|
|
234
|
+
if let mimeType = item.mimeType {
|
|
235
|
+
dict["mimeType"] = .string(mimeType)
|
|
236
|
+
}
|
|
237
|
+
return .object(dict)
|
|
238
|
+
}
|
|
239
|
+
return .object([
|
|
240
|
+
"content": .array(contentArray),
|
|
241
|
+
"isError": .bool(isError)
|
|
242
|
+
])
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// MARK: - Tool Handler Protocol
|
|
247
|
+
|
|
248
|
+
/// Protocol for handling MCP tool calls.
|
|
249
|
+
public protocol ToolHandler: Sendable {
|
|
250
|
+
/// Return all tool definitions for tools/list.
|
|
251
|
+
func listTools() -> [ToolDefinition]
|
|
252
|
+
|
|
253
|
+
/// Handle a tool call and return the result.
|
|
254
|
+
func callTool(name: String, arguments: JSONValue?) async throws -> MCPToolResult
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// A tool definition with name, description, and JSON Schema for inputs.
|
|
258
|
+
public struct ToolDefinition: Sendable {
|
|
259
|
+
public let name: String
|
|
260
|
+
public let description: String
|
|
261
|
+
public let inputSchema: JSONValue
|
|
262
|
+
|
|
263
|
+
func toJSONValue() -> JSONValue {
|
|
264
|
+
.object([
|
|
265
|
+
"name": .string(name),
|
|
266
|
+
"description": .string(description),
|
|
267
|
+
"inputSchema": inputSchema
|
|
268
|
+
])
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MARK: - Logger
|
|
273
|
+
|
|
274
|
+
/// Logs messages to stderr so stdout stays clean for MCP protocol.
|
|
275
|
+
public enum Log {
|
|
276
|
+
public static func info(_ message: String) {
|
|
277
|
+
write("[INFO] \(message)")
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public static func error(_ message: String) {
|
|
281
|
+
write("[ERROR] \(message)")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public static func debug(_ message: String) {
|
|
285
|
+
write("[DEBUG] \(message)")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private static func write(_ message: String) {
|
|
289
|
+
let line = message + "\n"
|
|
290
|
+
if let data = line.data(using: .utf8) {
|
|
291
|
+
FileHandle.standardError.write(data)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// MARK: - MCP Server
|
|
297
|
+
|
|
298
|
+
/// MCP server that communicates over stdin/stdout using JSON-RPC with
|
|
299
|
+
/// Content-Length framing.
|
|
300
|
+
public final class MCPServer: Sendable {
|
|
301
|
+
private let toolHandler: ToolHandler
|
|
302
|
+
private let serverName: String
|
|
303
|
+
private let serverVersion: String
|
|
304
|
+
|
|
305
|
+
public init(
|
|
306
|
+
toolHandler: ToolHandler,
|
|
307
|
+
serverName: String = "desktop-pilot-mcp",
|
|
308
|
+
serverVersion: String = "0.1.0"
|
|
309
|
+
) {
|
|
310
|
+
self.toolHandler = toolHandler
|
|
311
|
+
self.serverName = serverName
|
|
312
|
+
self.serverVersion = serverVersion
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Start the server and process messages from stdin until EOF.
|
|
316
|
+
public func run() async {
|
|
317
|
+
Log.info("Starting \(serverName) v\(serverVersion)")
|
|
318
|
+
|
|
319
|
+
let stdin = FileHandle.standardInput
|
|
320
|
+
|
|
321
|
+
var buffer = Data()
|
|
322
|
+
|
|
323
|
+
while true {
|
|
324
|
+
let chunk = stdin.availableData
|
|
325
|
+
if chunk.isEmpty {
|
|
326
|
+
Log.info("stdin closed, shutting down")
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
buffer.append(chunk)
|
|
331
|
+
|
|
332
|
+
while let (messageData, remainder) = extractMessage(from: buffer) {
|
|
333
|
+
buffer = remainder
|
|
334
|
+
await processMessage(messageData)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// MARK: - Message Framing
|
|
340
|
+
|
|
341
|
+
/// Extract one complete message from the buffer if a full Content-Length
|
|
342
|
+
/// framed message is available.
|
|
343
|
+
/// Returns the message body data and the remaining buffer, or nil.
|
|
344
|
+
private func extractMessage(from buffer: Data) -> (Data, Data)? {
|
|
345
|
+
guard let headerEnd = findHeaderEnd(in: buffer) else {
|
|
346
|
+
return nil
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let headerData = buffer[buffer.startIndex..<headerEnd]
|
|
350
|
+
guard let headerString = String(data: headerData, encoding: .utf8) else {
|
|
351
|
+
return nil
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
guard let contentLength = parseContentLength(from: headerString) else {
|
|
355
|
+
Log.error("Missing Content-Length in header: \(headerString)")
|
|
356
|
+
return nil
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let bodyStart = headerEnd + 4 // skip \r\n\r\n
|
|
360
|
+
let bodyEnd = bodyStart + contentLength
|
|
361
|
+
|
|
362
|
+
guard buffer.count >= bodyEnd else {
|
|
363
|
+
return nil
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let body = buffer[bodyStart..<bodyEnd]
|
|
367
|
+
let remainder = buffer[bodyEnd...]
|
|
368
|
+
|
|
369
|
+
return (Data(body), Data(remainder))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Find the position of \r\n\r\n in the data.
|
|
373
|
+
private func findHeaderEnd(in data: Data) -> Int? {
|
|
374
|
+
let separator: [UInt8] = [0x0D, 0x0A, 0x0D, 0x0A]
|
|
375
|
+
let bytes = [UInt8](data)
|
|
376
|
+
|
|
377
|
+
guard bytes.count >= 4 else { return nil }
|
|
378
|
+
|
|
379
|
+
for i in 0...(bytes.count - 4) {
|
|
380
|
+
if bytes[i] == separator[0]
|
|
381
|
+
&& bytes[i + 1] == separator[1]
|
|
382
|
+
&& bytes[i + 2] == separator[2]
|
|
383
|
+
&& bytes[i + 3] == separator[3]
|
|
384
|
+
{
|
|
385
|
+
return i
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return nil
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Parse "Content-Length: N" from the header string.
|
|
393
|
+
private func parseContentLength(from header: String) -> Int? {
|
|
394
|
+
for line in header.components(separatedBy: "\r\n") {
|
|
395
|
+
let parts = line.split(separator: ":", maxSplits: 1)
|
|
396
|
+
if parts.count == 2,
|
|
397
|
+
parts[0].trimmingCharacters(in: .whitespaces).lowercased() == "content-length",
|
|
398
|
+
let length = Int(parts[1].trimmingCharacters(in: .whitespaces))
|
|
399
|
+
{
|
|
400
|
+
return length
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return nil
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// MARK: - Message Processing
|
|
407
|
+
|
|
408
|
+
private func processMessage(_ data: Data) async {
|
|
409
|
+
let decoder = JSONDecoder()
|
|
410
|
+
|
|
411
|
+
let request: JSONRPCRequest
|
|
412
|
+
do {
|
|
413
|
+
request = try decoder.decode(JSONRPCRequest.self, from: data)
|
|
414
|
+
} catch {
|
|
415
|
+
Log.error("Failed to parse JSON-RPC request: \(error)")
|
|
416
|
+
sendResponse(JSONRPCResponse(id: nil, error: .parseError))
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
Log.debug("Received method: \(request.method)")
|
|
421
|
+
|
|
422
|
+
// Notifications (no id) don't get a response
|
|
423
|
+
if request.id == nil {
|
|
424
|
+
handleNotification(request)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let response = await handleRequest(request)
|
|
429
|
+
sendResponse(response)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private func handleNotification(_ request: JSONRPCRequest) {
|
|
433
|
+
switch request.method {
|
|
434
|
+
case "notifications/initialized":
|
|
435
|
+
Log.info("Client initialized")
|
|
436
|
+
default:
|
|
437
|
+
Log.debug("Unhandled notification: \(request.method)")
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private func handleRequest(_ request: JSONRPCRequest) async -> JSONRPCResponse {
|
|
442
|
+
switch request.method {
|
|
443
|
+
case "initialize":
|
|
444
|
+
return handleInitialize(request)
|
|
445
|
+
case "tools/list":
|
|
446
|
+
return handleToolsList(request)
|
|
447
|
+
case "tools/call":
|
|
448
|
+
return await handleToolsCall(request)
|
|
449
|
+
case "ping":
|
|
450
|
+
return JSONRPCResponse(id: request.id, result: .object([:]))
|
|
451
|
+
default:
|
|
452
|
+
return JSONRPCResponse(
|
|
453
|
+
id: request.id,
|
|
454
|
+
error: .methodNotFound(request.method)
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// MARK: - Method Handlers
|
|
460
|
+
|
|
461
|
+
private func handleInitialize(_ request: JSONRPCRequest) -> JSONRPCResponse {
|
|
462
|
+
Log.info("Handling initialize")
|
|
463
|
+
|
|
464
|
+
let result: JSONValue = .object([
|
|
465
|
+
"protocolVersion": .string("2024-11-05"),
|
|
466
|
+
"capabilities": .object([
|
|
467
|
+
"tools": .object([:])
|
|
468
|
+
]),
|
|
469
|
+
"serverInfo": .object([
|
|
470
|
+
"name": .string(serverName),
|
|
471
|
+
"version": .string(serverVersion)
|
|
472
|
+
])
|
|
473
|
+
])
|
|
474
|
+
|
|
475
|
+
return JSONRPCResponse(id: request.id, result: result)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private func handleToolsList(_ request: JSONRPCRequest) -> JSONRPCResponse {
|
|
479
|
+
let tools = toolHandler.listTools()
|
|
480
|
+
let toolsJSON: [JSONValue] = tools.map { $0.toJSONValue() }
|
|
481
|
+
|
|
482
|
+
let result: JSONValue = .object([
|
|
483
|
+
"tools": .array(toolsJSON)
|
|
484
|
+
])
|
|
485
|
+
|
|
486
|
+
return JSONRPCResponse(id: request.id, result: result)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private func handleToolsCall(_ request: JSONRPCRequest) async -> JSONRPCResponse {
|
|
490
|
+
guard let params = request.params,
|
|
491
|
+
let name = params.stringValue(forKey: "name") else {
|
|
492
|
+
return JSONRPCResponse(
|
|
493
|
+
id: request.id,
|
|
494
|
+
error: .invalidParams("Missing 'name' in tools/call params")
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let arguments: JSONValue?
|
|
499
|
+
if case .object(let paramsDict) = params {
|
|
500
|
+
arguments = paramsDict["arguments"]
|
|
501
|
+
} else {
|
|
502
|
+
arguments = nil
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
do {
|
|
506
|
+
let result = try await toolHandler.callTool(name: name, arguments: arguments)
|
|
507
|
+
return JSONRPCResponse(id: request.id, result: result.toJSONValue())
|
|
508
|
+
} catch {
|
|
509
|
+
Log.error("Tool '\(name)' failed: \(error)")
|
|
510
|
+
let errorResult = MCPToolResult.error("Tool execution failed: \(error)")
|
|
511
|
+
return JSONRPCResponse(id: request.id, result: errorResult.toJSONValue())
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// MARK: - Response Writing
|
|
516
|
+
|
|
517
|
+
private func sendResponse(_ response: JSONRPCResponse) {
|
|
518
|
+
let encoder = JSONEncoder()
|
|
519
|
+
encoder.outputFormatting = [.sortedKeys]
|
|
520
|
+
|
|
521
|
+
do {
|
|
522
|
+
let data = try encoder.encode(response)
|
|
523
|
+
let header = "Content-Length: \(data.count)\r\n\r\n"
|
|
524
|
+
|
|
525
|
+
guard let headerData = header.data(using: .utf8) else {
|
|
526
|
+
Log.error("Failed to encode response header")
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
FileHandle.standardOutput.write(headerData)
|
|
531
|
+
FileHandle.standardOutput.write(data)
|
|
532
|
+
} catch {
|
|
533
|
+
Log.error("Failed to encode response: \(error)")
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|