Skip to content

Commit 182d917

Browse files
committed
Enable servers to send sampling messages to clients
1 parent 8885d29 commit 182d917

File tree

12 files changed

+246
-125
lines changed

12 files changed

+246
-125
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"Mcp\\Example\\HttpDiscoveryUserProfile\\": "examples/http-discovery-userprofile/",
5858
"Mcp\\Example\\HttpSchemaShowcase\\": "examples/http-schema-showcase/",
5959
"Mcp\\Example\\StdioCachedDiscovery\\": "examples/stdio-cached-discovery/",
60+
"Mcp\\Example\\StdioClientCommunication\\": "examples/stdio-client-communication/",
6061
"Mcp\\Example\\StdioCustomDependencies\\": "examples/stdio-custom-dependencies/",
6162
"Mcp\\Example\\StdioDiscoveryCalculator\\": "examples/stdio-discovery-calculator/",
6263
"Mcp\\Example\\StdioEnvVariables\\": "examples/stdio-env-variables/",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\StdioClientCommunication;
13+
14+
use Mcp\Capability\Attribute\McpTool;
15+
use Mcp\Schema\Content\TextContent;
16+
use Mcp\Schema\Enum\LoggingLevel;
17+
use Mcp\Server\ClientAwareInterface;
18+
use Mcp\Server\ClientAwareTrait;
19+
use Psr\Log\LoggerInterface;
20+
21+
final class ClientAwareService implements ClientAwareInterface
22+
{
23+
use ClientAwareTrait;
24+
25+
public function __construct(
26+
private readonly LoggerInterface $logger,
27+
) {
28+
$this->logger->info('SamplingTool instantiated for sampling example.');
29+
}
30+
31+
#[McpTool('coordinate_incident_response', 'Coordinate an incident response with logging, progress, and sampling.')]
32+
public function coordinateIncident(string $incidentTitle): array
33+
{
34+
$this->getClientGateway()->log(
35+
LoggingLevel::Warning, \sprintf('Incident triage started: %s', $incidentTitle),
36+
);
37+
38+
$steps = [
39+
'Collecting telemetry',
40+
'Assessing scope',
41+
'Coordinating responders',
42+
];
43+
44+
foreach ($steps as $index => $step) {
45+
$progress = ($index + 1) / \count($steps);
46+
47+
$this->getClientGateway()->progress(progress: $progress, total: 1, message: $step);
48+
49+
usleep(180_000); // Simulate work being done
50+
}
51+
52+
$prompt = \sprintf(
53+
'Provide a concise response strategy for incident "%s" based on the steps completed: %s.',
54+
$incidentTitle,
55+
implode(', ', $steps)
56+
);
57+
58+
$result = $this->getClientGateway()->sample(
59+
prompt: $prompt,
60+
maxTokens: 350,
61+
timeout: 90,
62+
options: ['temperature' => 0.5]
63+
);
64+
65+
$recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';
66+
67+
$this->getClientGateway()->log(LoggingLevel::Info, \sprintf('Incident triage completed for %s', $incidentTitle));
68+
69+
return [
70+
'incident' => $incidentTitle,
71+
'recommended_actions' => $recommendation,
72+
'model' => $result->model,
73+
];
74+
}
75+
}

examples/stdio-client-communication/server.php

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,70 +13,18 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16-
use Mcp\Schema\Content\TextContent;
1716
use Mcp\Schema\Enum\LoggingLevel;
18-
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
1917
use Mcp\Schema\ServerCapabilities;
2018
use Mcp\Server;
2119
use Mcp\Server\ClientGateway;
2220
use Mcp\Server\Transport\StdioTransport;
2321

