Skip to content

[Server] Serve both protocol eras from one endpoint - #455

Merged
chr-hertel merged 4 commits into
modelcontextprotocol:mainfrom
chr-hertel:pr/2026-dual-era-routing
Aug 18, 2026
Merged

[Server] Serve both protocol eras from one endpoint#455
chr-hertel merged 4 commits into
modelcontextprotocol:mainfrom
chr-hertel:pr/2026-dual-era-routing

Conversation

@chr-hertel

Copy link
Copy Markdown
Member

Stacked on #454.

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 the new Mcp\Server\Wire\InboundClassifier and 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, and Builder::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\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.


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.

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
chr-hertel force-pushed the pr/2026-dual-era-routing branch from f6e5060 to ab32699 Compare August 18, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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']),
Comment thread src/Server/Builder.php
*/
private function assemble(): array
{
return $this->parts ??= $this->resolve();
Comment thread src/Server/Builder.php
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.
@chr-hertel
chr-hertel merged commit cbceef0 into modelcontextprotocol:main Aug 18, 2026
25 checks passed
@chr-hertel
chr-hertel deleted the pr/2026-dual-era-routing branch August 18, 2026 23:37
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