PHP code example of php-mcp / server

1. Go to this page and download the library: Download php-mcp/server library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

php-mcp / server example snippets




namespace App;

use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Attributes\Schema;

class CalculatorElements
{
    /**
     * Adds two numbers together.
     * 
     * @param int $a The first number
     * @param int $b The second number  
     * @return int The sum of the two numbers
     */
    #[McpTool(name: 'add_numbers')]
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    /**
     * Calculates power with validation.
     */
    #[McpTool(name: 'calculate_power')]
    public function power(
        #[Schema(type: 'number', minimum: 0, maximum: 1000)]
        float $base,
        
        #[Schema(type: 'integer', minimum: 0, maximum: 10)]
        int $exponent
    ): float {
        return pow($base, $exponent);
    }
}

#!/usr/bin/env php


declare(strict_types=1);

orts\StdioServerTransport;

try {
    // Build server configuration
    $server = Server::make()
        ->withServerInfo('PHP Calculator Server', '1.0.0') 
        ->build();

    // Discover MCP elements via attributes
    $server->discover(
        basePath: __DIR__,
        scanDirs: ['src']
    );

    // Start listening via stdio transport
    $transport = new StdioServerTransport();
    $server->listen($transport);

} catch (\Throwable $e) {
    fwrite(STDERR, "[CRITICAL ERROR] " . $e->getMessage() . "\n");
    exit(1);
}

use PhpMcp\Server\Server;
use PhpMcp\Schema\ServerCapabilities;

$server = Server::make()
    ->withServerInfo('My App Server', '2.1.0')
    ->withCapabilities(ServerCapabilities::make(
        resources: true,
        resourcesSubscribe: true,
        prompts: true,
        tools: true
    ))
    ->withPaginationLimit(100)
    ->build();

use Psr\Log\Logger;
use Psr\SimpleCache\CacheInterface;
use Psr\Container\ContainerInterface;

$server = Server::make()
    ->withServerInfo('Production Server', '1.0.0')
    ->withLogger($myPsrLogger)                    // PSR-3 Logger
    ->withCache($myPsrCache)                      // PSR-16 Cache  
    ->withContainer($myPsrContainer)              // PSR-11 Container
    ->withSession('cache', 7200)                  // Cache-backed sessions, 2hr TTL
    ->withPaginationLimit(50)                     // Limit list responses
    ->build();

// In-memory sessions (default, not persistent)
->withSession('array', 3600)

// Cache-backed sessions (persistent across restarts)  
->withSession('cache', 7200)

// Custom session handler (implement SessionHandlerInterface)
->withSessionHandler(new MyCustomSessionHandler(), 1800)

use PhpMcp\Server\Attributes\{McpTool, McpResource, McpResourceTemplate, McpPrompt};

class UserManager
{
    /**
     * Creates a new user account.
     */
    #[McpTool(name: 'create_user')]
    public function createUser(string $email, string $password, string $role = 'user'): array
    {
        // Create user logic
        return ['id' => 123, 'email' => $email, 'role' => $role];
    }

    /**
     * Get user configuration.
     */
    #[McpResource(
        uri: 'config://user/settings',
        mimeType: 'application/json'
    )]
    public function getUserConfig(): array
    {
        return ['theme' => 'dark', 'notifications' => true];
    }

    /**
     * Get user profile by ID.
     */
    #[McpResourceTemplate(
        uriTemplate: 'user://{userId}/profile',
        mimeType: 'application/json'
    )]
    public function getUserProfile(string $userId): array
    {
        return ['id' => $userId, 'name' => 'John Doe'];
    }

    /**
     * Generate welcome message prompt.
     */
    #[McpPrompt(name: 'welcome_user')]
    public function welcomeUserPrompt(string $username, string $role): array
    {
        return [
            ['role' => 'user', 'content' => "Create a welcome message for {$username} with role {$role}"]
        ];
    }
}

// Build server first
$server = Server::make()
    ->withServerInfo('My App Server', '1.0.0')
    ->build();

// Then discover elements
$server->discover(
    basePath: __DIR__,
    scanDirs: ['src/Handlers', 'src/Services'],  // Directories to scan
    excludeDirs: ['src/Tests'],                  // Directories to skip
    saveToCache: true                            // Cache results (default: true)
);

use App\Handlers\{EmailHandler, ConfigHandler, UserHandler, PromptHandler};
use PhpMcp\Schema\{ToolAnnotations, Annotations};

$server = Server::make()
    ->withServerInfo('Manual Registration Server', '1.0.0')
    
    // Register a tool with handler method
    ->withTool(
        [EmailHandler::class, 'sendEmail'],     // Handler: [class, method]
        name: 'send_email',                     // Tool name (optional)
        description: 'Send email to user',     // Description (optional)
        annotations: ToolAnnotations::make(     // Annotations (optional)
            title: 'Send Email Tool'
        )
    )
    
    // Register invokable class as tool
    ->withTool(UserHandler::class)             // Handler: Invokable class
    
    // Register a resource
    ->withResource(
        [ConfigHandler::class, 'getConfig'],
        uri: 'config://app/settings',          // URI (

$server->discover(
    basePath: __DIR__,
    scanDirs: ['src/Handlers', 'src/Services'],  // Scan these directories
    excludeDirs: ['tests', 'vendor'],            // Skip these directories
    force: false,                                // Force re-scan (default: false)
    saveToCache: true                            // Save to cache (default: true)
);

use PhpMcp\Server\Transports\StdioServerTransport;

$server = Server::make()
    ->withServerInfo('Stdio Server', '1.0.0')
    ->build();

$server->discover(__DIR__, ['src']);

// Create stdio transport (uses STDIN/STDOUT by default)
$transport = new StdioServerTransport();

// Start listening (blocking call)
$server->listen($transport);

use PhpMcp\Server\Transports\HttpServerTransport;

$server = Server::make()
    ->withServerInfo('HTTP Server', '1.0.0')
    ->withLogger($logger)  // Recommended for HTTP
    ->build();

$server->discover(__DIR__, ['src']);

// Create HTTP transport
$transport = new HttpServerTransport(
    host: '127.0.0.1',      // MCP protocol prohibits 0.0.0.0
    port: 8080,             // Port number
    mcpPathPrefix: 'mcp'    // URL prefix (/mcp/sse, /mcp/message)
);

$server->listen($transport);

use PhpMcp\Server\Transports\StreamableHttpServerTransport;

$server = Server::make()
    ->withServerInfo('Streamable Server', '1.0.0')
    ->withLogger($logger)
    ->withCache($cache)     // Required for resumability
    ->build();

$server->discover(__DIR__, ['src']);

// Create streamable transport with resumability
$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',      // MCP protocol prohibits 0.0.0.0
    port: 8080,
    mcpPathPrefix: 'mcp',
    enableJsonResponse: false  // Use SSE streaming (default)
);

$server->listen($transport);

// For fast-executing tools, enable JSON mode
$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',
    port: 8080,
    enableJsonResponse: true  // Immediate JSON responses
);

use PhpMcp\Server\Attributes\{McpTool, Schema};

#[McpTool(name: 'validate_user')]
public function validateUser(
    #[Schema(format: 'email')]              // PHP already knows it's string
    string $email,
    
    #[Schema(
        pattern: '^[A-Z][a-z]+$',
        description: 'Capitalized name'
    )]
    string $name,
    
    #[Schema(minimum: 18, maximum: 120)]    // PHP already knows it's integer
    int $age
): bool {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

/**
 * Process user data with nested validation.
 */
#[McpTool(name: 'create_user')]
#[Schema(
    properties: [
        'profile' => [
            'type' => 'object',
            'properties' => [
                'name' => ['type' => 'string', 'minLength' => 2],
                'age' => ['type' => 'integer', 'minimum' => 18],
                'email' => ['type' => 'string', 'format' => 'email']
            ],
            '

#[McpTool(name: 'process_api_request')]
#[Schema(definition: [
    'type' => 'object',
    'properties' => [
        'endpoint' => ['type' => 'string', 'format' => 'uri'],
        'method' => ['type' => 'string', 'enum' => ['GET', 'POST', 'PUT', 'DELETE']],
        'headers' => [
            'type' => 'object',
            'patternProperties' => [
                '^[A-Za-z0-9-]+$' => ['type' => 'string']
            ]
        ]
    ],
    '

// Simple values are auto-wrapped in TextContent
public function getString(): string { return "Hello World"; }           // → TextContent
public function getNumber(): int { return 42; }                        // → TextContent  
public function getBool(): bool { return true; }                       // → TextContent
public function getArray(): array { return ['key' => 'value']; }       // → TextContent (JSON)

// Null handling
public function getNull(): ?string { return null; }                    // → TextContent("(null)")
public function returnVoid(): void { /* no return */ }                 // → Empty content

use PhpMcp\Schema\Content\{TextContent, ImageContent, AudioContent, ResourceContent};

public function getFormattedCode(): TextContent
{
    return TextContent::code(' echo "Hello";', 'php');
}

public function getMarkdown(): TextContent  
{
    return TextContent::make('# Title\n\nContent here');
}

public function getImage(): ImageContent
{
    return ImageContent::make(
        data: base64_encode(file_get_contents('image.png')),
        mimeType: 'image/png'
    );
}

public function getAudio(): AudioContent
{
    return AudioContent::make(
        data: base64_encode(file_get_contents('audio.mp3')),
        mimeType: 'audio/mpeg'
    );
}

// File objects are automatically read and formatted
public function getFileContent(): \SplFileInfo
{
    return new \SplFileInfo('/path/to/file.txt');  // Auto-detects MIME type
}

// Stream resources are read completely
public function getStreamContent()
{
    $stream = fopen('/path/to/data.json', 'r');
    return $stream;  // Will be read and closed automatically
}

// Structured resource responses
public function getStructuredResource(): array
{
    return [
        'text' => 'File content here',
        'mimeType' => 'text/plain'
    ];
    
    // Or for binary data:
    // return [
    //     'blob' => base64_encode($binaryData),
    //     'mimeType' => 'application/octet-stream'
    // ];
}

// Client can send multiple requests in a single HTTP call:
[
    {"jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": {...}},
    {"jsonrpc": "2.0", "method": "notifications/ping"},              // notification
    {"jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": {...}}
]

// Server returns batch response (excluding notifications):
[
    {"jsonrpc": "2.0", "id": "1", "result": {...}},
    {"jsonrpc": "2.0", "id": "2", "result": {...}}
]

use PhpMcp\Server\Contracts\CompletionProviderInterface;
use PhpMcp\Server\Contracts\SessionInterface;
use PhpMcp\Server\Attributes\{McpResourceTemplate, CompletionProvider};

class UserIdCompletionProvider implements CompletionProviderInterface
{
    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        // Return completion suggestions based on current input
        $allUsers = ['user_1', 'user_2', 'user_3', 'admin_user'];
        
        // Filter based on what user has typed so far
        return array_filter($allUsers, fn($user) => str_starts_with($user, $currentValue));
    }
}

class UserService
{
    #[McpResourceTemplate(uriTemplate: 'user://{userId}/profile')]
    public function getUserProfile(
        #[CompletionProvider(UserIdCompletionProvider::class)]
        string $userId
    ): array {
        // Always validate input even with completion providers
        // Users can still pass any value regardless of completion suggestions
        if (!$this->isValidUserId($userId)) {
            throw new \InvalidArgumentException('Invalid user ID provided');
        }
        
        return ['id' => $userId, 'name' => 'John Doe'];
    }
}

use Psr\Container\ContainerInterface;

class DatabaseService
{
    public function __construct(private \PDO $pdo) {}
    
    #[McpTool(name: 'query_users')]
    public function queryUsers(): array
    {
        $stmt = $this->pdo->query('SELECT * FROM users');
        return $stmt->fetchAll();
    }
}

// Option 1: Use the basic container and manually add dependencies
$basicContainer = new \PhpMcp\Server\Defaults\BasicContainer();
$basicContainer->set(\PDO::class, new \PDO('sqlite::memory:'));

// Option 2: Use any PSR-11 compatible container (PHP-DI, Laravel, etc.)
$container = new \DI\Container();
$container->set(\PDO::class, new \PDO('mysql:host=localhost;dbname=app', $user, $pass));

$server = Server::make()
    ->withContainer($basicContainer)  // Handlers get dependencies auto-injected
    ->build();

use PhpMcp\Schema\ServerCapabilities;

$server = Server::make()
    ->withCapabilities(ServerCapabilities::make(
        resourcesSubscribe: true,  // Enable resource subscriptions
        prompts: true,
        tools: true
    ))
    ->build();

// In your resource handler, you can notify clients of changes:
#[McpResource(uri: 'file://config.json')]
public function getConfig(): array
{
    // When config changes, notify subscribers
    $this->notifyResourceChange('file://config.json');
    return ['setting' => 'value'];
}

use PhpMcp\Server\Contracts\EventStoreInterface;
use PhpMcp\Server\Defaults\InMemoryEventStore;
use PhpMcp\Server\Transports\StreamableHttpServerTransport;

// Use the built-in in-memory event store (for development/testing)
$eventStore = new InMemoryEventStore();

// Or implement your own persistent event store
class DatabaseEventStore implements EventStoreInterface
{
    public function storeEvent(string $streamId, string $message): string
    {
        // Store event in database and return unique event ID
        return $this->database->insert('events', [
            'stream_id' => $streamId,
            'message' => $message,
            'created_at' => now()
        ]);
    }

    public function replayEventsAfter(string $lastEventId, callable $sendCallback): void
    {
        // Replay events for resumability
        $events = $this->database->getEventsAfter($lastEventId);
        foreach ($events as $event) {
            $sendCallback($event['id'], $event['message']);
        }
    }
}

// Configure transport with event store
$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',
    port: 8080,
    eventStore: new DatabaseEventStore()  // Enable resumability
);

use PhpMcp\Server\Contracts\SessionHandlerInterface;

class DatabaseSessionHandler implements SessionHandlerInterface
{
    public function __construct(private \PDO $db) {}

    public function read(string $id): string|false
    {
        $stmt = $this->db->prepare('SELECT data FROM sessions WHERE id = ?');
        $stmt->execute([$id]);
        $session = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $session ? $session['data'] : false;
    }

    public function write(string $id, string $data): bool
    {
        $stmt = $this->db->prepare(
            'INSERT OR REPLACE INTO sessions (id, data, updated_at) VALUES (?, ?, ?)'
        );
        return $stmt->execute([$id, $data, time()]);
    }

    public function destroy(string $id): bool
    {
        $stmt = $this->db->prepare('DELETE FROM sessions WHERE id = ?');
        return $stmt->execute([$id]);
    }

    public function gc(int $maxLifetime): array
    {
        $cutoff = time() - $maxLifetime;
        $stmt = $this->db->prepare('DELETE FROM sessions WHERE updated_at < ?');
        $stmt->execute([$cutoff]);
        return []; // Return array of cleaned session IDs if needed
    }
}

// Use custom session handler
$server = Server::make()
    ->withSessionHandler(new DatabaseSessionHandler(), 3600)
    ->build();

$sslContext = [
    'ssl' => [
        'local_cert' => '/path/to/certificate.pem',
        'local_pk' => '/path/to/private-key.pem',
        'verify_peer' => false,
        'allow_self_signed' => true,
    ]
];

$transport = new StreamableHttpServerTransport(
    host: '0.0.0.0',
    port: 8443,
    sslContext: $sslContext
);

#[McpTool(name: 'divide_numbers')]
public function divideNumbers(float $dividend, float $divisor): float
{
    if ($divisor === 0.0) {
        // Any exception with descriptive message will be sent to client
        throw new \InvalidArgumentException('Division by zero is not allowed');
    }
    
    return $dividend / $divisor;
}

#[McpTool(name: 'calculate_factorial')]
public function calculateFactorial(int $number): int
{
    if ($number < 0) {
        throw new \InvalidArgumentException('Factorial is not defined for negative numbers');
    }
    
    if ($number > 20) {
        throw new \OverflowException('Number too large, factorial would cause overflow');
    }
    
    // Implementation continues...
    return $this->factorial($number);
}

use Psr\Log\LoggerInterface;

class DebugAwareHandler
{
    public function __construct(private LoggerInterface $logger) {}
    
    #[McpTool(name: 'debug_tool')]
    public function debugTool(string $data): array
    {
        $this->logger->info('Processing debug tool', ['input' => $data]);
        
        // For stdio transport, use STDERR for debug output
        fwrite(STDERR, "Debug: Processing data length: " . strlen($data) . "\n");
        
        return ['processed' => true];
    }
}
bash
composer 
json
{
    "mcpServers": {
        "php-calculator": {
            "command": "php",
            "args": ["/absolute/path/to/your/mcp-server.php"]
        }
    }
}
json
{
    "mcpServers": {
        "my-php-server": {
            "command": "php",
            "args": ["/absolute/path/to/server.php"]
        }
    }
}
dockerfile
FROM php:8.3-fpm-alpine

# Install system dependencies
RUN apk --no-cache add \
    nginx \
    supervisor \
    && docker-php-ext-enable opcache

# Install PHP extensions for MCP
RUN docker-php-ext-install pdo_mysql pdo_sqlite opcache

# Create application directory
WORKDIR /var/www/mcp

# Copy application code
COPY . /var/www/mcp
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/production.ini

# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction

# Set permissions
RUN chown -R www-data:www-data /var/www/mcp

# Expose port
EXPOSE 80

# Start supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
bash
# Navigate to an example directory
cd examples/01-discovery-stdio-calculator/

# Make the server executable
chmod +x server.php

# Run the server (or configure it in your MCP client)
./server.php