client: NodeOAuthClientProvider hangs event loop on browser keep-alive sockets after callback completion
Author: rohith500Created Sep 14, 2026Updated Sep 15, 2026
LabelsbugTypeScriptserverclient
Description
In @mcp-use/client, NodeOAuthClientProvider manages a temporary localhost HTTP server to listen for OAuth authorization callbacks (/callback) and serve launcher redirects (/authorize).
When the browser receives a response from the loopback server, the process often hangs or fails to exit cleanly because:
- Missing
Connection: closeHeader: Modern web browsers (Chrome, Safari, Edge) default to persistent HTTP/1.1 keep-alive connections. BecausehandleCallbackomits theConnection: closeheader on responses (including 200SUCCESS_HTML, 400FAILURE_HTML, and redirects), the browser's TCP socket remains open in an active keep-alive state. server.close()Does Not Terminate Existing Sockets: InstopLoopback():Under Node.js semantics,private stopLoopback(): void { if (this.pendingTimer) { clearTimeout(this.pendingTimer); this.pendingTimer = null; } if (this.server) { this.server.close(); this.server = null; } this.authorizationUrl = null; }http.Server.close([callback])stops accepting new connections, but does not close existing client connections. It waits indefinitely for connected clients to close their keep-alive sockets or for the keep-alive idle timeout (which can range from minutes in browsers to infinite in misbehaving clients) to elapse. Consequently, the Node.js event loop remains active, causing CLI tools and test suites to hang after authorization succeeds.
Steps to Reproduce
- Initialize
NodeOAuthClientProviderand trigger authorization:const provider = await NodeOAuthClientProvider.create("https://mcp.example.com"); await provider.redirectToAuthorization(new URL("https://auth.example.com/authorize?state=xyz")); - Open a persistent TCP connection to the callback server with HTTP/1.1 Keep-Alive:
const socket = net.connect(provider.callbackPort, "127.0.0.1"); socket.write("GET /callback?code=abc&state=xyz HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"); - Await authorization response:
const response = await provider.getAuthorizationResponse(); - Observe that
provider.stopLoopback()callsthis.server.close(), but the underlying TCP connection remains open in keep-alive mode, preventing Node's event loop from winding down.
Expected Behavior
- The loopback HTTP server should explicitly return
Connection: closeon all responses to signal the browser and HTTP clients to close their socket immediately upon receiving the response. - In
stopLoopback()and error teardowns,NodeOAuthClientProvidershould track active sockets and invokeserver.closeAllConnections()(orsocket.destroy()) so that keep-alive or in-flight connections are terminated without waiting for browser idle timeouts.
Proposed Solution
- Send
Connection: closeon all responses inhandleCallback:res.setHeader("connection", "close"); - Track open sockets in
startLoopback:private readonly sockets: Set<Socket> = new Set(); server.on("connection", (socket: Socket) => { this.sockets.add(socket); socket.once("close", () => this.sockets.delete(socket)); }); - In
stopLoopback(), invokeserver.closeAllConnections?.()and destroy all tracked sockets:if (typeof server.closeAllConnections === "function") { server.closeAllConnections(); } for (const socket of this.sockets) { socket.destroy(); } this.sockets.clear();
Source: mcp-use/mcp-use