105 lines
No EOL
3.5 KiB
Swift
105 lines
No EOL
3.5 KiB
Swift
import Foundation
|
|
|
|
/// A transport implementation that communicates over standard input/output
|
|
public actor StdioTransport: MCPTransport {
|
|
private var isConnected = false
|
|
private let processQueue = DispatchQueue(label: "com.mcp.stdio", qos: .userInitiated)
|
|
private let process: Process
|
|
private let inputPipe = Pipe()
|
|
private let outputPipe = Pipe()
|
|
private let errorPipe = Pipe()
|
|
|
|
public init(command: String, arguments: [String] = [], environment: [String: String]? = nil) {
|
|
self.process = Process()
|
|
self.process.executableURL = URL(fileURLWithPath: command)
|
|
self.process.arguments = arguments
|
|
if let env = environment {
|
|
self.process.environment = env
|
|
}
|
|
|
|
self.process.standardInput = inputPipe
|
|
self.process.standardOutput = outputPipe
|
|
self.process.standardError = errorPipe
|
|
}
|
|
|
|
public func connect() async throws {
|
|
guard !isConnected else { return }
|
|
|
|
try process.run()
|
|
isConnected = true
|
|
|
|
// Start reading output asynchronously
|
|
Task {
|
|
await readOutput()
|
|
}
|
|
}
|
|
|
|
public func disconnect() async {
|
|
guard isConnected else { return }
|
|
|
|
process.terminate()
|
|
isConnected = false
|
|
}
|
|
|
|
public func sendRequest(_ data: Data) async throws -> Data {
|
|
guard isConnected else {
|
|
throw MCPError.connectionClosed
|
|
}
|
|
|
|
// Write request
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
processQueue.async {
|
|
do {
|
|
let message = data + "\n".data(using: .utf8)!
|
|
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
|
|
|
|
// Read response
|
|
if let response = try self.outputPipe.fileHandleForReading.readToEnd() {
|
|
continuation.resume(returning: response)
|
|
} else {
|
|
continuation.resume(throwing: MCPError.invalidResponse)
|
|
}
|
|
} catch {
|
|
continuation.resume(throwing: MCPError.transportError(error))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func sendNotification(_ data: Data) async throws {
|
|
guard isConnected else {
|
|
throw MCPError.connectionClosed
|
|
}
|
|
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
processQueue.async {
|
|
do {
|
|
let message = data + "\n".data(using: .utf8)!
|
|
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
|
|
continuation.resume()
|
|
} catch {
|
|
continuation.resume(throwing: MCPError.transportError(error))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func readOutput() async {
|
|
let handle = outputPipe.fileHandleForReading
|
|
|
|
while isConnected {
|
|
do {
|
|
let data = try handle.read(upToCount: 4096)
|
|
if let data = data, !data.isEmpty {
|
|
// Process received data
|
|
// In a real implementation, you would parse the JSON-RPC messages
|
|
// and handle them appropriately
|
|
print("Received data: \(String(data: data, encoding: .utf8) ?? "")")
|
|
}
|
|
} catch {
|
|
print("Error reading output: \(error)")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} |