[Server] Serve both protocol eras from one endpoint - #455
Merged
chr-hertel merged 4 commits intoAug 18, 2026
Conversation
chr-hertel
requested review from
CodeWithKyrian,
Nyholm and
soyuka
as code owners
August 18, 2026 19:10
The two lifecycles share a transport, not a dispatcher. StreamableHttpTransport classifies each request - a 2026-07-28 envelope, an initialize handshake, or a session-bound follow-up - through InboundClassifier and routes it to the dispatcher that owns it, so one URL answers a modern client and a handshake-era one alike. Builder::build() carries both; withoutModernEra() opts out and setModernVersions() narrows what the modern leg answers for. InputRequiredShim lets a handler written for multi round-trip requests also serve a handshake-era client, by turning each ask into the request/response exchange that era has - so a handler is written once rather than twice. The conformance fixture collapses into one server for the same reason: both legs now hit the same URL.
chr-hertel
force-pushed
the
pr/2026-dual-era-routing
branch
from
August 18, 2026 23:08
f6e5060 to
ab32699
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Enables one HTTP endpoint to serve both handshake and modern protocol eras through request classification and shared dispatchers.
Changes:
- Adds dual-era request classification and routing.
- Shares builder-assembled components across dispatchers.
- Adds handshake compatibility for multi-round handlers and consolidates conformance fixtures.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/Server.php |
Connects both dispatchers to capable transports. |
src/Server/Builder.php |
Builds and configures dual-era servers. |
src/Server/ClientGateway.php |
Exposes generic client requests to the shim. |
src/Server/InputRequiredShim.php |
Adapts modern input requests for handshake clients. |
src/Server/Protocol.php |
Integrates input-required fulfillment. |
src/Server/Stateless/StatelessProtocol.php |
Shares version checks and exposes supported versions. |
src/Server/Transport/Http/Middleware/ProtocolVersionMiddleware.php |
Updates version-middleware documentation. |
src/Server/Transport/Http/StatelessResponder.php |
Centralizes modern HTTP responses. |
src/Server/Transport/StatelessAwareTransportInterface.php |
Defines modern-dispatcher attachment. |
src/Server/Transport/StatelessHttpTransport.php |
Uses the shared responder. |
src/Server/Transport/StreamableHttpTransport.php |
Classifies and routes both eras. |
src/Server/Wire/EraClassification.php |
Represents routing outcomes. |
src/Server/Wire/InboundClassifier.php |
Classifies inbound protocol traffic. |
tests/Unit/Server/BuilderTest.php |
Tests dual-era builder behavior. |
tests/Unit/Server/InputRequiredShimTest.php |
Tests shim round trips and limits. |
tests/Unit/Server/Transport/DualEraRoutingTest.php |
Tests shared-endpoint routing. |
tests/Unit/Server/Wire/InboundClassifierTest.php |
Covers classification rules. |
tests/Conformance/Elements.php |
Adds the JSON Schema fixture. |
tests/Conformance/Fixtures/nginx.conf |
Routes both eras to one fixture. |
tests/Conformance/server-stateless.php |
Removes the separate modern fixture. |
tests/Conformance/server.php |
Consolidates conformance server behavior. |
examples/server/bootstrap.php |
Documents dual-era example transport behavior. |
CHANGELOG.md |
Records the dual-era feature. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
157
to
159
| $handler = new MiddlewareRequestHandler( | ||
| $this->middleware, | ||
| $this->middleware ??= self::defaultMiddleware(), | ||
| \Closure::fromCallable([$this, 'handleRequest']), |
| */ | ||
| private function assemble(): array | ||
| { | ||
| return $this->parts ??= $this->resolve(); |
Comment on lines
+910
to
+915
| public function setModernVersions(array $versions): self | ||
| { | ||
| $this->modernVersions = $versions; | ||
|
|
||
| return $this; | ||
| } |
|
|
||
| /** | ||
| * @param list<ProtocolVersion>|null $supportedVersions Versions the server accepts. Defaults to {@see ProtocolVersion::handshakeVersions()}; modern revisions are excluded as their per-request negotiation is not served yet. | ||
| * @param list<ProtocolVersion>|null $supportedVersions Versions the server accepts. Defaults to {@see ProtocolVersion::handshakeVersions()}; an endpoint serving the modern era too passes those revisions as well, so a modern header is not turned away before the era is classified. |
The new fixture tool makes this scenario pass in both eras now.
…tom middleware Copilot review: setModernVersions() accepted revisions InboundClassifier would never route to the modern leg. StreamableHttpTransport now warns when a custom middleware list carries ProtocolVersionMiddleware, since it runs before era classification and rejects modern-era traffic by default. Also fixed a misleading docblock claim on the same middleware.
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.
Stacked on #454.
The two lifecycles share a transport, not a dispatcher.
StreamableHttpTransportclassifies each request — a2026-07-28envelope, aninitializehandshake, or a session-bound follow-up — through the newMcp\Server\Wire\InboundClassifierand routes it to the dispatcher that owns it, so one URL answers a modern client and a handshake-era one alike.Server::builder()->build()now carries both dispatchers.Builder::withoutModernEra()opts out, andBuilder::setModernVersions()narrows what the modern leg answers for. The builder's assembled parts (registry, session manager, discovery) are resolved once and shared, so one endpoint never ends up with two registries behind it.Mcp\Server\InputRequiredShimlets a handler written for multi round-trip requests also serve a handshake-era client, by turning each ask into the request/response exchange that era has — so a handler is written once rather than twice.The conformance fixture collapses into one server for the same reason: both legs now hit the same URL.
Cross-fork PRs can only target
main, so this diff also carries its ancestors until they merge. Only the last commit(s) belong to this PR — GitHub's "Commits" tab separates them.