[Schema][Server] Add the extensions framework SEP-2133 defines (#443)

* [Schema][Server] Add the extensions framework SEP-2133 defines

enableExtension() took any string and advertised it, with no way for an
extension to add a method: MessageFactory could not decode one, so nothing
downstream ever saw it.

Identifiers are now checked against the _meta key naming rules through
ExtensionIdentifier - a prefix is mandatory, since an unprefixed name has no
owner, and the modelcontextprotocol/mcp second labels the specification
reserves are recognised. An extension implementing
MethodProvidingExtensionInterface contributes both its message classes and
the handlers serving them.

MessageFactory::make() takes an $additional list of message classes, and
RequestHandlerInterface's result template is covariant so a handler
declaring a concrete result satisfies a collection typed by the interface.

* Fix stale @see reference in MethodProvidingExtensionInterface docblock

* [Schema][Server][Client] Make ExtensionIdentifier a value object

ExtensionInterface::getId() now returns ExtensionIdentifier, which
validates the SEP-2133 naming rules at construction instead of the
callers checking it. getMessages()/getRequestHandlers() move into
ExtensionInterface itself; AbstractExtension gives them empty
defaults for extensions that only announce a capability.
This commit is contained in:
Christopher Hertel 2026-08-18 20:47:27 +02:00 • committed by GitHub
commit 60711951d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -32,6 +32,7 @@ use Mcp\Exception\LogicException;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\Annotations;
use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Schema\Extension\AbstractExtension;
use Mcp\Schema\Extension\ExtensionInterface;
use Mcp\Schema\Icon;
use Mcp\Schema\Implementation;
@ -214,6 +215,9 @@ final class Builder
*/
private array $extensions = [];
/** @var list<class-string<\Mcp\Schema\JsonRpc\Request>|class-string<\Mcp\Schema\JsonRpc\Notification>> */
private array $extensionMessages = [];
/**
* @var LoaderInterface[]
*/
@ -280,18 +284,33 @@ final class Builder
* Enable one or more MCP protocol extensions, announced to clients under
* `capabilities.extensions` during the initialize handshake.
*
* @throws LogicException if the same extension is enabled more than once
* An extension also contributes the message classes its methods decode
* into and the handlers serving them, if any — see {@see AbstractExtension}
* for extensions that only announce a capability.
*
* @throws InvalidArgumentException if the identifier is not a valid `_meta` prefix
* @throws LogicException if the same extension is enabled more than once
*/
public function enableExtension(ExtensionInterface ...$extensions): self
{
foreach ($extensions as $extension) {
$id = $extension->getId();
$id = (string) $extension->getId();
if (isset($this->extensions[$id])) {
throw new LogicException(\sprintf('Extension "%s" is already enabled.', $id));
}
$this->extensions[$id] = $extension->getCapabilities();
// Without this the method cannot be decoded at all, so nothing
// downstream ever sees it.
foreach ($extension->getMessages() as $message) {
$this->extensionMessages[] = $message;
}
foreach ($extension->getRequestHandlers() as $handler) {
$this->requestHandlers[] = $handler;
}
}
return $this;
@ -709,7 +728,7 @@ final class Builder
$eagerlyLoaded = !$this->lazyLoading;
}
$messageFactory = MessageFactory::make();
$messageFactory = MessageFactory::make(additional: $this->extensionMessages);
$capabilities = $this->serverCapabilities ?? $this->detectCapabilities($registry, $eagerlyLoaded);