Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 23 additions & 30 deletions Demos/SwiftMCPDemo/Commands/HTTPSSECommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
import ArgumentParser
import SwiftMCP
import Logging
import ServiceLifecycle
#if canImport(OSLog)
import OSLog
#endif
Expand Down Expand Up @@ -99,9 +100,6 @@ final class HTTPSSECommand: AsyncParsableCommand {
@Option(name: .long, help: "Path to OAuth configuration JSON file")
var oauth: String?

// Make this a computed property instead of stored property
private var signalHandler: SignalHandler?

required init() {}

// Add manual Decodable conformance
Expand Down Expand Up @@ -139,17 +137,24 @@ final class HTTPSSECommand: AsyncParsableCommand {
try configureAuthentication(on: transport)
transport.serveOpenAPI = openapi

let tcpTransport = try await startTCPTransportIfNeeded(server: calculator)
await setupSignalHandling(httpTransport: transport, tcpTransport: tcpTransport)

do {
try await transport.run()
} catch {
if let tcpTransport {
try? await tcpTransport.stop()
}
throw error
// Each transport is a `Service`. The `ServiceGroup` starts them, traps
// SIGINT/SIGTERM, and drives an ordered graceful shutdown (with a
// timeout) — replacing the bespoke signal handler.
var services: [ServiceGroupConfiguration.ServiceConfiguration] = [
.init(service: transport, successTerminationBehavior: .gracefullyShutdownGroup)
]
if let tcpTransport = makeTCPTransportIfNeeded(server: calculator) {
services.append(.init(service: tcpTransport, successTerminationBehavior: .gracefullyShutdownGroup))
}

let group = ServiceGroup(
configuration: .init(
services: services,
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Logging.Logger(label: "com.cocoanetics.SwiftMCP.ServiceGroup")
)
)
try await group.run()
}

private func configureAuthentication(on transport: HTTPSSETransport) throws {
Expand Down Expand Up @@ -207,24 +212,12 @@ final class HTTPSSECommand: AsyncParsableCommand {
}
}

private func startTCPTransportIfNeeded(server: DemoServer) async throws -> TCPBonjourTransport? {
/// Builds the optional TCP+Bonjour transport. It is returned unstarted —
/// the `ServiceGroup` starts it by calling `run()`.
private func makeTCPTransportIfNeeded(server: DemoServer) -> TCPBonjourTransport? {
guard tcp else { return nil }
let transport = TCPBonjourTransport(server: server)
try await transport.start()
print("MCP Server \(server.serverName) started with TCP+Bonjour transport")
return transport
}

private func setupSignalHandling(
httpTransport: HTTPSSETransport,
tcpTransport: TCPBonjourTransport?
) async {
if let tcpTransport {
signalHandler = SignalHandler(transports: [httpTransport, tcpTransport])
} else {
signalHandler = SignalHandler(transport: httpTransport)
}
await signalHandler?.setup()
print("MCP Server \(server.serverName) will also expose a TCP+Bonjour transport")
return TCPBonjourTransport(server: server)
}
}
#endif
26 changes: 25 additions & 1 deletion Demos/SwiftMCPDemo/Commands/StdioCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ArgumentParser
import SwiftMCP
import Logging
import NIOCore
import ServiceLifecycle
#if canImport(OSLog)
import OSLog
#endif
Expand Down Expand Up @@ -40,6 +41,16 @@ struct StdioCommand: AsyncParsableCommand {
"""
)

/// A logger bound to stderr so `ServiceGroup` lifecycle messages never
/// interleave with the JSON-RPC responses written to stdout.
private static let lifecycleLogger: Logging.Logger = {
var logger = Logging.Logger(label: "com.cocoanetics.SwiftMCP.ServiceGroup") {
StreamLogHandler.standardError(label: $0)
}
logger.logLevel = .notice
return logger
}()

func run() async throws {
#if canImport(OSLog)
LoggingSystem.bootstrapWithOSLog()
Expand All @@ -52,7 +63,20 @@ struct StdioCommand: AsyncParsableCommand {
logToStderr("MCP Server \(calculator.serverName) (\(calculator.serverVersion)) started with Stdio transport")

let transport = StdioTransport(server: calculator)
try await transport.run()

// A `ServiceGroup` owns the run loop and traps SIGINT/SIGTERM,
// driving a graceful shutdown of the transport. The lifecycle logs
// go to stderr so they never corrupt the stdout JSON-RPC stream.
let group = ServiceGroup(
configuration: .init(
services: [
.init(service: transport, successTerminationBehavior: .gracefullyShutdownGroup)
],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Self.lifecycleLogger
)
)
try await group.run()
} catch let error as TransportError {
// Handle transport errors
let errorMessage = """
Expand Down
15 changes: 14 additions & 1 deletion Demos/SwiftMCPDemo/Commands/TCPBonjourCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
import ArgumentParser
import SwiftMCP
import Logging
import ServiceLifecycle
#if canImport(OSLog)
import OSLog
#endif
Expand Down Expand Up @@ -58,7 +59,19 @@ struct TCPBonjourCommand: AsyncParsableCommand {
acceptLocalOnly: true,
preferIPv4: ipv4Only
)
try await transport.run()

// A `ServiceGroup` owns the run loop and traps SIGINT/SIGTERM to
// drive a graceful shutdown of the transport.
let group = ServiceGroup(
configuration: .init(
services: [
.init(service: transport, successTerminationBehavior: .gracefullyShutdownGroup)
],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Logging.Logger(label: "com.cocoanetics.SwiftMCP.ServiceGroup")
)
)
try await group.run()
} catch let error as TransportError {
let errorMessage = """
Transport Error: \(error.localizedDescription)
Expand Down
98 changes: 0 additions & 98 deletions Demos/SwiftMCPDemo/SignalHandler.swift

This file was deleted.

52 changes: 23 additions & 29 deletions Demos/SwiftMCPIntentsDemo/Commands/HTTPSSECommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
import ArgumentParser
import SwiftMCP
import Logging
import ServiceLifecycle
#if canImport(OSLog)
import OSLog
#endif
Expand Down Expand Up @@ -47,8 +48,6 @@ final class HTTPSSECommand: AsyncParsableCommand {
@Option(name: .long, help: "Path to OAuth configuration JSON file")
var oauth: String?

private var signalHandler: SignalHandler?

required init() {}

required init(from decoder: Decoder) throws {
Expand Down Expand Up @@ -87,17 +86,24 @@ final class HTTPSSECommand: AsyncParsableCommand {
try configureAuthentication(on: transport)
transport.serveOpenAPI = openapi

let tcpTransport = try await startTCPTransportIfNeeded(server: server)
await setupSignalHandling(httpTransport: transport, tcpTransport: tcpTransport)

do {
try await transport.run()
} catch {
if let tcpTransport {
try? await tcpTransport.stop()
}
throw error
// Each transport is a `Service`. The `ServiceGroup` starts them, traps
// SIGINT/SIGTERM, and drives an ordered graceful shutdown (with a
// timeout) — replacing the bespoke signal handler.
var services: [ServiceGroupConfiguration.ServiceConfiguration] = [
.init(service: transport, successTerminationBehavior: .gracefullyShutdownGroup)
]
if let tcpTransport = makeTCPTransportIfNeeded(server: server) {
services.append(.init(service: tcpTransport, successTerminationBehavior: .gracefullyShutdownGroup))
}

let group = ServiceGroup(
configuration: .init(
services: services,
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Logging.Logger(label: "com.cocoanetics.SwiftMCP.ServiceGroup")
)
)
try await group.run()
}

private func configureAuthentication(on transport: HTTPSSETransport) throws {
Expand Down Expand Up @@ -155,24 +161,12 @@ final class HTTPSSECommand: AsyncParsableCommand {
}
}

private func startTCPTransportIfNeeded(server: any MCPServer) async throws -> TCPBonjourTransport? {
/// Builds the optional TCP+Bonjour transport. It is returned unstarted —
/// the `ServiceGroup` starts it by calling `run()`.
private func makeTCPTransportIfNeeded(server: any MCPServer) -> TCPBonjourTransport? {
guard tcp else { return nil }
let transport = TCPBonjourTransport(server: server)
try await transport.start()
print("MCP Server \(server.serverName) started with TCP+Bonjour transport")
return transport
}

private func setupSignalHandling(
httpTransport: HTTPSSETransport,
tcpTransport: TCPBonjourTransport?
) async {
if let tcpTransport {
signalHandler = SignalHandler(transports: [httpTransport, tcpTransport])
} else {
signalHandler = SignalHandler(transport: httpTransport)
}
await signalHandler?.setup()
print("MCP Server \(server.serverName) will also expose a TCP+Bonjour transport")
return TCPBonjourTransport(server: server)
}
}
#endif
26 changes: 25 additions & 1 deletion Demos/SwiftMCPIntentsDemo/Commands/StdioCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ArgumentParser
import SwiftMCP
import Logging
import NIOCore
import ServiceLifecycle
#if canImport(OSLog)
import OSLog
#endif
Expand All @@ -27,6 +28,16 @@ struct StdioCommand: AsyncParsableCommand {
"""
)

/// A logger bound to stderr so `ServiceGroup` lifecycle messages never
/// interleave with the JSON-RPC responses written to stdout.
private static let lifecycleLogger: Logging.Logger = {
var logger = Logging.Logger(label: "com.cocoanetics.SwiftMCP.ServiceGroup") {
StreamLogHandler.standardError(label: $0)
}
logger.logLevel = .notice
return logger
}()

func run() async throws {
#if canImport(OSLog)
LoggingSystem.bootstrapWithOSLog()
Expand All @@ -39,7 +50,20 @@ struct StdioCommand: AsyncParsableCommand {
do {
logToStderr("MCP Server \(server.serverName) (\(server.serverVersion)) started with Stdio transport")
let transport = StdioTransport(server: server)
try await transport.run()

// A `ServiceGroup` owns the run loop and traps SIGINT/SIGTERM,
// driving a graceful shutdown of the transport. The lifecycle logs
// go to stderr so they never corrupt the stdout JSON-RPC stream.
let group = ServiceGroup(
configuration: .init(
services: [
.init(service: transport, successTerminationBehavior: .gracefullyShutdownGroup)
],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Self.lifecycleLogger
)
)
try await group.run()
} catch let error as TransportError {
let errorMessage = """
Transport Error: \(error.localizedDescription)
Expand Down
Loading
Loading