Repository navigation
refactor(server): the SDK Server becomes a per-connection facade over one shared core (#1025 step 2, PR 1/5) - #1114
Merged
Merged
Conversation
… 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>
…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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
debugmcpdev
commented
Nov 11, 2026
debugmcpdev
left a comment
Collaborator
Author
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
httpstill builds one core per MCP session.The v2 SDK's 2026-07-28 serving entries build a
Serverper HTTP request (createMcpHandler) or per stdio connection plus a discarded probe instance (serveStdio), so nothing that outlives a request can hang off the SDKServer. TodayDebugMcpServeris one object: SDKServer+ dependency container + SessionManager + logger. This PR splits it.DebugMcpServeris the core. The dependency container, the SessionManager, the per-process services, the mode-gatedinstructionsand the newNotificationHub. It constructs no SDKServerany more.createProtocolServer(core, { era, deliver })(src/server/protocol-facade.ts) builds the SDKServer: the eight MCP handlers closing over the core, theonerror/oninitializedhooks (the[MCP Server] Client initializedline is unchanged), and one notification sink that is attached at construction and removed onserver.onclose.deliver: 'external'(the future per-request HTTP instance) attaches none. The SDK'screateMcpHandlerchainsoncloserather than replacing it, so the hook survives the serving entries.NotificationHub(src/server/notification-hub.ts) replaces the per-serverOutputResourceNotifier: one process-wide 150 ms coalescer per output URI, fanned out to every attached sink. A legacy facade filters pings by its ownresources/subscribeset (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.WeakMapkeyed on theToolContext), 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.protocolServerFactory(defaultcreateProtocolServer) and connect the facade to the transport where they connecteddebugMcpServer.serverbefore.Tests
tests/core/unit/server/notification-hub.test.ts(11) andprotocol-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.createDebugServerWithFacade;server-initializationnow pins that the core builds noServer; the stdio/http unit suites pin the facade wiring.typecheck:all0 errors (ratchet 0/0), lint, unit 411 files / 8222 passed, integrationtests/integration/cli/+mcp-server-self-debug40/40, conformance 7/7 (0.1.16, unchanged), Python stdio smoke (realresources/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-connectordering that chain depends on is commented; the release version is read once per core, not per facade;forgetUriguards 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 perhttpprocess, ownership and lifetime).🤖 Generated with Claude Code