Problem
In Streamable HTTP mode, every MCP session constructs a full DebugMcpServer (src/cli/http-command.ts:140-144), whose DI container calls createLogger('debug-mcp', …) (src/container/dependencies.ts:67). Each such winston logger is piped into the process-lifetime shared file transport (src/utils/logger.ts:49 cache; :208 attach), and:
DebugMcpServer.stop() (src/server.ts:2858-2869) never closes or detaches its logger;
- nothing in
src/ ever calls logger.close() — by design, since closing would kill the shared transport for all loggers (logger.ts:44-48).
Node's pipe() attaches unpipe/drain/error/close listeners on the destination that close over the source, so the shared transport accumulates one listener set + retained logger per HTTP session, forever. The HTTP stale-session reaper (MCP_HTTP_STALE_SESSION_MS) tears down the DebugMcpServer but cannot reclaim this. Under sessionful churn (the exact scenario the reaper exists for — see #337) this is a slow leak, and past ~10 sessions winston/stream maxListeners warnings become plausible.
Proposal
- Give
DebugMcpServer.stop() (or the DI container's disposal path) a way to detach its logger from the shared transport without closing the transport: logger.remove(transport) / logger.unpipe(transport) on the logger side leaves the shared transport alive for others.
- Add a unit test: create + stop N servers against one shared transport, assert the transport's listener counts don't grow.
Note (no action, recording while in the area)
The deprecated SSE transport's session map (src/cli/sse-command.ts:45) has no stale reaper — entries are removed only via transport/req close events (:96-120). HTTP mode got a reaper for exactly this gap. Since SSE is deprecated and shares one DebugMcpServer, the exposure is just the transport entry; not worth fixing separately, but worth knowing if a leak report ever comes in against SSE mode.
Problem
In Streamable HTTP mode, every MCP session constructs a full
DebugMcpServer(src/cli/http-command.ts:140-144), whose DI container callscreateLogger('debug-mcp', …)(src/container/dependencies.ts:67). Each such winston logger is piped into the process-lifetime shared file transport (src/utils/logger.ts:49cache;:208attach), and:DebugMcpServer.stop()(src/server.ts:2858-2869) never closes or detaches its logger;src/ever callslogger.close()— by design, since closing would kill the shared transport for all loggers (logger.ts:44-48).Node's
pipe()attachesunpipe/drain/error/closelisteners on the destination that close over the source, so the shared transport accumulates one listener set + retained logger per HTTP session, forever. The HTTP stale-session reaper (MCP_HTTP_STALE_SESSION_MS) tears down theDebugMcpServerbut cannot reclaim this. Under sessionful churn (the exact scenario the reaper exists for — see #337) this is a slow leak, and past ~10 sessions winston/stream maxListeners warnings become plausible.Proposal
DebugMcpServer.stop()(or the DI container's disposal path) a way to detach its logger from the shared transport without closing the transport:logger.remove(transport)/logger.unpipe(transport)on the logger side leaves the shared transport alive for others.Note (no action, recording while in the area)
The deprecated SSE transport's session map (
src/cli/sse-command.ts:45) has no stale reaper — entries are removed only via transport/req close events (:96-120). HTTP mode got a reaper for exactly this gap. Since SSE is deprecated and shares oneDebugMcpServer, the exposure is just the transport entry; not worth fixing separately, but worth knowing if a leak report ever comes in against SSE mode.