From c1639ed720f3736af3355721fa282c2c1acea544 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 9 Apr 2025 15:56:54 -0700 Subject: [PATCH 01/10] Conditionally import Glibc on Linux for fcntl calls --- Sources/MCP/Base/Transports.swift | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Sources/MCP/Base/Transports.swift b/Sources/MCP/Base/Transports.swift index f9e8151..d88bf2f 100644 --- a/Sources/MCP/Base/Transports.swift +++ b/Sources/MCP/Base/Transports.swift @@ -1,4 +1,3 @@ -import Darwin import Logging import struct Foundation.Data @@ -9,6 +8,13 @@ import struct Foundation.Data @preconcurrency import SystemPackage #endif +// Import for specific low-level operations not yet in Swift System +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + import Darwin.POSIX +#elseif os(Linux) + import Glibc +#endif + /// Protocol defining the transport layer for MCP communication public protocol Transport: Actor { var logger: Logger { get } @@ -72,14 +78,22 @@ public actor StdioTransport: Transport { } private func setNonBlocking(fileDescriptor: FileDescriptor) throws { - let flags = fcntl(fileDescriptor.rawValue, F_GETFL) - guard flags >= 0 else { - throw MCPError.transportError(Errno.badFileDescriptor) - } - let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK) - guard result >= 0 else { - throw MCPError.transportError(Errno.badFileDescriptor) - } + #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))) + } + + // 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 { @@ -133,7 +147,11 @@ public actor StdioTransport: Transport { public func send(_ message: Data) async throws { guard isConnected else { - throw MCPError.transportError(Errno.socketNotConnected) + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) + throw MCPError.transportError(Errno(rawValue: ENOTCONN)) + #else + throw MCPError.internalError("Transport not connected") + #endif } // Add newline as delimiter From f4aa3ca7ef22a47eb414fd7675177bcc9d207955 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 04:35:37 -0700 Subject: [PATCH 02/10] Extract transport implementations into separate files --- Sources/MCP/Base/Transports.swift | 408 ------------------ .../Base/Transports/NetworkTransport.swift | 242 +++++++++++ .../MCP/Base/Transports/StdioTransport.swift | 172 ++++++++ 3 files changed, 414 insertions(+), 408 deletions(-) create mode 100644 Sources/MCP/Base/Transports/NetworkTransport.swift create mode 100644 Sources/MCP/Base/Transports/StdioTransport.swift diff --git a/Sources/MCP/Base/Transports.swift b/Sources/MCP/Base/Transports.swift index d88bf2f..4e4350f 100644 --- a/Sources/MCP/Base/Transports.swift +++ b/Sources/MCP/Base/Transports.swift @@ -2,19 +2,6 @@ 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 os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - import Darwin.POSIX -#elseif os(Linux) - import Glibc -#endif - /// Protocol defining the transport layer for MCP communication public protocol Transport: Actor { var logger: Logger { get } @@ -31,398 +18,3 @@ public protocol Transport: Actor { /// Receives data in an async sequence func receive() -> AsyncThrowingStream } - -/// 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 - - 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() - } - } - - 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))) - } - - // 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() - - 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) - } - } 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() - } - } - } -} - -#if canImport(Network) - import Network - - /// Network connection based transport implementation - public actor NetworkTransport: Transport { - private let connection: NWConnection - public nonisolated let logger: Logger - - private var isConnected = false - private let messageStream: AsyncThrowingStream - private let messageContinuation: AsyncThrowingStream.Continuation - - // Track connection state for continuations - private var connectionContinuationResumed = false - - public init(connection: NWConnection, logger: Logger? = nil) { - self.connection = connection - self.logger = - logger - ?? Logger( - label: "mcp.transport.network", - factory: { _ in SwiftLogNoOpLogHandler() } - ) - - // Create message stream - var continuation: AsyncThrowingStream.Continuation! - self.messageStream = AsyncThrowingStream { continuation = $0 } - self.messageContinuation = continuation - } - - /// Connects to the network transport - public func connect() async throws { - guard !isConnected else { return } - - // Reset continuation state - connectionContinuationResumed = false - - // Wait for connection to be ready - try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in - guard let self = self else { - continuation.resume(throwing: MCPError.internalError("Transport deallocated")) - return - } - - connection.stateUpdateHandler = { [weak self] state in - guard let self = self else { return } - - Task { @MainActor in - switch state { - case .ready: - await self.handleConnectionReady(continuation: continuation) - case .failed(let error): - await self.handleConnectionFailed( - error: error, continuation: continuation) - case .cancelled: - await self.handleConnectionCancelled(continuation: continuation) - default: - // Wait for ready or failed state - break - } - } - } - - // Start the connection if it's not already started - if connection.state != .ready { - connection.start(queue: .main) - } else { - Task { @MainActor in - await self.handleConnectionReady(continuation: continuation) - } - } - } - } - - private func handleConnectionReady(continuation: CheckedContinuation) - async - { - if !connectionContinuationResumed { - connectionContinuationResumed = true - isConnected = true - logger.info("Network transport connected successfully") - continuation.resume() - // Start the receive loop after connection is established - Task { await self.receiveLoop() } - } - } - - private func handleConnectionFailed( - error: Swift.Error, continuation: CheckedContinuation - ) async { - if !connectionContinuationResumed { - connectionContinuationResumed = true - logger.error("Connection failed: \(error)") - continuation.resume(throwing: error) - } - } - - private func handleConnectionCancelled(continuation: CheckedContinuation) - async - { - if !connectionContinuationResumed { - connectionContinuationResumed = true - logger.warning("Connection cancelled") - continuation.resume(throwing: MCPError.internalError("Connection cancelled")) - } - } - - public func disconnect() async { - guard isConnected else { return } - isConnected = false - connection.cancel() - messageContinuation.finish() - logger.info("Network transport disconnected") - } - - public func send(_ message: Data) async throws { - guard isConnected else { - throw MCPError.internalError("Transport not connected") - } - - // Add newline as delimiter - var messageWithNewline = message - messageWithNewline.append(UInt8(ascii: "\n")) - - // Use a local actor-isolated variable to track continuation state - var sendContinuationResumed = false - - try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in - guard let self = self else { - continuation.resume(throwing: MCPError.internalError("Transport deallocated")) - return - } - - connection.send( - content: messageWithNewline, - completion: .contentProcessed { [weak self] error in - guard let self = self else { return } - - Task { @MainActor in - if !sendContinuationResumed { - sendContinuationResumed = true - if let error = error { - self.logger.error("Send error: \(error)") - continuation.resume( - throwing: MCPError.internalError("Send error: \(error)")) - } else { - continuation.resume() - } - } - } - }) - } - } - - public func receive() -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - Task { - do { - for try await message in messageStream { - continuation.yield(message) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - } - } - - private func receiveLoop() async { - var buffer = Data() - - while isConnected && !Task.isCancelled { - do { - let newData = try await receiveData() - buffer.append(newData) - - // Process complete messages - while let newlineIndex = buffer.firstIndex(of: UInt8(ascii: "\n")) { - let messageData = buffer[.. Data { - var receiveContinuationResumed = false - - return try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in - guard let self = self else { - continuation.resume(throwing: MCPError.internalError("Transport deallocated")) - return - } - - connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { - content, _, _, error in - Task { @MainActor in - if !receiveContinuationResumed { - receiveContinuationResumed = true - if let error = error { - continuation.resume(throwing: MCPError.transportError(error)) - } else if let content = content { - continuation.resume(returning: content) - } else { - continuation.resume( - throwing: MCPError.internalError("No data received")) - } - } - } - } - } - } - } -#endif diff --git a/Sources/MCP/Base/Transports/NetworkTransport.swift b/Sources/MCP/Base/Transports/NetworkTransport.swift new file mode 100644 index 0000000..5a5dd87 --- /dev/null +++ b/Sources/MCP/Base/Transports/NetworkTransport.swift @@ -0,0 +1,242 @@ +import Logging + +import struct Foundation.Data + +#if canImport(Network) + import Network + + /// Network connection based transport implementation + public actor NetworkTransport: Transport { + private let connection: NWConnection + public nonisolated let logger: Logger + + private var isConnected = false + private let messageStream: AsyncThrowingStream + private let messageContinuation: AsyncThrowingStream.Continuation + + // Track connection state for continuations + private var connectionContinuationResumed = false + + public init(connection: NWConnection, logger: Logger? = nil) { + self.connection = connection + self.logger = + logger + ?? Logger( + label: "mcp.transport.network", + factory: { _ in SwiftLogNoOpLogHandler() } + ) + + // Create message stream + var continuation: AsyncThrowingStream.Continuation! + self.messageStream = AsyncThrowingStream { continuation = $0 } + self.messageContinuation = continuation + } + + /// Connects to the network transport + public func connect() async throws { + guard !isConnected else { return } + + // Reset continuation state + connectionContinuationResumed = false + + // Wait for connection to be ready + try await withCheckedThrowingContinuation { + [weak self] (continuation: CheckedContinuation) in + guard let self = self else { + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) + return + } + + connection.stateUpdateHandler = { [weak self] state in + guard let self = self else { return } + + Task { @MainActor in + switch state { + case .ready: + await self.handleConnectionReady(continuation: continuation) + case .failed(let error): + await self.handleConnectionFailed( + error: error, continuation: continuation) + case .cancelled: + await self.handleConnectionCancelled(continuation: continuation) + default: + // Wait for ready or failed state + break + } + } + } + + // Start the connection if it's not already started + if connection.state != .ready { + connection.start(queue: .main) + } else { + Task { @MainActor in + await self.handleConnectionReady(continuation: continuation) + } + } + } + } + + private func handleConnectionReady(continuation: CheckedContinuation) + async + { + if !connectionContinuationResumed { + connectionContinuationResumed = true + isConnected = true + logger.info("Network transport connected successfully") + continuation.resume() + // Start the receive loop after connection is established + Task { await self.receiveLoop() } + } + } + + private func handleConnectionFailed( + error: Swift.Error, continuation: CheckedContinuation + ) async { + if !connectionContinuationResumed { + connectionContinuationResumed = true + logger.error("Connection failed: \(error)") + continuation.resume(throwing: error) + } + } + + private func handleConnectionCancelled(continuation: CheckedContinuation) + async + { + if !connectionContinuationResumed { + connectionContinuationResumed = true + logger.warning("Connection cancelled") + continuation.resume(throwing: MCPError.internalError("Connection cancelled")) + } + } + + public func disconnect() async { + guard isConnected else { return } + isConnected = false + connection.cancel() + messageContinuation.finish() + logger.info("Network transport disconnected") + } + + public func send(_ message: Data) async throws { + guard isConnected else { + throw MCPError.internalError("Transport not connected") + } + + // Add newline as delimiter + var messageWithNewline = message + messageWithNewline.append(UInt8(ascii: "\n")) + + // Use a local actor-isolated variable to track continuation state + var sendContinuationResumed = false + + try await withCheckedThrowingContinuation { + [weak self] (continuation: CheckedContinuation) in + guard let self = self else { + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) + return + } + + connection.send( + content: messageWithNewline, + completion: .contentProcessed { [weak self] error in + guard let self = self else { return } + + Task { @MainActor in + if !sendContinuationResumed { + sendContinuationResumed = true + if let error = error { + self.logger.error("Send error: \(error)") + continuation.resume( + throwing: MCPError.internalError("Send error: \(error)")) + } else { + continuation.resume() + } + } + } + }) + } + } + + public func receive() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + do { + for try await message in messageStream { + continuation.yield(message) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + } + + private func receiveLoop() async { + var buffer = Data() + + while isConnected && !Task.isCancelled { + do { + let newData = try await receiveData() + buffer.append(newData) + + // Process complete messages + while let newlineIndex = buffer.firstIndex(of: UInt8(ascii: "\n")) { + let messageData = buffer[.. Data { + var receiveContinuationResumed = false + + return try await withCheckedThrowingContinuation { + [weak self] (continuation: CheckedContinuation) in + guard let self = self else { + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) + return + } + + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { + content, _, _, error in + Task { @MainActor in + if !receiveContinuationResumed { + receiveContinuationResumed = true + if let error = error { + continuation.resume(throwing: MCPError.transportError(error)) + } else if let content = content { + continuation.resume(returning: content) + } else { + continuation.resume( + throwing: MCPError.internalError("No data received")) + } + } + } + } + } + } + } +#endif diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift new file mode 100644 index 0000000..99e7b0a --- /dev/null +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -0,0 +1,172 @@ +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 os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + import Darwin.POSIX +#elseif os(Linux) + 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 + 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() }) + + // 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() + } + } + + 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))) + } + + // 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() + + 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) + } + } 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() + } + } + } +} From e786e1c69a83c19979acdeb740943fe1656dcaa9 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 04:38:27 -0700 Subject: [PATCH 03/10] Fix warning in test --- Tests/MCPTests/RoundtripTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/MCPTests/RoundtripTests.swift b/Tests/MCPTests/RoundtripTests.swift index 39abda3..1197840 100644 --- a/Tests/MCPTests/RoundtripTests.swift +++ b/Tests/MCPTests/RoundtripTests.swift @@ -127,7 +127,7 @@ struct RoundtripTests { let pingTask = Task { try await client.ping() // Ping doesn't return anything, so just getting here without throwing is success - #expect(true) // Test passed if we reach this point + #expect(Bool(true)) } try await withThrowingTaskGroup(of: Void.self) { group in From bf0950374f83765c6c12094aa283e67a7f774180 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 04:40:51 -0700 Subject: [PATCH 04/10] Add ubuntu-latest to CI test matrix --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c23ed42..5b418ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,14 @@ permissions: jobs: test: - runs-on: macos-latest - strategy: matrix: + os: [macos-latest, ubuntu-latest] swift-version: - ^6 - name: Build and Test (Swift ${{ matrix.swift-version }}) + runs-on: ${{ matrix.os }} + name: Build and Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) steps: - uses: actions/checkout@v4 From 29c572167571a92fcc8908e9499dd4e23f50cb28 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 04:55:29 -0700 Subject: [PATCH 05/10] Switch to vapor/swiftly-action for setup step --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b418ab..7ef6399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,17 @@ jobs: matrix: os: [macos-latest, ubuntu-latest] swift-version: - - ^6 + - 6.0.3 + - 6.1.0 runs-on: ${{ matrix.os }} name: Build and Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) steps: - uses: actions/checkout@v4 - - uses: swift-actions/setup-swift@v2 + - uses: vapor/swiftly-action@v0.2 with: - swift-version: ${{ matrix.swift-version }} + toolchain: ${{ matrix.swift-version }} - name: Build run: swift build -v - name: Run tests From 1a0ae216a12e46a3dd18513159ad777a1dbff314 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 05:03:34 -0700 Subject: [PATCH 06/10] Configure Prettier extension to format GitHub Actions workflows --- .vscode/extensions.json | 2 +- .vscode/settings.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index baab102..6d8b1f0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["swiftlang.swift-vscode"] + "recommendations": ["swiftlang.swift-vscode", "esbenp.prettier-vscode"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 757f266..e4ad6b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "explicit" + }, + "[github-actions-workflow]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } From 4acc279c58029beece089cc57fbf82e00ef32a82 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 05:04:02 -0700 Subject: [PATCH 07/10] Setup with different actions depending on platform --- .github/workflows/ci.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ef6399..320e843 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] permissions: contents: read @@ -23,11 +23,21 @@ jobs: name: Build and Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) steps: - - uses: actions/checkout@v4 - - uses: vapor/swiftly-action@v0.2 - with: - toolchain: ${{ matrix.swift-version }} - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v + - uses: actions/checkout@v4 + + - name: Setup Swift on Linux + if: matrix.os == 'ubuntu-latest' + uses: vapor/swiftly-action@v0.2 + with: + toolchain: ${{ matrix.swift-version }} + - name: Setup Xcode on macOS + if: matrix.os == 'macos-latest' + uses: swift-actions/setup-swift@v2 + with: + swift-version: ${{ matrix.swift-version }} + + - name: Build + run: swift build -v + + - name: Run tests + run: swift test -v From 63becdc28f8c67d29769882b82691e74f95b505e Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 05:05:18 -0700 Subject: [PATCH 08/10] Rename step --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 320e843..47b2e9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: uses: vapor/swiftly-action@v0.2 with: toolchain: ${{ matrix.swift-version }} - - name: Setup Xcode on macOS + - name: Setup Swift on macOS if: matrix.os == 'macos-latest' uses: swift-actions/setup-swift@v2 with: From a5015ca7d345d7254aaca7508a73ebaeca7ddb68 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 05:06:45 -0700 Subject: [PATCH 09/10] Rename job --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47b2e9b..8d9301b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - 6.1.0 runs-on: ${{ matrix.os }} - name: Build and Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) + name: Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) steps: - uses: actions/checkout@v4 From 3862ebab5d782bbc00b7ad3efd82d6a018f6a942 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 05:17:29 -0700 Subject: [PATCH 10/10] Update README --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ef3f840..2c05a96 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,20 @@ Swift implementation of the [Model Context Protocol][mcp] (MCP). ## Requirements -- Swift 6.0+ / Xcode 16+ -- macOS 13.0+ -- iOS / Mac Catalyst 16.0+ -- watchOS 9.0+ -- tvOS 16.0+ -- visionOS 1.0+ +- Swift 6.0+ (Xcode 16+) + +### Supported Platforms + +| Platform | Minimum Version | +|----------|----------------| +| macOS | 13.0+ | +| iOS / Mac Catalyst | 16.0+ | +| 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. ## Installation