diff --git a/README.md b/README.md index 2c05a96..a274e96 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP). - Swift 6.0+ (Xcode 16+) -### Supported Platforms +## Platform Support | Platform | Minimum Version | |----------|----------------| @@ -15,9 +15,19 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP). | watchOS | 9.0+ | | tvOS | 16.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 diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift index 99e7b0a..7978a6d 100644 --- a/Sources/MCP/Base/Transports/StdioTransport.swift +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -9,164 +9,164 @@ import struct Foundation.Data #endif // 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 -#elseif os(Linux) +#elseif canImport(Glibc) import Glibc #endif -/// Standard input/output transport implementation -public actor StdioTransport: Transport { - private let input: FileDescriptor - private let output: FileDescriptor - public nonisolated let logger: Logger +#if canImport(Darwin) || canImport(Glibc) + /// Standard input/output transport implementation + public actor StdioTransport: Transport { + private let input: FileDescriptor + private let output: FileDescriptor + public nonisolated let logger: Logger - private var isConnected = false - private let messageStream: AsyncStream - private let messageContinuation: AsyncStream.Continuation + private var isConnected = false + private let messageStream: AsyncStream + private let messageContinuation: AsyncStream.Continuation - public init( - input: FileDescriptor = FileDescriptor.standardInput, - output: FileDescriptor = FileDescriptor.standardOutput, - logger: Logger? = nil - ) { - self.input = input - self.output = output - self.logger = - logger - ?? Logger( - label: "mcp.transport.stdio", - factory: { _ in SwiftLogNoOpLogHandler() }) + public init( + input: FileDescriptor = FileDescriptor.standardInput, + output: FileDescriptor = FileDescriptor.standardOutput, + logger: Logger? = nil + ) { + self.input = input + self.output = output + self.logger = + logger + ?? Logger( + label: "mcp.transport.stdio", + factory: { _ in SwiftLogNoOpLogHandler() }) - // Create message stream - var continuation: AsyncStream.Continuation! - self.messageStream = AsyncStream { continuation = $0 } - 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() + // Create message stream + var continuation: AsyncStream.Continuation! + self.messageStream = AsyncStream { continuation = $0 } + self.messageContinuation = continuation } - } - private func setNonBlocking(fileDescriptor: FileDescriptor) throws { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) - // Get current flags - let flags = fcntl(fileDescriptor.rawValue, F_GETFL) - guard flags >= 0 else { - throw MCPError.transportError(Errno(rawValue: CInt(errno))) - } + public func connect() async throws { + guard !isConnected else { return } - // Set non-blocking flag - let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK) - guard result >= 0 else { - 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 - } + // Set non-blocking mode + try setNonBlocking(fileDescriptor: input) + try setNonBlocking(fileDescriptor: output) - private func readLoop() async { - let bufferSize = 4096 - var buffer = [UInt8](repeating: 0, count: bufferSize) - var pendingData = Data() + isConnected = true + logger.info("Transport connected successfully") - while isConnected && !Task.isCancelled { - do { - let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in - try input.read(into: UnsafeMutableRawBufferPointer(pointer)) - } - - if bytesRead == 0 { - logger.notice("EOF received") - break - } - - pendingData.append(Data(buffer[..= 0 else { + throw MCPError.transportError(Errno(rawValue: CInt(errno))) + } - 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 { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) - throw MCPError.transportError(Errno(rawValue: ENOTCONN)) + // Set non-blocking flag + let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK) + guard result >= 0 else { + throw MCPError.transportError(Errno(rawValue: CInt(errno))) + } #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 } - // Add newline as delimiter - var messageWithNewline = message - messageWithNewline.append(UInt8(ascii: "\n")) + private func readLoop() async { + let bufferSize = 4096 + var buffer = [UInt8](repeating: 0, count: bufferSize) + var pendingData = Data() - var remaining = messageWithNewline - while !remaining.isEmpty { - do { - let written = try remaining.withUnsafeBytes { buffer in - try output.write(UnsafeRawBufferPointer(buffer)) + while isConnected && !Task.isCancelled { + do { + let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in + try input.read(into: UnsafeMutableRawBufferPointer(pointer)) + } + + if bytesRead == 0 { + logger.notice("EOF received") + break + } + + pendingData.append(Data(buffer[.. 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 { + 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) } } } - - public func receive() -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - Task { - for await message in messageStream { - continuation.yield(message) - } - continuation.finish() - } - } - } -} +#endif