Skip to content

Commit affcec0

Browse files
committed
Enable servers to send sampling messages to clients
1 parent fcf605d commit affcec0

File tree

13 files changed

+254
-15
lines changed

13 files changed

+254
-15
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"Mcp\\Example\\StdioDiscoveryCalculator\\": "examples/stdio-discovery-calculator/",
6161
"Mcp\\Example\\StdioEnvVariables\\": "examples/stdio-env-variables/",
6262
"Mcp\\Example\\StdioExplicitRegistration\\": "examples/stdio-explicit-registration/",
63+
"Mcp\\Example\\StdioToolSampling\\": "examples/stdio-tool-sampling/",
6364
"Mcp\\Tests\\": "tests/"
6465
}
6566
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\Example\StdioToolSampling;
13+
14+
use Mcp\Schema\Content\SamplingMessage;
15+
use Mcp\Schema\Content\TextContent;
16+
use Mcp\Schema\Enum\Role;
17+
use Mcp\Schema\Request\CreateSamplingMessageRequest;
18+
use Mcp\Schema\Result\CreateSamplingMessageResult;
19+
use Mcp\Server\ClientAwareInterface;
20+
use Mcp\Server\ClientAwareTrait;
21+
use Psr\Log\LoggerInterface;
22+
23+
final class SamplingTool implements ClientAwareInterface
24+
{
25+
use ClientAwareTrait;
26+
27+
public function __construct(
28+
private readonly LoggerInterface $logger,
29+
) {
30+
$this->logger->info('SamplingTool instantiated for sampling example.');
31+
}
32+
33+
public function trySampling(): string
34+
{
35+
$this->logger->info('About to send a sampling request to the client.');
36+
37+
$response = $this->getClientGateway()->request(
38+
new CreateSamplingMessageRequest(
39+
messages: [new SamplingMessage(Role::User, new TextContent('Hello from server!'))],
40+
maxTokens: 100,
41+
),
42+
);
43+
44+
\assert($response instanceof CreateSamplingMessageResult);
45+
46+
return 'Client Response: '.$response->content->text;
47+
}
48+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of the official PHP MCP SDK.
6+
*
7+
* A collaboration between Symfony and the PHP Foundation.
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
require_once dirname(__DIR__).'/bootstrap.php';
14+
chdir(__DIR__);
15+
16+
use Mcp\Example\StdioToolSampling\SamplingTool;
17+
use Mcp\Server;
18+
use Mcp\Server\Transport\StdioTransport;
19+
20+
logger()->info('Starting MCP Server with Sampling ...');
21+
22+
$server = Server::builder()
23+
->setServerInfo('Sampling Server', '1.0.0')
24+
->setLogger(logger())
25+
->setContainer(container())
26+
->addTool([SamplingTool::class, 'trySampling'], 'try_sampling')
27+
->build();
28+
29+
$transport = new StdioTransport(logger: logger());
30+
31+
$server->connect($transport);
32+
33+
$transport->listen();
34+
35+
logger()->info('Server listener stopped gracefully.');

src/Capability/Registry/ReferenceHandler.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Mcp\Exception\InvalidArgumentException;
1515
use Mcp\Exception\RegistryException;
16+
use Mcp\Server\ClientAwareInterface;
17+
use Mcp\Server\ClientGateway;
1618
use Psr\Container\ContainerInterface;
1719

1820
/**
@@ -21,6 +23,7 @@
2123
final class ReferenceHandler implements ReferenceHandlerInterface
2224
{
2325
public function __construct(
26+
private readonly ClientGateway $clientGateway,
2427
private readonly ?ContainerInterface $container = null,
2528
) {
2629
}
@@ -36,6 +39,10 @@ public function handle(ElementReference $reference, array $arguments): mixed
3639
$instance = $this->getClassInstance($reference->handler);
3740
$arguments = $this->prepareArguments($reflection, $arguments);
3841

42+
if ($instance instanceof ClientAwareInterface) {
43+
$instance->setClientGateway($this->clientGateway);
44+
}
45+
3946
return \call_user_func($instance, ...$arguments);
4047
}
4148

@@ -58,6 +65,11 @@ public function handle(ElementReference $reference, array $arguments): mixed
5865
[$className, $methodName] = $reference->handler;
5966
$reflection = new \ReflectionMethod($className, $methodName);
6067
$instance = $this->getClassInstance($className);
68+
69+
if ($instance instanceof ClientAwareInterface) {
70+
$instance->setClientGateway($this->clientGateway);
71+
}
72+
6173
$arguments = $this->prepareArguments($reflection, $arguments);
6274

6375
return \call_user_func([$instance, $methodName], ...$arguments);
@@ -131,6 +143,10 @@ private function getReflectionForCallable(callable $handler): \ReflectionMethod|
131143
if (\is_array($handler) && 2 === \count($handler)) {
132144
[$class, $method] = $handler;
133145

146+
if ($class instanceof ClientAwareInterface) {
147+
$class->setClientGateway($this->clientGateway);
148+
}
149+
134150
return new \ReflectionMethod($class, $method);
135151
}
136152

src/Schema/Content/SamplingMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Describes a message issued to or received from an LLM API during sampling.
1919
*
2020
* @phpstan-type SamplingMessageData = array{
21-
* role: string,
21+
* role: 'user'|'assistant',
2222
* content: TextContent|ImageContent|AudioContent
2323
* }
2424
*
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the official PHP MCP SDK.
7+
*
8+
* A collaboration between Symfony and the PHP Foundation.
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Mcp\Schema\Enum;
15+
16+
enum SamplingContext: string
17+
{
18+
case NONE = 'none';
19+
case THIS_SERVER = 'thisServer';
20+
case ALL_SERVERS = 'allServers';
21+
}

src/Schema/Request/CreateSamplingMessageRequest.php

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Mcp\Exception\InvalidArgumentException;
1515
use Mcp\Schema\Content\SamplingMessage;
16+
use Mcp\Schema\Enum\SamplingContext;
1617
use Mcp\Schema\JsonRpc\Request;
1718
use Mcp\Schema\ModelPreferences;
1819

@@ -25,29 +26,30 @@
2526
*/
2627
final class CreateSamplingMessageRequest extends Request
2728
{
29+
protected string|int $id = 'create_sampling_message';
30+
2831
/**
2932
* @param SamplingMessage[] $messages the messages to send to the model
3033
* @param int $maxTokens The maximum number of tokens to sample, as requested by the server.
3134
* The client MAY choose to sample fewer tokens than requested.
32-
* @param ModelPreferences|null $preferences The server's preferences for which model to select. The client MAY
35+
* @param ?ModelPreferences $preferences The server's preferences for which model to select. The client MAY
3336
* ignore these preferences.
34-
* @param string|null $systemPrompt An optional system prompt the server wants to use for sampling. The
37+
* @param ?string $systemPrompt An optional system prompt the server wants to use for sampling. The
3538
* client MAY modify or omit this prompt.
36-
* @param string|null $includeContext A request to include context from one or more MCP servers (including
39+
* @param ?SamplingContext $includeContext A request to include context from one or more MCP servers (including
3740
* the caller), to be attached to the prompt. The client MAY ignore this request.
38-
*
39-
* Allowed values: "none", "thisServer", "allServers"
40-
* @param float|null $temperature The temperature to use for sampling. The client MAY ignore this request.
41-
* @param string[]|null $stopSequences A list of sequences to stop sampling at. The client MAY ignore this request.
42-
* @param ?array<string, mixed> $metadata Optional metadata to pass through to the LLM provider. The format of
43-
* this metadata is provider-specific.
41+
* Allowed values: "none", "thisServer", "allServers"
42+
* @param ?float $temperature The temperature to use for sampling. The client MAY ignore this request.
43+
* @param ?string[] $stopSequences A list of sequences to stop sampling at. The client MAY ignore this request.
44+
* @param ?array<string, mixed> $metadata Optional metadata to pass through to the LLM provider. The format of
45+
* this metadata is provider-specific.
4446
*/
4547
public function __construct(
4648
public readonly array $messages,
4749
public readonly int $maxTokens,
4850
public readonly ?ModelPreferences $preferences = null,
4951
public readonly ?string $systemPrompt = null,
50-
public readonly ?string $includeContext = null,
52+
public readonly ?SamplingContext $includeContext = null,
5153
public readonly ?float $temperature = null,
5254
public readonly ?array $stopSequences = null,
5355
public readonly ?array $metadata = null,
@@ -114,7 +116,7 @@ protected function getParams(): array
114116
}
115117

116118
if (null !== $this->includeContext) {
117-
$params['includeContext'] = $this->includeContext;
119+
$params['includeContext'] = $this->includeContext->value;
118120
}
119121

120122
if (null !== $this->temperature) {

src/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Mcp;
1313

1414
use Mcp\Server\Builder;
15+
use Mcp\Server\ClientGateway;
1516
use Mcp\Server\Handler\JsonRpcHandler;
1617
use Mcp\Server\Transport\TransportInterface;
1718
use Psr\Log\LoggerInterface;
@@ -26,6 +27,7 @@ final class Server
2627
{
2728
public function __construct(
2829
private readonly JsonRpcHandler $jsonRpcHandler,
30+
private readonly ClientGateway $clientGateway,
2931
private readonly LoggerInterface $logger = new NullLogger(),
3032
) {
3133
}
@@ -43,6 +45,8 @@ public function connect(TransportInterface $transport): void
4345
'transport' => $transport::class,
4446
]);
4547

48+
$this->clientGateway->connect($transport);
49+
4650
$transport->onMessage(function (string $message, ?Uuid $sessionId) use ($transport) {
4751
foreach ($this->jsonRpcHandler->process($message, $sessionId) as [$response, $context]) {
4852
if (null === $response) {

src/Server/Builder.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ public function build(): Server
360360

361361
$capabilities = $this->explicitCapabilities ?? $registry->getCapabilities();
362362
$configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions);
363-
$referenceHandler = new ReferenceHandler($container);
363+
$clientGateway = new ClientGateway($logger);
364+
$referenceHandler = new ReferenceHandler($clientGateway, $container);
364365

365366
$methodHandlers = array_merge($this->customMethodHandlers, [
366367
new Handler\Request\PingHandler(),
@@ -384,7 +385,7 @@ public function build(): Server
384385
logger: $logger,
385386
);
386387

387-
return new Server($jsonRpcHandler, $logger);
388+
return new Server($jsonRpcHandler, $clientGateway, $logger);
388389
}
389390

390391
private function performDiscovery(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Server;
13+
14+
interface ClientAwareInterface
15+
{
16+
public function setClientGateway(ClientGateway $clientGateway): void;
17+
}

0 commit comments

Comments
 (0)