Skip to content

Commit f39625b

Browse files
authored
[Capability] Remove CompletionProvider's dead providerClass argument (#499)
* [Capability] Remove CompletionProvider's dead providerClass argument It always threw on construction, so it never worked in any release. `provider:` takes the same class-string and is now the first positional argument. * Reword Changelog
1 parent 749c035 commit f39625b

11 files changed

Lines changed: 197 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
55
0.9.0
66
-----
77

8+
* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument.
89
* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions.
910

1011
0.8.0

src/Capability/Attribute/CompletionProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ class CompletionProvider
2424
* @param class-string<ProviderInterface>|ProviderInterface|null $provider if a class-string, it will be resolved
2525
* from the container at the point of use
2626
* @param ?array<int, int|float|string> $values a list of values to use for completion
27+
* @param class-string|null $enum an enum whose cases are the completions
2728
*/
2829
public function __construct(
29-
public ?string $providerClass = null,
3030
public string|ProviderInterface|null $provider = null,
3131
public ?array $values = null,
3232
public ?string $enum = null,

src/Capability/Discovery/Discoverer.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,6 @@ private function getCompletionProviders(\ReflectionMethod $reflectionMethod): ar
325325

326326
if ($attributeInstance->provider) {
327327
$completionProviders[$param->getName()] = $attributeInstance->provider;
328-
} elseif ($attributeInstance->providerClass) {
329-
$completionProviders[$param->getName()] = $attributeInstance->provider;
330328
} elseif ($attributeInstance->values) {
331329
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
332330
} elseif ($attributeInstance->enum) {

src/Capability/Registry/Loader/ReflectedElementLoader.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ private function getHandlerDescription(\Closure|array|string $handler): string
287287
}
288288

289289
/**
290-
* @return array<string, ProviderInterface>
290+
* @return array<string, class-string<ProviderInterface>|ProviderInterface>
291291
*/
292292
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
293293
{
@@ -307,8 +307,6 @@ private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $r
307307

308308
if ($attributeInstance->provider) {
309309
$completionProviders[$param->getName()] = $attributeInstance->provider;
310-
} elseif ($attributeInstance->providerClass) {
311-
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
312310
} elseif ($attributeInstance->values) {
313311
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
314312
} elseif ($attributeInstance->enum) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Integration;
13+
14+
use Mcp\Schema\PromptReference;
15+
use PHPUnit\Framework\Attributes\TestDox;
16+
17+
/**
18+
* Argument completion driven by a `#[CompletionProvider(provider: …)]` class-string.
19+
*
20+
* The fixture's provider only exists in the container, so completions coming
21+
* back at all is what proves the attribute reached the registry and the
22+
* container was asked to build it.
23+
*
24+
* @see Fixture/completion.php for the server under test
25+
*/
26+
final class CompletionTest extends IntegrationTestCase
27+
{
28+
#[TestDox('a class-string provider completes from the container-built instance')]
29+
public function testClassStringProviderCompletesFromTheContainer(): void
30+
{
31+
$client = $this->connect('completion');
32+
33+
$result = $client->complete(new PromptReference('book_seat'), ['name' => 'seat', 'value' => '12']);
34+
35+
$this->assertSame(['12A', '12B'], $result->values);
36+
}
37+
38+
#[TestDox('an empty value offers every completion the provider knows')]
39+
public function testEmptyValueOffersEveryCompletion(): void
40+
{
41+
$client = $this->connect('completion');
42+
43+
$result = $client->complete(new PromptReference('book_seat'), ['name' => 'seat', 'value' => '']);
44+
45+
$this->assertSame(['12A', '12B', '14C'], $result->values);
46+
}
47+
48+
#[TestDox('an argument the prompt does not declare completes to nothing')]
49+
public function testUnknownArgumentCompletesToNothing(): void
50+
{
51+
$client = $this->connect('completion');
52+
53+
$result = $client->complete(new PromptReference('book_seat'), ['name' => 'unknown', 'value' => '1']);
54+
55+
$this->assertSame([], $result->values);
56+
}
57+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Integration\Fixture\Completion;
13+
14+
use Mcp\Capability\Attribute\CompletionProvider;
15+
use Mcp\Capability\Attribute\McpPrompt;
16+
17+
/**
18+
* @author Christopher Hertel <mail@christopher-hertel.de>
19+
*/
20+
final class BookingElements
21+
{
22+
/**
23+
* Confirms a seat booking.
24+
*
25+
* @param string $seat the seat to book
26+
*
27+
* @return array the prompt messages
28+
*/
29+
#[McpPrompt(name: 'book_seat')]
30+
public function bookSeat(
31+
#[CompletionProvider(provider: SeatCompletionProvider::class)]
32+
string $seat,
33+
): array {
34+
return [
35+
['role' => 'user', 'content' => \sprintf('Book seat %s for me.', $seat)],
36+
];
37+
}
38+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Integration\Fixture\Completion;
13+
14+
use Mcp\Capability\Completion\ProviderInterface;
15+
16+
/**
17+
* A provider that cannot be built without its seat map.
18+
*
19+
* The constructor takes a scalar the auto-wiring container cannot supply, so a
20+
* completion that comes back with seats in it proves the provider was taken
21+
* from the container rather than instantiated on the spot.
22+
*
23+
* @author Christopher Hertel <mail@christopher-hertel.de>
24+
*/
25+
final class SeatCompletionProvider implements ProviderInterface
26+
{
27+
/**
28+
* @param list<string> $seats
29+
*/
30+
public function __construct(
31+
private readonly array $seats,
32+
) {
33+
}
34+
35+
public function getCompletions(string $currentValue): array
36+
{
37+
return array_values(array_filter(
38+
$this->seats,
39+
static fn (string $seat): bool => str_starts_with($seat, $currentValue),
40+
));
41+
}
42+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
/*
13+
* Server for {@see \Mcp\Tests\Integration\CompletionTest}.
14+
*/
15+
16+
use Mcp\Capability\Registry\Container;
17+
use Mcp\Server;
18+
use Mcp\Server\Transport\StdioTransport;
19+
use Mcp\Tests\Integration\Fixture\Completion\SeatCompletionProvider;
20+
21+
require_once dirname(__DIR__, 3).'/vendor/autoload.php';
22+
23+
$container = new Container();
24+
$container->set(SeatCompletionProvider::class, new SeatCompletionProvider(['12A', '12B', '14C']));
25+
26+
Server::builder()
27+
->setServerInfo('integration-server', '1.0.0')
28+
->setContainer($container)
29+
->setDiscovery(__DIR__, ['Completion'])
30+
->build()
31+
->run(new StdioTransport());

tests/Unit/Capability/Attribute/CompletionProviderTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ public function testCanBeConstructedWithProviderClass(): void
2626
$this->assertNull($attribute->enum);
2727
}
2828

29+
public function testCanBeConstructedWithAPositionalProviderClass(): void
30+
{
31+
$attribute = new CompletionProvider(CompletionProviderFixture::class);
32+
33+
$this->assertSame(CompletionProviderFixture::class, $attribute->provider);
34+
$this->assertNull($attribute->values);
35+
$this->assertNull($attribute->enum);
36+
}
37+
2938
public function testCanBeConstructedWithProviderInstance(): void
3039
{
3140
$instance = new CompletionProviderFixture();

tests/Unit/Capability/Discovery/DiscoveryTest.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
9494
$this->assertEquals([InvocablePromptFixture::class, '__invoke'], $prompts['InvokableGreeterPrompt']->handler);
9595

9696
$this->assertArrayHasKey('content_creator', $prompts);
97-
$this->assertCount(3, $prompts['content_creator']->completionProviders);
97+
$this->assertCount(4, $prompts['content_creator']->completionProviders);
9898

9999
$templates = $discovery->getResourceTemplates();
100100
$this->assertCount(4, $templates);
@@ -165,7 +165,7 @@ public function testDiscoversEnhancedCompletionProvidersWithValuesAndEnumAttribu
165165
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
166166

167167
$this->assertArrayHasKey('content_creator', $prompts = $discovery->getPrompts());
168-
$this->assertCount(3, $prompts['content_creator']->completionProviders);
168+
$this->assertCount(4, $prompts['content_creator']->completionProviders);
169169

170170
$typeProvider = $prompts['content_creator']->completionProviders['type'];
171171
$this->assertInstanceOf(ListCompletionProvider::class, $typeProvider);
@@ -182,4 +182,14 @@ public function testDiscoversEnhancedCompletionProvidersWithValuesAndEnumAttribu
182182
$categoryProvider = $templates['content://{category}/{slug}']->completionProviders['category'];
183183
$this->assertInstanceOf(ListCompletionProvider::class, $categoryProvider);
184184
}
185+
186+
public function testDiscoversPositionalCompletionProviderAsClassString(): void
187+
{
188+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
189+
190+
$this->assertArrayHasKey('content_creator', $prompts = $discovery->getPrompts());
191+
192+
// Kept as a class-string so the container resolves it at the point of use.
193+
$this->assertEquals(CompletionProviderFixture::class, $prompts['content_creator']->completionProviders['author']);
194+
}
185195
}

0 commit comments

Comments
 (0)