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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ $server = Server::builder()
- [Server Builder](docs/server-builder.md) - Complete ServerBuilder reference and configuration
- [Transports](docs/transports.md) - STDIO and HTTP transport setup and usage
- [MCP Elements](docs/mcp-elements.md) - Creating tools, resources, and prompts
- [Client Communiocation](docs/client-communication.md) - Communicating back to the client from server-side

**Learning:**
- [Examples](docs/examples.md) - Comprehensive example walkthroughs
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"Mcp\\Example\\HttpDiscoveryUserProfile\\": "examples/http-discovery-userprofile/",
"Mcp\\Example\\HttpSchemaShowcase\\": "examples/http-schema-showcase/",
"Mcp\\Example\\StdioCachedDiscovery\\": "examples/stdio-cached-discovery/",
"Mcp\\Example\\StdioClientCommunication\\": "examples/stdio-client-communication/",
"Mcp\\Example\\StdioCustomDependencies\\": "examples/stdio-custom-dependencies/",
"Mcp\\Example\\StdioDiscoveryCalculator\\": "examples/stdio-discovery-calculator/",
"Mcp\\Example\\StdioEnvVariables\\": "examples/stdio-env-variables/",
Expand Down
101 changes: 101 additions & 0 deletions docs/client-communication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Client Communication

MCP supports various ways a server can communicate back to a server on top of the main request-response flow.

## Table of Contents

- [ClientGateway](#client-gateway)
- [Sampling](#sampling)
- [Logging](#logging)
- [Notification](#notification)
- [Progress](#progress)

## ClientGateway

Every communication back to client is handled using the `Mcp\Server\ClientGateway` and its dedicated methods per
operation. To use the `ClientGateway` in your code, there are two ways to do so:

### 1. Method Argument Injection

Every refernce of a MCP element, that translates to an actual method call, can just add an type-hinted argument for the
`ClientGateway` and the SDK will take care to include the gateway in the arguments of the method call:

```php
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server\ClientGateway;

class MyService
{
#[McpTool('my_tool', 'My Tool Description')]
public function myTool(ClientGateway $client): string
{
$client->log(...);
```

### 2. Implementing `ClientAwareInterface`

Whenever a service class of an MCP element implements the interface `Mcp\Server\ClientAwareInterface` the `setClient`
method of that class will get called while handling the reference, and in combination with `Mcp\Server\ClientAwareTrait`
this ends up with code like this:

```php
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server\ClientAwareInterface;
use Mcp\Server\ClientAwareTrait;

class MyService implements ClientAwareInterface
{
use ClientAwareTrait;

#[McpTool('my_tool', 'My Tool Description')]
public function myTool(): string
{
$this->log(...);
```

## Sampling

With [sampling](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling) servers can request clients to
execute "completions" or "generations" with a language model for them:

```php
$result = $clientGateway->sample('Roses are red, violets are', 350, 90, ['temperature' => 0.5]);
```

The `sample` method accepts four arguments:

1. `message`, which is **required** and accepts a string, an instance of `Content` or an array of `SampleMessage` instances.
2. `maxTokens`, which defaults to `1000`
3. `timeout` in seconds, which defaults to `120`
4. `options` which might include `system_prompt`, `preferences` for model choice, `includeContext`, `temperature`, `stopSequences` and `metadata`

[Find more details to sampling payload in the specification.](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling#protocol-messages)

## Logging

The [Logging](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging) utility enables servers
to send structured log messages as notifcation to clients:

```php
use Mcp\Schema\Enum\LoggingLevel;

$clientGateway->log(LoggingLevel::Warning, 'The end is near.');
```

## Progress

With a [Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress)
notification a server can update a client while an operation is ongoing:

```php
$clientGateway->progress(4.2, 10, 'Downloading needed images.');
```

## Notification

Lastly, the server can push all kind of notifications, that implement the `Mcp\Schema\JsonRpc\Notification` interface
to the client to:

```php
$clientGateway->notify($yourNotification);
```
10 changes: 10 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ $server = Server::builder()
->setDiscovery(__DIR__, ['.'], [], $cache)
```

### Client Communication

**File**: `examples/stdio-client-communication/`

**What it demostrates:**
- Server initiated communcation back to the client
- Logging, sampling, progress and notifications
- Using `ClientGateway` in service class via `ClientAwareInterface` and corresponding trait
- Using `ClientGateway` in tool method via method argument injection

## HTTP Examples

### Discovery User Profile
Expand Down
11 changes: 2 additions & 9 deletions examples/http-client-communication/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@

use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Exception\ToolCallException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\ClientGateway;
Expand Down Expand Up @@ -57,18 +55,13 @@ function (string $projectName, array $milestones, ClientGateway $client): array
implode(', ', $milestones)
);

$response = $client->sample(
prompt: $prompt,
$result = $client->sample(
message: $prompt,
maxTokens: 400,
timeout: 90,
options: ['temperature' => 0.4]
);

if ($response instanceof JsonRpcError) {
throw new ToolCallException(sprintf('Sampling request failed (%d): %s', $response->code, $response->message));
}

$result = $response->result;
$content = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';

$client->log(LoggingLevel::Info, 'Briefing ready, returning to caller.');
Expand Down
71 changes: 71 additions & 0 deletions examples/stdio-client-communication/ClientAwareService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\Example\StdioClientCommunication;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Server\ClientAwareInterface;
use Mcp\Server\ClientAwareTrait;
use Psr\Log\LoggerInterface;

final class ClientAwareService implements ClientAwareInterface
{
use ClientAwareTrait;

public function __construct(
private readonly LoggerInterface $logger,
) {
$this->logger->info('SamplingTool instantiated for sampling example.');
}

/**
* @return array{incident: string, recommended_actions: string, model: string}
*/
#[McpTool('coordinate_incident_response', 'Coordinate an incident response with logging, progress, and sampling.')]
public function coordinateIncident(string $incidentTitle): array
{
$this->log(LoggingLevel::Warning, \sprintf('Incident triage started: %s', $incidentTitle));

$steps = [
'Collecting telemetry',
'Assessing scope',
'Coordinating responders',
];

foreach ($steps as $index => $step) {
$progress = ($index + 1) / \count($steps);

$this->progress($progress, 1, $step);

usleep(180_000); // Simulate work being done
}

$prompt = \sprintf(
'Provide a concise response strategy for incident "%s" based on the steps completed: %s.',
$incidentTitle,
implode(', ', $steps)
);

$result = $this->sample($prompt, 350, 90, ['temperature' => 0.5]);

$recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';

$this->log(LoggingLevel::Info, \sprintf('Incident triage completed for %s', $incidentTitle));

return [
'incident' => $incidentTitle,
'recommended_actions' => $recommendation,
'model' => $result->model,
];
}
}
56 changes: 2 additions & 54 deletions examples/stdio-client-communication/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,18 @@
require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\ClientGateway;
use Mcp\Server\Transport\StdioTransport;

$capabilities = new ServerCapabilities(logging: true, tools: true);

$server = Server::builder()
->setServerInfo('STDIO Client Communication Demo', '1.0.0')
->setLogger(logger())
->setContainer(container())
->setCapabilities($capabilities)
->addTool(
function (string $incidentTitle, ClientGateway $client): array {
$client->log(LoggingLevel::Warning, sprintf('Incident triage started: %s', $incidentTitle));

$steps = [
'Collecting telemetry',
'Assessing scope',
'Coordinating responders',
];

foreach ($steps as $index => $step) {
$progress = ($index + 1) / count($steps);

$client->progress(progress: $progress, total: 1, message: $step);

usleep(180_000); // Simulate work being done
}

$prompt = sprintf(
'Provide a concise response strategy for incident "%s" based on the steps completed: %s.',
$incidentTitle,
implode(', ', $steps)
);

$sampling = $client->sample(
prompt: $prompt,
maxTokens: 350,
timeout: 90,
options: ['temperature' => 0.5]
);

if ($sampling instanceof JsonRpcError) {
throw new RuntimeException(sprintf('Sampling request failed (%d): %s', $sampling->code, $sampling->message));
}

$result = $sampling->result;
$recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';

$client->log(LoggingLevel::Info, sprintf('Incident triage completed for %s', $incidentTitle));

return [
'incident' => $incidentTitle,
'recommended_actions' => $recommendation,
'model' => $result->model,
];
},
name: 'coordinate_incident_response',
description: 'Coordinate an incident response with logging, progress, and sampling.'
)
->setCapabilities(new ServerCapabilities(logging: true, tools: true))
->setDiscovery(__DIR__)
->addTool(
function (string $dataset, ClientGateway $client): array {
$client->log(LoggingLevel::Info, sprintf('Running quality checks on dataset "%s"', $dataset));
Expand Down
21 changes: 19 additions & 2 deletions src/Capability/Registry/ReferenceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\RegistryException;
use Mcp\Server\ClientAwareInterface;
use Mcp\Server\ClientGateway;
use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface;

/**
Expand All @@ -31,12 +33,18 @@ public function __construct(
*/
public function handle(ElementReference $reference, array $arguments): mixed
{
$session = $arguments['_session'];

if (\is_string($reference->handler)) {
if (class_exists($reference->handler) && method_exists($reference->handler, '__invoke')) {
$reflection = new \ReflectionMethod($reference->handler, '__invoke');
$instance = $this->getClassInstance($reference->handler);
$arguments = $this->prepareArguments($reflection, $arguments);

if ($instance instanceof ClientAwareInterface) {
$instance->setClient(new ClientGateway($session));
}

return \call_user_func($instance, ...$arguments);
}

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

if (\is_callable($reference->handler)) {
$reflection = $this->getReflectionForCallable($reference->handler);
$reflection = $this->getReflectionForCallable($reference->handler, $session);
$arguments = $this->prepareArguments($reflection, $arguments);

return \call_user_func($reference->handler, ...$arguments);
Expand All @@ -59,6 +67,11 @@ public function handle(ElementReference $reference, array $arguments): mixed
[$className, $methodName] = $reference->handler;
$reflection = new \ReflectionMethod($className, $methodName);
$instance = $this->getClassInstance($className);

if ($instance instanceof ClientAwareInterface) {
$instance->setClient(new ClientGateway($session));
}

$arguments = $this->prepareArguments($reflection, $arguments);

return \call_user_func([$instance, $methodName], ...$arguments);
Expand Down Expand Up @@ -130,7 +143,7 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
/**
* Gets a ReflectionMethod or ReflectionFunction for a callable.
*/
private function getReflectionForCallable(callable $handler): \ReflectionMethod|\ReflectionFunction
private function getReflectionForCallable(callable $handler, SessionInterface $session): \ReflectionMethod|\ReflectionFunction
{
if (\is_string($handler)) {
return new \ReflectionFunction($handler);
Expand All @@ -143,6 +156,10 @@ private function getReflectionForCallable(callable $handler): \ReflectionMethod|
if (\is_array($handler) && 2 === \count($handler)) {
[$class, $method] = $handler;

if ($class instanceof ClientAwareInterface) {
$class->setClient(new ClientGateway($session));
}

return new \ReflectionMethod($class, $method);
}

Expand Down
Loading