* [Server] Serve both protocol eras from one endpoint
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.
* Trim redundant comments, fix stale in-memory claim in conformance fixture
* Drop stale json-schema-2020-12 baseline entries
The new fixture tool makes this scenario pass in both eras now.
* Reject handshake revisions in setModernVersions(), warn on unsafe custom 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.
The revision has no way for a server to put a request on a response stream,
so a handler that needs input from the user says so in its result instead: a
handler returning InputRequiredResult comes back as
resultType: "input_required" with the asks and an opaque requestState, and
the client retries the same request carrying inputResponses.
RequestContext::getInputContext() is what the handler reads them through.
RequestStateCodec signs and time-bounds the state, so a tampered or expired
one is refused rather than trusted - the state travels through the client,
which is not a place to keep something the server later believes.
* [Server][Tests] Serve the modern (SEP-2575) lifecycle
The revision removed the initialize handshake: every request carries its own
_meta - protocol version, client capabilities, client info - and answers
server/discover for what initialize used to return. There is no session to
keep, so there is nothing for the handshake-era dispatcher to do.
Builder::buildStateless() produces StatelessProtocol, and
StatelessHttpTransport serves it over one POST-only endpoint - GET and
DELETE are 405, and no Mcp-Session-Id exists to hand out.
Results go through a per-era wire codec rather than the handlers, so
resultType and the caching hints are stamped for 2026-07-28 and left off the
revisions that do not define them.
Covered by the modern conformance suite (make conformance-draft-server)
against its own baseline; unit coverage arrives with the fixes stacked on
this.
* Apply suggestions from code review
Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
* Fix critical stateless-lifecycle bugs from review
- Don't leak internal exception messages to clients on uncaught
\Throwable; use a generic message like the handshake-era protocol
and every built-in handler already do.
- Default a missing/invalid request id to null instead of "", and
stop answering notifications and malformed messages as if they
were "method not found" 404s.
- Read the POST body in a loop instead of a single read() call,
which PSR-7 allows to return short.