feat(mcp): session lifecycle hygiene#153
Conversation
Add stdio disconnect detection and refcount-gated watcher sessions for mcp/serve; document explicit no-MCP-idle-timeout policy in architecture and agent docs.
🦋 Changeset detectedLatest commit: b3c13da The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
More reviews will be available in 48 minutes and 37 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe PR implements session lifecycle hygiene for ChangesMCP Session Lifecycle Management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Roll back client count when watch start fails; await in-flight stop before acquire; align serve help with non-/health acquire scope.
Restore main's watch-ready-before-tools ordering via acquireClient before server.connect; align agents.md HTTP grace wording.
Align architecture, glossary, and cmd-mcp help with acquireClient ordering before server.connect.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/application/mcp-server.ts (1)
622-655: 💤 Low valueConsider guarding
disconnectMonitor.dispose()with optional chaining.The
shutdownfunction referencesdisconnectMonitor(line 627) before it's assigned (line 653). While closures defer evaluation until call time and stdio monitors are unlikely to fire synchronously, a defensive guard avoids a potential undefined error if the monitor's callback ever runs during construction (e.g., stdin already closed at startup).🛡️ Defensive fix
- disconnectMonitor.dispose(); + disconnectMonitor?.dispose();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/application/mcp-server.ts` around lines 622 - 655, The shutdown closure calls disconnectMonitor.dispose() before disconnectMonitor is assigned; change that call to safely handle an uninitialized monitor (e.g., use optional chaining/null-check) so shutdown uses disconnectMonitor?.dispose() or if (disconnectMonitor) disconnectMonitor.dispose(); update the reference inside the shutdown function (which is declared near function shutdown) and ensure createStdioDisconnectMonitor is still used to assign disconnectMonitor after shutdown is declared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/application/session-lifecycle.test.ts`:
- Around line 114-132: The test sets a spy on isProcessAlive (assigned to
variable alive) but currently calls alive.mockRestore() only at the end, risking
leaking the mock if the test throws; wrap the assertion flow that uses
createStdioDisconnectMonitor and the Bun.sleep/expect calls in a try/finally and
call alive.mockRestore() in the finally block so the spy is always restored even
on failure.
In `@src/application/session-lifecycle.ts`:
- Around line 153-159: stopWatcher can return when handle is undefined while a
watcher is still in the "starting" phase, allowing the watcher to become running
after shutdown; modify the shutdown logic so that stopWatcher (and the analogous
forceStop path) either await any in-progress startup or use a shared "stopping"
flag that starting code checks — specifically, add a boolean like isStopping
that stopWatcher sets before checking handle, and in the watcher-start code
(where handle is assigned) check isStopping immediately after assigning handle
and call current.stop() if true (or alternatively have stopWatcher await a
"starting" promise exposed by the startup sequence before returning); update
both stopWatcher and the forceStop sequence to use this pattern so no watcher
can be left running after clients are zeroed.
- Around line 162-166: In acquireClient(), clients is incremented before
awaiting ensureStarted(), so if ensureStarted() throws the refcount remains
incremented; modify acquireClient to increment clients then call ensureStarted()
inside a try block and decrement clients in the catch (or use try/finally with a
rethrow) so that on failure the refcount is rolled back; reference the
acquireClient function, the clients variable, cancelScheduledStop(), and
ensureStarted() when making the change.
- Around line 202-206: The release closure currently calls
session.releaseClient() without observing its promise, risking unhandled
rejections; update the release implementation (the release function that calls
session.releaseClient()) to handle the returned promise by attaching a .catch
handler (or making release async and awaiting with try/catch) and log or surface
the error using the session's logger (or console.error) so any failure in
session.releaseClient() is observed and reported.
---
Nitpick comments:
In `@src/application/mcp-server.ts`:
- Around line 622-655: The shutdown closure calls disconnectMonitor.dispose()
before disconnectMonitor is assigned; change that call to safely handle an
uninitialized monitor (e.g., use optional chaining/null-check) so shutdown uses
disconnectMonitor?.dispose() or if (disconnectMonitor)
disconnectMonitor.dispose(); update the reference inside the shutdown function
(which is declared near function shutdown) and ensure
createStdioDisconnectMonitor is still used to assign disconnectMonitor after
shutdown is declared.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c767d21a-8aae-4a3b-8efd-0a8c8ccdf052
📒 Files selected for processing (12)
.changeset/mcp-session-lifecycle-hygiene.mddocs/agents.mddocs/architecture.mddocs/glossary.mddocs/roadmap.mdsrc/application/http-server.test.tssrc/application/http-server.tssrc/application/mcp-server.tssrc/application/session-lifecycle.test.tssrc/application/session-lifecycle.tssrc/cli/cmd-mcp.tssrc/cli/cmd-serve.ts
stopWatcher now waits for starting before checking handle, so forceStop during HTTP first-request prime cannot orphan chokidar. Also observe fire-and-forget release/stop rejections and harden the parent-pid spy test with try/finally.
Summary
session-lifecycle.ts: stdio disconnect detection (stdin EOF, stdout EPIPE, parent-PID poll, SIGINT/SIGTERM) and refcount-gated watcher sessions/healthrequest; 5s release grace stops chokidar between requests without shutting down the listenerTest plan
bun test src/application/session-lifecycle.test.ts(11 tests)bun test src/application/http-server.test.ts(managed watch session)bun test src/application/mcp-server.test.tsbun run typecheckReview loop
0a7246b