diff --git a/examples/http-discovery-userprofile/UserIdCompletionProvider.php b/examples/http-discovery-userprofile/UserIdCompletionProvider.php index bc4022e..37b1c5b 100644 --- a/examples/http-discovery-userprofile/UserIdCompletionProvider.php +++ b/examples/http-discovery-userprofile/UserIdCompletionProvider.php @@ -11,7 +11,7 @@ namespace Mcp\Example\HttpDiscoveryUserProfile; -use Mcp\Capability\Prompt\Completion\ProviderInterface; +use Mcp\Capability\Completion\ProviderInterface; final class UserIdCompletionProvider implements ProviderInterface { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9520acd..7c7caee 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -12,12 +12,6 @@ parameters: count: 2 path: examples/http-discovery-userprofile/McpElements.php - - - message: '#^Call to an undefined method Mcp\\Capability\\Registry\\ResourceTemplateReference\:\:handle\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Capability/Registry/ResourceTemplateReference.php - - message: '#^PHPDoc tag @return with type array is incompatible with native type object\.$#' identifier: return.phpDocType diff --git a/src/Capability/Attribute/CompletionProvider.php b/src/Capability/Attribute/CompletionProvider.php index 2ef139b..9e8dc80 100644 --- a/src/Capability/Attribute/CompletionProvider.php +++ b/src/Capability/Attribute/CompletionProvider.php @@ -11,7 +11,7 @@ namespace Mcp\Capability\Attribute; -use Mcp\Capability\Prompt\Completion\ProviderInterface; +use Mcp\Capability\Completion\ProviderInterface; use Mcp\Exception\InvalidArgumentException; /** diff --git a/src/Capability/Completion/Completer.php b/src/Capability/Completion/Completer.php new file mode 100644 index 0000000..eaffaee --- /dev/null +++ b/src/Capability/Completion/Completer.php @@ -0,0 +1,75 @@ + + */ +final class Completer implements CompleterInterface +{ + public function __construct( + private readonly ReferenceProviderInterface $referenceProvider, + private readonly ?ContainerInterface $container = null, + ) { + } + + public function complete(CompletionCompleteRequest $request): CompletionCompleteResult + { + $argumentName = $request->argument['name'] ?? ''; + $currentValue = $request->argument['value'] ?? ''; + + $reference = match (true) { + 'ref/prompt' === $request->ref->type => $this->referenceProvider->getPrompt($request->ref->name), + 'ref/resource' === $request->ref->type => $this->referenceProvider->getResourceTemplate($request->ref->uri), + default => null, + }; + + if (null === $reference) { + return new CompletionCompleteResult([]); + } + + $providerClassOrInstance = $reference->completionProviders[$argumentName] ?? null; + if (null === $providerClassOrInstance) { + return new CompletionCompleteResult([]); + } + + if (\is_string($providerClassOrInstance)) { + if (!class_exists($providerClassOrInstance)) { + throw new RuntimeException(\sprintf('Completion provider class "%s" does not exist.', $providerClassOrInstance)); + } + + $provider = $this->container?->has($providerClassOrInstance) + ? $this->container->get($providerClassOrInstance) + : new $providerClassOrInstance(); + } else { + $provider = $providerClassOrInstance; + } + + if (!$provider instanceof ProviderInterface) { + throw new RuntimeException('Completion provider must implement ProviderInterface.'); + } + + $completions = $provider->getCompletions($currentValue); + + $total = \count($completions); + $hasMore = $total > 100; + $pagedCompletions = \array_slice($completions, 0, 100); + + return new CompletionCompleteResult($pagedCompletions, $total, $hasMore); + } +} diff --git a/src/Capability/Completion/CompleterInterface.php b/src/Capability/Completion/CompleterInterface.php new file mode 100644 index 0000000..9f9f871 --- /dev/null +++ b/src/Capability/Completion/CompleterInterface.php @@ -0,0 +1,25 @@ + + */ +interface CompleterInterface +{ + public function complete(CompletionCompleteRequest $request): CompletionCompleteResult; +} diff --git a/src/Capability/Prompt/Completion/EnumCompletionProvider.php b/src/Capability/Completion/EnumCompletionProvider.php similarity index 96% rename from src/Capability/Prompt/Completion/EnumCompletionProvider.php rename to src/Capability/Completion/EnumCompletionProvider.php index dd09662..4c2a014 100644 --- a/src/Capability/Prompt/Completion/EnumCompletionProvider.php +++ b/src/Capability/Completion/EnumCompletionProvider.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Mcp\Capability\Prompt\Completion; +namespace Mcp\Capability\Completion; use Mcp\Exception\InvalidArgumentException; diff --git a/src/Capability/Prompt/Completion/ListCompletionProvider.php b/src/Capability/Completion/ListCompletionProvider.php similarity index 94% rename from src/Capability/Prompt/Completion/ListCompletionProvider.php rename to src/Capability/Completion/ListCompletionProvider.php index 73a5dd4..05e7811 100644 --- a/src/Capability/Prompt/Completion/ListCompletionProvider.php +++ b/src/Capability/Completion/ListCompletionProvider.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Mcp\Capability\Prompt\Completion; +namespace Mcp\Capability\Completion; /** * @author Kyrian Obikwelu diff --git a/src/Capability/Prompt/Completion/ProviderInterface.php b/src/Capability/Completion/ProviderInterface.php similarity index 93% rename from src/Capability/Prompt/Completion/ProviderInterface.php rename to src/Capability/Completion/ProviderInterface.php index 04ec23b..84f3f23 100644 --- a/src/Capability/Prompt/Completion/ProviderInterface.php +++ b/src/Capability/Completion/ProviderInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Mcp\Capability\Prompt\Completion; +namespace Mcp\Capability\Completion; /** * @author Kyrian Obikwelu diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index a75705f..83dafa0 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -16,9 +16,9 @@ use Mcp\Capability\Attribute\McpResource; use Mcp\Capability\Attribute\McpResourceTemplate; use Mcp\Capability\Attribute\McpTool; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; -use Mcp\Capability\Prompt\Completion\ListCompletionProvider; -use Mcp\Capability\Prompt\Completion\ProviderInterface; +use Mcp\Capability\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\ListCompletionProvider; +use Mcp\Capability\Completion\ProviderInterface; use Mcp\Capability\Registry\PromptReference; use Mcp\Capability\Registry\ReferenceRegistryInterface; use Mcp\Capability\Registry\ResourceReference; diff --git a/src/Capability/Registry/PromptReference.php b/src/Capability/Registry/PromptReference.php index 50b2dd5..cff8924 100644 --- a/src/Capability/Registry/PromptReference.php +++ b/src/Capability/Registry/PromptReference.php @@ -22,8 +22,6 @@ use Mcp\Schema\Content\TextResourceContents; use Mcp\Schema\Enum\Role; use Mcp\Schema\Prompt; -use Mcp\Schema\Result\CompletionCompleteResult; -use Psr\Container\ContainerInterface; /** * @phpstan-import-type Handler from ElementReference @@ -45,33 +43,6 @@ public function __construct( parent::__construct($handler, $isManual); } - public function complete(ContainerInterface $container, string $argument, string $value): CompletionCompleteResult - { - $providerClassOrInstance = $this->completionProviders[$argument] ?? null; - if (null === $providerClassOrInstance) { - return new CompletionCompleteResult([]); - } - - if (\is_string($providerClassOrInstance)) { - if (!class_exists($providerClassOrInstance)) { - throw new RuntimeException("Completion provider class '{$providerClassOrInstance}' does not exist."); - } - - $provider = $container->get($providerClassOrInstance); - } else { - $provider = $providerClassOrInstance; - } - - $completions = $provider->getCompletions($value); - - $total = \count($completions); - $hasMore = $total > 100; - - $pagedCompletions = \array_slice($completions, 0, 100); - - return new CompletionCompleteResult($pagedCompletions, $total, $hasMore); - } - /** * Formats the raw result of a prompt generator into an array of MCP PromptMessages. * diff --git a/src/Capability/Registry/ResourceTemplateReference.php b/src/Capability/Registry/ResourceTemplateReference.php index 322e6a5..c6f7ec4 100644 --- a/src/Capability/Registry/ResourceTemplateReference.php +++ b/src/Capability/Registry/ResourceTemplateReference.php @@ -17,7 +17,6 @@ use Mcp\Schema\Content\ResourceContents; use Mcp\Schema\Content\TextResourceContents; use Mcp\Schema\ResourceTemplate; -use Mcp\Schema\Result\CompletionCompleteResult; use Psr\Container\ContainerInterface; /** @@ -64,38 +63,12 @@ public function read(ContainerInterface $container, string $uri): array { $arguments = array_merge($this->uriVariables, ['uri' => $uri]); - $result = $this->handle($container, $arguments); + $referenceHandler = new ReferenceHandler($container); + $result = $referenceHandler->handle($this, $arguments); return $this->formatResult($result, $uri, $this->resourceTemplate->mimeType); } - public function complete(ContainerInterface $container, string $argument, string $value): CompletionCompleteResult - { - $providerClassOrInstance = $this->completionProviders[$argument] ?? null; - if (null === $providerClassOrInstance) { - return new CompletionCompleteResult([]); - } - - if (\is_string($providerClassOrInstance)) { - if (!class_exists($providerClassOrInstance)) { - throw new RuntimeException(\sprintf('Completion provider class "%s" does not exist.', $providerClassOrInstance)); - } - - $provider = $container->get($providerClassOrInstance); - } else { - $provider = $providerClassOrInstance; - } - - $completions = $provider->getCompletions($value); - - $total = \count($completions); - $hasMore = $total > 100; - - $pagedCompletions = \array_slice($completions, 0, 100); - - return new CompletionCompleteResult($pagedCompletions, $total, $hasMore); - } - /** * @return array */ diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 7afd5bd..73bb53e 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -12,13 +12,13 @@ namespace Mcp\Server; use Mcp\Capability\Attribute\CompletionProvider; +use Mcp\Capability\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\ListCompletionProvider; use Mcp\Capability\Discovery\CachedDiscoverer; use Mcp\Capability\Discovery\Discoverer; use Mcp\Capability\Discovery\DocBlockParser; use Mcp\Capability\Discovery\HandlerResolver; use Mcp\Capability\Discovery\SchemaGenerator; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; -use Mcp\Capability\Prompt\Completion\ListCompletionProvider; use Mcp\Capability\Prompt\PromptGetter; use Mcp\Capability\Prompt\PromptGetterInterface; use Mcp\Capability\Registry; diff --git a/src/Server/Handler/JsonRpcHandler.php b/src/Server/Handler/JsonRpcHandler.php index 1ee39a2..e700caf 100644 --- a/src/Server/Handler/JsonRpcHandler.php +++ b/src/Server/Handler/JsonRpcHandler.php @@ -11,6 +11,7 @@ namespace Mcp\Server\Handler; +use Mcp\Capability\Completion\Completer; use Mcp\Capability\Prompt\PromptGetterInterface; use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceRegistryInterface; @@ -89,6 +90,7 @@ public static function make( new Handler\Request\ListResourceTemplatesHandler($referenceProvider, $paginationLimit), new Handler\Request\CallToolHandler($toolCaller, $logger), new Handler\Request\ListToolsHandler($referenceProvider, $paginationLimit), + new Handler\Request\CompletionCompleteHandler(new Completer($referenceProvider)), ], logger: $logger, ); diff --git a/src/Server/Handler/Request/CompletionCompleteHandler.php b/src/Server/Handler/Request/CompletionCompleteHandler.php new file mode 100644 index 0000000..ef4b302 --- /dev/null +++ b/src/Server/Handler/Request/CompletionCompleteHandler.php @@ -0,0 +1,52 @@ + + */ +final class CompletionCompleteHandler implements MethodHandlerInterface +{ + public function __construct( + private readonly CompleterInterface $completer, + ) { + } + + public function supports(HasMethodInterface $message): bool + { + return $message instanceof CompletionCompleteRequest; + } + + public function handle(CompletionCompleteRequest|HasMethodInterface $message, SessionInterface $session): Response|Error + { + \assert($message instanceof CompletionCompleteRequest); + + try { + $result = $this->completer->complete($message); + } catch (ExceptionInterface) { + return Error::forInternalError('Error while handling completion request', $message->getId()); + } + + return new Response($message->getId(), $result); + } +} diff --git a/tests/Unit/Capability/Attribute/CompletionProviderFixture.php b/tests/Unit/Capability/Attribute/CompletionProviderFixture.php index 839635e..6c623cf 100644 --- a/tests/Unit/Capability/Attribute/CompletionProviderFixture.php +++ b/tests/Unit/Capability/Attribute/CompletionProviderFixture.php @@ -11,7 +11,7 @@ namespace Mcp\Tests\Unit\Capability\Attribute; -use Mcp\Capability\Prompt\Completion\ProviderInterface; +use Mcp\Capability\Completion\ProviderInterface; class CompletionProviderFixture implements ProviderInterface { diff --git a/tests/Unit/Capability/Discovery/DiscoveryTest.php b/tests/Unit/Capability/Discovery/DiscoveryTest.php index 54fce07..0767ff1 100644 --- a/tests/Unit/Capability/Discovery/DiscoveryTest.php +++ b/tests/Unit/Capability/Discovery/DiscoveryTest.php @@ -11,9 +11,9 @@ namespace Mcp\Tests\Unit\Capability\Discovery; +use Mcp\Capability\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\ListCompletionProvider; use Mcp\Capability\Discovery\Discoverer; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; -use Mcp\Capability\Prompt\Completion\ListCompletionProvider; use Mcp\Capability\Registry; use Mcp\Capability\Registry\PromptReference; use Mcp\Capability\Registry\ResourceReference; diff --git a/tests/Unit/Capability/Prompt/Completion/EnumCompletionProviderTest.php b/tests/Unit/Capability/Prompt/Completion/EnumCompletionProviderTest.php index 8d81d0e..6321fdc 100644 --- a/tests/Unit/Capability/Prompt/Completion/EnumCompletionProviderTest.php +++ b/tests/Unit/Capability/Prompt/Completion/EnumCompletionProviderTest.php @@ -11,7 +11,7 @@ namespace Mcp\Tests\Unit\Capability\Prompt\Completion; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\EnumCompletionProvider; use Mcp\Exception\InvalidArgumentException; use Mcp\Tests\Unit\Fixtures\Enum\PriorityEnum; use Mcp\Tests\Unit\Fixtures\Enum\StatusEnum; diff --git a/tests/Unit/Capability/Prompt/Completion/ListCompletionProviderTest.php b/tests/Unit/Capability/Prompt/Completion/ListCompletionProviderTest.php index 80c5810..dd3439d 100644 --- a/tests/Unit/Capability/Prompt/Completion/ListCompletionProviderTest.php +++ b/tests/Unit/Capability/Prompt/Completion/ListCompletionProviderTest.php @@ -11,7 +11,7 @@ namespace Mcp\Tests\Unit\Capability\Prompt\Completion; -use Mcp\Capability\Prompt\Completion\ListCompletionProvider; +use Mcp\Capability\Completion\ListCompletionProvider; use PHPUnit\Framework\TestCase; class ListCompletionProviderTest extends TestCase diff --git a/tests/Unit/Capability/Prompt/PromptGetterTest.php b/tests/Unit/Capability/Prompt/PromptGetterTest.php index fe68ddc..874dbe3 100644 --- a/tests/Unit/Capability/Prompt/PromptGetterTest.php +++ b/tests/Unit/Capability/Prompt/PromptGetterTest.php @@ -11,7 +11,7 @@ namespace Mcp\Tests\Unit\Capability\Prompt; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\EnumCompletionProvider; use Mcp\Capability\Prompt\PromptGetter; use Mcp\Capability\Registry\PromptReference; use Mcp\Capability\Registry\ReferenceHandlerInterface; diff --git a/tests/Unit/Capability/Registry/RegistryTest.php b/tests/Unit/Capability/Registry/RegistryTest.php index 91ce0f7..e18f9d1 100644 --- a/tests/Unit/Capability/Registry/RegistryTest.php +++ b/tests/Unit/Capability/Registry/RegistryTest.php @@ -11,7 +11,7 @@ namespace Mcp\Tests\Unit\Capability\Registry; -use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; +use Mcp\Capability\Completion\EnumCompletionProvider; use Mcp\Capability\Registry; use Mcp\Schema\Prompt; use Mcp\Schema\Resource;