24-
$capabilities = new ServerCapabilities(logging: true, tools: true);
25-
2622
$server = Server::builder()
2723
->setServerInfo('STDIO Client Communication Demo', '1.0.0')
2824
->setLogger(logger())
2925
->setContainer(container())
30-
->setCapabilities($capabilities)
31-
->addTool(
32-
function (string $incidentTitle, ClientGateway $client): array {
33-
$client->log(LoggingLevel::Warning, sprintf('Incident triage started: %s', $incidentTitle));
34-
35-
$steps = [
36-
'Collecting telemetry',
37-
'Assessing scope',
38-
'Coordinating responders',
39-
];
40-
41-
foreach ($steps as $index => $step) {
42-
$progress = ($index + 1) / count($steps);
43-
44-
$client->progress(progress: $progress, total: 1, message: $step);
45-
46-
usleep(180_000); // Simulate work being done
47-
}
48-
49-
$prompt = sprintf(
50-
'Provide a concise response strategy for incident "%s" based on the steps completed: %s.',
51-
$incidentTitle,
52-
implode(', ', $steps)
53-
);
54-
55-
$sampling = $client->sample(
56-
prompt: $prompt,
57-
maxTokens: 350,
58-
timeout: 90,
59-
options: ['temperature' => 0.5]
60-
);
61-
62-
if ($sampling instanceof JsonRpcError) {
63-
throw new RuntimeException(sprintf('Sampling request failed (%d): %s', $sampling->code, $sampling->message));
64-
}
65-
66-
$result = $sampling->result;
67-
$recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';
68-
69-
$client->log(LoggingLevel::Info, sprintf('Incident triage completed for %s', $incidentTitle));
70-
71-
return [
72-
'incident' => $incidentTitle,
73-
'recommended_actions' => $recommendation,
74-
'model' => $result->model,
75-
];
76-
},
77-
name: 'coordinate_incident_response',
78-
description: 'Coordinate an incident response with logging, progress, and sampling.'
79-
)
26+
->setCapabilities(new ServerCapabilities(logging: true, tools: true))
27+
->setDiscovery(__DIR__)
8028
->addTool(
8129
function (string $dataset, ClientGateway $client): array {
8230
$client->log(LoggingLevel::Info, sprintf('Running quality checks on dataset "%s"', $dataset));

src/Capability/Registry/ReferenceHandler.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
use Mcp\Exception\InvalidArgumentException;
1515
use Mcp\Exception\RegistryException;
16+
use Mcp\Server\ClientAwareInterface;
1617
use Mcp\Server\ClientGateway;
18+
use Mcp\Server\Session\SessionInterface;
1719
use Psr\Container\ContainerInterface;
1820

1921
/**
@@ -37,6 +39,10 @@ public function handle(ElementReference $reference, array $arguments): mixed
3739
$instance = $this->getClassInstance($reference->handler);
3840
$arguments = $this->prepareArguments($reflection, $arguments);
3941

42+
if ($instance instanceof ClientAwareInterface) {
43+
$instance->setClientGateway(new ClientGateway($arguments['_session']));
44+
}
45+
4046
return \call_user_func($instance, ...$arguments);
4147
}
4248

@@ -49,7 +55,7 @@ public function handle(ElementReference $reference, array $arguments): mixed
4955
}
5056

5157
if (\is_callable($reference->handler)) {
52-
$reflection = $this->getReflectionForCallable($reference->handler);
58+
$reflection = $this->getReflectionForCallable($reference->handler, $arguments['_session']);
5359
$arguments = $this->prepareArguments($reflection, $arguments);
5460

5561
return \call_user_func($reference->handler, ...$arguments);
@@ -59,6 +65,11 @@ public function handle(ElementReference $reference, array $arguments): mixed
5965
[$className, $methodName] = $reference->handler;
6066
$reflection = new \ReflectionMethod($className, $methodName);
6167
$instance = $this->getClassInstance($className);
68+
69+
if ($instance instanceof ClientAwareInterface) {
70+
$instance->setClientGateway(new ClientGateway($arguments['_session']));
71+
}
72+
6273
$arguments = $this->prepareArguments($reflection, $arguments);
6374

6475
return \call_user_func([$instance, $methodName], ...$arguments);
@@ -130,7 +141,7 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
130141
/**
131142
* Gets a ReflectionMethod or ReflectionFunction for a callable.
132143
*/
133-
private function getReflectionForCallable(callable $handler): \ReflectionMethod|\ReflectionFunction
144+
private function getReflectionForCallable(callable $handler, SessionInterface $session): \ReflectionMethod|\ReflectionFunction
134145
{
135146
if (\is_string($handler)) {
136147
return new \ReflectionFunction($handler);
@@ -143,6 +154,10 @@ private function getReflectionForCallable(callable $handler): \ReflectionMethod|
143154
if (\is_array($handler) && 2 === \count($handler)) {
144155
[$class, $method] = $handler;
145156

157+
if ($class instanceof ClientAwareInterface) {
158+
$class->setClientGateway(new ClientGateway($session));
159+
}
160+
146161
return new \ReflectionMethod($class, $method);
147162
}
148163

src/Exception/ClientException.php

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+
namespace Mcp\Exception;
6+
7+
use Mcp\Schema\JsonRpc\Error;
8+
9+
class ClientException extends Exception
10+
{
11+
public function __construct(
12+
private readonly Error $error,
13+
) {
14+
parent::__construct($error->message);
15+
}
16+
17+
public function getError(): Error
18+
{
19+
return $this->error;
20+
}
21+
}

src/Exception/ConfigurationException.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
namespace Mcp\Exception;
1313

1414
/**
15-
* @author Oskar Stark <oskarstark@googlemail.com>
15+
* @author Christopher Hertel <mail@christopher-hertel.de>
1616
*/
17-
class ConfigurationException extends InvalidArgumentException
17+
class ConfigurationException extends Exception
1818
{
1919
}

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: 11 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

@@ -29,25 +30,24 @@ final class CreateSamplingMessageRequest extends Request
2930
* @param SamplingMessage[] $messages the messages to send to the model
3031
* @param int $maxTokens The maximum number of tokens to sample, as requested by the server.
3132
* 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
33+
* @param ?ModelPreferences $preferences The server's preferences for which model to select. The client MAY
3334
* ignore these preferences.
34-
* @param string|null $systemPrompt An optional system prompt the server wants to use for sampling. The
35+
* @param ?string $systemPrompt An optional system prompt the server wants to use for sampling. The
3536
* client MAY modify or omit this prompt.
36-
* @param string|null $includeContext A request to include context from one or more MCP servers (including
37+
* @param ?SamplingContext $includeContext A request to include context from one or more MCP servers (including
3738
* 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.
39+
* Allowed values: "none", "thisServer", "allServers"
40+
* @param ?float $temperature The temperature to use for sampling. The client MAY ignore this request.
41+
* @param ?string[] $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.
4444
*/
4545
public function __construct(
4646
public readonly array $messages,
4747
public readonly int $maxTokens,
4848
public readonly ?ModelPreferences $preferences = null,
4949
public readonly ?string $systemPrompt = null,
50-
public readonly ?string $includeContext = null,
50+
public readonly ?SamplingContext $includeContext = null,
5151
public readonly ?float $temperature = null,
5252
public readonly ?array $stopSequences = null,
5353
public readonly ?array $metadata = null,
@@ -114,7 +114,7 @@ protected function getParams(): array
114114
}
115115

116116
if (null !== $this->includeContext) {
117-
$params['includeContext'] = $this->includeContext;
117+
$params['includeContext'] = $this->includeContext->value;
118118
}
119119

120120
if (null !== $this->temperature) {
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)