[Server] Serve both protocol eras from one endpoint (#455)
* [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.
This commit is contained in:
parent
b97389325f
commit
cbceef06eb
1 changed files with 2060 additions and 323 deletions
|
|
@ -33,6 +33,7 @@ use Mcp\Server\Session\InMemorySessionStore;
|
|||
use Mcp\Server\Session\Session;
|
||||
use Mcp\Server\Subscription\NotificationBusInterface;
|
||||
use Mcp\Server\Wire\CachePolicy;
|
||||
use Mcp\Server\Wire\InboundClassifier;
|
||||
use Mcp\Server\Wire\Rev2026Codec;
|
||||
use Mcp\Server\Wire\WireCodecInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -116,6 +117,16 @@ final class StatelessProtocol
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The modern revisions this dispatcher answers for.
|
||||
*
|
||||
* @return list<ProtocolVersion>
|
||||
*/
|
||||
public function supportedVersions(): array
|
||||
{
|
||||
return $this->supportedVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the transport carrying this dispatcher has a header layer whose
|
||||
* required members must be present.
|
||||
|
|
@ -265,14 +276,10 @@ final class StatelessProtocol
|
|||
);
|
||||
}
|
||||
|
||||
if (null !== $headerVersion && $headerVersion !== $meta->protocolVersion) {
|
||||
return StatelessResult::error(
|
||||
Error::forHeaderMismatch(
|
||||
\sprintf('MCP-Protocol-Version header "%s" contradicts the "%s" declared in _meta.', $headerVersion, $meta->protocolVersion),
|
||||
$id,
|
||||
),
|
||||
400,
|
||||
);
|
||||
// The same check the HTTP entry runs before routing, so the edge and
|
||||
// this dispatcher cannot disagree about what a request claims.
|
||||
if (null !== $mismatch = InboundClassifier::crossCheckVersion($headerVersion, $meta->protocolVersion)) {
|
||||
return StatelessResult::error(Error::forHeaderMismatch($mismatch, $id), 400);
|
||||
}
|
||||
|
||||
$version = ProtocolVersion::tryFrom($meta->protocolVersion);
|
||||
|
|
@ -803,12 +810,6 @@ final class StatelessProtocol
|
|||
*/
|
||||
private function header(array $headers, string $name): ?string
|
||||
{
|
||||
foreach ($headers as $key => $value) {
|
||||
if (0 === strcasecmp($key, $name)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return InboundClassifier::header($headers, $name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue