Skip to content

Commit 960c1dd

Browse files
authored
Conditionalize availability of StdioTransport (#65)
* Conditionalize availability of StdioTransport * Update README with information about platform support
1 parent 1e61f20 commit 960c1dd

File tree

2 files changed

+141
-131
lines changed

2 files changed

+141
-131
lines changed

README.md

+14-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP).
66

77
- Swift 6.0+ (Xcode 16+)
88

9-
### Supported Platforms
9+
## Platform Support
1010

1111
| Platform | Minimum Version |
1212
|----------|----------------|
@@ -15,9 +15,19 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP).
1515
| watchOS | 9.0+ |
1616
| tvOS | 16.0+ |
1717
| visionOS | 1.0+ |
18-
| Linux |[^1] |
19-
20-
[^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.
18+
| Linux ||
19+
| Windows ||
20+
21+
> [!IMPORTANT]
22+
> MCP's transport layer handles communication between clients and servers.
23+
> The Swift SDK supports multiple transport mechanisms,
24+
> with different platform availability:
25+
>
26+
> * `StdioTransport` is available on Apple platforms
27+
> and Linux distributions with glibc, such as
28+
> Ubuntu, Debian, Fedora, CentOS, or RHEL.
29+
>
30+
> * `NetworkTransport` is available only on Apple platforms.
2131
2232
## Installation
2333

Sources/MCP/Base/Transports/StdioTransport.swift

+127-127
Original file line numberDiff line numberDiff line change
@@ -9,164 +9,164 @@ import struct Foundation.Data
99
#endif
1010

1111
// Import for specific low-level operations not yet in Swift System
12-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
12+
#if canImport(Darwin)
1313
import Darwin.POSIX
14-
#elseif os(Linux)
14+
#elseif canImport(Glibc)
1515
import Glibc
1616
#endif
1717

18-
/// Standard input/output transport implementation
19-
public actor StdioTransport: Transport {
20-
private let input: FileDescriptor
21-
private let output: FileDescriptor
22-
public nonisolated let logger: Logger
23-
24-
private var isConnected = false
25-
private let messageStream: AsyncStream<Data>
26-
private let messageContinuation: AsyncStream<Data>.Continuation
27-
28-
public init(
29-
input: FileDescriptor = FileDescriptor.standardInput,
30-
output: FileDescriptor = FileDescriptor.standardOutput,
31-
logger: Logger? = nil
32-
) {
33-
self.input = input
34-
self.output = output
35-
self.logger =
36-
logger
37-
?? Logger(
38-
label: "mcp.transport.stdio",
39-
factory: { _ in SwiftLogNoOpLogHandler() })
40-
41-
// Create message stream
42-
var continuation: AsyncStream<Data>.Continuation!
43-
self.messageStream = AsyncStream { continuation = $0 }
44-
self.messageContinuation = continuation
45-
}
18+
#if canImport(Darwin) || canImport(Glibc)
19+
/// Standard input/output transport implementation
20+
public actor StdioTransport: Transport {
21+
private let input: FileDescriptor
22+
private let output: FileDescriptor
23+
public nonisolated let logger: Logger
24+
25+
private var isConnected = false
26+
private let messageStream: AsyncStream<Data>
27+
private let messageContinuation: AsyncStream<Data>.Continuation
28+
29+
public init(
30+
input: FileDescriptor = FileDescriptor.standardInput,
31+
output: FileDescriptor = FileDescriptor.standardOutput,
32+
logger: Logger? = nil
33+
) {
34+
self.input = input
35+
self.output = output
36+
self.logger =
37+
logger
38+
?? Logger(
39+
label: "mcp.transport.stdio",
40+
factory: { _ in SwiftLogNoOpLogHandler() })
41+
42+
// Create message stream
43+
var continuation: AsyncStream<Data>.Continuation!
44+
self.messageStream = AsyncStream { continuation = $0 }
45+
self.messageContinuation = continuation
46+
}
4647

47-
public func connect() async throws {
48-
guard !isConnected else { return }
48+
public func connect() async throws {
49+
guard !isConnected else { return }
4950

50-
// Set non-blocking mode
51-
try setNonBlocking(fileDescriptor: input)
52-
try setNonBlocking(fileDescriptor: output)
51+
// Set non-blocking mode
52+
try setNonBlocking(fileDescriptor: input)
53+
try setNonBlocking(fileDescriptor: output)
5354

54-
isConnected = true
55-
logger.info("Transport connected successfully")
55+
isConnected = true
56+
logger.info("Transport connected successfully")
5657

57-
// Start reading loop in background
58-
Task {
59-
await readLoop()
58+
// Start reading loop in background
59+
Task {
60+
await readLoop()
61+
}
6062
}
61-
}
6263

63-
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
64-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux)
65-
// Get current flags
66-
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
67-
guard flags >= 0 else {
68-
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
69-
}
64+
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
65+
#if canImport(Darwin) || canImport(Glibc)
66+
// Get current flags
67+
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
68+
guard flags >= 0 else {
69+
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
70+
}
7071

71-
// Set non-blocking flag
72-
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
73-
guard result >= 0 else {
74-
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
75-
}
76-
#else
77-
// For platforms where non-blocking operations aren't supported
78-
throw MCPError.internalError("Setting non-blocking mode not supported on this platform")
79-
#endif
80-
}
72+
// Set non-blocking flag
73+
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
74+
guard result >= 0 else {
75+
throw MCPError.transportError(Errno(rawValue: CInt(errno)))
76+
}
77+
#else
78+
// For platforms where non-blocking operations aren't supported
79+
throw MCPError.internalError(
80+
"Setting non-blocking mode not supported on this platform")
81+
#endif
82+
}
8183

82-
private func readLoop() async {
83-
let bufferSize = 4096
84-
var buffer = [UInt8](repeating: 0, count: bufferSize)
85-
var pendingData = Data()
84+
private func readLoop() async {
85+
let bufferSize = 4096
86+
var buffer = [UInt8](repeating: 0, count: bufferSize)
87+
var pendingData = Data()
8688

87-
while isConnected && !Task.isCancelled {
88-
do {
89-
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
90-
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
91-
}
89+
while isConnected && !Task.isCancelled {
90+
do {
91+
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
92+
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
93+
}
9294

93-
if bytesRead == 0 {
94-
logger.notice("EOF received")
95-
break
96-
}
95+
if bytesRead == 0 {
96+
logger.notice("EOF received")
97+
break
98+
}
9799

98-
pendingData.append(Data(buffer[..<bytesRead]))
100+
pendingData.append(Data(buffer[..<bytesRead]))
99101

100-
// Process complete messages
101-
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
102-
let messageData = pendingData[..<newlineIndex]
103-
pendingData = pendingData[(newlineIndex + 1)...]
102+
// Process complete messages
103+
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
104+
let messageData = pendingData[..<newlineIndex]
105+
pendingData = pendingData[(newlineIndex + 1)...]
104106

105-
if !messageData.isEmpty {
106-
logger.debug("Message received", metadata: ["size": "\(messageData.count)"])
107-
messageContinuation.yield(Data(messageData))
107+
if !messageData.isEmpty {
108+
logger.debug(
109+
"Message received", metadata: ["size": "\(messageData.count)"])
110+
messageContinuation.yield(Data(messageData))
111+
}
108112
}
113+
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
114+
try? await Task.sleep(for: .milliseconds(10))
115+
continue
116+
} catch {
117+
if !Task.isCancelled {
118+
logger.error("Read error occurred", metadata: ["error": "\(error)"])
119+
}
120+
break
109121
}
110-
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
111-
try? await Task.sleep(for: .milliseconds(10))
112-
continue
113-
} catch {
114-
if !Task.isCancelled {
115-
logger.error("Read error occurred", metadata: ["error": "\(error)"])
116-
}
117-
break
118122
}
119-
}
120123

121-
messageContinuation.finish()
122-
}
124+
messageContinuation.finish()
125+
}
123126

124-
public func disconnect() async {
125-
guard isConnected else { return }
126-
isConnected = false
127-
messageContinuation.finish()
128-
logger.info("Transport disconnected")
129-
}
127+
public func disconnect() async {
128+
guard isConnected else { return }
129+
isConnected = false
130+
messageContinuation.finish()
131+
logger.info("Transport disconnected")
132+
}
130133

131-
public func send(_ message: Data) async throws {
132-
guard isConnected else {
133-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux)
134+
public func send(_ message: Data) async throws {
135+
guard isConnected else {
134136
throw MCPError.transportError(Errno(rawValue: ENOTCONN))
135-
#else
136-
throw MCPError.internalError("Transport not connected")
137-
#endif
138-
}
137+
}
139138

140-
// Add newline as delimiter
141-
var messageWithNewline = message
142-
messageWithNewline.append(UInt8(ascii: "\n"))
139+
// Add newline as delimiter
140+
var messageWithNewline = message
141+
messageWithNewline.append(UInt8(ascii: "\n"))
143142

144-
var remaining = messageWithNewline
145-
while !remaining.isEmpty {
146-
do {
147-
let written = try remaining.withUnsafeBytes { buffer in
148-
try output.write(UnsafeRawBufferPointer(buffer))
149-
}
150-
if written > 0 {
151-
remaining = remaining.dropFirst(written)
143+
var remaining = messageWithNewline
144+
while !remaining.isEmpty {
145+
do {
146+
let written = try remaining.withUnsafeBytes { buffer in
147+
try output.write(UnsafeRawBufferPointer(buffer))
148+
}
149+
if written > 0 {
150+
remaining = remaining.dropFirst(written)
151+
}
152+
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
153+
try await Task.sleep(for: .milliseconds(10))
154+
continue
155+
} catch {
156+
throw MCPError.transportError(error)
152157
}
153-
} catch let error where MCPError.isResourceTemporarilyUnavailable(error) {
154-
try await Task.sleep(for: .milliseconds(10))
155-
continue
156-
} catch {
157-
throw MCPError.transportError(error)
158158
}
159159
}
160-
}
161160

162-
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
163-
return AsyncThrowingStream { continuation in
164-
Task {
165-
for await message in messageStream {
166-
continuation.yield(message)
161+
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
162+
return AsyncThrowingStream { continuation in
163+
Task {
164+
for await message in messageStream {
165+
continuation.yield(message)
166+
}
167+
continuation.finish()
167168
}
168-
continuation.finish()
169169
}
170170
}
171171
}
172-
}
172+
#endif

0 commit comments

Comments
 (0)