|
34 | 34 | use Mcp\Server\Stateless\StatelessResult; |
35 | 35 | use Mcp\Server\Subscription\InMemoryNotificationBus; |
36 | 36 | use Mcp\Server\Wire\CachePolicy; |
| 37 | +use Mcp\Tests\Unit\Server\Extension\ThingExtension; |
| 38 | +use Mcp\Tests\Unit\Server\Extension\UnservedThingExtension; |
37 | 39 | use PHPUnit\Framework\Attributes\DataProvider; |
38 | 40 | use PHPUnit\Framework\Attributes\TestDox; |
39 | 41 | use PHPUnit\Framework\TestCase; |
@@ -791,6 +793,74 @@ public function testAcknowledgmentReflectsWhatTheServerCanDo(): void |
791 | 793 | $this->assertSame(['toolsListChanged' => true], (array) $first['params']['notifications']); |
792 | 794 | } |
793 | 795 |
|
| 796 | + #[TestDox('an extension method is served by the extension that claims it')] |
| 797 | + public function testExtensionMethodIsServed(): void |
| 798 | + { |
| 799 | + $protocol = Server::builder() |
| 800 | + ->setServerInfo('test-server', '1.0.0') |
| 801 | + ->enableExtension(new ThingExtension()) |
| 802 | + ->buildStateless([ProtocolVersion::V2026_07_28]); |
| 803 | + |
| 804 | + $answer = self::callWithHeaders($protocol, 'com.example/things.list', [], [ |
| 805 | + 'MCP-Protocol-Version' => ProtocolVersion::V2026_07_28->value, |
| 806 | + 'Mcp-Method' => 'com.example/things.list', |
| 807 | + ]); |
| 808 | + |
| 809 | + $this->assertSame(200, $answer['status']); |
| 810 | + $this->assertSame(['a', 'b'], $answer['body']['result']['things']); |
| 811 | + } |
| 812 | + |
| 813 | + #[TestDox('the extension is advertised under capabilities.extensions')] |
| 814 | + public function testExtensionIsAdvertised(): void |
| 815 | + { |
| 816 | + $protocol = Server::builder() |
| 817 | + ->setServerInfo('test-server', '1.0.0') |
| 818 | + ->enableExtension(new ThingExtension()) |
| 819 | + ->buildStateless([ProtocolVersion::V2026_07_28]); |
| 820 | + |
| 821 | + $answer = self::call($protocol, 'server/discover'); |
| 822 | + |
| 823 | + $this->assertSame(['flavour' => 'vanilla'], (array) $answer['body']['result']['capabilities']['extensions']['com.example/things']); |
| 824 | + } |
| 825 | + |
| 826 | + #[TestDox('a method of an extension this server has never heard of stays generic')] |
| 827 | + public function testUnknownExtensionMethodStaysGeneric(): void |
| 828 | + { |
| 829 | + $protocol = Server::builder() |
| 830 | + ->setServerInfo('test-server', '1.0.0') |
| 831 | + ->buildStateless([ProtocolVersion::V2026_07_28]); |
| 832 | + |
| 833 | + $answer = self::callWithHeaders($protocol, 'com.example/things.list', [], [ |
| 834 | + 'MCP-Protocol-Version' => ProtocolVersion::V2026_07_28->value, |
| 835 | + 'Mcp-Method' => 'com.example/things.list', |
| 836 | + ]); |
| 837 | + |
| 838 | + $this->assertSame(404, $answer['status']); |
| 839 | + $this->assertSame(Error::METHOD_NOT_FOUND, $answer['body']['error']['code']); |
| 840 | + // The extension was never enabled, so it never entered the method map |
| 841 | + // — there is nothing to name it by. |
| 842 | + $this->assertStringContainsString('com.example/things.list', $answer['body']['error']['message']); |
| 843 | + $this->assertStringNotContainsString('extension', $answer['body']['error']['message']); |
| 844 | + } |
| 845 | + |
| 846 | + #[TestDox('a method of an extension this server does not serve says so by name')] |
| 847 | + public function testUnservedExtensionMethodNamesItsExtension(): void |
| 848 | + { |
| 849 | + $protocol = Server::builder() |
| 850 | + ->setServerInfo('test-server', '1.0.0') |
| 851 | + ->enableExtension(new UnservedThingExtension()) |
| 852 | + ->buildStateless([ProtocolVersion::V2026_07_28]); |
| 853 | + |
| 854 | + $answer = self::callWithHeaders($protocol, 'com.example/things.list', [], [ |
| 855 | + 'MCP-Protocol-Version' => ProtocolVersion::V2026_07_28->value, |
| 856 | + 'Mcp-Method' => 'com.example/things.list', |
| 857 | + ]); |
| 858 | + |
| 859 | + $this->assertSame(404, $answer['status']); |
| 860 | + $this->assertSame(Error::METHOD_NOT_FOUND, $answer['body']['error']['code']); |
| 861 | + $this->assertStringContainsString('com.example/unserved-things', $answer['body']['error']['message']); |
| 862 | + } |
| 863 | + |
794 | 864 | #[TestDox('a notification is acknowledged with no body, never answered')] |
795 | 865 | public function testNotificationIsAcknowledged(): void |
796 | 866 | { |
|
0 commit comments