From 59e4582e43137a8a6118ecda7d08f14542fd94df Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 06:39:40 -0700 Subject: [PATCH 1/6] Refactor CI workflow with separate jobs for macOS, Linux, and Windows --- .github/workflows/ci.yml | 86 +++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d9301bc..4173dbb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,30 +11,88 @@ permissions: pull-requests: write jobs: - test: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + swift-versions: ${{ steps.set-matrix.outputs.swift-versions }} + env: + SWIFT_VERSIONS: | + 6.0.3 + 6.1.0 + steps: + - id: set-matrix + run: | + # Convert multi-line string to JSON array (jq is pre-installed on GitHub runners) + VERSIONS=$(echo "$SWIFT_VERSIONS" | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "swift-versions=$VERSIONS" >> $GITHUB_OUTPUT + + test-macos: + name: Test (macOS, Swift ${{ matrix.swift-version }}) + needs: setup strategy: matrix: - os: [macos-latest, ubuntu-latest] - swift-version: - - 6.0.3 - - 6.1.0 + swift-version: ${{ fromJson(needs.setup.outputs.swift-versions) }} + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: ${{ matrix.swift-version }} - runs-on: ${{ matrix.os }} - name: Test (${{ matrix.os }}, Swift ${{ matrix.swift-version }}) + - name: Build + run: swift build -v + + - name: Run tests + run: swift test -v + test-ubuntu: + name: Test (Ubuntu, Swift ${{ matrix.swift-version }}) + needs: setup + strategy: + matrix: + swift-version: ${{ fromJson(needs.setup.outputs.swift-versions) }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Swift on Linux - if: matrix.os == 'ubuntu-latest' + - name: Setup Swift uses: vapor/swiftly-action@v0.2 with: toolchain: ${{ matrix.swift-version }} - - name: Setup Swift 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 + + test-windows: + name: Test (Windows, Swift ${{ matrix.swift-version }}) + needs: setup + strategy: + matrix: + swift-version: ${{ fromJson(needs.setup.outputs.swift-versions) }} + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Swift + shell: powershell + run: | + # Download Swift + $url = "https://download.swift.org/swift-${{ matrix.swift-version }}-release/windows10/swift-${{ matrix.swift-version }}-RELEASE/swift-${{ matrix.swift-version }}-RELEASE-windows10.exe" + $output = "$env:TEMP\swift-installer.exe" + Invoke-WebRequest -Uri $url -OutFile $output + + # Install Swift silently + Start-Process -FilePath $output -ArgumentList "/quiet" -Wait + + # Add Swift to PATH + echo "C:\Library\Swift-development\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "C:\Library\Swift-development\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Build run: swift build -v From 1641153dd2a770b282c3881953e0c5425efb3755 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 06:40:04 -0700 Subject: [PATCH 2/6] Add support for Windows platform --- .../MCP/Base/Transports/StdioTransport.swift | 273 ++++++++++-------- 1 file changed, 145 insertions(+), 128 deletions(-) diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift index 99e7b0ae..ac4c36a1 100644 --- a/Sources/MCP/Base/Transports/StdioTransport.swift +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -9,164 +9,181 @@ 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 +#elseif canImport(ucrt) + import ucrt + import WinSDK #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 - } +#if canImport(Darwin) || canImport(Glibc) || canImport(ucrt) + /// 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 } + 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) || 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 - } + // 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))) + } + #elseif canImport(ucrt) + // Windows non-blocking mode setup + var mode: UInt32 = 1 // 1 = Non-blocking mode + let result = ioctlsocket(SOCKET(fileDescriptor.rawValue), FIONBIO, &mode) + guard result == 0 else { + let error = WSAGetLastError() + throw MCPError.transportError(POSIXErrorCode(rawValue: CInt(error))) + } + #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[.. 0 { - remaining = remaining.dropFirst(written) + // 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) } - } 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) + public func receive() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + for await message in messageStream { + continuation.yield(message) + } + continuation.finish() } - continuation.finish() } } } -} +#endif From 91528baced90b89d4de97a8b27619ba959a1db2e Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 06:44:30 -0700 Subject: [PATCH 3/6] Use WinGet to install Swift --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4173dbb4..53c76790 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,17 +82,17 @@ jobs: - name: Install Swift shell: powershell run: | - # Download Swift - $url = "https://download.swift.org/swift-${{ matrix.swift-version }}-release/windows10/swift-${{ matrix.swift-version }}-RELEASE/swift-${{ matrix.swift-version }}-RELEASE-windows10.exe" - $output = "$env:TEMP\swift-installer.exe" - Invoke-WebRequest -Uri $url -OutFile $output + # Enable Developer Mode + reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - # Install Swift silently - Start-Process -FilePath $output -ArgumentList "/quiet" -Wait + # Install Visual Studio dependencies + winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --silent --override "--wait --quiet --add Microsoft.VisualStudio.Component.Windows11SDK.22000 --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.VC.Tools.ARM64" + + # Install Swift + winget install --id Swift.Toolchain -e # Add Swift to PATH - echo "C:\Library\Swift-development\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - echo "C:\Library\Swift-development\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "$env:ProgramFiles\Swift\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Build run: swift build -v From 75b146bd4b0d53b072accf15f68a5bc2986ba1f8 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 06:47:58 -0700 Subject: [PATCH 4/6] Use compnerd/gha-setup-swift to install Swift --- .github/workflows/ci.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53c76790..30200aff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,20 +79,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Swift - shell: powershell - run: | - # Enable Developer Mode - reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - # Install Visual Studio dependencies - winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --silent --override "--wait --quiet --add Microsoft.VisualStudio.Component.Windows11SDK.22000 --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.VC.Tools.ARM64" - - # Install Swift - winget install --id Swift.Toolchain -e - - # Add Swift to PATH - echo "$env:ProgramFiles\Swift\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Setup Swift + uses: compnerd/gha-setup-swift@15100508ed98274c660b39a3d171f9b9b80926c5 + with: + branch: swift-${{ matrix.swift-version }}-release + tag: ${{ matrix.swift-version }}-RELEASE - name: Build run: swift build -v From 110439393d26bde008901883d8938665f564b0d9 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 06:51:28 -0700 Subject: [PATCH 5/6] Update Windows-specific code in StdioTransport --- .../MCP/Base/Transports/StdioTransport.swift | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift index ac4c36a1..a0cfed72 100644 --- a/Sources/MCP/Base/Transports/StdioTransport.swift +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -13,12 +13,16 @@ import struct Foundation.Data import Darwin.POSIX #elseif canImport(Glibc) import Glibc -#elseif canImport(ucrt) - import ucrt - import WinSDK +#elseif os(Windows) + #if canImport(ucrt) + import ucrt + #endif + #if canImport(WinSDK) + import WinSDK + #endif #endif -#if canImport(Darwin) || canImport(Glibc) || canImport(ucrt) +#if canImport(Darwin) || canImport(Glibc) || os(Windows) /// Standard input/output transport implementation public actor StdioTransport: Transport { private let input: FileDescriptor @@ -77,14 +81,19 @@ import struct Foundation.Data guard result >= 0 else { throw MCPError.transportError(Errno(rawValue: CInt(errno))) } - #elseif canImport(ucrt) - // Windows non-blocking mode setup - var mode: UInt32 = 1 // 1 = Non-blocking mode - let result = ioctlsocket(SOCKET(fileDescriptor.rawValue), FIONBIO, &mode) - guard result == 0 else { - let error = WSAGetLastError() - throw MCPError.transportError(POSIXErrorCode(rawValue: CInt(error))) - } + #elseif os(Windows) + #if canImport(ucrt) && canImport(WinSDK) + // Windows non-blocking mode setup + var mode: UInt32 = 1 // 1 = Non-blocking mode + let result = ioctlsocket(SOCKET(fileDescriptor.rawValue), FIONBIO, &mode) + guard result == 0 else { + let error = WSAGetLastError() + throw MCPError.transportError(POSIXErrorCode(rawValue: CInt(error))) + } + #else + throw MCPError.internalError( + "Windows socket libraries not available") + #endif #else // For platforms where non-blocking operations aren't supported throw MCPError.internalError( @@ -146,8 +155,12 @@ import struct Foundation.Data guard isConnected else { #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) throw MCPError.transportError(Errno(rawValue: ENOTCONN)) - #elseif canImport(ucrt) - throw MCPError.transportError(POSIXErrorCode(rawValue: WSAENOTCONN)) + #elseif os(Windows) + #if canImport(ucrt) && canImport(WinSDK) + throw MCPError.transportError(POSIXErrorCode(rawValue: WSAENOTCONN)) + #else + throw MCPError.internalError("Transport not connected") + #endif #else throw MCPError.internalError("Transport not connected") #endif From f1dd60a4702d71924e13e9f4858952238854e3c0 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 10 Apr 2025 07:00:59 -0700 Subject: [PATCH 6/6] Resolve Windows build failures by eliminating cyclic import dependencies --- .github/workflows/ci.yml | 4 +-- .../MCP/Base/Transports/StdioTransport.swift | 33 ++++++------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30200aff..83c85db5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: tag: ${{ matrix.swift-version }}-RELEASE - name: Build - run: swift build -v + run: swift build -v -Xswiftc -Xfrontend -Xswiftc -disable-implicit-concurrency-module-import -Xswiftc -Xfrontend -Xswiftc -disable-implicit-string-processing-module-import - name: Run tests - run: swift test -v + run: swift test -v -Xswiftc -Xfrontend -Xswiftc -disable-implicit-concurrency-module-import -Xswiftc -Xfrontend -Xswiftc -disable-implicit-string-processing-module-import diff --git a/Sources/MCP/Base/Transports/StdioTransport.swift b/Sources/MCP/Base/Transports/StdioTransport.swift index a0cfed72..1cfc048c 100644 --- a/Sources/MCP/Base/Transports/StdioTransport.swift +++ b/Sources/MCP/Base/Transports/StdioTransport.swift @@ -14,12 +14,8 @@ import struct Foundation.Data #elseif canImport(Glibc) import Glibc #elseif os(Windows) - #if canImport(ucrt) - import ucrt - #endif - #if canImport(WinSDK) - import WinSDK - #endif + import CRT + import WinSDK #endif #if canImport(Darwin) || canImport(Glibc) || os(Windows) @@ -82,18 +78,13 @@ import struct Foundation.Data throw MCPError.transportError(Errno(rawValue: CInt(errno))) } #elseif os(Windows) - #if canImport(ucrt) && canImport(WinSDK) - // Windows non-blocking mode setup - var mode: UInt32 = 1 // 1 = Non-blocking mode - let result = ioctlsocket(SOCKET(fileDescriptor.rawValue), FIONBIO, &mode) - guard result == 0 else { - let error = WSAGetLastError() - throw MCPError.transportError(POSIXErrorCode(rawValue: CInt(error))) - } - #else - throw MCPError.internalError( - "Windows socket libraries not available") - #endif + // Windows non-blocking mode setup + var mode: UInt32 = 1 // 1 = Non-blocking mode + let result = ioctlsocket(SOCKET(fileDescriptor.rawValue), FIONBIO, &mode) + guard result == 0 else { + let error = WSAGetLastError() + throw MCPError.transportError(POSIXErrorCode(rawValue: CInt(error))) + } #else // For platforms where non-blocking operations aren't supported throw MCPError.internalError( @@ -156,11 +147,7 @@ import struct Foundation.Data #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) throw MCPError.transportError(Errno(rawValue: ENOTCONN)) #elseif os(Windows) - #if canImport(ucrt) && canImport(WinSDK) - throw MCPError.transportError(POSIXErrorCode(rawValue: WSAENOTCONN)) - #else - throw MCPError.internalError("Transport not connected") - #endif + throw MCPError.transportError(POSIXErrorCode(rawValue: WSAENOTCONN)) #else throw MCPError.internalError("Transport not connected") #endif