* [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.
102 lines
2.9 KiB
PHP
102 lines
2.9 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the official PHP MCP SDK.
|
|
*
|
|
* A collaboration between Symfony and the PHP Foundation.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
use Http\Discovery\Psr17Factory;
|
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
|
use Mcp\Capability\Registry\Container;
|
|
use Mcp\Server\Transport\StdioTransport;
|
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
|
use Mcp\Server\Transport\TransportInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Log\AbstractLogger;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
|
|
|
|
set_exception_handler(static function (Throwable $t): never {
|
|
logger()->critical('Uncaught exception: '.$t->getMessage(), ['exception' => $t]);
|
|
|
|
exit(1);
|
|
});
|
|
|
|
/**
|
|
* The transport every example runs on.
|
|
*
|
|
* Over HTTP that is one endpoint serving both protocol eras: `StreamableHttpTransport`
|
|
* classifies each request and routes it to the lifecycle it belongs to, so every
|
|
* example here answers an `initialize` handshake and a 2026-07-28 envelope alike.
|
|
* Over stdio there is no such choice to make — that binding carries the handshake era.
|
|
*
|
|
* @return TransportInterface<int>|TransportInterface<ResponseInterface>
|
|
*/
|
|
function transport(): TransportInterface
|
|
{
|
|
if ('cli' === \PHP_SAPI) {
|
|
return new StdioTransport(logger: logger());
|
|
}
|
|
|
|
return new StreamableHttpTransport(
|
|
(new Psr17Factory())->createServerRequestFromGlobals(),
|
|
logger: logger(),
|
|
);
|
|
}
|
|
|
|
function shutdown(ResponseInterface|int $result): never
|
|
{
|
|
if ('cli' === \PHP_SAPI) {
|
|
exit($result);
|
|
}
|
|
|
|
(new SapiEmitter())->emit($result);
|
|
exit(0);
|
|
}
|
|
|
|
function logger(): LoggerInterface
|
|
{
|
|
return new class extends AbstractLogger {
|
|
public function log($level, string|Stringable $message, array $context = []): void
|
|
{
|
|
$debug = $_SERVER['DEBUG'] ?? false;
|
|
|
|
if (!$debug && 'debug' === $level) {
|
|
return;
|
|
}
|
|
|
|
$exception = $context['exception'] ?? null;
|
|
unset($context['exception']);
|
|
|
|
$logMessage = sprintf(
|
|
"[%s] %s %s\n",
|
|
strtoupper($level),
|
|
$message,
|
|
([] === $context || !$debug) ? '' : json_encode($context),
|
|
);
|
|
|
|
if ($exception instanceof Throwable) {
|
|
$logMessage .= sprintf('> %s', $exception->getMessage())."\n";
|
|
}
|
|
|
|
if (($_SERVER['FILE_LOG'] ?? false) || !defined('STDERR')) {
|
|
file_put_contents('dev.log', $logMessage, \FILE_APPEND);
|
|
} else {
|
|
fwrite(\STDERR, $logMessage);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function container(): Container
|
|
{
|
|
$container = new Container();
|
|
$container->set(LoggerInterface::class, logger());
|
|
|
|
return $container;
|
|
}
|