From c0ce9d67fe139021d34f8dd200364942c3a47450 Mon Sep 17 00:00:00 2001 From: Lee Ament Date: Tue, 8 Sep 2026 17:40:34 -0400 Subject: [PATCH] Fix socket leak (CLOSE_WAIT) in NetworkTransport when reconnection is disabled When reconnectionConfig.enabled is false and the receive loop's connection terminates for any reason -- including the peer closing gracefully -- the loop finishes the message stream without calling connection.cancel(). NWConnection.state never transitions to .cancelled or .failed on its own after a peer-initiated FIN, so nothing else cleans up the connection either. The socket is left stuck in CLOSE_WAIT until the process exits. The reconnecting branches already call connection.cancel() before retrying; this adds the same call to the two non-reconnecting "give up" branches so the underlying socket is always released. Verified against a server using reconnectionConfig: .disabled for per-client connections (mattt/iMCP): before this fix, a burst of ~250 client connect/disconnect cycles left 247+ sockets in CLOSE_WAIT within 10 seconds; after the fix, 0. Co-Authored-By: Claude Sonnet 5 --- Sources/MCP/Base/Transports/NetworkTransport.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/MCP/Base/Transports/NetworkTransport.swift b/Sources/MCP/Base/Transports/NetworkTransport.swift index 7c4d00f3..d19fcbd8 100644 --- a/Sources/MCP/Base/Transports/NetworkTransport.swift +++ b/Sources/MCP/Base/Transports/NetworkTransport.swift @@ -685,6 +685,8 @@ import Logging break } else { // We're not reconnecting, finish the message stream with error + // and release the underlying socket so it doesn't leak in CLOSE_WAIT. + connection.cancel() messageContinuation.finish( throwing: MCPError.transportError(error)) break @@ -728,6 +730,9 @@ import Logging break } else { + // Not reconnecting: release the underlying socket so it doesn't + // leak in CLOSE_WAIT (e.g. when the peer closes gracefully). + connection.cancel() messageContinuation.finish(throwing: error) } }