Skip to content

Crash in NetworkTransport on reconnect #137

Closed
nighthawk/mcp-swift-sdk
#1

Description

@nighthawk

Describe the bug
Came across a runtime crash in NetworkTransport when a MCP client reconnects (mcp-swift-sdk version 0.9.0)

To Reproduce
I haven't yet been able to reliably reproduce it. In my case the host macOS app was running, when I launched Zed where I had the MCP server configured. Zed seems to reconnect, but that caused a runtime crash in the host macOS app.

Logs

macOS crash log snippet:

0x1a76fa1f8 _assertionFailure(_:_:file:line:flags:) + 176
1   libswift_Concurrency.dylib    	       0x27ecd64e4 CheckedContinuation.resume(throwing:) + 348
2   MyHostApp                      	       0x1003beb94 closure #1 in NetworkTransport.handleReconnection(error:continuation:context:) + 180

Relevant code snippet:

        private func handleReconnection(
            error: Swift.Error,
            continuation: CheckedContinuation<Void, Swift.Error>,
            context: String
        ) async {
            if !isStopping,
                reconnectionConfig.enabled,
                reconnectAttempt < reconnectionConfig.maxAttempts
            {
                // Try to reconnect with exponential backoff
                reconnectAttempt += 1
                logger.info(
                    "Attempting reconnection after \(context) (\(reconnectAttempt)/\(reconnectionConfig.maxAttempts))..."
                )

                // Calculate backoff delay
                let delay = reconnectionConfig.backoffDelay(for: reconnectAttempt)

                // Schedule reconnection attempt after delay
                Task {
                    try? await Task.sleep(for: .seconds(delay))
                    if !isStopping {
                        // Cancel the current connection before attempting to reconnect.
                        self.connection.cancel()
                        // Resume original continuation with error; outer logic or a new call to connect() will handle retry.
                        continuation.resume(throwing: error)
                    } else {
                        continuation.resume(throwing: error)  // Stopping, so fail.
                    }
                }
            } else {
                // Not configured to reconnect, exceeded max attempts, or stopping
                self.connection.cancel()  // Ensure connection is cancelled
                continuation.resume(throwing: error)
            }
        }

Additional context

The crash is triggered when continuation.resume(throwing: ...) gets called multiple times for the same continuation. I haven't yet wrapped my head around the entire flow here and how this is possible, and it's hard to debug as it seems intermittent.

Suggestions appreciated for how to address this.

Activity

  1. provencher commented on Jun 19, 2025

    @provencher

    Ran into the same issue

  2. nighthawk commented on Jul 14, 2025

    @nighthawk
    Author

    Re-opening as I only merged that in my fork. It isn't yet merged in this repo. I've opened #147 for that.

  3. DePasqualeOrg commented on Jan 4, 2026

    @DePasqualeOrg

    In my PR #175, I replaced the continuation-passing pattern with AsyncStream:

    private func waitForConnectionReady() async throws {
        let stateStream = AsyncStream<NWConnection.State> { continuation in
            connection.stateUpdateHandler = { state in
                continuation.yield(state)
                switch state {
                case .ready, .failed, .cancelled:
                    continuation.finish()
                default:
                    break
                }
            }
    
            continuation.onTermination = { [weak self] _ in
                self?.connection.stateUpdateHandler = nil
            }
        }
    
        connection.start(queue: .main)
    
        for await state in stateStream {
            switch state {
            case .ready:
                return
            case .failed(let error):
                throw error
            case .cancelled:
                throw MCPError.internalError("Connection cancelled")
            case .waiting(let error):
                logger.debug("Connection waiting: \(error)")
            case .preparing:
                logger.debug("Connection preparing...")
            case .setup:
                logger.debug("Connection setup...")
            @unknown default:
                logger.warning("Unknown connection state")
            }
        }
    
        throw MCPError.internalError("Connection stream ended unexpectedly")
    }

    This is safe because:

    • AsyncStream.Continuation is thread-safe
    • finish() is idempotent
    • yield() after finish() is ignored
    • The stream finishes atomically on terminal states

    Changes

    Removed:

    • connectionContinuationResumed property
    • handleConnectionReady(continuation:)
    • handleConnectionFailed(error:continuation:)
    • handleConnectionCancelled(continuation:)
    • handleReconnection(error:continuation:context:)

    Added:

    • waitForConnectionReady() using AsyncStream

    Simplified:

    • connect() now calls waitForConnectionReady() directly
  4. nighthawk commented on Mar 27, 2026

    @nighthawk
    Author

    This seems to be fixed in 0.11 and up, see #199. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions