[Server] Add Bidirectional Client Communication Support (#109)
* feat(server): add bidirectional client communication support * fix(server): use correct session ID string conversion method * fix: clean up docblocks for request handler methods in Builder * fix: merge artifacts
This commit is contained in:
parent
0963d243bc
commit
f1b471a219
5 changed files with 1973 additions and 445 deletions
73
examples/custom-method-handlers/CallToolRequestHandler.php
Normal file
73
examples/custom-method-handlers/CallToolRequestHandler.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?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\CustomMethodHandlers;
|
||||
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Schema\Tool;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/** @implements RequestHandlerInterface<CallToolResult> */
|
||||
class CallToolRequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, Tool> $toolDefinitions
|
||||
*/
|
||||
public function __construct(private array $toolDefinitions)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $request instanceof CallToolRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response<CallToolResult>|Error
|
||||
*/
|
||||
public function handle(Request $request, SessionInterface $session): Response|Error
|
||||
{
|
||||
\assert($request instanceof CallToolRequest);
|
||||
|
||||
$name = $request->name;
|
||||
$args = $request->arguments ?? [];
|
||||
|
||||
if (!isset($this->toolDefinitions[$name])) {
|
||||
return new Error($request->getId(), Error::METHOD_NOT_FOUND, \sprintf('Tool not found: %s', $name));
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($name) {
|
||||
case 'say_hello':
|
||||
$greetName = (string) ($args['name'] ?? 'world');
|
||||
$result = [new TextContent(\sprintf('Hello, %s!', $greetName))];
|
||||
break;
|
||||
case 'sum':
|
||||
$a = (float) ($args['a'] ?? 0);
|
||||
$b = (float) ($args['b'] ?? 0);
|
||||
$result = [new TextContent((string) ($a + $b))];
|
||||
break;
|
||||
default:
|
||||
$result = [new TextContent('Unknown tool')];
|
||||
}
|
||||
|
||||
return new Response($request->getId(), new CallToolResult($result));
|
||||
} catch (\Throwable $e) {
|
||||
return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
examples/custom-method-handlers/ListToolsRequestHandler.php
Normal file
46
examples/custom-method-handlers/ListToolsRequestHandler.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?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\CustomMethodHandlers;
|
||||
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListToolsRequest;
|
||||
use Mcp\Schema\Result\ListToolsResult;
|
||||
use Mcp\Schema\Tool;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/** @implements RequestHandlerInterface<ListToolsResult> */
|
||||
class ListToolsRequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, Tool> $toolDefinitions
|
||||
*/
|
||||
public function __construct(private array $toolDefinitions)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $request instanceof ListToolsRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response<ListToolsResult>
|
||||
*/
|
||||
public function handle(Request $request, SessionInterface $session): Response
|
||||
{
|
||||
\assert($request instanceof ListToolsRequest);
|
||||
|
||||
return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,19 +13,11 @@
|
|||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
chdir(__DIR__);
|
||||
|
||||
use Mcp\Schema\Content\TextContent;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Request\ListToolsRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Schema\Result\ListToolsResult;
|
||||
use Mcp\Example\CustomMethodHandlers\CallToolRequestHandler;
|
||||
use Mcp\Example\CustomMethodHandlers\ListToolsRequestHandler;
|
||||
use Mcp\Schema\ServerCapabilities;
|
||||
use Mcp\Schema\Tool;
|
||||
use Mcp\Server;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Mcp\Server\Transport\StdioTransport;
|
||||
|
||||
logger()->info('Starting MCP Custom Method Handlers (Stdio) Server...');
|
||||
|
|
@ -58,73 +50,8 @@ $toolDefinitions = [
|
|||
),
|
||||
];
|
||||
|
||||
$listToolsHandler = new class($toolDefinitions) implements RequestHandlerInterface {
|
||||
/**
|
||||
* @param array<string, Tool> $toolDefinitions
|
||||
*/
|
||||
public function __construct(private array $toolDefinitions)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $request instanceof ListToolsRequest;
|
||||
}
|
||||
|
||||
public function handle(Request $request, SessionInterface $session): Response
|
||||
{
|
||||
assert($request instanceof ListToolsRequest);
|
||||
|
||||
return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
|
||||
}
|
||||
};
|
||||
|
||||
$callToolHandler = new class($toolDefinitions) implements RequestHandlerInterface {
|
||||
/**
|
||||
* @param array<string, Tool> $toolDefinitions
|
||||
*/
|
||||
public function __construct(private array $toolDefinitions)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $request instanceof CallToolRequest;
|
||||
}
|
||||
|
||||
public function handle(Request $request, SessionInterface $session): Response|Error
|
||||
{
|
||||
assert($request instanceof CallToolRequest);
|
||||
|
||||
$name = $request->name;
|
||||
$args = $request->arguments ?? [];
|
||||
|
||||
if (!isset($this->toolDefinitions[$name])) {
|
||||
return new Error($request->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name));
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($name) {
|
||||
case 'say_hello':
|
||||
$greetName = (string) ($args['name'] ?? 'world');
|
||||
$result = [new TextContent(sprintf('Hello, %s!', $greetName))];
|
||||
break;
|
||||
case 'sum':
|
||||
$a = (float) ($args['a'] ?? 0);
|
||||
$b = (float) ($args['b'] ?? 0);
|
||||
$result = [new TextContent((string) ($a + $b))];
|
||||
break;
|
||||
default:
|
||||
$result = [new TextContent('Unknown tool')];
|
||||
}
|
||||
|
||||
return new Response($request->getId(), new CallToolResult($result));
|
||||
} catch (Throwable $e) {
|
||||
return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$listToolsHandler = new ListToolsRequestHandler($toolDefinitions);
|
||||
$callToolHandler = new CallToolRequestHandler($toolDefinitions);
|
||||
$capabilities = new ServerCapabilities(tools: true, resources: false, prompts: false);
|
||||
|
||||
$server = Server::builder()
|
||||
|
|
|
|||
124
examples/http-client-communication/server.php
Normal file
124
examples/http-client-communication/server.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
chdir(__DIR__);
|
||||
|
||||
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
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\Session\FileSessionStore;
|
||||
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||
|
||||
$psr17Factory = new Psr17Factory();
|
||||
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||
$request = $creator->fromGlobals();
|
||||
|
||||
$sessionDir = __DIR__.'/sessions';
|
||||
$capabilities = new ServerCapabilities(logging: true, tools: true);
|
||||
|
||||
$server = Server::builder()
|
||||
->setServerInfo('HTTP Client Communication Demo', '1.0.0')
|
||||
->setLogger(logger())
|
||||
->setContainer(container())
|
||||
->setSession(new FileSessionStore($sessionDir))
|
||||
->setCapabilities($capabilities)
|
||||
->addTool(
|
||||
function (string $projectName, array $milestones, ClientGateway $client): array {
|
||||
$client->log(LoggingLevel::Info, sprintf('Preparing project briefing for "%s"', $projectName));
|
||||
|
||||
$totalSteps = max(1, count($milestones));
|
||||
|
||||
foreach ($milestones as $index => $milestone) {
|
||||
$progress = ($index + 1) / $totalSteps;
|
||||
$message = sprintf('Analyzing milestone "%s"', $milestone);
|
||||
|
||||
$client->progress(progress: $progress, total: 1, message: $message);
|
||||
|
||||
usleep(150_000); // Simulate work being done
|
||||
}
|
||||
|
||||
$prompt = sprintf(
|
||||
'Draft a concise stakeholder briefing for the project "%s". Highlight key milestones: %s. Focus on risks and next steps.',
|
||||
$projectName,
|
||||
implode(', ', $milestones)
|
||||
);
|
||||
|
||||
$response = $client->sample(
|
||||
prompt: $prompt,
|
||||
maxTokens: 400,
|
||||
timeout: 90,
|
||||
options: ['temperature' => 0.4]
|
||||
);
|
||||
|
||||
if ($response instanceof JsonRpcError) {
|
||||
throw new RuntimeException(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.');
|
||||
|
||||
return [
|
||||
'project' => $projectName,
|
||||
'milestones_reviewed' => $milestones,
|
||||
'briefing' => $content,
|
||||
'model' => $result->model,
|
||||
'stop_reason' => $result->stopReason,
|
||||
];
|
||||
},
|
||||
name: 'prepare_project_briefing',
|
||||
description: 'Compile a stakeholder briefing with live logging, progress updates, and LLM sampling.'
|
||||
)
|
||||
->addTool(
|
||||
function (string $serviceName, ClientGateway $client): array {
|
||||
$client->log(LoggingLevel::Info, sprintf('Starting maintenance checks for "%s"', $serviceName));
|
||||
|
||||
$steps = [
|
||||
'Verifying health metrics',
|
||||
'Checking recent deployments',
|
||||
'Reviewing alert stream',
|
||||
'Summarizing findings',
|
||||
];
|
||||
|
||||
foreach ($steps as $index => $step) {
|
||||
$progress = ($index + 1) / count($steps);
|
||||
|
||||
$client->progress(progress: $progress, total: 1, message: $step);
|
||||
|
||||
usleep(120_000); // Simulate work being done
|
||||
}
|
||||
|
||||
$client->log(LoggingLevel::Info, sprintf('Maintenance checks complete for "%s"', $serviceName));
|
||||
|
||||
return [
|
||||
'service' => $serviceName,
|
||||
'status' => 'operational',
|
||||
'notes' => 'No critical issues detected during automated sweep.',
|
||||
];
|
||||
},
|
||||
name: 'run_service_maintenance',
|
||||
description: 'Simulate service maintenance with logging and progress notifications.'
|
||||
)
|
||||
->build();
|
||||
|
||||
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory, logger());
|
||||
|
||||
$response = $server->run($transport);
|
||||
|
||||
(new SapiEmitter())->emit($response);
|
||||
115
examples/stdio-client-communication/server.php
Normal file
115
examples/stdio-client-communication/server.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env php
|
||||
<?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.
|
||||
*/
|
||||
|
||||
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.'
|
||||
)
|
||||
->addTool(
|
||||
function (string $dataset, ClientGateway $client): array {
|
||||
$client->log(LoggingLevel::Info, sprintf('Running quality checks on dataset "%s"', $dataset));
|
||||
|
||||
$tasks = [
|
||||
'Validating schema',
|
||||
'Scanning for anomalies',
|
||||
'Reviewing statistical summary',
|
||||
];
|
||||
|
||||
foreach ($tasks as $index => $task) {
|
||||
$progress = ($index + 1) / count($tasks);
|
||||
|
||||
$client->progress(progress: $progress, total: 1, message: $task);
|
||||
|
||||
usleep(140_000); // Simulate work being done
|
||||
}
|
||||
|
||||
$client->log(LoggingLevel::Info, sprintf('Dataset "%s" passed automated checks.', $dataset));
|
||||
|
||||
return [
|
||||
'dataset' => $dataset,
|
||||
'status' => 'passed',
|
||||
'notes' => 'No significant integrity issues detected during automated checks.',
|
||||
];
|
||||
},
|
||||
name: 'run_dataset_quality_checks',
|
||||
description: 'Perform dataset quality checks with progress updates and logging.'
|
||||
)
|
||||
->build();
|
||||
|
||||
$transport = new StdioTransport();
|
||||
|
||||
$status = $server->run($transport);
|
||||
|
||||
exit($status);
|
||||
Loading…
Reference in a new issue