PHP MCP Server

Una implementación del lado del servidor del Model Context Protocol (MCP) para aplicaciones PHP, que permite exponer partes de la aplicación como Herramientas, Recursos y Prompts MCP estandarizados.

Documentación

PHP MCP Server SDK

Latest Version on Packagist Total Downloads Tests License

Un SDK integral de PHP para construir servidores Model Context Protocol (MCP). Crea servidores MCP listos para producción en PHP con arquitectura moderna, pruebas exhaustivas y opciones de transporte flexibles.

Este SDK te permite exponer la funcionalidad de tu aplicación PHP como Herramientas, Recursos y Prompts estandarizados de MCP, permitiendo que asistentes de IA (como Claude de Anthropic, Cursor IDE, ChatGPT de OpenAI, etc.) interactúen con tu backend usando el estándar MCP.

🚀 Características Clave

  • 🏗️ Arquitectura Moderna: Construido con características de PHP 8.1+, estándares PSR y diseño modular
  • 📡 Múltiples Transportes: Soporta stdio, http+sse y el nuevo HTTP transmisible con reanudación
  • 🎯 Definición Basada en Atributos: Usa Atributos de PHP 8 (#[McpTool], #[McpResource], etc.) para registro de elementos sin configuración
  • 🔧 Manejadores Flexibles: Soporte para closures, métodos de clase, métodos estáticos y clases invocables
  • 📝 Generación Inteligente de Esquemas: Generación automática de esquemas JSON a partir de firmas de métodos con mejoras opcionales mediante el atributo #[Schema]
  • ⚡ Gestión de Sesiones: Manejo avanzado de sesiones con múltiples backends de almacenamiento
  • 🔄 Orientado a Eventos: Basado en ReactPHP para alta concurrencia y operaciones no bloqueantes
  • 📊 Procesamiento por Lotes: Soporte completo para solicitudes JSON-RPC por lotes
  • 💾 Caché Inteligente: Caché inteligente de elementos descubiertos con precedencia de anulación manual
  • 🧪 Proveedores de Completado: Soporte integrado para completado de argumentos en herramientas y prompts
  • 🔌 Inyección de Dependencias: Soporte completo de contenedores PSR-11 con auto-cableado
  • 📋 Pruebas Exhaustivas: Amplio conjunto de pruebas con pruebas de integración para todos los transportes

Este paquete soporta la versión 2025-03-26 del Model Context Protocol con compatibilidad hacia atrás.

📋 Requisitos

  • PHP >= 8.1
  • Composer
  • Para Transporte HTTP: Un entorno PHP orientado a eventos (CLI recomendado)
  • Extensiones: json, mbstring, pcre (normalmente habilitadas por defecto)

📦 Instalación

composer require php-mcp/server

💡 Usuarios de Laravel: Considera usar php-mcp/laravel para una integración mejorada con el framework, gestión de configuración y comandos Artisan.

⚡ Inicio Rápido: Servidor Stdio con Descubrimiento

Este ejemplo demuestra el patrón de uso más común: un servidor stdio usando descubrimiento por atributos.

1. Define tus Elementos MCP

Crea src/CalculatorElements.php:

<?php

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);
    }
}

2. Crea el Script del Servidor

Crea mcp-server.php:

#!/usr/bin/env php
<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\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);
}

3. Configura tu Cliente MCP

Añade a la configuración de tu cliente (por ejemplo, .cursor/mcp.json):

{
    "mcpServers": {
        "php-calculator": {
            "command": "php",
            "args": ["/absolute/path/to/your/mcp-server.php"]
        }
    }
}

4. Prueba el Servidor

Tu asistente de IA ahora puede llamar:

  • add_numbers - Suma dos enteros
  • calculate_power - Calcula potencias con restricciones de validación

🏗️ Descripción General de la Arquitectura

El PHP MCP Server utiliza una arquitectura moderna y desacoplada:

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   MCP Client    │◄──►│   Transport      │◄──►│   Protocol      │
│  (Claude, etc.) │    │ (Stdio/HTTP/SSE) │    │   (JSON-RPC)    │
└─────────────────┘    └──────────────────┘    └─────────────────┘
                                                         │
                       ┌─────────────────┐               │
                       │ Session Manager │◄──────────────┤
                       │ (Multi-backend) │               │
                       └─────────────────┘               │
                                                         │
