Skip to content

Commit d4456d8

Browse files
committed
Adopt server examples
1 parent c69acdb commit d4456d8

File tree

71 files changed

+2686
-213
lines changed

Some content is hidden

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

71 files changed

+2686
-213
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: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
17+
/**
18+
* @phpstan-type Config array{precision: int, allow_negative: bool}
19+
*/
20+
class McpElements
21+
{
22+
/**
23+
* @var Config
24+
*/
25+
private array $config = [
26+
'precision' => 2,
27+
'allow_negative' => true,
28+
];
29+
30+
/**
31+
* Performs a calculation based on the operation.
32+
*
33+
* Supports 'add', 'subtract', 'multiply', 'divide'.
34+
* Obeys the 'precision' and 'allow_negative' settings from the config resource.
35+
*
36+
* @param float $a the first operand
37+
* @param float $b the second operand
38+
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
39+
*
40+
* @return float|string the result of the calculation, or an error message string
41+
*/
42+
#[McpTool(name: 'calculate')]
43+
public function calculate(float $a, float $b, string $operation): float|string
44+
{
45+
// Use STDERR for logs
46+
fwrite(\STDERR, "Calculate tool called: a=$a, b=$b, op=$operation\n");
47+
48+
$op = strtolower($operation);
49+
50+
switch ($op) {
51+
case 'add':
52+
$result = $a + $b;
53+
break;
54+
case 'subtract':
55+
$result = $a - $b;
56+
break;
57+
case 'multiply':
58+
$result = $a * $b;
59+
break;
60+
case 'divide':
61+
if (0 == $b) {
62+
return 'Error: Division by zero.';
63+
}
64+
$result = $a / $b;
65+
break;
66+
default:
67+
return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
68+
}
69+
70+
if (!$this->config['allow_negative'] && $result < 0) {
71+
return 'Error: Negative results are disabled.';
72+
}
73+
74+
return round($result, $this->config['precision']);
75+
}
76+
77+
/**
78+
* Provides the current calculator configuration.
79+
* Can be read by clients to understand precision etc.
80+
*
81+
* @return Config the configuration array
82+
*/
83+
#[McpResource(
84+
uri: 'config://calculator/settings',
85+
name: 'calculator_config',
86+
description: 'Current settings for the calculator tool (precision, allow_negative).',
87+
mimeType: 'application/json' // Return as JSON
88+
)]
89+
public function getConfiguration(): array
90+
{
91+
fwrite(\STDERR, "Resource config://calculator/settings read.\n");
92+
93+
return $this->config;
94+
}
95+
96+
/**
97+
* Updates a specific configuration setting.
98+
* Note: This requires more robust validation in a real app.
99+
*
100+
* @param string $setting the setting key ('precision' or 'allow_negative')
101+
* @param mixed $value the new value (int for precision, bool for allow_negative)
102+
*
103+
* @return array{
104+
* success: bool,
105+
* error?: string,
106+
* message?: string
107+
* } success message or error
108+
*/
109+
#[McpTool(name: 'update_setting')]
110+
public function updateSetting(string $setting, mixed $value): array
111+
{
112+
fwrite(\STDERR, "Update Setting tool called: setting=$setting, value=".var_export($value, true)."\n");
113+
if (!\array_key_exists($setting, $this->config)) {
114+
return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
115+
}
116+
117+
if ('precision' === $setting) {
118+
if (!\is_int($value) || $value < 0 || $value > 10) {
119+
return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
120+
}
121+
$this->config['precision'] = $value;
122+
123+
// In real app, notify subscribers of config://calculator/settings change
124+
// $registry->notifyResourceChanged('config://calculator/settings');
125+
return ['success' => true, 'message' => "Precision updated to {$value}."];
126+
}
127+
128+
if (!\is_bool($value)) {
129+
// Attempt basic cast for flexibility
130+
if (\in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
131+
$value = true;
132+
} elseif (\in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
133+
$value = false;
134+
} else {
135+
return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
136+
}
137+
}
138+
$this->config['allow_negative'] = $value;
139+
140+
// $registry->notifyResourceChanged('config://calculator/settings');
141+
return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
142+
}
143+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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\Server;
17+
use Mcp\Server\Transport\StdioTransport;
18+
19+
logger()->info('Starting MCP Stdio Calculator Server...');
20+
21+
Server::make()
22+
->withServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
23+
->withLogger(logger())
24+
->withDiscovery(__DIR__, ['.'])
25+
->build()
26+
->connect(new StdioTransport(logger: logger()));
27+
28+
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)