Skip to content

Commit d41c8a6

Browse files
committed
Merge branch 'swift6_manual'
2 parents 1b66083 + b141203 commit d41c8a6

24 files changed

+971
-1002
lines changed

Demos/SwiftMCPDemo/Commands/HTTPSSECommand.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import OSLog
3636
- AI plugin manifest at `/.well-known/ai-plugin.json`
3737
- Compatible with AI plugin standards
3838
*/
39-
struct HTTPSSECommand: AsyncParsableCommand {
40-
static var configuration = CommandConfiguration(
39+
final class HTTPSSECommand: AsyncParsableCommand {
40+
static let configuration = CommandConfiguration(
4141
commandName: "httpsse",
4242
abstract: "Start an HTTP server with Server-Sent Events (SSE) support",
4343
discussion: """
@@ -70,7 +70,26 @@ struct HTTPSSECommand: AsyncParsableCommand {
7070
@Flag(name: .long, help: "Enable OpenAPI endpoints")
7171
var openapi: Bool = false
7272

73-
func run() async throws {
73+
// Make this a computed property instead of stored property
74+
private var signalHandler: SignalHandler? = nil
75+
76+
required init() {}
77+
78+
// Add manual Decodable conformance
79+
required init(from decoder: Decoder) throws {
80+
let container = try decoder.container(keyedBy: CodingKeys.self)
81+
self.port = try container.decode(Int.self, forKey: .port)
82+
self.token = try container.decodeIfPresent(String.self, forKey: .token)
83+
self.openapi = try container.decode(Bool.self, forKey: .openapi)
84+
}
85+
86+
private enum CodingKeys: String, CodingKey {
87+
case port
88+
case token
89+
case openapi
90+
}
91+
92+
func run() async throws {
7493
#if canImport(OSLog)
7594
LoggingSystem.bootstrapWithOSLog()
7695
#endif
@@ -101,7 +120,8 @@ struct HTTPSSECommand: AsyncParsableCommand {
101120
transport.serveOpenAPI = openapi
102121

103122
// Set up signal handling to shut down the transport on Ctrl+C
104-
setupSignalHandler(transport: transport)
123+
signalHandler = SignalHandler(transport: transport)
124+
await signalHandler?.setup()
105125

106126
// Run the server (blocking)
107127
try await transport.run()

Demos/SwiftMCPDemo/Commands/StdioCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import OSLog
2222
- Scripting and automation
2323
*/
2424
struct StdioCommand: AsyncParsableCommand {
25-
static var configuration = CommandConfiguration(
25+
static let configuration = CommandConfiguration(
2626
commandName: "stdio",
2727
abstract: "Read JSON-RPC requests from stdin and write responses to stdout",
2828
discussion: """

Demos/SwiftMCPDemo/MCPCommand.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Foundation
22
import ArgumentParser
33
import SwiftMCP
44
import Logging
5-
import AnyCodable
65
import NIOCore
76
import Dispatch
87
#if canImport(Darwin)
@@ -37,7 +36,7 @@ import OSLog
3736
*/
3837
@main
3938
struct MCPCommand: AsyncParsableCommand {
40-
static var configuration = CommandConfiguration(
39+
static let configuration = CommandConfiguration(
4140
commandName: "SwiftMCPDemo",
4241
abstract: "A utility for testing SwiftMCP functions",
4342
discussion: """

Demos/SwiftMCPDemo/SignalHandler.swift

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,70 @@ import Foundation
22
import Dispatch
33
import SwiftMCP
44

5-
// Keep a reference to the signal source so it isn't deallocated
6-
fileprivate var sigintSource: DispatchSourceSignal?
7-
fileprivate var isShuttingDown = false
8-
9-
/// Sets up a modern Swift signal handler for SIGINT.
10-
func setupSignalHandler(transport: HTTPSSETransport) {
11-
// Create a dedicated dispatch queue for signal handling.
12-
let signalQueue = DispatchQueue(label: "com.cocoanetics.signalQueue")
13-
// Create a dispatch source on that queue.
14-
sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue)
15-
16-
// Tell the system to ignore the default SIGINT handler.
17-
signal(SIGINT, SIG_IGN)
18-
19-
// Specify what to do when the signal is received.
20-
sigintSource?.setEventHandler {
21-
// Prevent multiple shutdown attempts
22-
guard !isShuttingDown else { return }
23-
isShuttingDown = true
5+
/// Handles SIGINT signals for graceful shutdown of the HTTP SSE transport
6+
public final class SignalHandler {
7+
/// Actor to manage signal handling state in a thread-safe way
8+
private actor State {
9+
private var sigintSource: DispatchSourceSignal?
10+
private var isShuttingDown = false
11+
private weak var transport: HTTPSSETransport?
2412

25-
print("\nShutting down...")
26-
27-
// Create a semaphore to wait for shutdown
28-
let semaphore = DispatchSemaphore(value: 0)
13+
init(transport: HTTPSSETransport) {
14+
self.transport = transport
15+
}
2916

30-
Task {
17+
func setupHandler(on queue: DispatchQueue) {
18+
// Create a dispatch source on the provided queue
19+
sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: queue)
20+
21+
// Tell the system to ignore the default SIGINT handler
22+
signal(SIGINT, SIG_IGN)
23+
24+
// Specify what to do when the signal is received
25+
sigintSource?.setEventHandler { [weak self] in
26+
Task { [weak self] in
27+
await self?.handleSignal()
28+
}
29+
}
30+
31+
// Start listening for the signal
32+
sigintSource?.resume()
33+
}
34+
35+
private func handleSignal() async {
36+
// Prevent multiple shutdown attempts
37+
guard !isShuttingDown else { return }
38+
isShuttingDown = true
39+
40+
print("\nShutting down...")
41+
42+
guard let transport = transport else {
43+
print("Transport no longer available")
44+
Foundation.exit(1)
45+
}
46+
3147
do {
3248
try await transport.stop()
33-
semaphore.signal()
49+
Foundation.exit(0)
3450
} catch {
3551
print("Error during shutdown: \(error)")
36-
semaphore.signal()
52+
Foundation.exit(1)
3753
}
3854
}
39-
40-
// Wait for shutdown to complete with timeout
41-
_ = semaphore.wait(timeout: .now() + .seconds(5))
42-
Foundation.exit(0)
4355
}
44-
45-
// Start listening for the signal.
46-
sigintSource?.resume()
56+
57+
// Instance state
58+
private let state: State
59+
60+
/// Creates a new signal handler for the given transport
61+
public init(transport: HTTPSSETransport) {
62+
self.state = State(transport: transport)
63+
}
64+
65+
/// Sets up the SIGINT handler
66+
public func setup() async {
67+
// Create a dedicated dispatch queue for signal handling
68+
let signalQueue = DispatchQueue(label: "com.cocoanetics.signalQueue")
69+
await state.setupHandler(on: signalQueue)
70+
}
4771
}

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 6.0
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import CompilerPluginSupport

Sources/SwiftMCP/Extensions/String+Hostname.swift

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,64 +32,4 @@ extension String {
3232

3333
return "localhost"
3434
}
35-
36-
/**
37-
Get the local IP address
38-
- Returns: The local IP address as a string, or nil if not available
39-
*/
40-
public static var localIPAddress: String? {
41-
var ifaddr: UnsafeMutablePointer<ifaddrs>?
42-
43-
guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else {
44-
return nil
45-
}
46-
47-
defer {
48-
freeifaddrs(ifaddr)
49-
}
50-
51-
// Iterate through linked list of interfaces
52-
var currentAddr: UnsafeMutablePointer<ifaddrs>? = firstAddr
53-
var foundAddress: String? = nil
54-
55-
while let addr = currentAddr {
56-
let interface = addr.pointee
57-
58-
// Check for IPv4 or IPv6 interface
59-
let addrFamily = interface.ifa_addr.pointee.sa_family
60-
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
61-
// Check interface name starts with "en" (Ethernet) or "wl" (WiFi)
62-
let name = String(cString: interface.ifa_name)
63-
if name.hasPrefix("en") || name.hasPrefix("wl") || name.hasPrefix("eth") {
64-
// Convert interface address to a human readable string
65-
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
66-
67-
#if canImport(Darwin)
68-
let saLen = socklen_t(interface.ifa_addr.pointee.sa_len)
69-
#else
70-
let saLen = addrFamily == UInt8(AF_INET) ?
71-
socklen_t(MemoryLayout<sockaddr_in>.size) :
72-
socklen_t(MemoryLayout<sockaddr_in6>.size)
73-
#endif
74-
75-
// Get address info
76-
if getnameinfo(interface.ifa_addr,
77-
saLen,
78-
&hostname, socklen_t(hostname.count),
79-
nil, 0,
80-
NI_NUMERICHOST) == 0 {
81-
if let address = String(validatingUTF8: hostname) {
82-
foundAddress = address
83-
break
84-
}
85-
}
86-
}
87-
}
88-
89-
// Move to next interface
90-
currentAddr = interface.ifa_next
91-
}
92-
93-
return foundAddress
94-
}
95-
}
35+
}

Sources/SwiftMCP/Models/JSONRPCMessage.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
//
77

88
import Foundation
9-
import AnyCodable
9+
@preconcurrency import AnyCodable
1010

1111
/// JSON-RPC Request structure used for communication with the MCP server
12-
public struct JSONRPCMessage: Codable {
12+
public struct JSONRPCMessage: Codable, Sendable {
1313

14-
public struct Error: Codable {
14+
public struct Error: Codable, Sendable {
1515
public var code: Int
1616
public var message: String
1717
}

Sources/SwiftMCP/Models/Resources/MCPResource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Protocol defining the requirements for an MCP resource
4-
public protocol MCPResource: Codable {
4+
public protocol MCPResource: Codable, Sendable {
55
/// The URI of the resource
66
var uri: URL { get }
77

Sources/SwiftMCP/Models/Resources/MCPResourceContent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Protocol defining the requirements for MCP resource content
4-
public protocol MCPResourceContent: Codable {
4+
public protocol MCPResourceContent: Codable, Sendable {
55
/// The URI of the resource
66
var uri: URL { get }
77

Sources/SwiftMCP/Models/Tools/MCPTool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ extension MCPTool {
7676
- Returns: A new dictionary with default values added for missing parameters
7777
- Throws: MCPToolError if required parameters are missing or if parameter conversion fails
7878
*/
79-
public func enrichArguments(_ arguments: [String: Any], forObject object: Any) throws -> [String: Any] {
79+
public func enrichArguments(_ arguments: [String: Sendable], forObject object: Any) throws -> [String: Sendable] {
8080
// Use the provided function name or fall back to the tool's name
8181
let metadataKey = "__mcpMetadata_\(name)"
8282

0 commit comments

Comments
 (0)