Skip to content

refactor!: extract LegacyServer/LegacyClient; NEW Server/Client compose them#2134

Draft
felixweinberger wants to merge 5 commits into
fweinberger/v2-sectionedfrom
fweinberger/v2-legacy-extraction
Draft

refactor!: extract LegacyServer/LegacyClient; NEW Server/Client compose them#2134
felixweinberger wants to merge 5 commits into
fweinberger/v2-sectionedfrom
fweinberger/v2-legacy-extraction

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented May 20, 2026

2026-06 stateless stack (v2-stateless label):

# PR
1 #2128 tasks-delete (mechanical)
2 #2129 schema-sync (mechanical)
3 #2130 Dispatcher extraction (zero-Δ refactor)
4 #2131 HTTP-stateless (the substantive review)
5 #2132 stdio/InMemory transports (additive)
6 #2133 docs + changeset
7 #2134 LegacyServer/LegacyClient extraction

Extracts LegacyServer/LegacyClient (the Protocol-derived, session-stateful classes) and introduces NEW Server/Client that own the 2026 stateless path and compose the legacy class via .legacy.

Motivation and Context

After #2131 the sectioned Server/Client carry both the 2026 stateless surface and the pre-2026 session-dependent surface side by side. That works, but nothing in the type system tells a caller which methods are session-only. This PR makes the boundary structural:

  • The NEW Server/Client no longer extend Protocol. They expose the 2026 stateless surface plus the version-agnostic typed request methods.
  • Session-dependent methods (createMessage/elicitInput/listRoots/sendLoggingMessage/ping on the server; subscribeResource/unsubscribeResource/sendRootsListChanged/ping on the client; raw request/notification/setNotificationHandler on both) live only on LegacyServer/LegacyClient, reachable via the explicit .legacy escape hatch.
  • _assertSession() guards the legacy methods at runtime and throws SdkErrorCode.SessionRequired with a migration URL when called on a 2026 connection.

Both classes share one handler registry and negotiated state (single source of truth on the legacy instance), so behavior is identical to the sectioned base; this PR just moves the seam into the type system.

How Has This Been Tested?

pnpm typecheck:all && lint:fix:all && build:all && test:all && docs:check all green. Integration tests updated to call session-dependent methods through .legacy.

Breaking Changes

Server/Client no longer extend Protocol. Session-dependent methods move to .legacy.*. See docs/migration.md.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Reviewable per-commit:

  1. refactor(server): rename Server -> LegacyServer (verbatim)git mv + class rename, zero body changes
  2. refactor(server)!: NEW Server class (Protocol-free) composes LegacyServer
  3. refactor(client): rename Client -> LegacyClient (verbatim)git mv + class rename, zero body changes
  4. refactor(client)!: NEW Client class (Protocol-free) composes LegacyClient
  5. test: adapt tests to .legacy getter for session-dependent methods

git mv server.ts legacyServer.ts + class rename. Zero body changes.
Importers updated to `LegacyServer as Server` so behavior is identical
and the rename shows as a true file rename in history.
…rver

`Server` no longer extends `Protocol`. It owns the 2026 stateless dispatch
path (subscriptions, statelessHandlers, _dispatchStateless,
_buildDispatchServerContext, _ondiscover) and the dual-mode surface
(connect/close/transport/onclose/onerror, _fanoutNotify, send*ListChanged),
lifted verbatim from the sectioned `LegacyServer` with field reads
re-pointed at `this.config.*` / `this._legacy.*`.

`LegacyServer` keeps the session-dependent block. Both share one handler
registry via `_legacy._dispatch`/`_legacy.setRequestHandler`.
`server.legacy` is the explicit escape hatch.

LegacyServer body changes are limited to:
- `_assertSession()` guard inserted at top of createMessage / elicitInput /
  listRoots / sendLoggingMessage / ping
- ctor: dropped `subscriptions` init + `server/discover` registration
  (lifted to NEW Server)

Adds `SdkErrorCode.SessionRequired`.
git mv client.ts legacyClient.ts + class rename. Zero body changes.
Importers updated to `LegacyClient as Client` so behavior is identical
and the rename shows as a true file rename in history.
…ient

`Client` no longer extends `Protocol`. It owns the 2026 stateless send
path (_isStateless/_buildMeta/_withMeta/_collect/_send/_negotiate/
subscribe/_listChangedLoop) and the typed request methods
(callTool/listTools/getPrompt/listPrompts/readResource/listResources/
listResourceTemplates/complete/setLoggingLevel), lifted verbatim from
the sectioned `LegacyClient` with field reads re-pointed at
`this.config.*` / `this._legacy.*`.

`LegacyClient` keeps the session-dependent block. Both share one handler
registry via `_legacy._dispatch`/`_legacy.setRequestHandler`; negotiated
server state lives on `_legacy` (single source of truth) and is written
by both `_negotiate` (via `_setNegotiated`) and `_initialize`.
`client.legacy` is the explicit escape hatch.

LegacyClient body changes are limited to:
- `_assertSession()` guard inserted at top of ping / subscribeResource /
  unsubscribeResource / sendRootsListChanged
- ctor: dropped now-unused `_enforceStrictCapabilities` /
  `_pendingListChangedConfig` / `_cachedToolOutputValidators` fields
  (lifted to NEW Client)
- `_initialize` / `_setupListChangedHandler` visibility widened to @internal
Integration tests that exercise the pre-2026 session-dependent surface
(server.createMessage / elicitInput / listRoots / sendLoggingMessage /
ping / getClientCapabilities / getClientVersion / oninitialized /
createElicitationCompletionNotifier / notification / request /
setNotificationHandler; client.sendRootsListChanged / ping /
subscribeResource / unsubscribeResource / request / notification) now
go through `.legacy`.

LegacyTestClient unchanged: it extends NEW Client and pins versions, so
`connect()` skips the discover probe exactly as before.

clientSend statelessClient() helper updated to set `_transport` /
negotiated state on `c.legacy` (private state moved there).
@felixweinberger felixweinberger added the v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless) label May 20, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

⚠️ No Changeset found

Latest commit: dfae0de

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2134

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2134

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2134

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2134

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2134

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2134

commit: dfae0de

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

Labels

v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant