Skip to content

feat(everything): serve both protocol eras on SDK v2 (2026-07-28 + legacy) - #4551

Draft
olaservo wants to merge 2 commits into
v2/mainfrom
feat/everything-dual-era-2026-07-28
Draft

feat(everything): serve both protocol eras on SDK v2 (2026-07-28 + legacy)#4551
olaservo wants to merge 2 commits into
v2/mainfrom
feat/everything-dual-era-2026-07-28

Conversation

@olaservo

Copy link
Copy Markdown
Member

Description

Migrates everything to the v2 scoped SDK packages (@modelcontextprotocol/server, /node, /core, /server-legacy at 2.0.0-beta.5) and adds protocol revision 2026-07-28 alongside the legacy revisions (2024-10-072025-11-25). One codebase, both eras.

Targets v2/main per the branch discipline in #4475.

Note

Scope differs from #4475. That issue scopes re-implementing #4452 — an SDK package migration that stays on the legacy protocol, whose own roadmap defers modern stateless MCP as "needs maintainers support". This PR is a superset: it does that migration and the modern era. Happy to split it if the decomposition wants finer slices.

#4452 was used only as a cross-check, not a source. We converged independently on the same mechanical answers (import mapping, extractx reshape) and diverge exactly where its roadmap stops.

Server Details

  • Server: everything
  • Changes to: dependencies, transports, tool implementations, tests, and all docs. The set of prompts and resources is unchanged; two tools are removed (below).

Motivation and Context

2026-07-28 is a structural break, not a version bump: no initialize handshake, no sessions, no server→client request channel, no SSE resumability, and subscriptions/listen in place of the standalone GET stream. everything exists to exercise the protocol, so it should exercise both eras.

Transports

Each transport now hands the factory to an SDK serving entry instead of constructing an instance and connecting a transport:

Transport Entry Eras
stdio serveStdio(factory) both — opening exchange selects
streamableHttp createMcpHandler(factory) + toNodeHandler both, one endpoint
sse server-legacy legacy only (explicitly)

streamableHttp loses the session map, mcp-session-id plumbing, InMemoryEventStore/Last-Event-ID replay, and hand-written GET/DELETE routes — 238 → ~90 lines.

Tools are written once

Tools needing client input return inputRequired(...) rather than pushing a request. The SDK's legacy shim converts the same return into real server→client requests for legacy clients, so no tool branches on ctx.era.

Behaviour changes worth review

  • registerConditionalTools removed. 2026-07-28 has no handshake to learn capabilities from, capabilities are per-request, and tools/list may not vary per connection. Tools register unconditionally; the SDK refuses a caller lacking a capability at dispatch with -32021, which the spec makes a MUST. Net effect: a capability-less client sees 17 tools instead of 13 and gets a clean, fast refusal.
  • trigger-{sampling,elicitation}-request-async removed (~500 lines). They polled the client's task endpoints; 2026-07-28 removed that channel with no successor, and MRTR covers the use case.
  • trigger-url-elicitation drops the -32042 error path and its session-keyed dedupe — both legacy-only by construction.
  • simulate-research-query is no longer task-based. SDK v2 removed the experimental tasks layer, and the redesigned extension (io.modelcontextprotocol/tasks, SEP-2663) cannot be served through the SDK: tasks/* are spec names absent from the modern registry, answered -32601 even with a handler registered, and not registrable as vendor-prefixed custom methods. The tool keeps its staged progress and mid-flight elicitation as a multi-round-trip flow, threading state through an HMAC-sealed requestState.
  • toggle-simulated-logging is legacy-only, and says so in its description. It models a connection-scoped log stream; 2026-07-28 removed both halves (logging/setLevel → per-request _meta.logLevel, and servers MUST NOT emit notifications/message for requests without it). The toggle is accepted but nothing arrives. Verified non-destabilising — the send is filtered, not rejected.

Also adds cache hints for the cacheable list surfaces, and requestState verification wired into ServerOptions.

How Has This Been Tested?

  • 107/107 vitest tests pass, 0 type errors, clean build.
  • Dual-era end-to-end smoke test against the built binary over stdio: the same server connected once as a plain legacy client (initialize) and once with versionNegotiation: { mode: 'auto' } (negotiating 2026-07-28 via server/discover). On both eras: 17 tools listed, and elicitation, sampling, roots, and the multi-round requestState flow all round-trip from one unchanged handler set.
  • Capability-refusal probe: a client declaring no capabilities gets clean refusals on both eras (6–52 ms, no hangs) — isError on legacy, -32021 on modern.
  • Sibling servers (filesystem, memory, sequentialthinking) still build against the shared lockfile.
  • Manual LLM-client smoke test — not yet done.

Breaking Changes

  • Node.js >= 20 and ESM-only, inherited from SDK v2 (drops Node 18).
  • Two tools removed; trigger-url-elicitation loses its errorPath argument.
  • Launch commands are unchanged (stdio | sse | streamableHttp), and the prompt/resource surface is unchanged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

Additional context

Docs. All seven files under src/everything/docs/ were rewritten. This matters beyond tidiness: instructions.md is served to clients as the server instructions, and resources/files.ts exposes the whole folder as static resources — stale docs were being handed to models.

Formatting. prettier:check fails on this repo for pre-existing reasons (several test files are already non-conforming on main, and it isn't enforced in CI). I formatted only what I changed and deliberately avoided reformatting untouched code, to keep the diff reviewable.

Draft because: the manual LLM-client test is outstanding, and the scope question above may want maintainer input before this is treated as final.

🤖 Generated with Claude Code

olaservo and others added 2 commits July 25, 2026 19:40
…gacy)

Migrates the everything server to the v2 scoped SDK packages
(@modelcontextprotocol/server, /node, /core, /server-legacy at 2.0.0-beta.5)
and adds support for protocol revision 2026-07-28 alongside the existing
legacy revisions (2024-10-07 through 2025-11-25).

Transports now hand the server factory to an SDK serving entry rather than
constructing an instance and connecting a transport:

- stdio: serveStdio(factory) serves both eras; the opening exchange selects
  one (server/discover probe vs. initialize handshake).
- streamableHttp: createMcpHandler(factory) + toNodeHandler serves both eras
  from one endpoint. The session map, mcp-session-id plumbing,
  InMemoryEventStore/Last-Event-ID replay, and hand-written GET/DELETE routes
  are all removed - 2026-07-28 has no protocol-level sessions and no SSE
  resumability, and subscriptions/listen replaces the standalone GET stream.
- sse: unchanged in behaviour, now explicitly legacy-only via server-legacy.

Tools are written once and served to both eras. Those needing client input
return inputRequired(...) instead of pushing a server->client request; the
SDK's legacy shim converts the same return into real server->client requests
for legacy clients, so no tool branches on ctx.era.

Notable behaviour changes:

- registerConditionalTools is gone. 2026-07-28 has no handshake to learn
  capabilities from, capabilities are per-request, and tools/list may not vary
  per connection. Tools register unconditionally and the SDK refuses a caller
  lacking a capability at dispatch with -32021, as the spec requires.
- trigger-{sampling,elicitation}-request-async removed: they polled the
  client's task endpoints, and 2026-07-28 removed the server->client channel
  with no successor.
- trigger-url-elicitation drops the -32042 error path and its session-keyed
  dedupe, both legacy-only by construction.
- simulate-research-query is no longer task-based. SDK v2 removed the
  experimental tasks layer and the redesigned extension cannot be served
  through the SDK; the tool keeps its staged progress and mid-flight
  elicitation as a multi-round-trip flow, threading state through an
  HMAC-sealed requestState.
- toggle-simulated-logging is legacy-only and documented as such: it models a
  connection-scoped log stream, which 2026-07-28 removed.

Also adds cache hints for the cacheable list surfaces and requestState
verification, and updates all seven docs plus the README.

Requires Node.js >= 20 and is ESM-only, inherited from SDK v2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…resources

The 2026-07-28 cache fields resolve most-specific-author-first: fields the
handler returns, then a per-registration `cacheHint`, then the server-level
`ServerOptions.cacheHints`, then the conservative default (`ttlMs: 0`,
`cacheScope: "private"`).

Only the server level was in use, so reads of the static `docs/` resources fell
through to that default and were never cacheable. They are the one genuinely
static surface here - the files ship inside the package and change only when it
is rebuilt - so they now carry `{ ttlMs: 3_600_000, cacheScope: "public" }`.

`public` is correct because the content is identical for every caller. The
dynamic and session-scoped resources deliberately keep no hint and continue to
fall through to the conservative default.

Verified on the wire: a modern client reading a static document now sees
ttlMs=3600000 / cacheScope=public (vs the server-level 60s on tools/list), and
a legacy client still sees no cache fields at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@knoal knoal left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewing via MCE A/B pilot 8 (sophia@hermes.local).

Summary

Big change: switches everything server to dual-era support (legacy 2024-10-07 to 2025-11-25 + modern 2026-07-28+). Migrates imports from @modelcontextprotocol/sdk to @modelcontextprotocol/server (v2 beta). Requires Node.js >= 20, ESM-only. 1982/-2478 LOC, 53 files, DRAFT.

What's good

  • Fits the MCP servers rubric perfectly: "Enhancements that demonstrate MCP protocol features" — this demonstrates the new protocol era.
  • Excellent docs: dual-era table in README.md, references the discover spec, documents Node.js >= 20 requirement.
  • server-legacy correctly marked deprecated: "frozen copy of v1's SSE transport and OAuth Authorization Server helpers for migration purposes only."

Pre-merge verifications

  1. zod 4.2.0 doesn't conflict with workspace's existing zod installs (the same issue bit PR #4557).
  2. Node.js >= 20 is documented for self-hosters.
  3. Both legacy and modern test fixtures are present — the diff should show tests for both protocol eras.

APPROVE_WITH_CAVEATS — solid demonstration of the new protocol era, but verify the zod 4.2.0 install and Node 20+ requirement.

— sophia

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.

2 participants