┌─────────────────┐    ┌──────────────────┐              │
│   Dispatcher    │◄───│   Server Core    │◄─────────────┤
│ (Method Router) │    │   Configuration  │              │
└─────────────────┘    └──────────────────┘              │
         │                                               │
         ▼                                               │
┌─────────────────┐    ┌──────────────────┐              │
│    Registry     │    │   Elements       │◄─────────────┘
│  (Element Store)│◄──►│ (Tools/Resources │
└─────────────────┘    │  Prompts/etc.)   │
                       └──────────────────┘

Componentes Principales

  • ServerBuilder: Interfaz de configuración fluida (Server::make()->...->build())
  • Server: Coordinador central que contiene todos los componentes configurados
  • Protocol: Manejador JSON-RPC 2.0 que conecta transportes y lógica central
  • SessionManager: Almacenamiento de sesiones multi-backend (array, caché, personalizado)
  • Dispatcher: Enrutamiento de métodos y procesamiento de solicitudes
  • Registry: Almacenamiento de elementos con caché inteligente y reglas de precedencia
  • Elements: Componentes MCP registrados (Herramientas, Recursos, Prompts, Plantillas)

Opciones de Transporte

  1. StdioServerTransport: E/S estándar para lanzamientos directos del cliente
  2. HttpServerTransport: HTTP + Server-Sent Events para integración web
  3. StreamableHttpServerTransport: HTTP mejorado con reanudación y event sourcing

⚙️ Configuración del Servidor

Configuración Básica

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();

Configuración Avanzada con Dependencias

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();

Opciones de Gestión de Sesiones

// 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)

🎯 Definición de Elementos MCP

El servidor proporciona dos formas potentes de definir elementos MCP: Descubrimiento Basado en Atributos (recomendado) y Registro Manual. Ambos se pueden combinar, con precedencia para los registros manuales.

Tipos de Elementos

  • 🔧 Herramientas: Funciones/acciones ejecutables (por ejemplo, calculate, send_email, query_database)
  • 📄 Recursos: Contenido/datos estáticos (por ejemplo, config://settings, file://readme.txt)
  • 📋 Plantillas de Recursos: Recursos dinámicos con patrones de URI (por ejemplo, user://{id}/profile)
  • 💬 Prompts: Iniciadores/plantillas de conversación (por ejemplo, summarize, translate)

1. 🏷️ Descubrimiento Basado en Atributos (Recomendado)

Usa atributos de PHP 8 para marcar métodos o clases invocables como elementos MCP. El servidor los descubrirá mediante escaneo del sistema de archivos.

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}"]
        ];
    }
}

Proceso de Descubrimiento:

// 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)
);

Atributos Disponibles:

  • #[McpTool]: Acciones ejecutables
  • #[McpResource]: Contenido estático accesible mediante URI
  • #[McpResourceTemplate]: Recursos dinámicos con plantillas de URI
  • #[McpPrompt]: Plantillas de conversación y generadores de prompts

2. 🔧 Registro Manual

Registra elementos programáticamente usando ServerBuilder antes de llamar a build(). Útil para registro dinámico, closures, o cuando prefieres control explícito.

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 closure as tool
    ->withTool(
        function(int $a, int $b): int {         // Handler: Closure
            return $a + $b;
        },
        name: 'add_numbers',
        description: 'Add two numbers together'
    )
    
    // Register a resource with closure
    ->withResource(
        function(): array {                     // Handler: Closure
            return ['timestamp' => time(), 'server' => 'php-mcp'];
        },
        uri: 'config://runtime/status',         // URI (required)
        mimeType: 'application/json'           // MIME type (optional)
    )
    
    // Register a resource template
    ->withResourceTemplate(
        [UserHandler::class, 'getUserProfile'],
        uriTemplate: 'user://{userId}/profile'  // URI template (required)
    )
    
    // Register a prompt with closure
    ->withPrompt(
        function(string $topic, string $tone = 'professional'): array {
            return [
                ['role' => 'user', 'content' => "Write about {$topic} in a {$tone} tone"]
            ];
        },
        name: 'writing_prompt'                  // Prompt name (optional)
    )
    
    ->build();

