Conditionalize availability of StdioTransport (#65)
* Conditionalize availability of StdioTransport * Update README with information about platform support
This commit is contained in:
parent
1e61f205a1
commit
960c1dda0b
2 changed files with 141 additions and 131 deletions
16
README.md
16
README.md
|
|
@ -6,7 +6,7 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP).
|
||||||
|
|
||||||
- Swift 6.0+ (Xcode 16+)
|
- Swift 6.0+ (Xcode 16+)
|
||||||
|
|
||||||
### Supported Platforms
|
## Platform Support
|
||||||
|
|
||||||
| Platform | Minimum Version |
|
| Platform | Minimum Version |
|
||||||
|----------|----------------|
|
|----------|----------------|
|
||||||
|
|
@ -15,9 +15,19 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP).
|
||||||
| watchOS | 9.0+ |
|
| watchOS | 9.0+ |
|
||||||
| tvOS | 16.0+ |
|
| tvOS | 16.0+ |
|
||||||
| visionOS | 1.0+ |
|
| visionOS | 1.0+ |
|
||||||
| Linux | ✓ [^1] |
|
| Linux | ✓ |
|
||||||
|
| Windows | ✓ |
|
||||||
|
|
||||||
[^1]: Linux support requires glibc-based distributions such as Ubuntu, Debian, Fedora, CentOS, or RHEL. Alpine Linux and other musl-based distributions are not supported.
|
> [!IMPORTANT]
|
||||||
|
> MCP's transport layer handles communication between clients and servers.
|
||||||
|
> The Swift SDK supports multiple transport mechanisms,
|
||||||
|
> with different platform availability:
|
||||||
|
>
|
||||||
|
> * `StdioTransport` is available on Apple platforms
|
||||||
|
> and Linux distributions with glibc, such as
|
||||||
|
> Ubuntu, Debian, Fedora, CentOS, or RHEL.
|
||||||
|
>
|
||||||
|
> * `NetworkTransport` is available only on Apple platforms.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,164 +9,164 @@ import struct Foundation.Data
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Import for specific low-level operations not yet in Swift System
|
// Import for specific low-level operations not yet in Swift System
|
||||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
#if canImport(Darwin)
|
||||||
import Darwin.POSIX
|
import Darwin.POSIX
|
||||||
#elseif os(Linux)
|
#elseif canImport(Glibc)
|
||||||
import Glibc
|
import Glibc
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Standard input/output transport implementation
|
#if canImport(Darwin) || canImport(Glibc)
|
||||||
public actor StdioTransport: Transport {
|
/// Standard input/output transport implementation
|
||||||
private let input: FileDescriptor
|
public actor StdioTransport: Transport {
|
||||||
private let output: FileDescriptor
|
private let input: FileDescriptor
|
||||||
public nonisolated let logger: Logger
|
private let output: FileDescriptor
|
||||||
|
public nonisolated let logger: Logger
|
||||||
|
|
||||||
private var isConnected = false
|
private var isConnected = false
|
||||||
private let messageStream: AsyncStream<Data>
|
private let messageStream: AsyncStream<Data>
|
||||||
private let messageContinuation: AsyncStream<Data>.Continuation
|
private let messageContinuation: AsyncStream<Data>.Continuation
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
input: FileDescriptor = FileDescriptor.standardInput,
|
input: FileDescriptor = FileDescriptor.standardInput,
|
||||||
output: FileDescriptor = FileDescriptor.standardOutput,
|
output: FileDescriptor = FileDescriptor.standardOutput,
|
||||||
logger: Logger? = nil
|
logger: Logger? = nil
|
||||||
) {
|
) {
|
||||||
self.input = input
|
self.input = input
|
||||||
self.output = output
|
self.output = output
|
||||||
self.logger =
|
self.logger =
|
||||||
logger
|
logger
|
||||||
?? Logger(
|
?? Logger(
|
||||||
label: "mcp.transport.stdio",
|
label: "mcp.transport.stdio",
|
||||||
factory: { _ in SwiftLogNoOpLogHandler() })
|
factory: { _ in SwiftLogNoOpLogHandler() })
|
||||||
|
|
||||||
// Create message stream
|
// Create message stream
|
||||||
var continuation: AsyncStream<Data>.Continuation!
|
var continuation: AsyncStream<Data>.Continuation!
|
||||||
self.messageStream = AsyncStream { continuation = $0 }
|
self.messageStream = AsyncStream { continuation = $0 }
|
||||||
self.messageContinuation = continuation
|
self.messageContinuation = continuation
|
||||||
}
|
|
||||||
|
|
||||||
public func connect() async throws {
|
|
||||||
guard !isConnected else { return }
|
|
||||||
|
|
||||||
// Set non-blocking mode
|
|
||||||
try setNonBlocking(fileDescriptor: input)
|
|
||||||
try setNonBlocking(fileDescriptor: output)
|
|
||||||
|
|
||||||
isConnected = true
|
|
||||||
logger.info("Transport connected successfully")
|
|
||||||
|
|
||||||
// Start reading loop in background
|
|
||||||
Task {
|
|
||||||
await readLoop()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
|
public func connect() async throws {
|
||||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux)
|
guard !isConnected else { return }
|
||||||
// Get current flags
|
|
||||||
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
|
|
||||||
guard flags >= 0 else {
|
|
||||||
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set non-blocking flag
|
// Set non-blocking mode
|
||||||
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
|
try setNonBlocking(fileDescriptor: input)
|
||||||
guard result >= 0 else {
|
try setNonBlocking(fileDescriptor: output)
|
||||||
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// For platforms where non-blocking operations aren't supported
|
|
||||||
throw MCPError.internalError("Setting non-blocking mode not supported on this platform")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private func readLoop() async {
|
isConnected = true
|
||||||
let bufferSize = 4096
|
logger.info("Transport connected successfully")
|
||||||
var buffer = [UInt8](repeating: 0, count: bufferSize)
|
|
||||||
var pendingData = Data()
|
|
||||||
|
|
||||||
while isConnected && !Task.isCancelled {
|
// Start reading loop in background
|
||||||
do {
|
Task {
|
||||||
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
|
await readLoop()
|
||||||
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytesRead == 0 {
|
|
||||||
logger.notice("EOF received")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingData.append(Data(buffer[..<bytesRead]))
|
|
||||||
|
|
||||||
// Process complete messages
|
|
||||||
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
|
|
||||||
let messageData = pendingData[..<newlineIndex]
|
|
||||||
pendingData = pendingData[(newlineIndex + 1)...]
|
|
||||||
|
|
||||||
if !messageData.isEmpty {
|
|
||||||
logger.debug("Message received", metadata: ["size": "\(messageData.count)"])
|
|
||||||
messageContinuation.yield(Data(messageData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
|
|
||||||
try? await Task.sleep(for: .milliseconds(10))
|
|
||||||
continue
|
|
||||||
} catch {
|
|
||||||
if !Task.isCancelled {
|
|
||||||
logger.error("Read error occurred", metadata: ["error": "\(error)"])
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageContinuation.finish()
|
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
|
||||||
}
|
#if canImport(Darwin) || canImport(Glibc)
|
||||||
|
// Get current flags
|
||||||
|
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
|
||||||
|
guard flags >= 0 else {
|
||||||
|
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
|
||||||
|
}
|
||||||
|
|
||||||
public func disconnect() async {
|
// Set non-blocking flag
|
||||||
guard isConnected else { return }
|
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
|
||||||
isConnected = false
|
guard result >= 0 else {
|
||||||
messageContinuation.finish()
|
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
|
||||||
logger.info("Transport disconnected")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public func send(_ message: Data) async throws {
|
|
||||||
guard isConnected else {
|
|
||||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux)
|
|
||||||
throw MCPError.transportError(Errno(rawValue: ENOTCONN))
|
|
||||||
#else
|
#else
|
||||||
throw MCPError.internalError("Transport not connected")
|
// For platforms where non-blocking operations aren't supported
|
||||||
|
throw MCPError.internalError(
|
||||||
|
"Setting non-blocking mode not supported on this platform")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add newline as delimiter
|
private func readLoop() async {
|
||||||
var messageWithNewline = message
|
let bufferSize = 4096
|
||||||
messageWithNewline.append(UInt8(ascii: "\n"))
|
var buffer = [UInt8](repeating: 0, count: bufferSize)
|
||||||
|
var pendingData = Data()
|
||||||
|
|
||||||
var remaining = messageWithNewline
|
while isConnected && !Task.isCancelled {
|
||||||
while !remaining.isEmpty {
|
do {
|
||||||
do {
|
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
|
||||||
let written = try remaining.withUnsafeBytes { buffer in
|
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
|
||||||
try output.write(UnsafeRawBufferPointer(buffer))
|
}
|
||||||
|
|
||||||
|
if bytesRead == 0 {
|
||||||
|
logger.notice("EOF received")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingData.append(Data(buffer[..<bytesRead]))
|
||||||
|
|
||||||
|
// Process complete messages
|
||||||
|
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
|
||||||
|
let messageData = pendingData[..<newlineIndex]
|
||||||
|
pendingData = pendingData[(newlineIndex + 1)...]
|
||||||
|
|
||||||
|
if !messageData.isEmpty {
|
||||||
|
logger.debug(
|
||||||
|
"Message received", metadata: ["size": "\(messageData.count)"])
|
||||||
|
messageContinuation.yield(Data(messageData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
|
||||||
|
try? await Task.sleep(for: .milliseconds(10))
|
||||||
|
continue
|
||||||
|
} catch {
|
||||||
|
if !Task.isCancelled {
|
||||||
|
logger.error("Read error occurred", metadata: ["error": "\(error)"])
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if written > 0 {
|
}
|
||||||
remaining = remaining.dropFirst(written)
|
|
||||||
|
messageContinuation.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func disconnect() async {
|
||||||
|
guard isConnected else { return }
|
||||||
|
isConnected = false
|
||||||
|
messageContinuation.finish()
|
||||||
|
logger.info("Transport disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func send(_ message: Data) async throws {
|
||||||
|
guard isConnected else {
|
||||||
|
throw MCPError.transportError(Errno(rawValue: ENOTCONN))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add newline as delimiter
|
||||||
|
var messageWithNewline = message
|
||||||
|
messageWithNewline.append(UInt8(ascii: "\n"))
|
||||||
|
|
||||||
|
var remaining = messageWithNewline
|
||||||
|
while !remaining.isEmpty {
|
||||||
|
do {
|
||||||
|
let written = try remaining.withUnsafeBytes { buffer in
|
||||||
|
try output.write(UnsafeRawBufferPointer(buffer))
|
||||||
|
}
|
||||||
|
if written > 0 {
|
||||||
|
remaining = remaining.dropFirst(written)
|
||||||
|
}
|
||||||
|
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
|
||||||
|
try await Task.sleep(for: .milliseconds(10))
|
||||||
|
continue
|
||||||
|
} catch {
|
||||||
|
throw MCPError.transportError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
|
||||||
|
return AsyncThrowingStream { continuation in
|
||||||
|
Task {
|
||||||
|
for await message in messageStream {
|
||||||
|
continuation.yield(message)
|
||||||
|
}
|
||||||
|
continuation.finish()
|
||||||
}
|
}
|
||||||
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
|
|
||||||
try await Task.sleep(for: .milliseconds(10))
|
|
||||||
continue
|
|
||||||
} catch {
|
|
||||||
throw MCPError.transportError(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
|
|
||||||
return AsyncThrowingStream { continuation in
|
|
||||||
Task {
|
|
||||||
for await message in messageStream {
|
|
||||||
continuation.yield(message)
|
|
||||||
}
|
|
||||||
continuation.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue