Skip to content

Commit 2193188

Browse files
feat: enhance completion providers with values and enum support
- Add CompletionProvider attribute support for values array and enum class - Create ListCompletionProvider for simple list-based completions - Create EnumCompletionProvider for enum-based completions - Update Discoverer and ServerBuilder to auto-create provider instances - Enhance serialization/deserialization for provider instances - Add comprehensive test coverage for all completion provider types
1 parent 48f878f commit 2193188

25 files changed

+713
-477
lines changed

examples/02-discovery-http-userprofile/McpElements.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function __construct(LoggerInterface $logger)
4343
)]
4444

4545
public function getUserProfile(
46-
#[CompletionProvider(providerClass: UserIdCompletionProvider::class)]
46+
#[CompletionProvider(values: ['101', '102', '103'])]
4747
string $userId
4848
): array {
4949
$this->logger->info('Reading resource: user profile', ['userId' => $userId]);
@@ -116,7 +116,7 @@ public function testToolWithoutParams()
116116
*/
117117
#[McpPrompt(name: 'generate_bio_prompt')]
118118
public function generateBio(
119-
#[CompletionProvider(providerClass: UserIdCompletionProvider::class)]
119+
#[CompletionProvider(provider: UserIdCompletionProvider::class)]
120120
string $userId,
121121
string $tone = 'professional'
122122
): array {

src/Attributes/CompletionProvider.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@
1111
class CompletionProvider
1212
{
1313
/**
14-
* @param class-string<CompletionProviderInterface> $providerClass FQCN of the completion provider class.
14+
* @param class-string<CompletionProviderInterface>|CompletionProviderInterface|null $provider If a class-string, it will be resolved from the container at the point of use.
1515
*/
16-
public function __construct(public string $providerClass)
17-
{
16+
public function __construct(
17+
public string|CompletionProviderInterface|null $provider = null,
18+
public ?array $values = null,
19+
public ?string $enum = null,
20+
) {
21+
if (count(array_filter([$provider, $values, $enum])) !== 1) {
22+
throw new \InvalidArgumentException('Only one of provider, values, or enum can be set');
23+
}
1824
}
1925
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Defaults;
6+
7+
use PhpMcp\Server\Contracts\CompletionProviderInterface;
8+
use PhpMcp\Server\Contracts\SessionInterface;
9+
10+
class EnumCompletionProvider implements CompletionProviderInterface
11+
{
12+
private array $values;
13+
14+
public function __construct(string $enumClass)
15+
{
16+
if (!enum_exists($enumClass)) {
17+
throw new \InvalidArgumentException("Class {$enumClass} is not an enum");
18+
}
19+
20+
$this->values = array_map(
21+
fn($case) => isset($case->value) && is_string($case->value) ? $case->value : $case->name,
22+
$enumClass::cases()
23+
);
24+
}
25+
26+
public function getCompletions(string $currentValue, SessionInterface $session): array
27+
{
28+
if (empty($currentValue)) {
29+
return $this->values;
30+
}
31+
32+
return array_values(array_filter(
33+
$this->values,
34+
fn(string $value) => str_starts_with($value, $currentValue)
35+
));
36+
}
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Defaults;
6+
7+
use PhpMcp\Server\Contracts\CompletionProviderInterface;
8+
use PhpMcp\Server\Contracts\SessionInterface;
9+
10+
class ListCompletionProvider implements CompletionProviderInterface
11+
{
12+
public function __construct(private array $values) {}
13+
14+
public function getCompletions(string $currentValue, SessionInterface $session): array
15+
{
16+
if (empty($currentValue)) {
17+
return $this->values;
18+
}
19+
20+
return array_values(array_filter(
21+
$this->values,
22+
fn(string $value) => str_starts_with($value, $currentValue)
23+
));
24+
}
25+
}

src/Dispatcher.php

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ public function handleCompletionComplete(CompletionCompleteRequest $request, Ses
340340
throw McpServerException::invalidParams("Argument '{$argumentName}' not found in prompt '{$identifier}'.");
341341
}
342342

343-
$providerClass = $registeredPrompt->getCompletionProvider($argumentName);
343+
return $registeredPrompt->complete($this->container, $argumentName, $currentValue, $session);
344344
} elseif ($ref->type === 'ref/resource') {
345345
$identifier = $ref->uri;
346346
$registeredResourceTemplate = $this->registry->getResourceTemplate($identifier);
@@ -360,26 +360,10 @@ public function handleCompletionComplete(CompletionCompleteRequest $request, Ses
360360
throw McpServerException::invalidParams("URI variable '{$argumentName}' not found in resource template '{$identifier}'.");
361361
}
362362

363-
$providerClass = $registeredResourceTemplate->getCompletionProvider($argumentName);
363+
return $registeredResourceTemplate->complete($this->container, $argumentName, $currentValue, $session);
364364
} else {
365365
throw McpServerException::invalidParams("Invalid ref type '{$ref->type}' for completion complete request.");
366366
}
367-
368-
if (! $providerClass) {
369-
$this->logger->warning("No completion provider found for argument '{$argumentName}' in '{$ref->type}' '{$identifier}'.");
370-
return new CompletionCompleteResult([]);
371-
}
372-
373-
$provider = $this->container->get($providerClass);
374-
375-
$completions = $provider->getCompletions($currentValue, $session);
376-
377-
$total = count($completions);
378-
$hasMore = $total > 100;
379-
380-
$pagedCompletions = array_slice($completions, 0, 100);
381-
382-
return new CompletionCompleteResult($pagedCompletions, $total, $hasMore);
383367
}
384368

385369
public function handleNotificationInitialized(InitializedNotification $notification, SessionInterface $session): EmptyResult

src/Elements/RegisteredPrompt.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use PhpMcp\Schema\Content\TextContent;
1515
use PhpMcp\Schema\Content\TextResourceContents;
1616
use PhpMcp\Schema\Enum\Role;
17+
use PhpMcp\Schema\Result\CompletionCompleteResult;
18+
use PhpMcp\Server\Contracts\CompletionProviderInterface;
19+
use PhpMcp\Server\Contracts\SessionInterface;
1720
use Psr\Container\ContainerInterface;
1821
use Throwable;
1922

@@ -47,9 +50,31 @@ public function get(ContainerInterface $container, array $arguments): array
4750
return $this->formatResult($result);
4851
}
4952

50-
public function getCompletionProvider(string $argumentName): ?string
53+
public function complete(ContainerInterface $container, string $argument, string $value, SessionInterface $session): CompletionCompleteResult
5154
{
52-
return $this->completionProviders[$argumentName] ?? null;
55+
$providerClassOrInstance = $this->completionProviders[$argument] ?? null;
56+
if ($providerClassOrInstance === null) {
57+
return new CompletionCompleteResult([]);
58+
}
59+
60+
if (is_string($providerClassOrInstance)) {
61+
if (! class_exists($providerClassOrInstance)) {
62+
throw new \RuntimeException("Completion provider class '{$providerClassOrInstance}' does not exist.");
63+
}
64+
65+
$provider = $container->get($providerClassOrInstance);
66+
} else {
67+
$provider = $providerClassOrInstance;
68+
}
69+
70+
$completions = $provider->getCompletions($value, $session);
71+
72+
$total = count($completions);
73+
$hasMore = $total > 100;
74+
75+
$pagedCompletions = array_slice($completions, 0, 100);
76+
77+
return new CompletionCompleteResult($pagedCompletions, $total, $hasMore);
5378
}
5479

5580
/**
@@ -268,9 +293,14 @@ private function formatResourceContent(array $content, string $indexStr): Embedd
268293

269294
public function toArray(): array
270295
{
296+
$completionProviders = [];
297+
foreach ($this->completionProviders as $argument => $provider) {
298+
$completionProviders[$argument] = serialize($provider);
299+
}
300+
271301
return [
272302
'schema' => $this->schema->toArray(),
273-
'completionProviders' => $this->completionProviders,
303+
'completionProviders' => $completionProviders,
274304
...parent::toArray(),
275305
];
276306
}
@@ -282,11 +312,16 @@ public static function fromArray(array $data): self|false
282312
return false;
283313
}
284314

315+
$completionProviders = [];
316+
foreach ($data['completionProviders'] ?? [] as $argument => $provider) {
317+
$completionProviders[$argument] = unserialize($provider);
318+
}
319+
285320
return new self(
286321
Prompt::fromArray($data['schema']),
287322
$data['handler'],
288323
$data['isManual'] ?? false,
289-
$data['completionProviders'] ?? [],
324+
$completionProviders,
290325
);
291326
} catch (Throwable $e) {
292327
return false;

src/Elements/RegisteredResourceTemplate.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use PhpMcp\Schema\Content\ResourceContents;
1010
use PhpMcp\Schema\Content\TextResourceContents;
1111
use PhpMcp\Schema\ResourceTemplate;
12+
use PhpMcp\Schema\Result\CompletionCompleteResult;
13+
use PhpMcp\Server\Contracts\SessionInterface;
1214
use Psr\Container\ContainerInterface;
1315
use Throwable;
1416

@@ -48,11 +50,34 @@ public function read(ContainerInterface $container, string $uri): array
4850
return $this->formatResult($result, $uri, $this->schema->mimeType);
4951
}
5052

51-
public function getCompletionProvider(string $argumentName): ?string
53+
public function complete(ContainerInterface $container, string $argument, string $value, SessionInterface $session): CompletionCompleteResult
5254
{
53-
return $this->completionProviders[$argumentName] ?? null;
55+
$providerClassOrInstance = $this->completionProviders[$argument] ?? null;
56+
if ($providerClassOrInstance === null) {
57+
return new CompletionCompleteResult([]);
58+
}
59+
60+
if (is_string($providerClassOrInstance)) {
61+
if (! class_exists($providerClassOrInstance)) {
62+
throw new \RuntimeException("Completion provider class '{$providerClassOrInstance}' does not exist.");
63+
}
64+
65+
$provider = $container->get($providerClassOrInstance);
66+
} else {
67+
$provider = $providerClassOrInstance;
68+
}
69+
70+
$completions = $provider->getCompletions($value, $session);
71+
72+
$total = count($completions);
73+
$hasMore = $total > 100;
74+
75+
$pagedCompletions = array_slice($completions, 0, 100);
76+
77+
return new CompletionCompleteResult($pagedCompletions, $total, $hasMore);
5478
}
5579

80+
5681
public function getVariableNames(): array
5782
{
5883
return $this->variableNames;
@@ -265,9 +290,14 @@ private function guessMimeTypeFromString(string $content): string
265290

266291
public function toArray(): array
267292
{
293+
$completionProviders = [];
294+
foreach ($this->completionProviders as $argument => $provider) {
295+
$completionProviders[$argument] = serialize($provider);
296+
}
297+
268298
return [
269299
'schema' => $this->schema->toArray(),
270-
'completionProviders' => $this->completionProviders,
300+
'completionProviders' => $completionProviders,
271301
...parent::toArray(),
272302
];
273303
}
@@ -279,11 +309,16 @@ public static function fromArray(array $data): self|false
279309
return false;
280310
}
281311

312+
$completionProviders = [];
313+
foreach ($data['completionProviders'] ?? [] as $argument => $provider) {
314+
$completionProviders[$argument] = unserialize($provider);
315+
}
316+
282317
return new self(
283318
ResourceTemplate::fromArray($data['schema']),
284319
$data['handler'],
285320
$data['isManual'] ?? false,
286-
$data['completionProviders'] ?? [],
321+
$completionProviders,
287322
);
288323
} catch (Throwable $e) {
289324
return false;

src/ServerBuilder.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use PhpMcp\Server\Attributes\CompletionProvider;
1818
use PhpMcp\Server\Contracts\SessionHandlerInterface;
1919
use PhpMcp\Server\Defaults\BasicContainer;
20+
use PhpMcp\Server\Defaults\EnumCompletionProvider;
21+
use PhpMcp\Server\Defaults\ListCompletionProvider;
2022
use PhpMcp\Server\Exception\ConfigurationException;
2123

2224
use PhpMcp\Server\Session\ArraySessionHandler;
@@ -506,7 +508,14 @@ private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $r
506508
$completionAttributes = $param->getAttributes(CompletionProvider::class, \ReflectionAttribute::IS_INSTANCEOF);
507509
if (!empty($completionAttributes)) {
508510
$attributeInstance = $completionAttributes[0]->newInstance();
509-
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
511+
512+
if ($attributeInstance->provider) {
513+
$completionProviders[$param->getName()] = $attributeInstance->provider;
514+
} elseif ($attributeInstance->values) {
515+
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
516+
} elseif ($attributeInstance->enum) {
517+
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
518+
}
510519
}
511520
}
512521

src/Utils/Discoverer.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use PhpMcp\Server\Attributes\McpResource;
1515
use PhpMcp\Server\Attributes\McpResourceTemplate;
1616
use PhpMcp\Server\Attributes\McpTool;
17+
use PhpMcp\Server\Defaults\EnumCompletionProvider;
18+
use PhpMcp\Server\Defaults\ListCompletionProvider;
1719
use PhpMcp\Server\Exception\McpServerException;
1820
use PhpMcp\Server\Registry;
1921
use Psr\Log\LoggerInterface;
@@ -263,7 +265,14 @@ private function getCompletionProviders(\ReflectionMethod $reflectionMethod): ar
263265
$completionAttributes = $param->getAttributes(CompletionProvider::class, \ReflectionAttribute::IS_INSTANCEOF);
264266
if (!empty($completionAttributes)) {
265267
$attributeInstance = $completionAttributes[0]->newInstance();
266-
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
268+
269+
if ($attributeInstance->provider) {
270+
$completionProviders[$param->getName()] = $attributeInstance->provider;
271+
} elseif ($attributeInstance->values) {
272+
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
273+
} elseif ($attributeInstance->enum) {
274+
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
275+
}
267276
}
268277
}
269278

0 commit comments

Comments
 (0)