Skip to content

Commit 83504fd

Browse files
feat(server): add bidirectional client communication support
1 parent 54a5d14 commit 83504fd

38 files changed

+1974
-446
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"Mcp\\Example\\StdioDiscoveryCalculator\\": "examples/stdio-discovery-calculator/",
6161
"Mcp\\Example\\StdioEnvVariables\\": "examples/stdio-env-variables/",
6262
"Mcp\\Example\\StdioExplicitRegistration\\": "examples/stdio-explicit-registration/",
63+
"Mcp\\Example\\CustomMethodHandlers\\": "examples/custom-method-handlers/",
6364
"Mcp\\Tests\\": "tests/"
6465
}
6566
},
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\CustomMethodHandlers;
13+
14+
use Mcp\Schema\Content\TextContent;
15+
use Mcp\Schema\JsonRpc\Error;
16+
use Mcp\Schema\JsonRpc\Request;
17+
use Mcp\Schema\JsonRpc\Response;
18+
use Mcp\Schema\Request\CallToolRequest;
19+
use Mcp\Schema\Result\CallToolResult;
20+
use Mcp\Schema\Tool;
21+
use Mcp\Server\Handler\Request\RequestHandlerInterface;
22+
use Mcp\Server\Session\SessionInterface;
23+
24+
/** @implements RequestHandlerInterface<CallToolResult> */
25+
class CallToolRequestHandler implements RequestHandlerInterface
26+
{
27+
/**
28+
* @param array<string, Tool> $toolDefinitions
29+
*/
30+
public function __construct(private array $toolDefinitions)
31+
{
32+
}
33+
34+
public function supports(Request $request): bool
35+
{
36+
return $request instanceof CallToolRequest;
37+
}
38+
39+
/**
40+
* @return Response<CallToolResult>|Error
41+
*/
42+
public function handle(Request $request, SessionInterface $session): Response|Error
43+
{
44+
\assert($request instanceof CallToolRequest);
45+
46+
$name = $request->name;
47+
$args = $request->arguments ?? [];
48+
49+
if (!isset($this->toolDefinitions[$name])) {
50+
return new Error($request->getId(), Error::METHOD_NOT_FOUND, \sprintf('Tool not found: %s', $name));
51+
}
52+
53+
try {
54+
switch ($name) {
55+
case 'say_hello':
56+
$greetName = (string) ($args['name'] ?? 'world');
57+
$result = [new TextContent(\sprintf('Hello, %s!', $greetName))];
58+
break;
59+
case 'sum':
60+
$a = (float) ($args['a'] ?? 0);
61+
$b = (float) ($args['b'] ?? 0);
62+
$result = [new TextContent((string) ($a + $b))];
63+
break;
64+
default:
65+
$result = [new TextContent('Unknown tool')];
66+
}
67+
68+
return new Response($request->getId(), new CallToolResult($result));
69+
} catch (\Throwable $e) {
70+
return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
71+
}
72+
}
73+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\CustomMethodHandlers;
13+
14+
use Mcp\Schema\JsonRpc\Request;
15+
use Mcp\Schema\JsonRpc\Response;
16+
use Mcp\Schema\Request\ListToolsRequest;
17+
use Mcp\Schema\Result\ListToolsResult;
18+
use Mcp\Schema\Tool;
19+
use Mcp\Server\Handler\Request\RequestHandlerInterface;
20+
use Mcp\Server\Session\SessionInterface;
21+
22+
/** @implements RequestHandlerInterface<ListToolsResult> */
23+
class ListToolsRequestHandler implements RequestHandlerInterface
24+
{
25+
/**
26+
* @param array<string, Tool> $toolDefinitions
27+
*/
28+
public function __construct(private array $toolDefinitions)
29+
{
30+
}
31+
32+
public function supports(Request $request): bool
33+
{
34+
return $request instanceof ListToolsRequest;
35+
}
36+
37+
/**
38+
* @return Response<ListToolsResult>
39+
*/
40+
public function handle(Request $request, SessionInterface $session): Response
41+
{
42+
\assert($request instanceof ListToolsRequest);
43+
44+
return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
45+
}
46+
}

examples/custom-method-handlers/server.php

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,11 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16-
use Mcp\Schema\Content\TextContent;
17-
use Mcp\Schema\JsonRpc\Error;
18-
use Mcp\Schema\JsonRpc\Request;
19-
use Mcp\Schema\JsonRpc\Response;
20-
use Mcp\Schema\Request\CallToolRequest;
21-
use Mcp\Schema\Request\ListToolsRequest;
22-
use Mcp\Schema\Result\CallToolResult;
23-
use Mcp\Schema\Result\ListToolsResult;
16+
use Mcp\Example\CustomMethodHandlers\CallToolRequestHandler;
17+
use Mcp\Example\CustomMethodHandlers\ListToolsRequestHandler;
2418
use Mcp\Schema\ServerCapabilities;
2519
use Mcp\Schema\Tool;
2620
use Mcp\Server;
27-
use Mcp\Server\Handler\Request\RequestHandlerInterface;
28-
use Mcp\Server\Session\SessionInterface;
2921
use Mcp\Server\Transport\StdioTransport;
3022

3123
logger()->info('Starting MCP Custom Method Handlers (Stdio) Server...');
@@ -58,73 +50,8 @@
5850
),
5951
];
6052

