Skip to content

Conditionalize availability of StdioTransport #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|----------|----------------|
Expand All @@ -15,9 +15,19 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP).
| watchOS | 9.0+ |
| tvOS | 16.0+ |
| visionOS | 1.0+ |
| Linux | ✓ [^1] |

[^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.
| Linux | ✓ |
| Windows | ✓ |

> [!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

Expand Down
254 changes: 127 additions & 127 deletions Sources/MCP/Base/Transports/StdioTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

private var isConnected = false
private let messageStream: AsyncStream<Data>
private let messageContinuation: AsyncStream<Data>.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() })

// Create message stream
var continuation: AsyncStream<Data>.Continuation!
self.messageStream = AsyncStream { continuation = $0 }
self.messageContinuation = continuation
}
#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<Data>
private let messageContinuation: AsyncStream<Data>.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() })

// Create message stream
var continuation: AsyncStream<Data>.Continuation!
self.messageStream = AsyncStream { continuation = $0 }
self.messageContinuation = continuation
}

public func connect() async throws {
guard !isConnected else { return }
public func connect() async throws {
guard !isConnected else { return }

// Set non-blocking mode
try setNonBlocking(fileDescriptor: input)
try setNonBlocking(fileDescriptor: output)
// Set non-blocking mode
try setNonBlocking(fileDescriptor: input)
try setNonBlocking(fileDescriptor: output)

isConnected = true
logger.info("Transport connected successfully")
isConnected = true
logger.info("Transport connected successfully")

// Start reading loop in background
Task {
await readLoop()
// Start reading loop in background
Task {
await readLoop()
}
}
}

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)))
}
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)))
}

// 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 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
}

private func readLoop() async {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
var pendingData = Data()
private func readLoop() async {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
var pendingData = Data()

while isConnected && !Task.isCancelled {
do {
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
}
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
}
if bytesRead == 0 {
logger.notice("EOF received")
break
}

pendingData.append(Data(buffer[..<bytesRead]))
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)...]
// 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))
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
}
} 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()
}
messageContinuation.finish()
}

public func disconnect() async {
guard isConnected else { return }
isConnected = false
messageContinuation.finish()
logger.info("Transport disconnected")
}
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)
public func send(_ message: Data) async throws {
guard isConnected else {
throw MCPError.transportError(Errno(rawValue: ENOTCONN))
#else
throw MCPError.internalError("Transport not connected")
#endif
}
}

// Add newline as delimiter
var messageWithNewline = message
messageWithNewline.append(UInt8(ascii: "\n"))
// 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)
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)
}
} 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)
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
return AsyncThrowingStream { continuation in
Task {
for await message in messageStream {
continuation.yield(message)
}
continuation.finish()
}
continuation.finish()
}
}
}
}
#endif