Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
0.9.0
-----

* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument.
* 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.

0.8.0
Expand Down
2 changes: 1 addition & 1 deletion src/Capability/Attribute/CompletionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class CompletionProvider
* @param class-string<ProviderInterface>|ProviderInterface|null $provider if a class-string, it will be resolved
* from the container at the point of use
* @param ?array<int, int|float|string> $values a list of values to use for completion
* @param class-string|null $enum an enum whose cases are the completions
*/
public function __construct(
public ?string $providerClass = null,
public string|ProviderInterface|null $provider = null,
public ?array $values = null,
public ?string $enum = null,
Expand Down
2 changes: 0 additions & 2 deletions src/Capability/Discovery/Discoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,6 @@ private function getCompletionProviders(\ReflectionMethod $reflectionMethod): ar

if ($attributeInstance->provider) {
$completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->providerClass) {
$completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->values) {
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
} elseif ($attributeInstance->enum) {
Expand Down
4 changes: 1 addition & 3 deletions src/Capability/Registry/Loader/ReflectedElementLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ private function getHandlerDescription(\Closure|array|string $handler): string
}

/**
* @return array<string, ProviderInterface>
* @return array<string, class-string<ProviderInterface>|ProviderInterface>
*/
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
{
Expand All @@ -307,8 +307,6 @@ private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $r

if ($attributeInstance->provider) {
$completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->providerClass) {
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
} elseif ($attributeInstance->values) {
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
} elseif ($attributeInstance->enum) {
Expand Down
57 changes: 57 additions & 0 deletions tests/Integration/CompletionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Integration;

use Mcp\Schema\PromptReference;
use PHPUnit\Framework\Attributes\TestDox;

/**
* Argument completion driven by a `#[CompletionProvider(provider: …)]` class-string.
*
* The fixture's provider only exists in the container, so completions coming
* back at all is what proves the attribute reached the registry and the
* container was asked to build it.
*
* @see Fixture/completion.php for the server under test
*/
final class CompletionTest extends IntegrationTestCase
{
#[TestDox('a class-string provider completes from the container-built instance')]
public function testClassStringProviderCompletesFromTheContainer(): void
{
$client = $this->connect('completion');

$result = $client->complete(new PromptReference('book_seat'), ['name' => 'seat', 'value' => '12']);

$this->assertSame(['12A', '12B'], $result->values);
}

#[TestDox('an empty value offers every completion the provider knows')]
public function testEmptyValueOffersEveryCompletion(): void
{
$client = $this->connect('completion');

$result = $client->complete(new PromptReference('book_seat'), ['name' => 'seat', 'value' => '']);

$this->assertSame(['12A', '12B', '14C'], $result->values);
}

#[TestDox('an argument the prompt does not declare completes to nothing')]
public function testUnknownArgumentCompletesToNothing(): void
{
$client = $this->connect('completion');

$result = $client->complete(new PromptReference('book_seat'), ['name' => 'unknown', 'value' => '1']);

$this->assertSame([], $result->values);
}
}
38 changes: 38 additions & 0 deletions tests/Integration/Fixture/Completion/BookingElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Integration\Fixture\Completion;

use Mcp\Capability\Attribute\CompletionProvider;
use Mcp\Capability\Attribute\McpPrompt;

/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class BookingElements
{
/**
* Confirms a seat booking.
*
* @param string $seat the seat to book
*
* @return array the prompt messages
*/
#[McpPrompt(name: 'book_seat')]
public function bookSeat(
#[CompletionProvider(provider: SeatCompletionProvider::class)]
string $seat,
): array {
return [
['role' => 'user', 'content' => \sprintf('Book seat %s for me.', $seat)],
];
}
}
42 changes: 42 additions & 0 deletions tests/Integration/Fixture/Completion/SeatCompletionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Integration\Fixture\Completion;

use Mcp\Capability\Completion\ProviderInterface;

/**
* A provider that cannot be built without its seat map.
*
* The constructor takes a scalar the auto-wiring container cannot supply, so a
* completion that comes back with seats in it proves the provider was taken
* from the container rather than instantiated on the spot.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class SeatCompletionProvider implements ProviderInterface
{
/**
* @param list<string> $seats
*/
public function __construct(
private readonly array $seats,
) {
}

public function getCompletions(string $currentValue): array
{
return array_values(array_filter(
$this->seats,
static fn (string $seat): bool => str_starts_with($seat, $currentValue),
));
}
}
31 changes: 31 additions & 0 deletions tests/Integration/Fixture/completion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/*
* Server for {@see \Mcp\Tests\Integration\CompletionTest}.
*/

use Mcp\Capability\Registry\Container;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
use Mcp\Tests\Integration\Fixture\Completion\SeatCompletionProvider;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

$container = new Container();
$container->set(SeatCompletionProvider::class, new SeatCompletionProvider(['12A', '12B', '14C']));

Server::builder()
->setServerInfo('integration-server', '1.0.0')
->setContainer($container)
->setDiscovery(__DIR__, ['Completion'])
->build()
->run(new StdioTransport());
9 changes: 9 additions & 0 deletions tests/Unit/Capability/Attribute/CompletionProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public function testCanBeConstructedWithProviderClass(): void
$this->assertNull($attribute->enum);
}

public function testCanBeConstructedWithAPositionalProviderClass(): void
{
$attribute = new CompletionProvider(CompletionProviderFixture::class);

$this->assertSame(CompletionProviderFixture::class, $attribute->provider);
$this->assertNull($attribute->values);
$this->assertNull($attribute->enum);
}

public function testCanBeConstructedWithProviderInstance(): void
{
$instance = new CompletionProviderFixture();
Expand Down
14 changes: 12 additions & 2 deletions tests/Unit/Capability/Discovery/DiscoveryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
$this->assertEquals([InvocablePromptFixture::class, '__invoke'], $prompts['InvokableGreeterPrompt']->handler);

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

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

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

$typeProvider = $prompts['content_creator']->completionProviders['type'];
$this->assertInstanceOf(ListCompletionProvider::class, $typeProvider);
Expand All @@ -182,4 +182,14 @@ public function testDiscoversEnhancedCompletionProvidersWithValuesAndEnumAttribu
$categoryProvider = $templates['content://{category}/{slug}']->completionProviders['category'];
$this->assertInstanceOf(ListCompletionProvider::class, $categoryProvider);
}

public function testDiscoversPositionalCompletionProviderAsClassString(): void
{
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);

$this->assertArrayHasKey('content_creator', $prompts = $discovery->getPrompts());

// Kept as a class-string so the container resolves it at the point of use.
$this->assertEquals(CompletionProviderFixture::class, $prompts['content_creator']->completionProviders['author']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
use Mcp\Capability\Attribute\CompletionProvider;
use Mcp\Capability\Attribute\McpPrompt;
use Mcp\Capability\Attribute\McpResourceTemplate;
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
use Mcp\Tests\Unit\Fixtures\Enum\PriorityEnum;
use Mcp\Tests\Unit\Fixtures\Enum\StatusEnum;

class EnhancedCompletionHandler
{
/**
* Create content with list and enum completion providers.
* Create content with list, enum and positional provider completion providers.
*/
#[McpPrompt(name: 'content_creator')]
public function createContent(
Expand All @@ -30,9 +31,11 @@ public function createContent(
string $status,
#[CompletionProvider(enum: PriorityEnum::class)]
string $priority,
#[CompletionProvider(CompletionProviderFixture::class)]
string $author,
): array {
return [
['role' => 'user', 'content' => "Create a {$type} with status {$status} and priority {$priority}"],
['role' => 'user', 'content' => "Create a {$type} with status {$status} and priority {$priority} for {$author}"],
];
}

Expand Down