Skip to content

Commit b7f6a77

Browse files
committed
Adopt server examples
1 parent c69acdb commit b7f6a77

File tree

79 files changed

+2980
-753
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2980
-753
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.php-cs-fixer.cache
33
composer.lock
44
vendor
5+
examples/**/dev.log

composer.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"ext-fileinfo": "*",
2323
"opis/json-schema": "^2.4",
2424
"phpdocumentor/reflection-docblock": "^5.6",
25+
"psr/container": "^2.0",
2526
"psr/event-dispatcher": "^1.0",
2627
"psr/log": "^1.0 || ^2.0 || ^3.0",
2728
"symfony/finder": "^6.4 || ^7.2",
@@ -34,7 +35,9 @@
3435
"phpunit/phpunit": "^10.5",
3536
"psr/cache": "^3.0",
3637
"symfony/console": "^6.4 || ^7.0",
37-
"symfony/event-dispatcher": "^6.4 || ^7.0"
38+
"symfony/dependency-injection": "^6.4 || ^7.0",
39+
"symfony/event-dispatcher": "^6.4 || ^7.0",
40+
"symfony/process": "^6.4 || ^7.0"
3841
},
3942
"suggest": {
4043
"symfony/console": "To use SymfonyConsoleTransport for STDIO",
@@ -47,6 +50,14 @@
4750
},
4851
"autoload-dev": {
4952
"psr-4": {
53+
"Mcp\\Example\\StdioCalculatorExample\\": "examples/01-discovery-stdio-calculator/",
54+
"Mcp\\Example\\HttpUserProfileExample\\": "examples/02-discovery-http-userprofile/",
55+
"Mcp\\Example\\ManualStdioExample\\": "examples/03-manual-registration-stdio/",
56+
"Mcp\\Example\\CombinedHttpExample\\": "examples/04-combined-registration-http/",
57+
"Mcp\\Example\\StdioEnvVariables\\": "examples/05-stdio-env-variables/",
58+
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
59+
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
60+
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
5061
"Mcp\\Tests\\": "tests/"
5162
}
5263
},
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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\StdioCalculatorExample;
13+
14+
use Mcp\Capability\Attribute\McpResource;
15+
use Mcp\Capability\Attribute\McpTool;
16+
use Psr\Log\LoggerInterface;
17+
use Psr\Log\NullLogger;
18+
19+
/**
20+
* @phpstan-type Config array{precision: int, allow_negative: bool}
21+
*/
22+
class McpElements
23+
{
24+
/**
25+
* @var Config
26+
*/
27+
private array $config = [
28+
'precision' => 2,
29+
'allow_negative' => true,
30+
];
31+
32+
public function __construct(
33+
private readonly LoggerInterface $logger = new NullLogger(),
34+
) {
35+
}
36+
37+
/**
38+
* Performs a calculation based on the operation.
39+
*
40+
* Supports 'add', 'subtract', 'multiply', 'divide'.
41+
* Obeys the 'precision' and 'allow_negative' settings from the config resource.
42+
*
43+
* @param float $a the first operand
44+
* @param float $b the second operand
45+
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
46+
*
47+
* @return float|string the result of the calculation, or an error message string
48+
*/
49+
#[McpTool(name: 'calculate')]
50+
public function calculate(float $a, float $b, string $operation): float|string
51+
{
52+
$this->logger->info(\sprintf('Calculating: %f %s %f', $a, $operation, $b));
53+
54+
$op = strtolower($operation);
55+
56+
switch ($op) {
57+
case 'add':
58+
$result = $a + $b;
59+
break;
60+
case 'subtract':
61+
$result = $a - $b;
62+
break;
63+
case 'multiply':
64+
$result = $a * $b;
65+
break;
66+
case 'divide':
67+
if (0 == $b) {
68+
return 'Error: Division by zero.';
69+
}
70+
$result = $a / $b;
71+
break;
72+
default:
73+
return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
74+
}
75+
76+
if (!$this->config['allow_negative'] && $result < 0) {
77+
return 'Error: Negative results are disabled.';
78+
}
79+
80+
return round($result, $this->config['precision']);
81+
}
82+
83+
/**
84+
* Provides the current calculator configuration.
85+
* Can be read by clients to understand precision etc.
86+
*
87+
* @return Config the configuration array
88+
*/
89+
#[McpResource(
90+
uri: 'config://calculator/settings',
91+
name: 'calculator_config',
92+
description: 'Current settings for the calculator tool (precision, allow_negative).',
93+
mimeType: 'application/json',
94+
)]
95+
public function getConfiguration(): array
96+
{
97+
$this->logger->info('Resource config://calculator/settings read.');
98+
99+
return $this->config;
100+
}
101+
102+
/**
103+
* Updates a specific configuration setting.
104+
* Note: This requires more robust validation in a real app.
105+
*
106+
* @param string $setting the setting key ('precision' or 'allow_negative')
107+
* @param mixed $value the new value (int for precision, bool for allow_negative)
108+
*
109+
* @return array{
110+
* success: bool,
111+
* error?: string,
112+
* message?: string
113+
* } success message or error
114+
*/
115+
#[McpTool(name: 'update_setting')]
116+
public function updateSetting(string $setting, mixed $value): array
117+
{
118+
$this->logger->info(\sprintf('Setting tool called: setting=%s, value=%s', $setting, var_export($value, true)));
119+
if (!\array_key_exists($setting, $this->config)) {
120+
return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
121+
}
122+
123+
if ('precision' === $setting) {
124+
if (!\is_int($value) || $value < 0 || $value > 10) {
125+
return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
126+
}
127+
$this->config['precision'] = $value;
128+
129+
// In real app, notify subscribers of config://calculator/settings change
130+
// $registry->notifyResourceChanged('config://calculator/settings');
131+
return ['success' => true, 'message' => "Precision updated to {$value}."];
132+
}
133+
134+
if (!\is_bool($value)) {
135+
// Attempt basic cast for flexibility
136+
if (\in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
137+
$value = true;
138+
} elseif (\in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
139+
$value = false;
140+
} else {
141+
return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
142+
}
143+
}
144+
$this->config['allow_negative'] = $value;
145+
146+
// $registry->notifyResourceChanged('config://calculator/settings');
147+
return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
148+
}
149+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of the official PHP MCP SDK.
6+
*
7+
* A collaboration between Symfony and the PHP Foundation.
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
require_once dirname(__DIR__).'/bootstrap.php';
14+
chdir(__DIR__);
15+
16+
use Mcp\Capability\Registry\Container;
17+
use Mcp\Server;
18+
use Mcp\Server\Transport\StdioTransport;
19+
use Psr\Log\LoggerInterface;
20+
21+
logger()->info('Starting MCP Stdio Calculator Server...');
22+
23+
Server::make()
24+
->withServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
25+
->withContainer(container())
26+
->withLogger(logger())
27+
->withDiscovery(__DIR__, ['.'])
28+
->build()
29+
->connect(new StdioTransport(logger: logger()));
30+
31+
logger()->info('Server listener stopped gracefully.');
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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\HttpUserProfileExample;
13+
14+
use Mcp\Capability\Attribute\CompletionProvider;
15+
use Mcp\Capability\Attribute\McpPrompt;
16+
use Mcp\Capability\Attribute\McpResource;
17+
use Mcp\Capability\Attribute\McpResourceTemplate;
18+
use Mcp\Capability\Attribute\McpTool;
19+
use Psr\Log\LoggerInterface;
20+
21+
class McpElements
22+
{
23+
// Simulate a simple user database
24+
private array $users = [
25+
'101' => ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'],
26+
'102' => ['name' => 'Bob', 'email' => '[email protected]', 'role' => 'user'],
27+
'103' => ['name' => 'Charlie', 'email' => '[email protected]', 'role' => 'user'],
28+
];
29+
30+
public function __construct(
31+
private LoggerInterface $logger,
32+
) {
33+
$this->logger->debug('HttpUserProfileExample McpElements instantiated.');
34+
}
35+
36+
/**
37+
* Retrieves the profile data for a specific user.
38+
*
39+
* @param string $userId the ID of the user (from URI)
40+
*
41+
* @return array user profile data
42+
*
43+
* @throws McpServerException if the user is not found
44+
*/
45+
#[McpResourceTemplate(
46+
uriTemplate: 'user://{userId}/profile',
47+
name: 'user_profile',
48+
description: 'Get profile information for a specific user ID.',
49+
mimeType: 'application/json'
50+
)]
51+
public function getUserProfile(
52+
#[CompletionProvider(values: ['101', '102', '103'])]
53+
string $userId,
54+
): array {
55+
$this->logger->info('Reading resource: user profile', ['userId' => $userId]);
56+
if (!isset($this->users[$userId])) {
57+
// Throwing an exception that Processor can turn into an error response
58+
throw McpServerException::invalidParams("User profile not found for ID: {$userId}");
59+
}
60+
61+
return $this->users[$userId];
62+
}
63+
64+
/**
65+
* Retrieves a list of all known user IDs.
66+
*
67+
* @return array list of user IDs
68+
*/
69+
#[McpResource(
70+
uri: 'user://list/ids',
71+
name: 'user_id_list',
72+
description: 'Provides a list of all available user IDs.',
73+
mimeType: 'application/json'
74+
)]
75+
public function listUserIds(): array
76+
{
77+
$this->logger->info('Reading resource: user ID list');
78+
79+
return array_keys($this->users);
80+
}
81+
82+
/**
83+
* Sends a welcome message to a user.
84+
* (This is a placeholder - in a real app, it might queue an email).
85+
*
86+
* @param string $userId the ID of the user to message
87+
* @param string|null $customMessage an optional custom message part
88+
*
89+
* @return array status of the operation
90+
*/
91+
#[McpTool(name: 'send_welcome')]
92+
public function sendWelcomeMessage(string $userId, ?string $customMessage = null): array
93+
{
94+
$this->logger->info('Executing tool: send_welcome', ['userId' => $userId]);
95+
if (!isset($this->users[$userId])) {
96+
return ['success' => false, 'error' => "User ID {$userId} not found."];
97+
}
98+
$user = $this->users[$userId];
99+
$message = "Welcome, {$user['name']}!";
100+
if ($customMessage) {
101+
$message .= ' '.$customMessage;
102+
}
103+
// Simulate sending
104+
$this->logger->info("Simulated sending message to {$user['email']}: {$message}");
105+
106+
return ['success' => true, 'message_sent' => $message];
107+
}
108+
109+
#[McpTool(name: 'test_tool_without_params')]
110+
public function testToolWithoutParams(): array
111+
{
112+
return ['success' => true, 'message' => 'Test tool without params'];
113+
}
114+
115+
/**
116+
* Generates a prompt to write a bio for a user.
117+
*
118+
* @param string $userId the user ID to generate the bio for
119+
* @param string $tone Desired tone (e.g., 'formal', 'casual').
120+
*
121+
* @return array prompt messages
122+
*
123+
* @throws McpServerException if user not found
124+
*/
125+
#[McpPrompt(name: 'generate_bio_prompt')]
126+
public function generateBio(
127+
#[CompletionProvider(provider: UserIdCompletionProvider::class)]
128+
string $userId,
129+
string $tone = 'professional',
130+
): array {
131+
$this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
132+
if (!isset($this->users[$userId])) {
133+
throw McpServerException::invalidParams("User not found for bio prompt: {$userId}");
134+
}
135+
$user = $this->users[$userId];
136+
137+
return [
138+
['role' => 'user', 'content' => "Write a short, {$tone} biography for {$user['name']} (Role: {$user['role']}, Email: {$user['email']}). Highlight their role within the system."],
139+
];
140+
}
141+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\HttpUserProfileExample;
13+
14+
use Mcp\Capability\Prompt\Completion\ProviderInterface;
15+
16+
class UserIdCompletionProvider implements ProviderInterface
17+
{
18+
public function getCompletions(string $currentValue): array
19+
{
20+
$availableUserIds = ['101', '102', '103'];
21+
22+
return array_filter($availableUserIds, fn (string $userId) => str_contains($userId, $currentValue));
23+
}
24+
}

0 commit comments

Comments
 (0)