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.
@@ -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
+ }