Skip to content

Commit bff54d9

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

File tree

13 files changed

+261
-132
lines changed

13 files changed

+261
-132
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/",

examples/http-client-communication/server.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
1717
use Mcp\Schema\Content\TextContent;
1818
use Mcp\Schema\Enum\LoggingLevel;
19-
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
2019
use Mcp\Schema\ServerCapabilities;
2120
use Mcp\Server;
2221
use Mcp\Server\ClientGateway;
@@ -56,18 +55,13 @@ function (string $projectName, array $milestones, ClientGateway $client): array
5655
implode(', ', $milestones)
5756
);
5857

59-
$response = $client->sample(
58+
$result = $client->sample(
6059
prompt: $prompt,
6160
maxTokens: 400,
6261
timeout: 90,
6362
options: ['temperature' => 0.4]
6463
);
6564

66-
if ($response instanceof JsonRpcError) {
67-
throw new RuntimeException(sprintf('Sampling request failed (%d): %s', $response->code, $response->message));
68-
}
69-
70-
$result = $response->result;
7165
$content = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';
7266

7367
$client->log(LoggingLevel::Info, 'Briefing ready, returning to caller.');
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
/**
32+
* @return array{incident: string, recommended_actions: string, model: string}
33+
*/
34+
#[McpTool('coordinate_incident_response', 'Coordinate an incident response with logging, progress, and sampling.')]
35+
public function coordinateIncident(string $incidentTitle): array
36+
{
37+
$this->getClientGateway()->log(
38+
LoggingLevel::Warning, \sprintf('Incident triage started: %s', $incidentTitle),
39+
);
40+
41+
$steps = [
42+
'Collecting telemetry',
43+
'Assessing scope',
44+
'Coordinating responders',
45+
];
46+
47+
foreach ($steps as $index => $step) {
48+
$progress = ($index + 1) / \count($steps);
49+
50+
$this->getClientGateway()->progress(progress: $progress, total: 1, message: $step);
51+
52+
usleep(180_000); // Simulate work being done
53+
}
54+
55+
$prompt = \sprintf(
56+
'Provide a concise response strategy for incident "%s" based on the steps completed: %s.',
57+
$incidentTitle,
58+
implode(', ', $steps)
59+
);
60+
61+
$result = $this->getClientGateway()->sample(
62+
prompt: $prompt,
63+
maxTokens: 350,
64+
timeout: 90,
65+
options: ['temperature' => 0.5]
66+
);
67+
68+
$recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';
69+
70+
$this->getClientGateway()->log(LoggingLevel::Info, \sprintf('Incident triage completed for %s', $incidentTitle));
71+
72+
return [
73+
'incident' => $incidentTitle,
74+
'recommended_actions' => $recommendation,
75+
'model' => $result->model,
76+
];
77+
}
78+
}

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: 19 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
/**
@@ -31,12 +33,18 @@ public function __construct(
3133
*/
3234
public function handle(ElementReference $reference, array $arguments): mixed
3335
{
36+
$session = $arguments['_session'];
37+
3438
if (\is_string($reference->handler)) {
3539
if (class_exists($reference->handler) && method_exists($reference->handler, '__invoke')) {
3640
$reflection = new \ReflectionMethod($reference->handler, '__invoke');
3741
$instance = $this->getClassInstance($reference->handler);
3842
$arguments = $this->prepareArguments($reflection, $arguments);
3943

44+
if ($instance instanceof ClientAwareInterface) {
45+
$instance->setClientGateway(new ClientGateway($session));
46+
}
47+
4048
return \call_user_func($instance, ...$arguments);
4149
}
4250

@@ -49,7 +57,7 @@ public function handle(ElementReference $reference, array $arguments): mixed
4957
}
5058

5159
if (\is_callable($reference->handler)) {
52-
$reflection = $this->getReflectionForCallable($reference->handler);
60+
$reflection = $this->getReflectionForCallable($reference->handler, $session);
5361
$arguments = $this->prepareArguments($reflection, $arguments);
5462

5563
return \call_user_func($reference->handler, ...$arguments);
@@ -59,6 +67,11 @@ public function handle(ElementReference $reference, array $arguments): mixed
5967
[$className, $methodName] = $reference->handler;
6068
$reflection = new \ReflectionMethod($className, $methodName);
6169
$instance = $this->getClassInstance($className);
70+
71+
if ($instance instanceof ClientAwareInterface) {
72+
$instance->setClientGateway(new ClientGateway($session));
73+
}
74+
6275
$arguments = $this->prepareArguments($reflection, $arguments);
6376

6477
return \call_user_func([$instance, $methodName], ...$arguments);
@@ -130,7 +143,7 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
130143
/**
131144
* Gets a ReflectionMethod or ReflectionFunction for a callable.
132145
*/
133-
private function getReflectionForCallable(callable $handler): \ReflectionMethod|\ReflectionFunction
146+
private function getReflectionForCallable(callable $handler, SessionInterface $session): \ReflectionMethod|\ReflectionFunction
134147
{
135148
if (\is_string($handler)) {
136149
return new \ReflectionFunction($handler);
@@ -143,6 +156,10 @@ private function getReflectionForCallable(callable $handler): \ReflectionMethod|
143156
if (\is_array($handler) && 2 === \count($handler)) {
144157
[$class, $method] = $handler;
145158

159+
if ($class instanceof ClientAwareInterface) {
160+
$class->setClientGateway(new ClientGateway($session));
161+
}
162+
146163
return new \ReflectionMethod($class, $method);
147164
}
148165

src/Exception/ClientException.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Exception;
15+
16+
use Mcp\Schema\JsonRpc\Error;
17+
18+
class ClientException extends Exception
19+
{
20+
public function __construct(
21+
private readonly Error $error,
22+
) {
23+
parent::__construct($error->message);
24+
}
25+
26+
public function getError(): Error
27+
{
28+
return $this->error;
29+
}
30+
}

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) {

0 commit comments

Comments
 (0)