diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c8beb0..a037cead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Fix `Psr16NotificationBus` subscribers missing the notifications published after the bus stayed quiet for longer than its TTL: the sequence counter no longer expires, and a subscriber whose cursor is ahead of a lost counter reads what the bus holds. * Fix `Session::save()` wiping the other keys of a session that the store failed to read, e.g. when `Psr16SessionStore` hits a cache error: it now returns `false` instead of writing the changed keys alone. * Add `Psr6SessionStore` and `Psr6NotificationBus`, which work like `Psr16SessionStore` and `Psr16NotificationBus` over a PSR-6 cache pool. `Builder::setDiscovery()` also accepts a PSR-6 pool as `$cache`. +* Fix a nullable tool parameter with a default (e.g. `?string $category = 'books'`) receiving the default when the client explicitly sends `null`: `ReferenceHandler` now passes the `null` through. ### Client diff --git a/src/Capability/Registry/ReferenceHandler.php b/src/Capability/Registry/ReferenceHandler.php index ff10de31..06498bc2 100644 --- a/src/Capability/Registry/ReferenceHandler.php +++ b/src/Capability/Registry/ReferenceHandler.php @@ -142,7 +142,7 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array } } - if (isset($arguments[$paramName])) { + if (isset($arguments[$paramName]) || (\array_key_exists($paramName, $arguments) && $parameter->allowsNull())) { $argument = $arguments[$paramName]; try { $finalArgs[$paramPosition] = $this->castArgumentType($argument, $parameter); diff --git a/tests/Unit/Capability/Registry/ReferenceHandlerTest.php b/tests/Unit/Capability/Registry/ReferenceHandlerTest.php index 830f97e3..5ce038e9 100644 --- a/tests/Unit/Capability/Registry/ReferenceHandlerTest.php +++ b/tests/Unit/Capability/Registry/ReferenceHandlerTest.php @@ -136,6 +136,25 @@ public function search(string $query, ClientGateway $gateway): string $this->assertInstanceOf(ClientGateway::class, $handler->receivedGateway); } + public function testHandlePassesAnExplicitNullToANullableParameterWithADefault(): void + { + $session = $this->createMock(SessionInterface::class); + $session->method('getId')->willReturn(Uuid::v4()); + + $handler = new class { + public function search(?string $category = 'books'): string + { + return var_export($category, true); + } + }; + + $reference = new ElementReference([$handler, 'search']); + $context = ['_session' => $session, '_request' => new PingRequest()]; + + $this->assertSame('NULL', (new ReferenceHandler())->handle($reference, $context + ['category' => null])); + $this->assertSame("'books'", (new ReferenceHandler())->handle($reference, $context)); + } + public function testHandleResolvesAClassStringAndMethodPairToAnInstance(): void { $session = $this->createMock(SessionInterface::class);