El servidor soporta tres formatos flexibles de manejadores: [ClassName::class, 'methodName'] para manejadores de métodos de clase, InvokableClass::class para manejadores de clases invocables (clases con método __invoke), y cualquier callable de PHP incluyendo closures, métodos estáticos como [SomeClass::class, 'staticMethod'], o nombres de funciones. Los manejadores basados en clases se resuelven mediante el contenedor PSR-11 configurado para inyección de dependencias. Los registros manuales nunca se almacenan en caché y tienen precedencia sobre los elementos descubiertos con el mismo identificador.

[!IMPORTANTE] Cuando se usan closures como manejadores, el servidor genera esquemas JSON mínimos basados solo en las sugerencias de tipo de PHP, ya que no hay docblocks ni contexto de clase disponibles. Para esquemas más detallados con restricciones de validación, descripciones y formatos, tienes dos opciones:

  • Usa el atributo #[Schema] para una generación de esquemas mejorada
  • Proporciona un parámetro $inputSchema personalizado al registrar herramientas con ->withTool()

🏆 Precedencia de Elementos y Descubrimiento

Reglas de Precedencia:

  • Los registros manuales siempre anulan los elementos descubiertos/almacenados en caché con el mismo identificador
  • Los elementos descubiertos se almacenan en caché para rendimiento (configurable)
  • La caché se invalida automáticamente en ejecuciones de descubrimiento nuevas

Proceso de Descubrimiento:

$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)
);

Comportamiento de Caché:

  • Solo se almacenan en caché los elementos descubiertos (nunca los registros manuales)
  • La caché se carga automáticamente durante build() si está disponible
  • Las llamadas nuevas a discover() limpian y reconstruyen la caché
  • Usa force: true para omitir la verificación de descubrimiento ya ejecutado

🚀 Ejecución del Servidor (Transportes)

El núcleo del servidor es independiente del transporte. Elige un transporte según tus necesidades de despliegue:

1. 📟 Transporte Stdio

Mejor para: Ejecución directa del cliente, herramientas de línea de comandos, despliegues simples

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);

Configuración del Cliente:

{
    "mcpServers": {
        "my-php-server": {
            "command": "php",
            "args": ["/absolute/path/to/server.php"]
        }
    }
}

⚠️ Importante: Cuando uses el transporte stdio, nunca escribas en STDOUT en tus manejadores (usa STDERR para depuración). STDOUT está reservado para la comunicación JSON-RPC.

2. 🌐 Transporte HTTP + Server-Sent Events (Obsoleto)

⚠️ Nota: Este transporte está obsoleto en la última versión del protocolo MCP, pero sigue disponible para compatibilidad hacia atrás. Para proyectos nuevos, usa StreamableHttpServerTransport que proporciona características mejoradas y mejor cumplimiento del protocolo.

Mejor para: Aplicaciones heredadas que requieren compatibilidad hacia atrás

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);

Configuración del Cliente:

{
    "mcpServers": {
        "my-http-server": {
            "url": "http://localhost:8080/mcp/sse"
        }
    }
}

Endpoints:

  • Conexión SSE: GET /mcp/sse
  • Envío de Mensajes: POST /mcp/message?clientId={clientId}

3. 🔄 Transporte HTTP Transmisible (Recomendado)

Mejor para: Despliegues de producción, servidores MCP remotos, múltiples clientes, conexiones reanudables

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)
    stateless: false            // Enable stateless mode for session-less clients
);

$server->listen($transport);

Modo de Respuesta JSON:

La opción enableJsonResponse controla cómo se entregan las respuestas:

  • false (por defecto): Usa flujos Server-Sent Events (SSE) para las respuestas. Mejor para herramientas que pueden tardar en procesarse.
  • true: Devuelve respuestas JSON inmediatas sin abrir flujos SSE. Úsalo cuando tus herramientas se ejecuten rápidamente y no necesiten transmisión.
// For fast-executing tools, enable JSON mode
$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',
    port: 8080,
    enableJsonResponse: true  // Immediate JSON responses
);

Modo Sin Estado:

Para clientes que tienen problemas con la gestión de sesiones, habilita el modo sin estado:

$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',
    port: 8080,
    stateless: true  // Each request is independent
);

En modo sin estado, los IDs de sesión se generan internamente pero no se exponen a los clientes, y cada solicitud se trata como independiente sin estado de sesión persistente.

Características:

  • Conexiones reanudables - los clientes pueden reconectarse y reproducir eventos perdidos
  • Event sourcing - todos los eventos se almacenan para reproducción
  • Modo JSON - respuestas opcionales solo JSON para herramientas rápidas
  • Gestión de sesiones mejorada - estado de sesión persistente
  • Soporte de múltiples clientes - diseñado para clientes concurrentes
  • Modo sin estado - operación sin sesión para clientes simples

📋 Generación de Esquemas y Validación

El servidor genera automáticamente esquemas JSON para los parámetros de las herramientas usando un sistema de prioridad sofisticado que combina sugerencias de tipo de PHP, información de docblocks y el atributo opcional #[Schema]. Estos esquemas generados se usan tanto para la validación de entrada como para proporcionar información de esquema a los clientes MCP.

Prioridad de Generación de Esquemas

El servidor sigue este orden de precedencia al generar esquemas:

  1. Atributo #[Schema] con definition - Anulación completa del esquema (mayor precedencia)
  2. Atributo #[Schema] a nivel de parámetro - Mejoras de esquema específicas para parámetros
  3. Atributo #[Schema] a nivel de método - Configuración de esquema para todo el método
  4. Sugerencias de tipo PHP + docblocks - Inferencia automática del código (menor precedencia)

Cuando se proporciona un definition en el atributo Schema, se omite toda la inferencia automática y se usa la definición completa tal cual.

Atributos de Esquema a Nivel de Parámetro

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;
}

Esquema a Nivel de Método

/**
 * 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']
            ],
            'required' => ['name', 'email']
        ]
    ],
    required: ['profile']
)]
public function createUser(array $userData): array
{
    // PHP type hint provides base 'array' type
    // Method-level Schema adds object structure validation
    return ['id' => 123, 'status' => 'created'];
}

Anulación Completa del Esquema (Solo a Nivel de Método)

#[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']
            ]
        ]
    ],
    'required' => ['endpoint', 'method']
])]
public function processApiRequest(string $endpoint, string $method, array $headers): array
{
    // PHP type hints are completely ignored when definition is provided
    // The schema definition above takes full precedence
    return ['status' => 'processed', 'endpoint' => $endpoint];
}

⚠️ Importante: La anulación completa de la definición del esquema debe usarse raramente. Omite toda la inferencia automática de esquemas y requiere que definas todo el esquema JSON manualmente. Úsala solo si dominas la especificación JSON Schema y tienes requisitos de validación complejos que no se pueden lograr mediante el sistema de prioridad. En la mayoría de los casos, los atributos #[Schema] a nivel de parámetro y método proporcionan suficiente flexibilidad.

🎨 Formato de Valores de Retorno

El servidor formatea automáticamente los valores de retorno de tus manejadores en tipos de contenido MCP apropiados:

Formato Automático

// 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

Tipos de Contenido Avanzados

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

public function getFormattedCode(): TextContent
{
    return TextContent::code('<?php 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'
    );
}

Manejo de Archivos y Flujos

// 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'
    // ];
}

🔄 Procesamiento por Lotes

El servidor maneja automáticamente solicitudes JSON-RPC por lotes:

// 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": {...}}
]

🔧 Características Avanzadas

Proveedores de Completado

Los proveedores de completado permiten a los clientes MCP ofrecer sugerencias de autocompletado en sus interfaces de usuario. Están diseñados específicamente para Plantillas de Recursos y Prompts para ayudar a los usuarios a descubrir opciones disponibles para partes dinámicas como variables de plantilla o argumentos de prompts.

Nota: Las herramientas y los recursos se pueden descubrir mediante comandos MCP estándar (tools/list, resources/list), por lo que no se necesitan proveedores de completado para ellos. Los proveedores de completado se usan solo para plantillas de recursos (variables de URI) y argumentos de prompts.

El atributo #[CompletionProvider] soporta tres tipos de fuentes de completado:

1. Clases de Proveedores Personalizados

Para lógica de completado compleja, implementa la CompletionProviderInterface:

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

class UserIdCompletionProvider implements CompletionProviderInterface
{
    public function __construct(private DatabaseService $db) {}

    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        // Dynamic completion from database
        return $this->db->searchUsers($currentValue);
    }
}

class UserService
{
    #[McpResourceTemplate(uriTemplate: 'user://{userId}/profile')]
    public function getUserProfile(
        #[CompletionProvider(provider: UserIdCompletionProvider::class)]  // Class string - resolved from container
        string $userId
    ): array {
        return ['id' => $userId, 'name' => 'John Doe'];
    }
}

También puedes pasar instancias de proveedores preconfiguradas:

class DocumentService  
{
    #[McpPrompt(name: 'document_prompt')]
    public function generatePrompt(
        #[CompletionProvider(provider: new UserIdCompletionProvider($database))]  // Pre-configured instance
        string $userId,
        
        #[CompletionProvider(provider: $this->categoryProvider)]  // Instance from property
        string $category
    ): array {
        return [['role' => 'user', 'content' => "Generate document for user {$userId} in {$category}"]];
    }
}

2. Completados de Lista Simple

Para listas de completado estáticas, usa el parámetro values:

use PhpMcp\Server\Attributes\{McpPrompt, CompletionProvider};

class ContentService
{
    #[McpPrompt(name: 'content_generator')]
    public function generateContent(
        #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide', 'documentation'])]
        string $contentType,
        
        #[CompletionProvider(values: ['beginner', 'intermediate', 'advanced', 'expert'])]
        string $difficulty
    ): array {
        return [['role' => 'user', 'content' => "Create a {$difficulty} level {$contentType}"]];
    }
}

3. Completados Basados en Enums

Para clases enum, usa el parámetro enum:

enum Priority: string
{
    case LOW = 'low';
    case MEDIUM = 'medium';
    case HIGH = 'high';
    case CRITICAL = 'critical';
}

enum Status  // Unit enum (no backing values)
{
    case DRAFT;
    case PUBLISHED;
    case ARCHIVED;
}

class TaskService
{
    #[McpTool(name: 'create_task')]
    public function createTask(
        string $title,
        
        #[CompletionProvider(enum: Priority::class)]  // String-backed enum uses values
        string $priority,
        
        #[CompletionProvider(enum: Status::class)]    // Unit enum uses case names
        string $status
    ): array {
        return ['id' => 123, 'title' => $title, 'priority' => $priority, 'status' => $status];
    }
}

Registro Manual con Proveedores de Completado

$server = Server::make()
    ->withServerInfo('Completion Demo', '1.0.0')
    
    // Using provider class (resolved from container)
    ->withPrompt(
        [DocumentHandler::class, 'generateReport'],
        name: 'document_report'
        // Completion providers are auto-discovered from method attributes
    )
    
    // Using closure with inline completion providers
    ->withPrompt(
        function(
            #[CompletionProvider(values: ['json', 'xml', 'csv', 'yaml'])]
            string $format,
            
            #[CompletionProvider(enum: Priority::class)]
            string $priority
        ): array {
            return [['role' => 'user', 'content' => "Export data in {$format} format with {$priority} priority"]];
        },
        name: 'export_data'
    )
    
    ->build();

Resolución de Proveedores de Completado

El servidor maneja automáticamente la resolución de proveedores:

  • Cadenas de clase (MyProvider::class) → Resueltas desde el contenedor PSR-11 con inyección de dependencias
  • Instancias (new MyProvider()) → Usadas directamente tal cual
  • Arreglos de valores (['a', 'b', 'c']) → Envueltos automáticamente en ListCompletionProvider
  • Clases enum (MyEnum::class) → Envueltas automáticamente en EnumCompletionProvider

Importante: Los proveedores de completado solo ofrecen sugerencias a los usuarios en la interfaz del cliente MCP. Los usuarios aún pueden ingresar cualquier valor, así que siempre valida los parámetros en tus manejadores independientemente de las restricciones del proveedor de completado.

Inyección de Dependencias Personalizada

Tus manejadores de elementos MCP pueden usar inyección de dependencias por constructor para acceder a servicios como bases de datos, APIs u otra lógica de negocio. Cuando los manejadores tienen dependencias en el constructor, debes proporcionar un contenedor PSR-11 preconfigurado que contenga esas dependencias.

Por defecto, el servidor usa un BasicContainer - una implementación simple que intenta auto-cablear dependencias instanciando clases con constructores sin parámetros. Para dependencias que requieren configuración (como conexiones a bases de datos), puedes agregarlas manualmente al BasicContainer o usar un contenedor PSR-11 más avanzado como PHP-DI o el contenedor de Laravel.

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();

Suscripciones a Recursos

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'];
}

Reanudabilidad y Almacén de Eventos

Para implementaciones de producción que usan StreamableHttpServerTransport, puedes implementar reanudabilidad con event sourcing proporcionando un almacén de eventos personalizado:

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
);

Manejadores de Sesión Personalizados

Implementa almacenamiento de sesión personalizado creando una clase que implemente SessionHandlerInterface:

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();

Soporte de Middleware

Tanto HttpServerTransport como StreamableHttpServerTransport soportan middleware compatible con PSR-7 para interceptar y modificar solicitudes y respuestas HTTP. El middleware te permite extraer funcionalidad común como autenticación, registro, manejo de CORS y validación de solicitudes en componentes reutilizables.

El middleware debe ser un callable PHP válido que acepte un ServerRequestInterface PSR-7 como primer argumento y un callable como segundo argumento.

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use React\Promise\PromiseInterface;

class AuthMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $apiKey = $request->getHeaderLine('Authorization');
        if (empty($apiKey)) {
            return new Response(401, [], 'Authorization required');
        }
        
        $request = $request->withAttribute('user_id', $this->validateApiKey($apiKey));
        $result = $next($request);
        
        return match (true) {
            $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
            $result instanceof ResponseInterface => $this->handle($result),
            default => $result
        };
    }
    
    private function handle($response)
    {
        return $response instanceof ResponseInterface
            ? $response->withHeader('X-Auth-Provider', 'mcp-server')
            : $response;
    }
}

$middlewares = [
    new AuthMiddleware(),
    new LoggingMiddleware(),
    function(ServerRequestInterface $request, callable $next) {
        $result = $next($request);
        return match (true) {
            $result instanceof PromiseInterface => $result->then(function($response) {
                return $response instanceof ResponseInterface 
                    ? $response->withHeader('Access-Control-Allow-Origin', '*')
                    : $response;
            }),
            $result instanceof ResponseInterface => $result->withHeader('Access-Control-Allow-Origin', '*'),
            default => $result
        };
    }
];

$transport = new StreamableHttpServerTransport(
    host: '127.0.0.1',
    port: 8080,
    middlewares: $middlewares
);

Consideraciones Importantes:

  • Manejo de Respuestas: El middleware debe manejar tanto retornos síncronos ResponseInterface como asíncronos PromiseInterface de $next($request), ya que ReactPHP opera de forma asíncrona
  • Patrón Invocable: El patrón recomendado es usar clases invocables con un método handle() separado para procesar respuestas, haciendo la lógica asíncrona reutilizable
  • Orden de Ejecución: El middleware se ejecuta en el orden proporcionado, siendo el último middleware el más cercano a tus manejadores MCP

Configuración de Contexto SSL

Para implementaciones HTTPS de StreamableHttpServerTransport, configura las opciones de contexto SSL:

$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
);

Referencia de Contexto SSL: Para opciones completas de contexto SSL, consulta la documentación de Opciones de Contexto SSL de PHP.

🔍 Manejo de Errores y Depuración

El servidor proporciona capacidades integrales de manejo de errores y depuración:

Manejo de Excepciones

Los manejadores de herramientas pueden lanzar cualquier excepción PHP cuando ocurren errores. El servidor convierte automáticamente estas excepciones en respuestas de error JSON-RPC apropiadas para clientes MCP.

#[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);
}

El servidor convertirá estas excepciones en respuestas de error JSON-RPC apropiadas que los clientes MCP puedan entender y mostrar a los usuarios.

Registro y Depuración

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];
    }
}

🚀 Implementación en Producción

Dado que $server->listen() ejecuta un proceso persistente, puedes implementarlo usando cualquier estrategia que se adapte a las necesidades de tu infraestructura. El servidor puede implementarse en VPS, instancias en la nube, contenedores o cualquier entorno que soporte procesos de larga duración.

Aquí hay dos enfoques populares de implementación a considerar:

Opción 1: VPS con Supervisor + Nginx (Recomendado)

Mejor para: La mayoría de implementaciones de producción, rentable, control total

# 1. Install your application on VPS
git clone https://github.com/yourorg/your-mcp-server.git /var/www/mcp-server
cd /var/www/mcp-server
composer install --no-dev --optimize-autoloader

# 2. Install Supervisor
sudo apt-get install supervisor

# 3. Create Supervisor configuration
sudo nano /etc/supervisor/conf.d/mcp-server.conf

Configuración de Supervisor:

[program:mcp-server]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/mcp-server/server.php --transport=http --host=127.0.0.1 --port=8080
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/mcp-server.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=3

Configuración de Nginx con SSL:

# /etc/nginx/sites-available/mcp-server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mcp.yourdomain.com;

    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # MCP Server proxy
    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Important for SSE connections
        proxy_buffering off;
        proxy_cache off;
        
        proxy_pass http://127.0.0.1:8080/;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name mcp.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

Iniciar Servicios:

# Enable and start supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start mcp-server:*

# Enable and start nginx
sudo systemctl enable nginx
sudo systemctl restart nginx

# Check status
sudo supervisorctl status

Configuración del Cliente:

{
  "mcpServers": {
    "my-server": {
      "url": "https://mcp.yourdomain.com/mcp"
    }
  }
}

Opción 2: Implementación con Docker

Mejor para: Entornos contenedorizados, Kubernetes, plataformas en la nube

Dockerfile de Producción:

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"]

docker-compose.yml:

services:
  mcp-server:
    build: .
    ports:
      - "8080:80"
    environment:
      - MCP_ENV=production
      - MCP_LOG_LEVEL=info
    volumes:
      - ./storage:/var/www/mcp/storage
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Optional: Add database if needed
  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secure_password
      MYSQL_DATABASE: mcp_server
    volumes:
      - mysql_data:/var/lib/mysql
    restart: unless-stopped

volumes:
  mysql_data:

Mejores Prácticas de Seguridad

  1. Configuración del Firewall:
# Only allow necessary ports
sudo ufw allow ssh
sudo ufw allow 80
sudo ufw allow 443
sudo ufw deny 8080  # MCP port should not be publicly accessible
sudo ufw enable
  1. Configuración SSL/TLS:
# Install Certbot for Let's Encrypt
sudo apt install certbot python3-certbot-nginx

# Generate SSL certificate
sudo certbot --nginx -d mcp.yourdomain.com

📚 Ejemplos y Casos de Uso

Explora ejemplos completos en el directorio examples/:

Ejemplos Disponibles

  • 01-discovery-stdio-calculator/ - Calculadora básica stdio con descubrimiento de atributos
  • 02-discovery-http-userprofile/ - Servidor HTTP con gestión de perfiles de usuario
  • 03-manual-registration-stdio/ - Patrones de registro manual de elementos
  • 04-combined-registration-http/ - Combinación de elementos manuales y descubiertos
  • 05-stdio-env-variables/ - Manejo de variables de entorno
  • 06-custom-dependencies-stdio/ - Inyección de dependencias con gestión de tareas
  • 07-complex-tool-schema-http/ - Ejemplos avanzados de validación de esquemas
  • 08-schema-showcase-streamable/ - Demostración integral de características de esquemas

Ejecutar Ejemplos

# 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

🚧 Migración desde v2.x

Si migras desde la versión 2.x, ten en cuenta estos cambios clave:

Actualizaciones de Esquema

  • Usa el paquete php-mcp/schema para DTOs en lugar de clases internas
  • Los tipos de contenido se movieron al espacio de nombres PhpMcp\Schema\Content\*
  • Firmas de métodos actualizadas para mejor seguridad de tipos

Gestión de Sesiones

  • Nueva gestión de sesiones con múltiples backends
  • Usa ->withSession() o ->withSessionHandler() para configuración
  • Las sesiones ahora son persistentes entre reconexiones (con backend de caché)

Cambios de Transporte

  • Nuevo StreamableHttpServerTransport con reanudabilidad
  • Manejo de errores mejorado y event sourcing
  • Mejor procesamiento de solicitudes por lotes

🧪 Pruebas

# Install development dependencies
composer install --dev

# Run the test suite
composer test

# Run tests with coverage (requires Xdebug)
composer test:coverage

# Run code style checks
composer lint

🤝 Contribuciones

¡Agradecemos las contribuciones! Consulta CONTRIBUTING.md para las pautas.

📄 Licencia

La Licencia MIT (MIT). Consulta LICENSE para más detalles.

🙏 Agradecimientos