* Support 2025-06-18 Spec (#167) * Support OpenAI _meta for Tool * Update init * Add title and outputSchema * Concurreny issue * General fields * Add Generate Fields to Server elements * 4 tab spacing * Update Version * Elicitation * Format * More tests and inits * Elicitation in README * Fix orphaned test * Add title to more structs * Public init * Fix meta validation and make tests more generic * Format and make Result properties public again * Revert more visibility changes * feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181) * chore: fixed unit tests * feat: added icon and progess base structs * feat: added progress notification and meta to callTool * feat: added tests * feat: simplified RequestMeta encoding/decoding * tests: added unit test to track progress * chore: rebased v0.11.x * feat: implemented cancellation * feat: added overloaded callTool method * fix: encoding Response _meta fix * chore: added old version to the versions list * feat: 2025-11-25 phase 2; added Logging and Completions (#196) * feat: added Logging and Completions * feat: updated EmbeddedResource for the latest spec * chore: changed encoding of meta in Prompts to encodeIfPresent * feat: compliance tests + http server transport (#197) * feat: http server transport * feat: added http server transport * feat: removed unused files * feat: added conformance tests * feat: improved conformace testing * chore: removed redundant files * feat/2025-11-25: sampling, elicitation, roots (#198) * feat: Roots implementation * feat: added unit testing * feat: improved unit tests and fixed NetworkTransport crash * feat: implemented sampling and elicitation * feat: improved sampling tests * feat: improved elicitation tests * feat: improved confirmance tests * feat: improved Prompts unit tests * feat: removed test code * feat: updated conformance tests * chore: removed redundant commas * chore: removed 6.0.3 swift version from ci * chore: improved unit tests a little bit * chore: update README.md * chore: add server progress tracking example to README.md * chore: updated README.md * chore: updated transports table formatting * chore: updated MockNetworkConnection --------- Co-authored-by: Austin Evans <austinjevans@me.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
235 lines
9.2 KiB
Swift
235 lines
9.2 KiB
Swift
import Logging
|
|
|
|
import struct Foundation.Data
|
|
|
|
#if canImport(System)
|
|
import System
|
|
#else
|
|
@preconcurrency import SystemPackage
|
|
#endif
|
|
|
|
// Import for specific low-level operations not yet in Swift System
|
|
#if canImport(Darwin)
|
|
import Darwin.POSIX
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#elseif canImport(Musl)
|
|
import Musl
|
|
#endif
|
|
|
|
#if canImport(Darwin) || canImport(Glibc) || canImport(Musl)
|
|
/// An implementation of the MCP stdio transport protocol.
|
|
///
|
|
/// This transport implements the [stdio transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio)
|
|
/// specification from the Model Context Protocol.
|
|
///
|
|
/// The stdio transport works by:
|
|
/// - Reading JSON-RPC messages from standard input
|
|
/// - Writing JSON-RPC messages to standard output
|
|
/// - Using newline characters as message delimiters
|
|
/// - Supporting non-blocking I/O operations
|
|
///
|
|
/// This transport is the recommended option for most MCP applications due to its
|
|
/// simplicity and broad platform support.
|
|
///
|
|
/// - Important: This transport is available on Apple platforms and Linux distributions with glibc
|
|
/// (Ubuntu, Debian, Fedora, CentOS, RHEL).
|
|
///
|
|
/// ## 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 {
|
|
private let input: FileDescriptor
|
|
private let output: FileDescriptor
|
|
/// Logger instance for transport-related events
|
|
public nonisolated let logger: Logger
|
|
|
|
private var isConnected = false
|
|
private let messageStream: AsyncThrowingStream<Data, Swift.Error>
|
|
private let messageContinuation: AsyncThrowingStream<Data, Swift.Error>.Continuation
|
|
|
|
/// Creates a new stdio transport with the specified file descriptors
|
|
///
|
|
/// - Parameters:
|
|
/// - input: File descriptor for reading (defaults to standard input)
|
|
/// - output: File descriptor for writing (defaults to standard output)
|
|
/// - logger: Optional logger instance for transport events
|
|
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: AsyncThrowingStream<Data, Swift.Error>.Continuation!
|
|
self.messageStream = AsyncThrowingStream { continuation = $0 }
|
|
self.messageContinuation = continuation
|
|
}
|
|
|
|
/// Establishes connection with the transport
|
|
///
|
|
/// This method configures the file descriptors for non-blocking I/O
|
|
/// and starts the background message reading loop.
|
|
///
|
|
/// - Throws: Error if the file descriptors cannot be configured
|
|
public func connect() async throws {
|
|
guard !isConnected else { return }
|
|
|
|
// Set non-blocking mode
|
|
try setNonBlocking(fileDescriptor: input)
|
|
try setNonBlocking(fileDescriptor: output)
|
|
|
|
isConnected = true
|
|
logger.debug("Transport connected successfully")
|
|
|
|
// Start reading loop in background
|
|
Task {
|
|
await readLoop()
|
|
}
|
|
}
|
|
|
|
/// Configures a file descriptor for non-blocking I/O
|
|
///
|
|
/// - Parameter fileDescriptor: The file descriptor to configure
|
|
/// - Throws: Error if the operation fails
|
|
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
|
|
#if canImport(Darwin) || canImport(Glibc) || canImport(Musl)
|
|
// 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
|
|
}
|
|
|
|
/// Continuous loop that reads and processes incoming messages
|
|
///
|
|
/// This method runs in the background while the transport is connected,
|
|
/// parsing complete messages delimited by newlines and yielding them
|
|
/// to the message stream.
|
|
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))
|
|
}
|
|
|
|
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.trace(
|
|
"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()
|
|
}
|
|
|
|
/// Disconnects from the transport
|
|
///
|
|
/// This stops the message reading loop and releases associated resources.
|
|
public func disconnect() async {
|
|
guard isConnected else { return }
|
|
isConnected = false
|
|
messageContinuation.finish()
|
|
logger.debug("Transport disconnected")
|
|
}
|
|
|
|
/// Sends a message over the transport.
|
|
///
|
|
/// This method supports sending both individual JSON-RPC messages and JSON-RPC batches.
|
|
/// Batches should be encoded as a JSON array containing multiple request/notification objects
|
|
/// according to the JSON-RPC 2.0 specification.
|
|
///
|
|
/// - Parameter message: The message data to send (without a trailing newline)
|
|
/// - Throws: Error if the message cannot be sent
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<Data, Swift.Error> {
|
|
return messageStream
|
|
}
|
|
}
|
|
#endif
|