[Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks (#429)

* [Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks

Add ClientGateway::supportsExtension(), Client\Builder::enableExtension() and
ClientCapabilities::withExtensions(); introduce side-agnostic ExtensionInterface
and deprecate ServerExtensionInterface; document attribute-based UI linkage.

* Drop ServerExtensionInterface in favour of ExtensionInterface
This commit is contained in:
Christopher Hertel 2026-08-18 01:46:53 +02:00 • committed by GitHub
commit 6f58fe596a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,8 +14,10 @@ namespace Mcp\Client;
use Mcp\Client;
use Mcp\Client\Handler\Notification\NotificationHandlerInterface;
use Mcp\Client\Handler\Request\RequestHandlerInterface;
use Mcp\Exception\LogicException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Schema\Extension\ExtensionInterface;
use Mcp\Schema\Implementation;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@ -33,6 +35,9 @@ final class Builder
private ?string $title = null;
private ?ProtocolVersion $protocolVersion = null;
private ?ClientCapabilities $capabilities = null;
/** @var array<string, array<string, mixed>> */
private array $extensions = [];
private int $initTimeout = 30;
private int $requestTimeout = 120;
private int $maxRetries = 3;
@ -79,6 +84,27 @@ final class Builder
return $this;
}
/**
* Enable one or more MCP protocol extensions, announced to the server under
* `capabilities.extensions` in the initialize request.
*
* @throws LogicException if the same extension is enabled more than once
*/
public function enableExtension(ExtensionInterface ...$extensions): self
{
foreach ($extensions as $extension) {
$id = $extension->getId();
if (isset($this->extensions[$id])) {
throw new LogicException(\sprintf('Extension "%s" is already enabled.', $id));
}
$this->extensions[$id] = $extension->getCapabilities();
}
return $this;
}
/**
* Set initialization timeout in seconds.
*/
@ -159,9 +185,17 @@ final class Builder
title: $this->title,
);
$capabilities = $this->capabilities ?? new ClientCapabilities();
// Extensions enabled via enableExtension() are folded into caller-supplied
// capabilities too, so setCapabilities() does not silently drop them.
if ([] !== $this->extensions) {
$capabilities = $capabilities->withExtensions($this->extensions);
}
$config = new Configuration(
clientInfo: $clientInfo,
capabilities: $this->capabilities ?? new ClientCapabilities(),
capabilities: $capabilities,
protocolVersion: $this->protocolVersion ?? ProtocolVersion::V2025_11_25,
initTimeout: $this->initTimeout,
requestTimeout: $this->requestTimeout,