Skip to content

zestic/php-mcp-psr-15

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PHP MCP PSR-15 Integration

A PSR-15 middleware and handler library for integrating Model Context Protocol (MCP) servers into any PSR-15 compatible framework.

Features

  • Framework Agnostic: Works with any PSR-15 compatible framework (Mezzio, Slim, Laravel, Symfony, etc.)
  • Flexible Routing: Support for multiple endpoint patterns and route parameters
  • Streamable HTTP Transport: Full support for Server-Sent Events (SSE) and JSON-RPC over HTTP
  • Bring Your Own Middleware: Use your existing auth, CORS, rate limiting, and other middleware
  • Auto-Discovery: Automatic MCP element discovery with caching
  • Type Safe: Full PHP 8.1+ type hints and modern syntax
  • Standards Compliant: Built on PSR-7, PSR-11, PSR-15, and PSR-16
  • Production Ready: Comprehensive testing, error handling, and monitoring support

Installation

composer require php-mcp/psr-15

Requirements

  • PHP 8.1+
  • PSR-15 compatible framework
  • php-mcp/server ^3.3

Quick Start

Basic Setup

use PhpMcp\Psr15\McpPsr15Builder;
use PhpMcp\Server\Server;
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\StreamFactory;

// Create your MCP server
$server = Server::make()
    ->withServerInfo('My MCP Server', '1.0.0')
    ->withTool(fn(int $a, int $b) => $a + $b, 'add')
    ->build();

// Build PSR-15 components
$mcpSetup = McpPsr15Builder::create(
    $server,
    new ResponseFactory(),
    new StreamFactory()
)
->withAutoDiscover(scanDirs: ['src/Mcp'])
->build();

// Use with your PSR-15 framework
$handler = $mcpSetup['handler'];
$middleware = $mcpSetup['middleware'];

Framework Integration

Mezzio/Laminas

// In your route configuration
$app->route('/mcp', [
    ...$mcpSetup['middleware'],
    $mcpSetup['handler'],
], ['GET', 'POST', 'OPTIONS'], 'mcp');

Slim Framework

$app->any('/mcp[/{clientId}]', $mcpSetup['handler'])
    ->add($mcpSetup['middleware'][2] ?? null) // Rate limit
    ->add($mcpSetup['middleware'][1] ?? null) // Auth
    ->add($mcpSetup['middleware'][0] ?? null); // CORS

Laravel

// Convert PSR-15 to Laravel-compatible
Route::match(['GET', 'POST', 'OPTIONS'], '/mcp/{clientId?}',
    function (Request $request, string $clientId = null) use ($mcpSetup) {
        // Convert Laravel request to PSR-7 and process
        // See examples/laravel/ for full implementation
    }
);

Routing Patterns

The PSR-15 integration supports flexible routing patterns:

Basic Patterns

  • /mcp - Basic MCP endpoint
  • /mcp/{clientId} - Client-specific endpoint
  • /mcp/{clientId}/{sessionId} - Session-specific endpoint
  • /mcp/status - Status/health check endpoint

HTTP Methods

  • GET: Server-Sent Events (SSE) connections and server info
  • POST: JSON-RPC message handling
  • OPTIONS: CORS preflight requests

Client ID Extraction

The handler automatically extracts client IDs from:

  1. Route parameters ({clientId})
  2. Query parameters (?clientId=abc)
  3. Headers (X-Client-ID: abc)

Middleware Configuration

CORS Configuration

->withCors([
    'allowed_origins' => ['http://localhost:3000', 'https://app.example.com'],
    'allowed_methods' => ['GET', 'POST', 'OPTIONS'],
    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Client-ID'],
    'exposed_headers' => ['X-RateLimit-Remaining'],
    'max_age' => 86400,
    'credentials' => false,
])

Authentication

->withAuth(function(string $token, ServerRequestInterface $request): bool {
    // Simple token validation
    return $token === $_ENV['MCP_API_TOKEN'];

    // Or JWT validation
    try {
        $payload = JWT::decode($token, $key, ['HS256']);
        return isset($payload->sub);
    } catch (Exception $e) {
        return false;
    }
})