61-
$listToolsHandler = new class($toolDefinitions) implements RequestHandlerInterface {
62-
/**
63-
* @param array<string, Tool> $toolDefinitions
64-
*/
65-
public function __construct(private array $toolDefinitions)
66-
{
67-
}
68-
69-
public function supports(Request $request): bool
70-
{
71-
return $request instanceof ListToolsRequest;
72-
}
73-
74-
public function handle(Request $request, SessionInterface $session): Response
75-
{
76-
assert($request instanceof ListToolsRequest);
77-
78-
return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
79-
}
80-
};
81-
82-
$callToolHandler = new class($toolDefinitions) implements RequestHandlerInterface {
83-
/**
84-
* @param array<string, Tool> $toolDefinitions
85-
*/
86-
public function __construct(private array $toolDefinitions)
87-
{
88-
}
89-
90-
public function supports(Request $request): bool
91-
{
92-
return $request instanceof CallToolRequest;
93-
}
94-
95-
public function handle(Request $request, SessionInterface $session): Response|Error
96-
{
97-
assert($request instanceof CallToolRequest);
98-
99-
$name = $request->name;
100-
$args = $request->arguments ?? [];
101-
102-
if (!isset($this->toolDefinitions[$name])) {
103-
return new Error($request->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name));
104-
}
105-
106-
try {
107-
switch ($name) {
108-
case 'say_hello':
109-
$greetName = (string) ($args['name'] ?? 'world');
110-
$result = [new TextContent(sprintf('Hello, %s!', $greetName))];
111-
break;
112-
case 'sum':
113-
$a = (float) ($args['a'] ?? 0);
114-
$b = (float) ($args['b'] ?? 0);
115-
$result = [new TextContent((string) ($a + $b))];
116-
break;
117-
default:
118-
$result = [new TextContent('Unknown tool')];
119-
}
120-
121-
return new Response($request->getId(), new CallToolResult($result));
122-
} catch (Throwable $e) {
123-
return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
124-
}
125-
}
126-
};
127-
53+
$listToolsHandler = new ListToolsRequestHandler($toolDefinitions);
54+
$callToolHandler = new CallToolRequestHandler($toolDefinitions);
12855
$capabilities = new ServerCapabilities(tools: true, resources: false, prompts: false);
12956

13057
$server = Server::builder()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
require_once dirname(__DIR__).'/bootstrap.php';
13+
chdir(__DIR__);
14+
15+
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
16+
use Mcp\Schema\Content\TextContent;
17+
use Mcp\Schema\Enum\LoggingLevel;
18+
use Mcp\Schema\JsonRpc\Error as JsonRpcError;
19+
use Mcp\Schema\ServerCapabilities;
20+
use Mcp\Server;
21+
use Mcp\Server\ClientGateway;
22+
use Mcp\Server\Session\FileSessionStore;
23+
use Mcp\Server\Transport\StreamableHttpTransport;
24+
use Nyholm\Psr7\Factory\Psr17Factory;
25+
use Nyholm\Psr7Server\ServerRequestCreator;
26+
27+
$psr17Factory = new Psr17Factory();
28+
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
29+
$request = $creator->fromGlobals();
30+
31+
$sessionDir = __DIR__.'/sessions';
32+
$capabilities = new ServerCapabilities(logging: true, tools: true);
33+
34+
$server = Server::builder()
35+
->setServerInfo('HTTP Client Communication Demo', '1.0.0')
36+
->setLogger(logger())
37+
->setContainer(container())
38+
->setSession(new FileSessionStore($sessionDir))
39+
->setCapabilities($capabilities)
40+
->addTool(
41+
function (string $projectName, array $milestones, ClientGateway $client): array {
42+
$client->log(LoggingLevel::Info, sprintf('Preparing project briefing for "%s"', $projectName));
43+
44+
$totalSteps = max(1, count($milestones));
45+
46+
foreach ($milestones as $index => $milestone) {
47+
$progress = ($index + 1) / $totalSteps;
48+
$message = sprintf('Analyzing milestone "%s"', $milestone);
49+
50+
$client->progress(progress: $progress, total: 1, message: $message);
51+
52+
usleep(150_000); // Simulate work being done
53+
}
54+
55+
$prompt = sprintf(
56+
'Draft a concise stakeholder briefing for the project "%s". Highlight key milestones: %s. Focus on risks and next steps.',
57+
$projectName,
58+
implode(', ', $milestones)
59+
);
60+
61+
$response = $client->sample(
62+
prompt: $prompt,
63+
maxTokens: 400,
64+
timeout: 90,
65+
options: ['temperature' => 0.4]
66+
);
67+
68+
if ($response instanceof JsonRpcError) {
69+
throw new RuntimeException(sprintf('Sampling request failed (%d): %s', $response->code, $response->message));
70+
}
71+
72+
$result = $response->result;
73+
$content = $result->content instanceof TextContent ? trim((string) $result->content->text) : '';
74+
75+
$client->log(LoggingLevel::Info, 'Briefing ready, returning to caller.');
76+
77+
return [
78+
'project' => $projectName,
79+
'milestones_reviewed' => $milestones,
80+
'briefing' => $content,
81+
'model' => $result->model,
82+
'stop_reason' => $result->stopReason,
83+
];
84+
},
85+
name: 'prepare_project_briefing',
86+
description: 'Compile a stakeholder briefing with live logging, progress updates, and LLM sampling.'
87+
)
88+
->addTool(
89+
function (string $serviceName, ClientGateway $client): array {
90+
$client->log(LoggingLevel::Info, sprintf('Starting maintenance checks for "%s"', $serviceName));
91+
92+
$steps = [
93+
'Verifying health metrics',
94+
'Checking recent deployments',
95+
'Reviewing alert stream',
96+
'Summarizing findings',
97+
];
98+
99+
foreach ($steps as $index => $step) {
100+
$progress = ($index + 1) / count($steps);
101+
102+
$client->progress(progress: $progress, total: 1, message: $step);
103+
104+
usleep(120_000); // Simulate work being done
105+
}
106+
107+
$client->log(LoggingLevel::Info, sprintf('Maintenance checks complete for "%s"', $serviceName));
108+
109+
return [
110+
'service' => $serviceName,
111+
'status' => 'operational',
112+
'notes' => 'No critical issues detected during automated sweep.',
113+
];
114+
},
115+
name: 'run_service_maintenance',
116+
description: 'Simulate service maintenance with logging and progress notifications.'
117+
)
118+
->build();
119+
120+
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory, logger());
121+
122+
$response = $server->run($transport);
123+
124+
(new SapiEmitter())->emit($response);

0 commit comments

Comments
 (0)