diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift index 45522fac..d5b97851 100644 --- a/Sources/MCP/Base/Transports/StdioTransport.swift +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -17,6 +17,11 @@ import struct Foundation.Data import Musl #endif +#if os(Windows) + import class Foundation.FileHandle + import class Foundation.Thread +#endif + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) /// An implementation of the MCP stdio transport protocol. /// @@ -221,6 +226,142 @@ import struct Foundation.Data } } + /// Receives messages from the transport. + /// + /// Messages may be individual JSON-RPC requests, notifications, responses, + /// or batches containing multiple requests/notifications encoded as JSON arrays. + /// Each message is guaranteed to be a complete JSON object or array. + /// + /// - Returns: An AsyncThrowingStream of Data objects representing JSON-RPC messages + public func receive() -> AsyncThrowingStream { + return messageStream + } + } +#elseif os(Windows) + /// An implementation of the MCP stdio transport protocol for Windows. + /// + /// This transport implements the [stdio transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) + /// specification from the Model Context Protocol. + /// + /// Windows has no POSIX non-blocking file-descriptor I/O, so this + /// implementation speaks the same wire contract — newline-delimited + /// JSON-RPC messages on standard input/output — over Foundation's + /// `FileHandle`. Reads run on a dedicated thread so that a blocking + /// stdin read never parks a thread of the cooperative pool. + /// + /// A trailing carriage return is stripped from each received line in + /// case a Windows-side client writes CRLF line endings. + /// + /// ## Example Usage + /// + /// ```swift + /// import MCP + /// + /// // Initialize the client + /// let client = Client(name: "MyApp", version: "1.0.0") + /// + /// // Create a transport and connect + /// let transport = StdioTransport() + /// try await client.connect(transport: transport) + /// ``` + public actor StdioTransport: Transport { + /// Logger instance for transport-related events + public nonisolated let logger: Logger + + private var isConnected = false + private let messageStream: AsyncThrowingStream + private let messageContinuation: AsyncThrowingStream.Continuation + + /// Creates a new stdio transport over standard input/output + /// + /// - Parameter logger: Optional logger instance for transport events + public init(logger: Logger? = nil) { + self.logger = + logger + ?? Logger( + label: "mcp.transport.stdio", + factory: { _ in SwiftLogNoOpLogHandler() }) + + // Create message stream + var continuation: AsyncThrowingStream.Continuation! + self.messageStream = AsyncThrowingStream { continuation = $0 } + self.messageContinuation = continuation + } + + /// Establishes connection with the transport + /// + /// This starts the background message reading thread. + public func connect() async throws { + guard !isConnected else { return } + isConnected = true + logger.debug("Transport connected successfully") + + let continuation = messageContinuation + let logger = logger + Thread.detachNewThread { + let standardInput = FileHandle.standardInput + var pendingData = Data() + while true { + let chunk = standardInput.availableData + if chunk.isEmpty { + // EOF — the client closed our standard input + logger.notice("EOF received") + continuation.finish() + return + } + pendingData.append(chunk) + + // Process complete messages + while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) { + var messageData = pendingData[pendingData.startIndex..