Rate Limiting

->withRateLimit([
    'requests_per_minute' => 60,
    'burst_limit' => 10,
    'storage' => 'memory', // memory, cache, redis
])

Auto-Discovery

->withAutoDiscover(
    basePath: getcwd(),
    scanDirs: ['src/Mcp', 'app/Mcp'],
    excludeDirs: ['tests', 'vendor'],
    saveToCache: true,
    cacheTtl: 3600
)

Usage Examples

Server-Sent Events (SSE)

const eventSource = new EventSource('/mcp?clientId=my-client', {
    headers: {
        'Authorization': 'Bearer your-token-here'
    }
});

eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log('MCP message:', data);
};

eventSource.onerror = function(event) {
    console.error('SSE error:', event);
};

JSON-RPC Requests

// List available tools
fetch('/mcp/my-client', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here'
    },
    body: JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'tools/list'
    })
})
.then(response => response.json())
.then(data => console.log('Tools:', data.result));

// Call a tool
fetch('/mcp/my-client', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here'
    },
    body: JSON.stringify({
        jsonrpc: '2.0',
        id: 2,
        method: 'tools/call',
        params: {
            name: 'add',
            arguments: { a: 5, b: 3 }
        }
    })
})
.then(response => response.json())
.then(data => console.log('Result:', data.result));

Status Check

curl -X GET "http://localhost:8080/mcp/status"

Testing

Run the test suite:

composer test

Run with coverage:

composer test:coverage

Manual Testing

# Status check
curl -X GET "http://localhost:8080/mcp/status"

# CORS preflight
curl -X OPTIONS "http://localhost:8080/mcp" \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST"

# JSON-RPC request
curl -X POST "http://localhost:8080/mcp" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Architecture

Core Components

  • McpHandler: PSR-15 RequestHandlerInterface that processes HTTP requests
  • McpPsr15Builder: Fluent builder for configuration
  • Psr7Transport: Bridge between PSR-7 and MCP server
  • Middleware: CORS, Authentication, Rate Limiting components
  • RouteHelper: Framework-agnostic routing utilities

Request Flow

  1. HTTP Request → Framework Router → PSR-15 Middleware Stack
  2. Middleware Stack → CORS → Auth → Rate Limit → Custom Middleware
  3. McpHandler → Parse HTTP request → Extract MCP message
  4. Psr7Transport → Convert to MCP transport format
  5. MCP Server → Process via Protocol and Dispatcher
  6. Response Path → MCP Response → PSR-7 Response → HTTP Response

Examples

See the examples/ directory for complete framework integration examples:

  • Mezzio: Clean middleware pipeline with container integration
  • Slim: Optional route parameters with fluent middleware API
  • Laravel: PSR-7 bridge integration with Sanctum authentication

Best Practices

  1. Use middleware composition for clean separation of concerns
  2. Implement proper CORS for web client access
  3. Add authentication for production deployments
  4. Enable rate limiting to prevent abuse
  5. Provide status endpoints for monitoring
  6. Document your API with the /docs endpoint
  7. Use route parameters for client/session identification
  8. Handle errors gracefully with proper HTTP status codes

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

MIT License. See LICENSE for details.

Related Packages

Quick Start

Basic Setup

<?php

use PhpMcp\Psr15\McpPsr15Builder;
use PhpMcp\Server\Server;

// Create your MCP server
$server = Server::make()
    ->withServerInfo('My MCP Server', '1.0.0')
    ->build();

// Build PSR-15 components
$mcpSetup = McpPsr15Builder::create($server, $responseFactory, $streamFactory)
    ->withAutoDiscover(scanDirs: ['src/Mcp'])
    ->withCors()
    ->build();

// Use $mcpSetup['handler'] and $mcpSetup['middleware'] in your framework

Framework Integration Examples

Laminas Mezzio

1. Install Dependencies

composer require laminas/laminas-diactoros
composer require php-mcp/psr-15

2. Create MCP Handler Factory

Create src/Mcp/Factory/McpHandlerFactory.php:

<?php

namespace App\Mcp\Factory;

use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Server\Server;
use Psr\Container\ContainerInterface;
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\StreamFactory;

class McpHandlerFactory
{
    public function __invoke(ContainerInterface $container): McpHandler
    {
        $server = $container->get(Server::class);
        $config = $container->get('config')['mcp'] ?? [];
        
        return new McpHandler(
            server: $server,
            responseFactory: new ResponseFactory(),
            streamFactory: new StreamFactory(),
            options: $config['psr15'] ?? []
        );
    }
}

3. Create Server Factory

Create src/Mcp/Factory/ServerFactory.php:

<?php

namespace App\Mcp\Factory;

use PhpMcp\Server\Server;
use Psr\Container\ContainerInterface;

class ServerFactory
{
    public function __invoke(ContainerInterface $container): Server
    {
        $config = $container->get('config')['mcp'] ?? [];
        
        $server = Server::make()
            ->withServerInfo(
                name: $config['server']['name'] ?? 'Mezzio MCP Server',
                version: $config['server']['version'] ?? '1.0.0'
            )
            ->withLogger($container->get('logger'))
            ->withContainer($container)
            ->build();

        // Auto-discover MCP elements
        if ($config['auto_discover'] ?? true) {
            $server->discover(
                basePath: getcwd(),
                scanDirs: $config['scan_dirs'] ?? ['src/Mcp'],
                excludeDirs: $config['exclude_dirs'] ?? []
            );
        }

        return $server;
    }
}

4. Configure Dependencies

In config/dependencies.php:

<?php

use App\Mcp\Factory\McpHandlerFactory;
use App\Mcp\Factory\ServerFactory;
use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Psr15\Middleware\CorsMiddleware;
use PhpMcp\Server\Server;

return [
    'dependencies' => [
        'factories' => [
            Server::class => ServerFactory::class,
            McpHandler::class => McpHandlerFactory::class,
            CorsMiddleware::class => function() {
                return new CorsMiddleware([
                    'allowed_origins' => ['*'],
                    'allowed_methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
                    'allowed_headers' => ['Content-Type', 'Last-Event-ID', 'Authorization'],
                ]);
            },
        ],
    ],
];

5. Configure Routes

In config/routes.php:

<?php

use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Psr15\Middleware\CorsMiddleware;

return function (
    \Mezzio\Application $app,
    \Mezzio\MiddlewareFactory $factory,
    \Psr\Container\ContainerInterface $container
): void {
    
    // MCP endpoint with middleware stack
    $app->route('/mcp', [
        CorsMiddleware::class,
        McpHandler::class,
    ], ['GET', 'POST', 'DELETE', 'OPTIONS'], 'mcp.endpoint');
};

6. Create MCP Configuration

Create config/autoload/mcp.global.php:

<?php

return [
    'mcp' => [
        'server' => [
            'name' => 'Mezzio MCP Server',
            'version' => '1.0.0',
        ],
        'auto_discover' => true,
        'scan_dirs' => ['src/Mcp'],
        'exclude_dirs' => ['src/Mcp/Tests'],
        'psr15' => [
            'auto_discover' => true,
            'base_path' => getcwd(),
            'save_to_cache' => true,
        ],
    ],
];

7. Create MCP Elements

Create src/Mcp/UserService.php:

<?php

namespace App\Mcp;

use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Attributes\McpResource;

class UserService
{
    #[McpTool(name: 'get_user_info')]
    public function getUserInfo(int $userId): array
    {
        return [
            'id' => $userId,
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ];
    }

    #[McpResource(uri: 'users://stats', mimeType: 'application/json')]
    public function getUserStats(): array
    {
        return [
            'total_users' => 1250,
            'active_users' => 890,
            'new_today' => 15,
        ];
    }
}

Slim Framework

1. Install Dependencies

composer require slim/slim:"4.*"
composer require slim/psr7
composer require php-di/php-di
composer require php-mcp/psr-15

2. Create Application

Create public/index.php:

<?php

use DI\Container;
use PhpMcp\Psr15\McpPsr15Builder;
use PhpMcp\Server\Server;
use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\ResponseFactory;
use Slim\Psr7\Factory\StreamFactory;

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

// Create DI container
$container = new Container();

// Configure MCP Server
$container->set(Server::class, function() use ($container) {
    return Server::make()
        ->withServerInfo('Slim MCP Server', '1.0.0')
        ->withContainer($container)
        ->build();
});

// Create Slim app
AppFactory::setContainer($container);
$app = AppFactory::create();

// Configure MCP with builder
$server = $container->get(Server::class);
$mcpSetup = McpPsr15Builder::create($server, new ResponseFactory(), new StreamFactory())
    ->withAutoDiscover(
        basePath: __DIR__ . '/..',
        scanDirs: ['src/Mcp'],
        saveToCache: true
    )
    ->withCors([
        'allowed_origins' => ['http://localhost:3000', 'https://app.example.com'],
        'allowed_methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
    ])
    ->withAuth(function(string $token): bool {
        // Simple token validation - replace with your auth logic
        return $token === $_ENV['MCP_API_TOKEN'] ?? 'secret-key';
    })
    ->build();

// Register MCP routes
$mcpGroup = $app->group('/mcp', function ($group) use ($mcpSetup) {
    $group->any('', $mcpSetup['handler']);
});

// Apply middleware in reverse order (Slim applies them in LIFO order)
foreach (array_reverse($mcpSetup['middleware']) as $middleware) {
    $mcpGroup->add($middleware);
}

// Add other routes
$app->get('/', function ($request, $response) {
    $response->getBody()->write('MCP Server is running!');
    return $response;
});

$app->run();

3. Create MCP Elements

Create src/Mcp/CalculatorService.php:

<?php

namespace App\Mcp;

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

class CalculatorService
{
    #[McpTool(name: 'add_numbers', description: 'Add two numbers together')]
    #[Schema([
        'type' => 'object',
        'properties' => [
            'a' => ['type' => 'number', 'description' => 'First number'],
            'b' => ['type' => 'number', 'description' => 'Second number'],
        ],
        'required' => ['a', 'b'],
    ])]
    public function add(float $a, float $b): float
    {
        return $a + $b;
    }

    #[McpTool(name: 'multiply_numbers')]
    public function multiply(
        #[Schema(['type' => 'number', 'minimum' => 0])] float $x,
        #[Schema(['type' => 'number', 'minimum' => 0])] float $y
    ): float {
        return $x * $y;
    }
}

4. Environment Configuration

Create .env:

MCP_API_TOKEN=your-secret-api-token-here
MCP_AUTO_DISCOVER=true
MCP_CACHE_TTL=3600

Spiral Framework

1. Install Dependencies

composer require spiral/framework
composer require nyholm/psr7
composer require php-mcp/psr-15

2. Create Bootloader

Create app/src/Bootloader/McpBootloader.php:

<?php

namespace App\Bootloader;

use App\Mcp\Factory\McpHandlerFactory;
use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Psr15\Middleware\CorsMiddleware;
use PhpMcp\Server\Server;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Boot\DirectoriesInterface;
use Spiral\Core\Container;

class McpBootloader extends Bootloader
{
    protected const SINGLETONS = [
        Server::class => [self::class, 'createServer'],
        McpHandler::class => McpHandlerFactory::class,
    ];

    public function boot(Container $container): void
    {
        $container->bindSingleton(CorsMiddleware::class, function() {
            return new CorsMiddleware([
                'allowed_origins' => ['*'],
                'allowed_methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
            ]);
        });
    }

    private function createServer(
        Container $container,
        DirectoriesInterface $dirs
    ): Server {
        $server = Server::make()
            ->withServerInfo('Spiral MCP Server', '1.0.0')
            ->withContainer($container)
            ->build();

        // Auto-discover
        $server->discover(
            basePath: $dirs->get('root'),
            scanDirs: ['app/src/Mcp']
        );

        return $server;
    }
}

3. Register Routes

In app/config/http.php:

<?php

use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Psr15\Middleware\CorsMiddleware;
use Spiral\Router\Route;

return [
    'routes' => [
        Route::methods(['GET', 'POST', 'DELETE', 'OPTIONS'], '/mcp', [
            CorsMiddleware::class,
            McpHandler::class,
        ])->withName('mcp.endpoint'),
    ],
];

Advanced Configuration

Custom Authentication

$mcpSetup = McpPsr15Builder::create($server, $responseFactory, $streamFactory)
    ->withAuth(function(string $token, ServerRequestInterface $request): bool {
        // JWT validation
        try {
            $payload = JWT::decode($token, $secretKey, ['HS256']);
            return $payload->role === 'mcp-client';
        } catch (Exception $e) {
            return false;
        }
    })
    ->build();

Rate Limiting Middleware

use PhpMcp\Psr15\Middleware\RateLimitMiddleware;

$mcpSetup = McpPsr15Builder::create($server, $responseFactory, $streamFactory)
    ->withMiddleware(new RateLimitMiddleware([
        'requests_per_minute' => 60,
        'burst_limit' => 10,
    ]))
    ->build();

Custom CORS Configuration

$mcpSetup = McpPsr15Builder::create($server, $responseFactory, $streamFactory)
    ->withCors([
        'allowed_origins' => ['https://app.example.com', 'https://admin.example.com'],
        'allowed_methods' => ['GET', 'POST', 'DELETE'],
        'allowed_headers' => ['Content-Type', 'Authorization', 'X-API-Key'],
        'exposed_headers' => ['X-RateLimit-Remaining'],
        'max_age' => 86400,
        'credentials' => true,
    ])
    ->build();

Testing

Unit Testing the Handler

<?php

use PhpMcp\Psr15\Handler\McpHandler;
use PhpMcp\Server\Server;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;

class McpHandlerTest extends TestCase
{
    public function testHandlesGetRequest(): void
    {
        $server = Server::make()->build();
        $handler = new McpHandler($server, $responseFactory, $streamFactory);
        
        $request = $this->createMock(ServerRequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        
        $response = $handler->handle($request);
        
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('text/event-stream', $response->getHeaderLine('Content-Type'));
    }
}

Integration Testing

<?php

use PhpMcp\Psr15\McpPsr15Builder;
use PhpMcp\Server\Server;

class McpIntegrationTest extends TestCase
{
    public function testFullMcpStack(): void
    {
        $server = Server::make()
            ->withTool(fn($x, $y) => $x + $y, 'add')
            ->build();
            
        $mcpSetup = McpPsr15Builder::create($server, $responseFactory, $streamFactory)
            ->withCors()
            ->build();
            
        // Test that handler and middleware are properly configured
        $this->assertInstanceOf(McpHandler::class, $mcpSetup['handler']);
        $this->assertCount(1, $mcpSetup['middleware']); // CORS middleware
    }
}

Production Deployment

Docker Example

FROM php:8.1-fpm-alpine

# Install extensions
RUN docker-php-ext-install pdo_mysql opcache

# Copy application
COPY . /var/www/html
WORKDIR /var/www/html

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

# Configure PHP for production
COPY docker/php.ini /usr/local/etc/php/conf.d/production.ini

EXPOSE 9000
CMD ["php-fpm"]

Nginx Configuration

server {
    listen 80;
    server_name mcp.example.com;
    root /var/www/html/public;
    index index.php;

    location /mcp {
        try_files $uri $uri/ /index.php$is_args$args;
        
        # Enable Server-Sent Events
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        chunked_transfer_encoding off;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

MIT License. See LICENSE file for details.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages