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
2 changes: 1 addition & 1 deletion .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
run: composer install --no-scripts

- name: Tests
run: vendor/bin/phpunit
run: vendor/bin/phpunit --exclude-group inspector

qa:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.php-cs-fixer.cache
composer.lock
vendor
examples/**/dev.log
13 changes: 12 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"ext-fileinfo": "*",
"opis/json-schema": "^2.4",
"phpdocumentor/reflection-docblock": "^5.6",
"psr/container": "^2.0",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/finder": "^6.4 || ^7.2",
Expand All @@ -34,7 +35,9 @@
"phpunit/phpunit": "^10.5",
"psr/cache": "^3.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/event-dispatcher": "^6.4 || ^7.0"
"symfony/dependency-injection": "^6.4 || ^7.0",
"symfony/event-dispatcher": "^6.4 || ^7.0",
"symfony/process": "^6.4 || ^7.0"
},
"suggest": {
"symfony/console": "To use SymfonyConsoleTransport for STDIO",
Expand All @@ -47,6 +50,14 @@
},
"autoload-dev": {
"psr-4": {
"Mcp\\Example\\StdioCalculatorExample\\": "examples/01-discovery-stdio-calculator/",
"Mcp\\Example\\HttpUserProfileExample\\": "examples/02-discovery-http-userprofile/",
"Mcp\\Example\\ManualStdioExample\\": "examples/03-manual-registration-stdio/",
"Mcp\\Example\\CombinedHttpExample\\": "examples/04-combined-registration-http/",
"Mcp\\Example\\StdioEnvVariables\\": "examples/05-stdio-env-variables/",
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
"Mcp\\Tests\\": "tests/"
}
},
Expand Down
149 changes: 149 additions & 0 deletions examples/01-discovery-stdio-calculator/McpElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?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\StdioCalculatorExample;

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
* @phpstan-type Config array{precision: int, allow_negative: bool}
*/
class McpElements
{
/**
* @var Config
*/
private array $config = [
'precision' => 2,
'allow_negative' => true,
];

public function __construct(
private readonly LoggerInterface $logger = new NullLogger(),
) {
}

/**
* Performs a calculation based on the operation.
*
* Supports 'add', 'subtract', 'multiply', 'divide'.
* Obeys the 'precision' and 'allow_negative' settings from the config resource.
*
* @param float $a the first operand
* @param float $b the second operand
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
*
* @return float|string the result of the calculation, or an error message string
*/
#[McpTool(name: 'calculate')]
public function calculate(float $a, float $b, string $operation): float|string
{
$this->logger->info(\sprintf('Calculating: %f %s %f', $a, $operation, $b));

$op = strtolower($operation);

switch ($op) {
case 'add':
$result = $a + $b;
break;
case 'subtract':
$result = $a - $b;
break;
case 'multiply':
$result = $a * $b;
break;
case 'divide':
if (0 == $b) {
return 'Error: Division by zero.';
}
$result = $a / $b;
break;
default:
return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
}

if (!$this->config['allow_negative'] && $result < 0) {
return 'Error: Negative results are disabled.';
}

return round($result, $this->config['precision']);
}

/**
* Provides the current calculator configuration.
* Can be read by clients to understand precision etc.
*
* @return Config the configuration array
*/
#[McpResource(
uri: 'config://calculator/settings',
name: 'calculator_config',
description: 'Current settings for the calculator tool (precision, allow_negative).',
mimeType: 'application/json',
)]
public function getConfiguration(): array
{
$this->logger->info('Resource config://calculator/settings read.');

return $this->config;
}

/**
* Updates a specific configuration setting.
* Note: This requires more robust validation in a real app.
*
* @param string $setting the setting key ('precision' or 'allow_negative')
* @param mixed $value the new value (int for precision, bool for allow_negative)
*
* @return array{
* success: bool,
* error?: string,
* message?: string
* } success message or error
*/
#[McpTool(name: 'update_setting')]
public function updateSetting(string $setting, mixed $value): array
{
$this->logger->info(\sprintf('Setting tool called: setting=%s, value=%s', $setting, var_export($value, true)));
if (!\array_key_exists($setting, $this->config)) {
return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
}

if ('precision' === $setting) {
if (!\is_int($value) || $value < 0 || $value > 10) {
return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
}
$this->config['precision'] = $value;

// In real app, notify subscribers of config://calculator/settings change
// $registry->notifyResourceChanged('config://calculator/settings');
return ['success' => true, 'message' => "Precision updated to {$value}."];
}

if (!\is_bool($value)) {
// Attempt basic cast for flexibility
if (\in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
$value = true;
} elseif (\in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
$value = false;
} else {
return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
}
}
$this->config['allow_negative'] = $value;

// $registry->notifyResourceChanged('config://calculator/settings');
return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
}
}
29 changes: 29 additions & 0 deletions examples/01-discovery-stdio-calculator/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/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\Server;
use Mcp\Server\Transport\StdioTransport;

logger()->info('Starting MCP Stdio Calculator Server...');

Server::make()
->withServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
->withContainer(container())
->withLogger(logger())
->withDiscovery(__DIR__, ['.'])
->build()
->connect(new StdioTransport(logger: logger()));

logger()->info('Server listener stopped gracefully.');
141 changes: 141 additions & 0 deletions examples/02-discovery-http-userprofile/McpElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?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\HttpUserProfileExample;

use Mcp\Capability\Attribute\CompletionProvider;
use Mcp\Capability\Attribute\McpPrompt;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpResourceTemplate;
use Mcp\Capability\Attribute\McpTool;
use Psr\Log\LoggerInterface;

class McpElements
{
// Simulate a simple user database
private array $users = [
'101' => ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'],
'102' => ['name' => 'Bob', 'email' => '[email protected]', 'role' => 'user'],
'103' => ['name' => 'Charlie', 'email' => '[email protected]', 'role' => 'user'],
];

public function __construct(
private LoggerInterface $logger,
) {
$this->logger->debug('HttpUserProfileExample McpElements instantiated.');
}

/**
* Retrieves the profile data for a specific user.
*
* @param string $userId the ID of the user (from URI)
*
* @return array user profile data
*
* @throws McpServerException if the user is not found
*/
#[McpResourceTemplate(
uriTemplate: 'user://{userId}/profile',
name: 'user_profile',
description: 'Get profile information for a specific user ID.',
mimeType: 'application/json'
)]
public function getUserProfile(
#[CompletionProvider(values: ['101', '102', '103'])]
string $userId,
): array {
$this->logger->info('Reading resource: user profile', ['userId' => $userId]);
if (!isset($this->users[$userId])) {
// Throwing an exception that Processor can turn into an error response
throw McpServerException::invalidParams("User profile not found for ID: {$userId}");
}

return $this->users[$userId];
}

/**
* Retrieves a list of all known user IDs.
*
* @return array list of user IDs
*/
#[McpResource(
uri: 'user://list/ids',
name: 'user_id_list',
description: 'Provides a list of all available user IDs.',
mimeType: 'application/json'
)]
public function listUserIds(): array
{
$this->logger->info('Reading resource: user ID list');

return array_keys($this->users);
}

/**
* Sends a welcome message to a user.
* (This is a placeholder - in a real app, it might queue an email).
*
* @param string $userId the ID of the user to message
* @param string|null $customMessage an optional custom message part
*
* @return array status of the operation
*/
#[McpTool(name: 'send_welcome')]
public function sendWelcomeMessage(string $userId, ?string $customMessage = null): array
{
$this->logger->info('Executing tool: send_welcome', ['userId' => $userId]);
if (!isset($this->users[$userId])) {
return ['success' => false, 'error' => "User ID {$userId} not found."];
}
$user = $this->users[$userId];
$message = "Welcome, {$user['name']}!";
if ($customMessage) {
$message .= ' '.$customMessage;
}
// Simulate sending
$this->logger->info("Simulated sending message to {$user['email']}: {$message}");

return ['success' => true, 'message_sent' => $message];
}

#[McpTool(name: 'test_tool_without_params')]
public function testToolWithoutParams(): array
{
return ['success' => true, 'message' => 'Test tool without params'];
}

/**
* Generates a prompt to write a bio for a user.
*
* @param string $userId the user ID to generate the bio for
* @param string $tone Desired tone (e.g., 'formal', 'casual').
*
* @return array prompt messages
*
* @throws McpServerException if user not found
*/
#[McpPrompt(name: 'generate_bio_prompt')]
public function generateBio(
#[CompletionProvider(provider: UserIdCompletionProvider::class)]
string $userId,
string $tone = 'professional',
): array {
$this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
if (!isset($this->users[$userId])) {
throw McpServerException::invalidParams("User not found for bio prompt: {$userId}");
}
$user = $this->users[$userId];

return [
['role' => 'user', 'content' => "Write a short, {$tone} biography for {$user['name']} (Role: {$user['role']}, Email: {$user['email']}). Highlight their role within the system."],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\HttpUserProfileExample;

use Mcp\Capability\Prompt\Completion\ProviderInterface;

class UserIdCompletionProvider implements ProviderInterface
{
public function getCompletions(string $currentValue): array
{
$availableUserIds = ['101', '102', '103'];

return array_filter($availableUserIds, fn (string $userId) => str_contains($userId, $currentValue));
}
}
Loading