Skip to content

refactor(server): the SDK Server becomes a per-connection facade over one shared core (#1025 step 2, PR 1/5) - #1114

Merged
debugmcpdev merged 3 commits into
mainfrom
refactor/1025-protocol-facade
Nov 11, 2026
Merged

debugmcpdev merged 3 commits into
mainfrom
refactor/1025-protocol-facade

Conversation

@debugmcpdev

@debugmcpdev debugmcpdev commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

PR 1 of 5 for #1025 step 2 (protocol 2026-07-28 beside 2025-11-25; plan and decisions on the issue). A pure refactor: the wire is unchanged, both transports still speak the 2025-11-25 era, and http still builds one core per MCP session.

The v2 SDK's 2026-07-28 serving entries build a Server per HTTP request (createMcpHandler) or per stdio connection plus a discarded probe instance (serveStdio), so nothing that outlives a request can hang off the SDK Server. Today DebugMcpServer is one object: SDK Server + dependency container + SessionManager + logger. This PR splits it.

  • DebugMcpServer is the core. The dependency container, the SessionManager, the per-process services, the mode-gated instructions and the new NotificationHub. It constructs no SDK Server any more.
  • createProtocolServer(core, { era, deliver }) (src/server/protocol-facade.ts) builds the SDK Server: the eight MCP handlers closing over the core, the onerror/oninitialized hooks (the [MCP Server] Client initialized line is unchanged), and one notification sink that is attached at construction and removed on server.onclose. deliver: 'external' (the future per-request HTTP instance) attaches none. The SDK's createMcpHandler chains onclose rather than replacing it, so the hook survives the serving entries.
  • NotificationHub (src/server/notification-hub.ts) replaces the per-server OutputResourceNotifier: one process-wide 150 ms coalescer per output URI, fanned out to every attached sink. A legacy facade filters pings by its own resources/subscribe set (FacadeSubscriptions); a modern pin will send every URI and let the SDK's listen router filter. create_debug_session, child adoption and session removal all notify through the hub.
  • The tools/call dispatch index is built once per core (a WeakMap keyed on the ToolContext), not per facade, so a facade costs a few closures. Measured in the facade suite: a second facade over a built core stays well under the 20 ms bound.
  • stdio-command / http-command take an injected protocolServerFactory (default createProtocolServer) and connect the facade to the transport where they connected debugMcpServer.server before.

Tests

  • New: tests/core/unit/server/notification-hub.test.ts (11) and protocol-facade.test.ts (10): coalescing per URI, fan-out, sink removal on close, filtered vs unfiltered sinks, no sink for external delivery, the eight handlers, the hooks, the cost bound.
  • The server unit suites build the core with one facade through the helper createDebugServerWithFacade; server-initialization now pins that the core builds no Server; the stdio/http unit suites pin the facade wiring.
  • Gates run locally: typecheck:all 0 errors (ratchet 0/0), lint, unit 411 files / 8222 passed, integration tests/integration/cli/ + mcp-server-self-debug 40/40, conformance 7/7 (0.1.16, unchanged), Python stdio smoke (real resources/subscribe + notifications/resources/updated) and HTTP smoke 7/7.

One stated deviation. The hub arms the 150 ms coalescer on every output event and the facade filters when it fires; the old notifier filtered when arming. A client that subscribes inside a 150 ms window after output arrived therefore receives one ping for that output, where before it received none until the next output. The resource did change, so the ping is true; strict equivalence would have coupled the hub to the facades' subscription sets.

Review follow-ups (second commit). A suite drives a real SDK Server/Client pair over the in-memory transport and pins that the facade's sink is detached when either side closes (the chain PR 2's per-process core relies on); the http onclose-before-connect ordering that chain depends on is commented; the release version is read once per core, not per facade; forgetUri guards each sink like the sends; a composed two-facade subscription test; CLAUDE.md and the system overview name the two new modules.

No user-visible change; labelled no-changelog. Next: PR 2 (one shared core per http process, ownership and lifetime).

🤖 Generated with Claude Code

… one shared core (#1025)

The 2026-07-28 serving entries of the v2 SDK build a `Server` per HTTP request
(`createMcpHandler`) or per stdio connection plus a discarded probe instance
(`serveStdio`), so nothing that outlives a request can hang off the SDK Server.
Step 2 of #1025 therefore needs the debug state in one shared core and the
Server as a cheap facade over it. This is that split, with the wire unchanged:
both transports still speak the 2025-11-25 era and `http` still builds one
core per MCP session.

- `DebugMcpServer` is the core: the dependency container, the SessionManager,
  the per-process services, the mode-gated `instructions` and the new
  `NotificationHub`. It constructs no SDK Server any more.
- `createProtocolServer(core, { era, deliver })` (src/server/protocol-facade.ts)
  builds the Server: the eight MCP handlers closing over the core, the error and
  initialized hooks, and one notification sink that lives exactly as long as the
  facade does (`deliver: 'external'`, the future per-request HTTP instance,
  attaches none).
- `NotificationHub` (src/server/notification-hub.ts) replaces the per-server
  `OutputResourceNotifier`: one process-wide 150 ms coalescer per output URI,
  fanned out to every attached sink; a legacy facade filters by its own
  `resources/subscribe` set (`FacadeSubscriptions`), a modern pin will not.
- The tools/call dispatch index is built once per core, not per facade, so a
  facade costs a few closures.
- stdio-command and http-command take an injected `protocolServerFactory`
  (default `createProtocolServer`) and connect the facade to the transport.

Tests: notification-hub and protocol-facade suites (new); the server unit
suites build the core with one facade through `createDebugServerWithFacade`;
initialization pins that the core builds no Server; stdio/http unit suites pin
the facade wiring. Gates: typecheck:all 0, lint, unit, integration cli +
self-debug 40/40, conformance 7/7, python/http e2e smokes 7/7.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@debugmcpdev debugmcpdev added the no-changelog PR needs no CHANGELOG fragment (refactor, CI-internal, or otherwise not user-visible) label Nov 10, 2026
…lose chain; read the version once per core (#1025)

Review follow-ups on the facade split:

- A new suite drives a real SDK Server/Client pair over the in-memory
  transport and asserts the facade's notification sink is detached when either
  side closes — the chain (transport close → Protocol._onclose → server.onclose
  → detach) that the per-process core of the next PR relies on. The http
  command's onclose-before-connect ordering, which that chain depends on, is
  now commented.
- The release version is read once on the core (`version`), not per facade:
  the helper walks the file system, and the modern HTTP leg will build a facade
  per request.
- `NotificationHub.forgetUri` runs each sink's `forget` under the same guard as
  the sends, so a failing sink cannot escape into the SessionManager's
  'session-removed' emit.
- A composed test: two legacy facades over one core are pinged only for their
  own subscriptions. The cost bound allows for a loaded CI runner.
- CLAUDE.md and the system overview name the two new modules.

Accepted deviation, stated: the hub arms the 150 ms coalescer on every output
event and the facade filters when it fires (the old notifier filtered when
arming), so a client that subscribes inside a 150 ms window after output
arrived receives one ping for that output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@debugmcpdev debugmcpdev left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed the core/facade separation, notification fan-out, connection lifecycle and SDK close-chain tests at 91745a9. No blocking code findings. The original head passed all applicable CI checks; current-main validation is required again before merge. This is the first PR in the #1114 → #1115 → #1116 → #1117 → #1118 stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog PR needs no CHANGELOG fragment (refactor, CI-internal, or otherwise not user-visible)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants