Adopt swift-service-lifecycle for server transports#133
Merged
Conversation
Replace the bespoke SIGINT-only signal handling with swift-server's ServiceGroup, so the server transports start, run, and shut down through a standard, well-tested lifecycle. - HTTPSSETransport, StdioTransport, and TCPBonjourTransport conform to `ServiceLifecycle.Service`; their `run()` observes graceful shutdown via `withGracefulShutdownHandler` and returns cleanly. Backward compatible: called standalone (no group), behavior is unchanged. - Both demo CLIs build a `ServiceGroup` with `[.sigterm, .sigint]` instead of the hand-rolled `SignalHandler` (both copies deleted). - The dependency is NIO-free (swift-log + swift-async-algorithms) and gated behind the `Server` trait, so the core still builds NIO-free. - Fix a latent stdio bug surfaced by the migration: consolidate the two duplicated read loops into one `readLoop()` that returns on stdin EOF instead of spinning at ~10 Hz until killed. Benefits: SIGTERM now triggers a graceful drain (exit 0) instead of a hard kill (143) under docker/launchd/systemd; shutdown unwinds via async return with a bounded timeout instead of Foundation.exit(); and the Service conformance lets consumers drop MCP transports into their own ServiceGroup (Hummingbird/Vapor/etc.). Verified: full + NIO-free-core + Server-off builds; 421/421 tests; SIGTERM/SIGINT -> exit 0; stdio exits on EOF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The server transports currently start and stop through a bespoke
SignalHandlerthat traps only SIGINT and exits viaFoundation.exit(). This PR adopts swift-service-lifecycle so the transports run inside a standardServiceGroupwith proper, ordered, signal-driven graceful shutdown.The dependency is NIO-free (only swift-log — already a dependency — and swift-async-algorithms),
swift-tools 6.1, imposes no platform-floor bump, and is gated behind theServertrait, so the core still builds NIO-free.Why — benefits
1. SIGTERM now triggers graceful shutdown
The old handler caught only SIGINT. Every production launcher —
docker stop, launchd, systemd, Kubernetes — terminates with SIGTERM, which was ignored, so the server was hard-killed (exit 143), dropping in-flight requests and skipping cleanup. Now both signals drive a graceful drain → exit 0.2. Clean async exit + shutdown timeout
Foundation.exit(0)hard-exited past async unwinding, deinits, and log flushing, andshutdownGracefullyhad no timeout (a wedged transport could hang teardown forever).ServiceGroup.run()now returns normally — the process unwinds cleanly — with a bounded graceful-shutdown timeout.3. Ecosystem interoperability
The transports conform to
ServiceLifecycle.Service, so consumers embedding SwiftMCP in a Swift-on-server app (Hummingbird 2, the Vapor ecosystem, anything from the swift-server group) can drop an MCP transport into their existingServiceGroupalongside their HTTP server / DB pool / metrics, under one coordinated lifecycle.4. Coordinated, ordered multi-service shutdown
The HTTP + TCP combination is now one group with shared signal handling and ordered teardown, replacing the manual "stop the TCP transport in a
catchblock" juggling.5. Less bespoke code to own
Deletes 182 lines of hand-rolled signal / dispatch-source handling (two copies of
SignalHandler.swift). Net −83 lines even after adding theServiceconformances — signal masking, re-entrancy guards, and exit semantics are now the library's concern.What changed
Library —
HTTPSSETransport,StdioTransport, andTCPBonjourTransportconform toService; eachrun()observes graceful shutdown viawithGracefulShutdownHandlerand returns cleanly. Backward compatible: called standalone (outside a group), the handler never fires and behavior is identical to before.Demos — both CLIs build a
ServiceGroup(gracefulShutdownSignals: [.sigterm, .sigint])instead ofSignalHandler+transport.run(). The stdio command uses a stderr-bound lifecycle logger soServiceGroupmessages never corrupt the stdout JSON-RPC stream.Package.swift — adds
swift-service-lifecycle(from: "2.6.0"), gated.when(traits: ["Server"])on the library and both demo targets.Bonus: stdio EOF fix
The migration surfaced a latent bug: the stdio read loop slept-and-retried on
readLine() == nil, so after the host closed stdin it spun at ~10 Hz until killed. The two duplicated loops are consolidated into onereadLoop()that returns on EOF — combined withsuccessTerminationBehavior: .gracefullyShutdownGroup, closing stdin now shuts the server down cleanly.Verification
swift build(default traits, full)swift build --target SwiftMCP --disable-default-traits— NIO-free core (service-lifecycle not linked)swift build --traits "Client,OpenAPI"—Servertrait offswift test— 421/421initializeresponse, and the process now exits on its own on stdin EOF🤖 Generated with Claude Code