Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to `mcp/sdk` will be documented in this file.

* [BC Break] `DnsRebindingProtectionMiddleware` always validates `Host` (a missing one is rejected) and checks `Origin` separately against the new `allowedOrigins`, including scheme and port and empty by default. Same-origin requests are always allowed and both lists accept `['*']`. `allowedOrigins` is the new second constructor argument; pass the factories by name.
* Fix the client throwing a native `\JsonException` for request params it cannot encode (e.g. invalid UTF-8): it now throws `InvalidArgumentException`, and a request handler result it cannot encode is answered with an internal error instead of no answer.
* [BC Break] Add `RegistryInterface::publishesListChanges()`: a registry passed to `setRegistry()` and wired to the notification bus with a `PublishingEventDispatcher` gets `listChanged` announced on `server/discover`. A decorating registry forwards the answer of the one it wraps.
* Fix servers announcing `listChanged` where no list changes are delivered: the `initialize` result never carries it, and `server/discover` only with a notification bus and the built-in registry. A server with only an event dispatcher no longer announces it.
* Fix concurrent requests of one session overwriting each other's session changes: `Session::save()` now writes only the keys it changed, onto what the store holds at that point.
* Fix `ClientGateway::progress()` sending another call's progress token when calls of one session run in parallel: the token now comes from the gateway's own request.
Expand Down
17 changes: 16 additions & 1 deletion docs/run/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,22 @@ use Mcp\Server\Subscription\Psr16NotificationBus;
```

Registry changes (`registerTool()`, `unregisterPrompt()`, …) are published automatically, with or
without an event dispatcher, unless you pass your own registry with `setRegistry()`.
without an event dispatcher, unless you pass your own registry with `setRegistry()`. Then you wire
it to the bus yourself, and the builder announces `listChanged` for it:

```php
use Mcp\Capability\Registry;
use Mcp\Server\Subscription\PublishingEventDispatcher;

$bus = new InMemoryNotificationBus();
$registry = new Registry(new PublishingEventDispatcher($bus));

$server = Server::builder()
->setRegistry($registry)
->setNotificationBus($bus)
->build();
```

Anything else — `notifications/resources/updated` above all — is published by the
application:

Expand Down
6 changes: 6 additions & 0 deletions src/Capability/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Server\Subscription\PublishingEventDispatcher;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -238,6 +239,11 @@ public function hasPrompt(string $name): bool
return isset($this->prompts[$name]);
}

public function publishesListChanges(): bool
{
return $this->eventDispatcher instanceof PublishingEventDispatcher;
}

public function hasTools(): bool
{
$this->load();
Expand Down
7 changes: 7 additions & 0 deletions src/Capability/RegistryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Server\Subscription\PublishingEventDispatcher;

/**
* @phpstan-import-type Handler from ElementReference
Expand Down Expand Up @@ -97,6 +98,12 @@ public function unregisterResourceTemplate(string $uriTemplate): void;
*/
public function unregisterPrompt(string $name): void;

/**
* Whether list changes reach a notification bus, e.g. through a {@see PublishingEventDispatcher}.
* A decorator forwards the answer of the registry it wraps.
*/
public function publishesListChanges(): bool;

public function hasTool(string $name): bool;

public function hasResource(string $uri): bool;
Expand Down
9 changes: 5 additions & 4 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ public function setServerInfo(
*
* With the built-in registry, registry changes are published automatically,
* with or without an event dispatcher, and `listChanged` is announced. A
* registry passed with setRegistry() is neither published nor announced.
* registry passed with setRegistry() is announced only if
* {@see RegistryInterface::publishesListChanges()} says so, as {@see Registry}
* does with a {@see PublishingEventDispatcher}.
* Anything else, `notifications/resources/updated` for example, is
* published by the application calling {@see NotificationBusInterface::publish()}.
*/
Expand Down Expand Up @@ -1156,9 +1158,8 @@ private function resolve(): array
*/
private function detectCapabilities(RegistryInterface $registry, bool $eagerlyLoaded): ServerCapabilities
{
// Only the bus delivers list changes, and only the built-in registry is
// wired to it; a custom registry's events never reach a client here.
$listChanged = null !== $this->notificationBus && !$this->hasCustomRegistry;
// Only the bus delivers list changes, and only if the registry publishes to it.
$listChanged = null !== $this->notificationBus && $registry->publishesListChanges();

if ($eagerlyLoaded) {
$hasResources = $registry->hasResources() || $registry->hasResourceTemplates();
Expand Down
35 changes: 34 additions & 1 deletion tests/Unit/Server/BuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public function testListChangedIsNotAnnouncedWithoutANotificationBus(): void
$this->assertFalse($capabilities->promptsListChanged);
}

#[TestDox('listChanged is not announced for a custom registry, the builder cannot wire it to the bus')]
#[TestDox('listChanged is not announced for a custom registry that does not publish its changes')]
public function testListChangedIsNotAnnouncedForACustomRegistry(): void
{
$server = Server::builder()
Expand All @@ -288,6 +288,39 @@ public function testListChangedIsNotAnnouncedForACustomRegistry(): void
$this->assertFalse($capabilities->promptsListChanged);
}

#[TestDox('listChanged is announced for a custom registry that publishes its changes to the bus')]
public function testListChangedIsAnnouncedForACustomRegistryPublishingChanges(): void
{
$bus = new InMemoryNotificationBus();

$server = Server::builder()
->setServerInfo('test', '1.0.0')
->setRegistry(new Registry(new PublishingEventDispatcher($bus)))
->setNotificationBus($bus)
->build();

$capabilities = $this->extractServerCapabilities($server);

$this->assertTrue($capabilities->toolsListChanged);
$this->assertTrue($capabilities->resourcesListChanged);
$this->assertTrue($capabilities->promptsListChanged);
}

#[TestDox('listChanged is not announced for a custom registry publishing its changes without a notification bus')]
public function testListChangedIsNotAnnouncedForACustomRegistryPublishingChangesWithoutABus(): void
{
$server = Server::builder()
->setServerInfo('test', '1.0.0')
->setRegistry(new Registry(new PublishingEventDispatcher(new InMemoryNotificationBus())))
->build();

$capabilities = $this->extractServerCapabilities($server);

$this->assertFalse($capabilities->toolsListChanged);
$this->assertFalse($capabilities->resourcesListChanged);
$this->assertFalse($capabilities->promptsListChanged);
}

#[TestDox('setLazyLoading() returns the builder for fluent chaining')]
public function testSetLazyLoadingReturnsSelf(): void
{
Expand Down
Loading