Skip to content

feat(mcp): session lifecycle hygiene#153

Merged
SutuSebastian merged 5 commits into
mainfrom
feat/mcp-session-lifecycle-hygiene
May 27, 2026
Merged

feat(mcp): session lifecycle hygiene#153
SutuSebastian merged 5 commits into
mainfrom
feat/mcp-session-lifecycle-hygiene

Conversation

@SutuSebastian
Copy link
Copy Markdown
Contributor

@SutuSebastian SutuSebastian commented May 27, 2026

Summary

  • session-lifecycle.ts: stdio disconnect detection (stdin EOF, stdout EPIPE, parent-PID poll, SIGINT/SIGTERM) and refcount-gated watcher sessions
  • MCP: one watch client for the stdio session; exits on disconnect only — no idle timeout
  • HTTP serve: watcher starts per authenticated non-/health request; 5s release grace stops chokidar between requests without shutting down the listener
  • Docs: architecture § Session lifecycle wiring, agents.md, glossary, roadmap, CLI help

Test 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.ts
  • bun run typecheck

Review loop

Cycle Findings Fix
1 acquire rollback, stopInFlight race, serve help 0a7246b
2 NO FINDINGS

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-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

🦋 Changeset detected

Latest 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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Warning

Review limit reached

@SutuSebastian, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e60a88c0-4cc9-4c39-ada0-a5779d341ddb

📥 Commits

Reviewing files that changed from the base of the PR and between 920f9bd and b3c13da.

📒 Files selected for processing (8)
  • docs/agents.md
  • docs/architecture.md
  • docs/glossary.md
  • src/application/mcp-server.ts
  • src/application/session-lifecycle.test.ts
  • src/application/session-lifecycle.ts
  • src/cli/cmd-mcp.ts
  • src/cli/cmd-serve.ts
📝 Walkthrough

Walkthrough

The PR implements session lifecycle hygiene for codemap mcp and codemap serve by introducing a managed watch session abstraction with stdio disconnect monitoring, refactoring both MCP and HTTP servers to coordinate watcher lifecycle through reference counting, and updating documentation to describe the new termination and watcher-start/stop behavior.

Changes

MCP Session Lifecycle Management

Layer / File(s) Summary
Session lifecycle utilities and comprehensive tests
src/application/session-lifecycle.ts, src/application/session-lifecycle.test.ts
Core module exports isProcessAlive, createStdioDisconnectMonitor (stdin EOF, stdout EPIPE, parent PID polling), createManagedWatchSession (lazy-start reference-counted watcher), and bindWatchClientRelease (HTTP response release binding). Tests verify all disconnect paths, client acquire/release with grace-period scheduling, and exactly-once release semantics.
MCP server refactored for managed session lifecycle
src/application/mcp-server.ts
Replaces inline watcher loop with managed watch session tied to index priming. Creates a single long-lived stdio client via acquireClient(). Consolidates shutdown via a unified handler triggered by transport close, stdio disconnect monitor (stdin EOF, stdout EPIPE, parent exit), and SIGINT/SIGTERM.
HTTP server refactored with managed watch session and request-level coordination
src/application/http-server.ts, src/application/http-server.test.ts
Adds optional managedWatchSession to HttpServerOpts with 5s release grace for HTTP. Non-liveness requests acquire a watch client and auto-release on response finish/close via bindWatchClientRelease. /health explicitly avoids watch client acquisition. Shutdown forces session stop before closing listener. Integration test verifies /health does not trigger watching, tool requests do start watching, and graceful stop works.
Documentation, CLI help, and changeset
docs/architecture.md, docs/agents.md, docs/glossary.md, docs/roadmap.md, src/cli/cmd-mcp.ts, src/cli/cmd-serve.ts, .changeset/mcp-session-lifecycle-hygiene.md
Architecture doc now describes MCP's disconnect-driven shutdown via createStdioDisconnectMonitor, managed watch session draining, and new "Session lifecycle wiring" section detailing no-idle-timeout for MCP and HTTP release-grace semantics. Agents doc explains MCP uptime and exit triggers. Glossary adds codemap mcp definition and Session lifecycle section. Roadmap specifies concrete disconnect signals (stdin EOF, stdout EPIPE, parent PID, SIGINT/SIGTERM) and refcount-gating. CLI help text clarifies termination conditions and watch start/stop timing. Changeset documents the feature.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • stainless-code/codemap#47: Introduces the chokidar-based watcher loop (runWatchLoop in watcher.ts), which this PR replaces with the new managed session abstraction in MCP and HTTP server integration.
  • stainless-code/codemap#35: Implements the original codemap mcp CLI command and MCP server startup; this PR refactors its lifecycle handling to use stdio disconnect monitoring and managed watch sessions.
  • stainless-code/codemap#44: Introduces the HTTP serve command foundation; this PR adds the managed watch session coordination and request-scoped watcher client acquisition for --watch behavior.

A rabbit hops through the code, paws twitching with glee,
Watch clients released, discarded with spree,
MCP stands steady, HTTP sways free,
Session hygiene blessed—no leaks we shall see! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(mcp): session lifecycle hygiene' directly aligns with the main implementation focus: adding session lifecycle management utilities (stdio disconnect detection, parent-PID polling, refcount-gated watcher sessions) and integrating them into the MCP and HTTP servers. This change encompasses the core improvements across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-session-lifecycle-hygiene

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/application/mcp-server.ts (1)

622-655: 💤 Low value

Consider guarding disconnectMonitor.dispose() with optional chaining.

The shutdown function references disconnectMonitor (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

📥 Commits

Reviewing files that changed from the base of the PR and between 150c6c6 and 920f9bd.

📒 Files selected for processing (12)
  • .changeset/mcp-session-lifecycle-hygiene.md
  • docs/agents.md
  • docs/architecture.md
  • docs/glossary.md
  • docs/roadmap.md
  • src/application/http-server.test.ts
  • src/application/http-server.ts
  • src/application/mcp-server.ts
  • src/application/session-lifecycle.test.ts
  • src/application/session-lifecycle.ts
  • src/cli/cmd-mcp.ts
  • src/cli/cmd-serve.ts

Comment thread src/application/session-lifecycle.test.ts
Comment thread src/application/session-lifecycle.ts
Comment thread src/application/session-lifecycle.ts
Comment thread src/application/session-lifecycle.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.
@SutuSebastian SutuSebastian merged commit 048278b into main May 27, 2026
11 checks passed
@SutuSebastian SutuSebastian deleted the feat/mcp-session-lifecycle-hygiene branch May 27, 2026 13:28
@github-actions github-actions Bot mentioned this pull request May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant