php-mcp/server

repository·main·Indexed 21 days ago

https://github.com/php-mcp/server

A PHP SDK for building Model Context Protocol (MCP) servers, enabling AI assistants to interact with PHP applications via standardized Tools, Resources, and Prompts. It supports attribute-based discovery (#[McpTool], #[McpResource], #[McpPrompt]), manual registration, and multiple transport layers including StdioServerTransport, HttpServerTransport, and StreamableHttpServerTransport. The SDK integrates with PSR-3, PSR-16, and PSR-11 standards and requires PHP >= 8.1.

Tokens
16.5K
Snippets
46
Records
53
Agent score
75%

What's inside php-mcp-server

  1. How the PHP MCP Server Architecture works

    main

    The SDK uses a decoupled architecture to bridge MCP clients (like Claude or Cursor) with your PHP logic:

    • Transport Layer: Handles communication via StdioServerTransport (direct launch), HttpServerTransport (SSE), or StreamableHttpServerTransport (resumable HTTP).
    • Protocol Layer: A JSON-RPC 2.0 handler that translates transport messages into core logic.
    • Server Core & Dispatcher: The Server coordinates components, while the Dispatcher routes incoming method requests to the correct handlers.
    • Registry & Elements: The Registry stores discovered MCP components (Tools, Resources, Prompts) and manages caching.
    • Session Manager: Handles stateful interactions using various backends (array, cache, or custom).
  2. How Completion Provider resolution works

    main

    The server automatically resolves the #[CompletionProvider] attribute based on the type of value provided:

    • Class strings (MyProvider::class): Resolved from the configured PSR-11 container using dependency injection.
    • Instances (new MyProvider()): Used directly as-is.
    • Values arrays (['a', 'b', 'c']): Automatically wrapped in a ListCompletionProvider.
    • Enum classes (MyEnum::class): Automatically wrapped in an EnumCompletionProvider.
  3. Generate JSON schemas for tool parameters

    main

    The server automatically generates JSON schemas for tool parameters using a priority system. These schemas are used for both input validation and providing information to MCP clients. The priority order is:

    1. #[Schema(definition: [...])] (Highest precedence - complete override)
    2. Parameter-level #[Schema] (Enhancements to specific parameters)
    3. Method-level #[Schema] (Configuration for the entire method)
    4. PHP type hints + docblocks (Lowest precedence - automatic inference)

    Use parameter-level attributes to add constraints like format, pattern, minimum, or maximum to existing PHP types.

    use PhpMcp\​Server​Attributes\\{McpTool, Schema};
    
    #[McpTool(name: 'validate_user')]
    public function validateUser(
        #[Schema(format: 'email')]              // Enhances string type
        string $email,
        
        #[Schema(
            pattern: '^[A-Z][a-z]+$',
            description: 'Capitalized name'
        )]
        string $name,
        
        #[Schema(minimum: 18, maximum: 120)]    // Enhances integer type
        int $age
    ): bool {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
  4. Handle JSON-RPC batch requests

    main

    The server natively supports JSON-RPC 2.0 batch requests. Clients can send an array of requests (including notifications) in a single call. The server will return an array of results corresponding to the IDs provided in the batch (excluding notifications).

    // Example Batch Request Structure
    [
        {"jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": {...}},
        {"jsonrpc": "2.0", "method": "notifications/ping"},
        {"jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": {...}}
    ]
    
    // Example Batch Response
    [
        {"jsonrpc": "2.0", "id": "1", "result": {...}},
        {"jsonrpc": "2.0", "id": "2", "result": {...}}
    ]
  5. Debug MCP servers using logging and STDERR

    main

    For debugging, you can use any PSR-3 compatible LoggerInterface within your tool handlers. Additionally, when using stdio transport, you should write debug information to STDERR to avoid interfering with the JSON-RPC communication on STDOUT.

    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];
        }
    }
  6. Use Streamable HTTP Transport for production

    main

    The StreamableHttpServerTransport is the recommended transport for production, remote servers, and multiple clients. It supports resumable connections and enhanced session management.

    Key Configuration Options:

    • host: The server host (Note: 0.0.0.0 is prohibited by the MCP protocol; use a specific IP like 127.0.0.1).
    • port: The port number.
    • mcpPathPrefix: The URL prefix (e.g., mcp).
    • enableJsonResponse: If true, returns immediate JSON responses instead of SSE streams. Use this for fast-executing tools.
    • stateless: If true, enables stateless mode where each request is independent and session IDs are not exposed to clients.
    • withCache($cache): Required for resumability features.

    Client Configuration Example:

    {
        "mcpServers": {
            "my-http-server": {
                "url": "http://localhost:8080/mcp/sse"
            }
        }
    }
    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']);
    
    $transport = new StreamableHttpServerTransport(
        host: '127.0.0.1',
        port: 8080,
        mcpPathPrefix: 'mcp',
        enableJsonResponse: false, // Use SSE streaming
        stateless: false
    );
    
    $server->listen($transport);
  7. Register MCP elements manually

    main

    For dynamic registration, closures, or explicit control, use the ServerBuilder methods before calling build(). Manual registrations take precedence over discovered elements with the same identifier.

    Supported Handler Formats:

    • [ClassName::class, 'methodName']: Class method handlers.
    • InvokableClass::class: Classes with an __invoke method.
    • callable: Closures, static methods, or function names.

    Note on Closures: When using closures, the server generates minimal JSON schemas based only on PHP type hints. For detailed schemas, use the #[Schema] attribute or provide a custom $inputSchema in withTool().

    use App\​Handlers\\EmailHandler;
    use PhpMcp\\Schema\\ToolAnnotations;
    
    $server = Server::make()
        ->withServerInfo('Manual Registration Server', '1.0.0')
        
        // Register a tool with a class method
        ->withTool(
            [EmailHandler::class, 'sendEmail'],
            name: 'send_email',
            description: 'Send email to user',
            annotations: ToolAnnotations::make(title: 'Send Email Tool')
        )
        
        // Register an invokable class
        ->withTool(UserHandler::class)
        
        // Register a closure
        ->withTool(
            function(int $a, int $b): int { return $a + $b; },
            name: 'add_numbers'
        )
        
        // Register a resource with a closure
        ->withResource(
            function(): array { return ['status' => 'ok']; },
            uri: 'config://runtime/status'
        )
        
        // Register a resource template
        ->withResourceTemplate(
            [UserHandler::class, 'getUserProfile'],
            uriTemplate: 'user://{userId}/profile'
        )
        
        // Register a prompt with a closure
        ->withPrompt(
            function(string $topic): array {
                return [['role' => 'user', 'content' => "Write about {$topic}"]];
            },
            name: 'writing_prompt'
        )
        ->build();
  8. Define MCP elements using Attribute-Based Discovery

    main

    You can define MCP elements by marking methods or classes with PHP 8 attributes. The server will automatically discover these via filesystem scanning. This is the recommended approach for most use cases.

    Supported element types:

    • Tools (#[McpTool]): Executable functions/actions.
    • Resources (#[McpResource]): Static content/data accessible via a URI.
    • Resource Templates (#[McpResourceTemplate]): Dynamic resources with URI patterns (e.g., user://{id}/profile).
    • Prompts (#[McpPrompt]): Conversation starters or templates.
    use PhpMcp\​Server\Attributes\\{McpTool, McpResource, McpResourceTemplate, McpPrompt};
    
    class UserManager
    {
        #[McpTool(name: 'create_user')]
        public function createUser(string $email, string $password, string $role = 'user'): array
        {
            return ['id' => 123, 'email' => $email, 'role' => $role];
        }
    
        #[McpResource(uri: 'config://user/settings', mimeType: 'application/json')]
        public function getUserConfig(): array
        {
            return ['theme' => 'dark'];
        }
    
        #[McpResourceTemplate(uriTemplate: 'user://{userId}/profile', mimeType: 'application/json')]
        public function getUserProfile(string $userId): array
        {
            return ['id' => $userId, 'name' => 'John Doe'];
        }
    
        #[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"]
            ];
        }
    }
  9. Configure Custom Dependency Injection for Handlers

    main

    MCP element handlers can use constructor dependency injection. To use it, you must provide a PSR-11 compatible container to the server via withContainer().

    • BasicContainer: The default implementation. It attempts to auto-wire dependencies by instantiating classes with parameterless constructors. You can manually add dependencies using $container->set(ClassName::class, $instance).
    • Advanced Containers: You can use any PSR-11 container like PHP-DI or Laravel's container for more complex dependency management.
    use PhpMcp// ...
    
    // 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
    $container = new \DI\Container();
    $container->set(\PDO::class, new \PDO('mysql:host=localhost;dbname=app', $user, $pass));
    
    $server = Server::make()
        ->withContainer($container)
        ->build();
  10. Use Stdio Transport for local execution

    main

    The StdioServerTransport is best for direct client execution (like CLI tools) and simple deployments. It uses STDIN and STDOUT for JSON-RPC communication.

    ⚠️ Critical Warning: Never write to STDOUT within your handlers; use STDERR for debugging. STDOUT is reserved for the MCP protocol.

    Client Configuration Example:

    {
        "mcpServers": {
            "my-php-server": {
                "command": "php",
                "args": ["/absolute/path/to/server.php"]
            }
        }
    }
    use PhpMcp\\Server\\Transports\\StdioServerTransport;
    
    $server = Server::make()
        ->withServerInfo('Stdio Server', '1.0.0')
        ->build();
    
    $server->discover(__DIR__, ['src']);
    
    $transport = new StdioServerTransport();
    $server->listen($transport);
  11. Quick Start: Create a Stdio Server with Attribute Discovery

    main

    This pattern allows you to automatically register MCP elements (Tools, Resources, etc.) by scanning your source code for PHP Attributes.

    1. Define Elements using Attributes

    Use #[McpTool] to expose methods as tools. You can enhance parameter validation using the #[Schema] attribute.

    2. Create the Server Script

    Use Server::make() to build the server, call discover() to scan your directories, and listen() with a StdioServerTransport to start the process.

    3. Configure the Client

    In your MCP client configuration (e.g., .cursor/mcp.json), point the command to php and provide the absolute path to your server script in args.

    <?php
    
    namespace App;
    
    use PhpMcp//... (See full example in documentation)
    
    #[McpTool(name: 'add_numbers')]
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }
  12. Deploy MCP server using Docker

    main

    For containerized environments, use a Dockerfile based on php:8.3-fpm-alpine. Ensure you install nginx and supervisor within the container to manage the PHP process and handle web traffic. Use docker-compose.yml to manage the server alongside other dependencies like databases.

    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
    
    WORKDIR /var/www/mcp
    COPY . /var/www/mcp
    
    # Start supervisor
    CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]