Skip to content

Commit a61ba29

Browse files
committed
Enable servers to send sampling messages to clients
1 parent 757b959 commit a61ba29

File tree

16 files changed

+433
-136
lines changed

16 files changed

+433
-136
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ $server = Server::builder()
254254
- [Server Builder](docs/server-builder.md) - Complete ServerBuilder reference and configuration
255255
- [Transports](docs/transports.md) - STDIO and HTTP transport setup and usage
256256
- [MCP Elements](docs/mcp-elements.md) - Creating tools, resources, and prompts
257+
- [Client Communiocation](docs/client-communication.md) - Communicating back to the client from server-side
257258

258259
**Learning:**
259260
- [Examples](docs/examples.md) - Comprehensive example walkthroughs

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/",

docs/client-communication.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Client Communication
2+
3+
MCP supports various ways a server can communicate back to a server on top of the main request-response flow.
4+
5+
## Table of Contents
6+
7+
- [ClientGateway](#client-gateway)
8+
- [Sampling](#sampling)
9+
- [Logging](#logging)
10+
- [Notification](#notification)
11+
- [Progress](#progress)
12+
13+
## ClientGateway
14+
15+
Every communication back to client is handled using the `Mcp\Server\ClientGateway` and its dedicated methods per
16+
operation. To use the `ClientGateway` in your code, there are two ways to do so:
17+
18+
### 1. Method Argument Injection
19+
20+
Every refernce of a MCP element, that translates to an actual method call, can just add an type-hinted argument for the
21+
`ClientGateway` and the SDK will take care to include the gateway in the arguments of the method call:
22+
23+
```php
24+
use Mcp\Capability\Attribute\McpTool;
25+
use Mcp\Server\ClientGateway;
26+
27+
class MyService
28+
{
29+
#[McpTool('my_tool', 'My Tool Description')]
30+
public function myTool(ClientGateway $client): string
31+
{
32+
$client->log(...);
33+
```
34+
35+
### 2. Implementing `ClientAwareInterface`
36+
37+
Whenever a service class of an MCP element implements the interface `Mcp\Server\ClientAwareInterface` the `setClient`
38+
method of that class will get called while handling the reference, and in combination with `Mcp\Server\ClientAwareTrait`
39+
this ends up with code like this:
40+
41+
```php
42+
use Mcp\Capability\Attribute\McpTool;
43+
use Mcp\Server\ClientAwareInterface;
44+
use Mcp\Server\ClientAwareTrait;
45+
46+
class MyService implements ClientAwareInterface
47+
{
48+
use ClientAwareTrait;
49+
50+
#[McpTool('my_tool', 'My Tool Description')]
51+
public function myTool(): string
52+
{
53+
$this->log(...);
54+
```
55+
56+
## Sampling
57+
58+
With [sampling](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling) servers can request clients to
59+
execute "completions" or "generations" with a language model for them:
60+
61+
```php
62+
$result = $clientGateway->sample('Roses are red, violets are', 350, 90, ['temperature' => 0.5]);
63+
```
64+
65+
The `sample` method accepts four arguments:
66+
67+
1. `message`, which is **required** and accepts a string, an instance of `Content` or an array of `SampleMessage` instances.
68+
2. `maxTokens`, which defaults to `1000`
69+
3. `timeout` in seconds, which defaults to `120`
70+
4. `options` which might include `system_prompt`, `preferences` for model choice, `includeContext`, `temperature`, `stopSequences` and `metadata`
71+
72+
[Find more details to sampling payload in the specification.](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling#protocol-messages)
73+
74+
## Logging
75+
76+
The [Logging](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging) utility enables servers
77+
to send structured log messages as notifcation to clients:
78+
79+
```php
80+
use Mcp\Schema\Enum\LoggingLevel;
81+
82+
$clientGateway->log(LoggingLevel::Warning, 'The end is near.');
83+
```
84+
85+
## Progress
86+
87+
With a [Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress)
88+
notification a server can update a client while an operation is ongoing:
89+
90+
```php
91+
$clientGateway->progress(4.2, 10, 'Downloading needed images.');
92+
```
93+
94+
## Notification
95+
96+
Lastly, the server can push all kind of notifications, that implement the `Mcp\Schema\JsonRpc\Notification` interface
97+
to the client to:
98+
99+
```php
100+
$clientGateway->notify($yourNotification);
101+
```

docs/examples.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ $server = Server::builder()
163163
->setDiscovery(__DIR__, ['.'], [], $cache)
164164
```
165165

166+
### Client Communication
167+
168+
**File**: `examples/stdio-client-communication/`
169+
170+
**What it demostrates:**
171+
- Server initiated communcation back to the client
172+
- Logging, sampling, progress and notifications
173+
- Using `ClientGateway` in service class via `ClientAwareInterface` and corresponding trait
174+
- Using `ClientGateway` in tool method via method argument injection
175+
166176
## HTTP Examples
167177

168178
### Discovery User Profile

examples/http-client-communication/server.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414

1515
use Http\Discovery\Psr17Factory;
1616
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
17-
use Mcp\Exception\ToolCallException;
1817
use Mcp\Schema\Content\TextContent;
1918
use Mcp\Schema\Enum\LoggingLevel;
20-
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
2119
use Mcp\Schema\ServerCapabilities;
2220
use Mcp\Server;
2321
use Mcp\Server\ClientGateway;
@@ -57,18 +55,13 @@ function (string $projectName, array $milestones, ClientGateway $client): array
5755
implode(', ', $milestones)
5856
);
5957

60-
$response = $client->sample(
61-
prompt: $prompt,
58+
$result = $client->sample(
59+
message: $prompt,
6260
maxTokens: 400,
6361
timeout: 90,
6462
options: ['temperature' => 0.4]
6563
);
6664

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

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

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->setClient(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->setClient(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->setClient(new ClientGateway($session));
161+
}
162+
146163
return new \ReflectionMethod($class, $method);
147164
}
148165

0 commit comments

Comments
 (0)