#
tokens: 49837/50000 100/154 files (page 1/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 5. Use http://codebase.md/php-mcp/server?page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       ├── changelog.yml
│       └── tests.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── composer.json
├── CONTRIBUTING.md
├── examples
│   ├── 01-discovery-stdio-calculator
│   │   ├── McpElements.php
│   │   └── server.php
│   ├── 02-discovery-http-userprofile
│   │   ├── McpElements.php
│   │   ├── server.php
│   │   └── UserIdCompletionProvider.php
│   ├── 03-manual-registration-stdio
│   │   ├── server.php
│   │   └── SimpleHandlers.php
│   ├── 04-combined-registration-http
│   │   ├── DiscoveredElements.php
│   │   ├── ManualHandlers.php
│   │   └── server.php
│   ├── 05-stdio-env-variables
│   │   ├── EnvToolHandler.php
│   │   └── server.php
│   ├── 06-custom-dependencies-stdio
│   │   ├── McpTaskHandlers.php
│   │   ├── server.php
│   │   └── Services.php
│   ├── 07-complex-tool-schema-http
│   │   ├── EventTypes.php
│   │   ├── McpEventScheduler.php
│   │   └── server.php
│   └── 08-schema-showcase-streamable
│       ├── SchemaShowcaseElements.php
│       └── server.php
├── LICENSE
├── phpunit.xml
├── README.md
├── src
│   ├── Attributes
│   │   ├── CompletionProvider.php
│   │   ├── McpPrompt.php
│   │   ├── McpResource.php
│   │   ├── McpResourceTemplate.php
│   │   ├── McpTool.php
│   │   └── Schema.php
│   ├── Configuration.php
│   ├── Context.php
│   ├── Contracts
│   │   ├── CompletionProviderInterface.php
│   │   ├── EventStoreInterface.php
│   │   ├── LoggerAwareInterface.php
│   │   ├── LoopAwareInterface.php
│   │   ├── ServerTransportInterface.php
│   │   ├── SessionHandlerInterface.php
│   │   └── SessionInterface.php
│   ├── Defaults
│   │   ├── ArrayCache.php
│   │   ├── BasicContainer.php
│   │   ├── DefaultUuidSessionIdGenerator.php
│   │   ├── EnumCompletionProvider.php
│   │   ├── FileCache.php
│   │   ├── InMemoryEventStore.php
│   │   ├── ListCompletionProvider.php
│   │   └── SystemClock.php
│   ├── Dispatcher.php
│   ├── Elements
│   │   ├── RegisteredElement.php
│   │   ├── RegisteredPrompt.php
│   │   ├── RegisteredResource.php
│   │   ├── RegisteredResourceTemplate.php
│   │   └── RegisteredTool.php
│   ├── Exception
│   │   ├── ConfigurationException.php
│   │   ├── DiscoveryException.php
│   │   ├── McpServerException.php
│   │   ├── ProtocolException.php
│   │   └── TransportException.php
│   ├── Protocol.php
│   ├── Registry.php
│   ├── Server.php
│   ├── ServerBuilder.php
│   ├── Session
│   │   ├── ArraySessionHandler.php
│   │   ├── CacheSessionHandler.php
│   │   ├── Session.php
│   │   ├── SessionManager.php
│   │   └── SubscriptionManager.php
│   ├── Transports
│   │   ├── HttpServerTransport.php
│   │   ├── StdioServerTransport.php
│   │   └── StreamableHttpServerTransport.php
│   └── Utils
│       ├── Discoverer.php
│       ├── DocBlockParser.php
│       ├── HandlerResolver.php
│       ├── SchemaGenerator.php
│       └── SchemaValidator.php
└── tests
    ├── Fixtures
    │   ├── Discovery
    │   │   ├── DiscoverablePromptHandler.php
    │   │   ├── DiscoverableResourceHandler.php
    │   │   ├── DiscoverableTemplateHandler.php
    │   │   ├── DiscoverableToolHandler.php
    │   │   ├── EnhancedCompletionHandler.php
    │   │   ├── InvocablePromptFixture.php
    │   │   ├── InvocableResourceFixture.php
    │   │   ├── InvocableResourceTemplateFixture.php
    │   │   ├── InvocableToolFixture.php
    │   │   ├── NonDiscoverableClass.php
    │   │   └── SubDir
    │   │       └── HiddenTool.php
    │   ├── Enums
    │   │   ├── BackedIntEnum.php
    │   │   ├── BackedStringEnum.php
    │   │   ├── PriorityEnum.php
    │   │   ├── StatusEnum.php
    │   │   └── UnitEnum.php
    │   ├── General
    │   │   ├── CompletionProviderFixture.php
    │   │   ├── DocBlockTestFixture.php
    │   │   ├── InvokableHandlerFixture.php
    │   │   ├── PromptHandlerFixture.php
    │   │   ├── RequestAttributeChecker.php
    │   │   ├── ResourceHandlerFixture.php
    │   │   ├── ToolHandlerFixture.php
    │   │   └── VariousTypesHandler.php
    │   ├── Middlewares
    │   │   ├── ErrorMiddleware.php
    │   │   ├── FirstMiddleware.php
    │   │   ├── HeaderMiddleware.php
    │   │   ├── RequestAttributeMiddleware.php
    │   │   ├── SecondMiddleware.php
    │   │   ├── ShortCircuitMiddleware.php
    │   │   └── ThirdMiddleware.php
    │   ├── Schema
    │   │   └── SchemaGenerationTarget.php
    │   ├── ServerScripts
    │   │   ├── HttpTestServer.php
    │   │   ├── StdioTestServer.php
    │   │   └── StreamableHttpTestServer.php
    │   └── Utils
    │       ├── AttributeFixtures.php
    │       ├── DockBlockParserFixture.php
    │       └── SchemaGeneratorFixture.php
    ├── Integration
    │   ├── DiscoveryTest.php
    │   ├── HttpServerTransportTest.php
    │   ├── SchemaGenerationTest.php
    │   ├── StdioServerTransportTest.php
    │   └── StreamableHttpServerTransportTest.php
    ├── Mocks
    │   ├── Clients
    │   │   ├── MockJsonHttpClient.php
    │   │   ├── MockSseClient.php
    │   │   └── MockStreamHttpClient.php
    │   └── Clock
    │       └── FixedClock.php
    ├── Pest.php
    ├── TestCase.php
    └── Unit
        ├── Attributes
        │   ├── CompletionProviderTest.php
        │   ├── McpPromptTest.php
        │   ├── McpResourceTemplateTest.php
        │   ├── McpResourceTest.php
        │   └── McpToolTest.php
        ├── ConfigurationTest.php
        ├── Defaults
        │   ├── EnumCompletionProviderTest.php
        │   └── ListCompletionProviderTest.php
        ├── DispatcherTest.php
        ├── Elements
        │   ├── RegisteredElementTest.php
        │   ├── RegisteredPromptTest.php
        │   ├── RegisteredResourceTemplateTest.php
        │   ├── RegisteredResourceTest.php
        │   └── RegisteredToolTest.php
        ├── ProtocolTest.php
        ├── RegistryTest.php
        ├── ServerBuilderTest.php
        ├── ServerTest.php
        ├── Session
        │   ├── ArraySessionHandlerTest.php
        │   ├── CacheSessionHandlerTest.php
        │   ├── SessionManagerTest.php
        │   └── SessionTest.php
        └── Utils
            ├── DocBlockParserTest.php
            ├── HandlerResolverTest.php
            └── SchemaValidatorTest.php
```

# Files

--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------

```php
<?php

$finder = PhpCsFixer\Finder::create()
    ->exclude([
        'examples',
        'vendor',
        'tests/Mocks',
    ])
    ->in(__DIR__);

return (new PhpCsFixer\Config)
    ->setRules([
        '@PSR12' => true,
        'array_syntax' => ['syntax' => 'short'],
    ])
    ->setFinder($finder);

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Composer dependencies
/vendor/
/composer.lock

# PHPUnit
.phpunit.result.cache

# PHP CS Fixer
/.php-cs-fixer.cache

# Editor directories and files
/.idea
/.vscode
*.sublime-project
*.sublime-workspace

# Operating system files
.DS_Store
Thumbs.db

# Local environment files
/.env
/.env.backup
/.env.local

# PHP CodeSniffer
/.phpcs.xml
/.phpcs.xml.dist
/phpcs.xml
/phpcs.xml.dist

# PHPStan
/phpstan.neon
/phpstan.neon.dist

# Local development tools
/.php_cs
/.php_cs.cache
/.php_cs.dist
/_ide_helper.php

# Build artifacts
/build/
/coverage/

# PHPUnit coverage reports
/clover.xml
/coverage.xml
/coverage/

# Laravel generated files
bootstrap/cache/
.phpunit.result.cache

# Local Composer dependencies
composer.phar

workbench
playground

# Log files
*.log

# Cache files
cache
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# PHP MCP Server SDK

[![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/server.svg?style=flat-square)](https://packagist.org/packages/php-mcp/server)
[![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/server.svg?style=flat-square)](https://packagist.org/packages/php-mcp/server)
[![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/server/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/server/actions/workflows/tests.yml)
[![License](https://img.shields.io/packagist/l/php-mcp/server.svg?style=flat-square)](LICENSE)

**A comprehensive PHP SDK for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) servers. Create production-ready MCP servers in PHP with modern architecture, extensive testing, and flexible transport options.**

This SDK enables you to expose your PHP application's functionality as standardized MCP **Tools**, **Resources**, and **Prompts**, allowing AI assistants (like Anthropic's Claude, Cursor IDE, OpenAI's ChatGPT, etc.) to interact with your backend using the MCP standard.

## 🚀 Key Features

- **🏗️ Modern Architecture**: Built with PHP 8.1+ features, PSR standards, and modular design
- **📡 Multiple Transports**: Supports `stdio`, `http+sse`, and new **streamable HTTP** with resumability
- **🎯 Attribute-Based Definition**: Use PHP 8 Attributes (`#[McpTool]`, `#[McpResource]`, etc.) for zero-config element registration
- **🔧 Flexible Handlers**: Support for closures, class methods, static methods, and invokable classes
- **📝 Smart Schema Generation**: Automatic JSON schema generation from method signatures with optional `#[Schema]` attribute enhancements
- **⚡ Session Management**: Advanced session handling with multiple storage backends
- **🔄 Event-Driven**: ReactPHP-based for high concurrency and non-blocking operations  
- **📊 Batch Processing**: Full support for JSON-RPC batch requests
- **💾 Smart Caching**: Intelligent caching of discovered elements with manual override precedence
- **🧪 Completion Providers**: Built-in support for argument completion in tools and prompts
- **🔌 Dependency Injection**: Full PSR-11 container support with auto-wiring
- **📋 Comprehensive Testing**: Extensive test suite with integration tests for all transports

This package supports the **2025-03-26** version of the Model Context Protocol with backward compatibility.

## 📋 Requirements

- **PHP** >= 8.1
- **Composer**
- **For HTTP Transport**: An event-driven PHP environment (CLI recommended)
- **Extensions**: `json`, `mbstring`, `pcre` (typically enabled by default)

## 📦 Installation

```bash
composer require php-mcp/server
```

> **💡 Laravel Users**: Consider using [`php-mcp/laravel`](https://github.com/php-mcp/laravel) for enhanced framework integration, configuration management, and Artisan commands.

## ⚡ Quick Start: Stdio Server with Discovery

This example demonstrates the most common usage pattern - a `stdio` server using attribute discovery.

**1. Define Your MCP Elements**

Create `src/CalculatorElements.php`:

```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. Create the Server Script**

Create `mcp-server.php`:

```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. Configure Your MCP Client**

Add to your client configuration (e.g., `.cursor/mcp.json`):

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

**4. Test the Server**

Your AI assistant can now call:
- `add_numbers` - Add two integers
- `calculate_power` - Calculate power with validation constraints

## 🏗️ Architecture Overview

The PHP MCP Server uses a modern, decoupled architecture:

```
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   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.)   │
                       └──────────────────┘
```

### Core Components

- **`ServerBuilder`**: Fluent configuration interface (`Server::make()->...->build()`)
- **`Server`**: Central coordinator containing all configured components
- **`Protocol`**: JSON-RPC 2.0 handler bridging transports and core logic
- **`SessionManager`**: Multi-backend session storage (array, cache, custom)
- **`Dispatcher`**: Method routing and request processing
- **`Registry`**: Element storage with smart caching and precedence rules
- **`Elements`**: Registered MCP components (Tools, Resources, Prompts, Templates)

### Transport Options

1. **`StdioServerTransport`**: Standard I/O for direct client launches
2. **`HttpServerTransport`**: HTTP + Server-Sent Events for web integration  
3. **`StreamableHttpServerTransport`**: Enhanced HTTP with resumability and event sourcing

## ⚙️ Server Configuration

### Basic Configuration

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

### Advanced Configuration with Dependencies

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

### Session Management Options

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

## 🎯 Defining MCP Elements

The server provides two powerful ways to define MCP elements: **Attribute-Based Discovery** (recommended) and **Manual Registration**. Both can be combined, with manual registrations taking precedence.

### Element Types

- **🔧 Tools**: Executable functions/actions (e.g., `calculate`, `send_email`, `query_database`)
- **📄 Resources**: Static content/data (e.g., `config://settings`, `file://readme.txt`)
- **📋 Resource Templates**: Dynamic resources with URI patterns (e.g., `user://{id}/profile`)  
- **💬 Prompts**: Conversation starters/templates (e.g., `summarize`, `translate`)

### 1. 🏷️ Attribute-Based Discovery (Recommended)

Use PHP 8 attributes to mark methods or invokable classes as MCP elements. The server will discover them via filesystem scanning.

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

**Discovery Process:**

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

**Available Attributes:**

- **`#[McpTool]`**: Executable actions
- **`#[McpResource]`**: Static content accessible via URI
- **`#[McpResourceTemplate]`**: Dynamic resources with URI templates  
- **`#[McpPrompt]`**: Conversation templates and prompt generators

### 2. 🔧 Manual Registration 

Register elements programmatically using the `ServerBuilder` before calling `build()`. Useful for dynamic registration, closures, or when you prefer explicit control.

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

The server supports three flexible handler formats: `[ClassName::class, 'methodName']` for class method handlers, `InvokableClass::class` for invokable class handlers (classes with `__invoke` method), and any PHP callable including closures, static methods like `[SomeClass::class, 'staticMethod']`, or function names. Class-based handlers are resolved via the configured PSR-11 container for dependency injection. Manual registrations are never cached and take precedence over discovered elements with the same identifier.

> [!IMPORTANT]
> When using closures as handlers, the server generates minimal JSON schemas based only on PHP type hints since there are no docblocks or class context available. For more detailed schemas with validation constraints, descriptions, and formats, you have two options:
> 
> - Use the [`#[Schema]` attribute](#-schema-generation-and-validation) for enhanced schema generation
> - Provide a custom `$inputSchema` parameter when registering tools with `->withTool()`

### 🏆 Element Precedence & Discovery

**Precedence Rules:**
- Manual registrations **always** override discovered/cached elements with the same identifier
- Discovered elements are cached for performance (configurable)
- Cache is automatically invalidated on fresh discovery runs

**Discovery Process:**

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

**Caching Behavior:**
- Only **discovered** elements are cached (never manual registrations)
- Cache loaded automatically during `build()` if available
- Fresh `discover()` calls clear and rebuild cache
- Use `force: true` to bypass discovery-already-ran check

## 🚀 Running the Server (Transports)

The server core is transport-agnostic. Choose a transport based on your deployment needs:

### 1. 📟 Stdio Transport

**Best for**: Direct client execution, command-line tools, simple deployments

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

**Client Configuration:**
```json
{
    "mcpServers": {
        "my-php-server": {
            "command": "php",
            "args": ["/absolute/path/to/server.php"]
        }
    }
}
```

> ⚠️ **Important**: When using stdio transport, **never** write to `STDOUT` in your handlers (use `STDERR` for debugging). `STDOUT` is reserved for JSON-RPC communication.

### 2. 🌐 HTTP + Server-Sent Events Transport (Deprecated)

> ⚠️ **Note**: This transport is deprecated in the latest MCP protocol version but remains available for backwards compatibility. For new projects, use the [StreamableHttpServerTransport](#3--streamable-http-transport-new) which provides enhanced features and better protocol compliance.

**Best for**: Legacy applications requiring backwards compatibility

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

**Client Configuration:**
```json
{
    "mcpServers": {
        "my-http-server": {
            "url": "http://localhost:8080/mcp/sse"
        }
    }
}
```

**Endpoints:**
- **SSE Connection**: `GET /mcp/sse`
- **Message Sending**: `POST /mcp/message?clientId={clientId}`

### 3. 🔄 Streamable HTTP Transport (Recommended)

**Best for**: Production deployments, remote MCP servers, multiple clients, resumable connections

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

**JSON Response Mode:**

The `enableJsonResponse` option controls how responses are delivered:

- **`false` (default)**: Uses Server-Sent Events (SSE) streams for responses. Best for tools that may take time to process.
- **`true`**: Returns immediate JSON responses without opening SSE streams. Use this when your tools execute quickly and don't need streaming.

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

**Stateless Mode:**

For clients that have issues with session management, enable stateless mode:

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

In stateless mode, session IDs are generated internally but not exposed to clients, and each request is treated as independent without persistent session state.

**Features:**
- **Resumable connections** - clients can reconnect and replay missed events
- **Event sourcing** - all events are stored for replay
- **JSON mode** - optional JSON-only responses for fast tools
- **Enhanced session management** - persistent session state
- **Multiple client support** - designed for concurrent clients
- **Stateless mode** - session-less operation for simple clients

## 📋 Schema Generation and Validation

The server automatically generates JSON schemas for tool parameters using a sophisticated priority system that combines PHP type hints, docblock information, and the optional `#[Schema]` attribute. These generated schemas are used both for input validation and for providing schema information to MCP clients.

### Schema Generation Priority

The server follows this order of precedence when generating schemas:

1. **`#[Schema]` attribute with `definition`** - Complete schema override (highest precedence)
2. **Parameter-level `#[Schema]` attribute** - Parameter-specific schema enhancements
3. **Method-level `#[Schema]` attribute** - Method-wide schema configuration
4. **PHP type hints + docblocks** - Automatic inference from code (lowest precedence)

When a `definition` is provided in the Schema attribute, all automatic inference is bypassed and the complete definition is used as-is.

### Parameter-Level Schema Attributes

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

### Method-Level Schema

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

### Complete Schema Override (Method-Level Only)

```php
#[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];
}
```

> ⚠️ **Important**: Complete schema definition override should rarely be used. It bypasses all automatic schema inference and requires you to define the entire JSON schema manually. Only use this if you're well-versed with JSON Schema specification and have complex validation requirements that cannot be achieved through the priority system. In most cases, parameter-level and method-level `#[Schema]` attributes provide sufficient flexibility.

## 🎨 Return Value Formatting

The server automatically formats return values from your handlers into appropriate MCP content types:

### Automatic Formatting

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

### Advanced Content Types

```php
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'
    );
}
```

### File and Stream Handling

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

## 🔄 Batch Processing

The server automatically handles JSON-RPC batch requests:

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

## 🔧 Advanced Features

### Completion Providers

Completion providers enable MCP clients to offer auto-completion suggestions in their user interfaces. They are specifically designed for **Resource Templates** and **Prompts** to help users discover available options for dynamic parts like template variables or prompt arguments.

> **Note**: Tools and resources can be discovered via standard MCP commands (`tools/list`, `resources/list`), so completion providers are not needed for them. Completion providers are used only for resource templates (URI variables) and prompt arguments.

The `#[CompletionProvider]` attribute supports three types of completion sources:

#### 1. Custom Provider Classes

For complex completion logic, implement the `CompletionProviderInterface`:

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

You can also pass pre-configured provider instances:

```php
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. Simple List Completions

For static completion lists, use the `values` parameter:

```php
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. Enum-Based Completions

For enum classes, use the `enum` parameter:

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

#### Manual Registration with Completion Providers

```php
$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();
```

#### Completion Provider Resolution

The server automatically handles provider resolution:

- **Class strings** (`MyProvider::class`) → Resolved from PSR-11 container with dependency injection
- **Instances** (`new MyProvider()`) → Used directly as-is
- **Values arrays** (`['a', 'b', 'c']`) → Automatically wrapped in `ListCompletionProvider`
- **Enum classes** (`MyEnum::class`) → Automatically wrapped in `EnumCompletionProvider`

> **Important**: Completion providers only offer suggestions to users in the MCP client interface. Users can still input any value, so always validate parameters in your handlers regardless of completion provider constraints.

### Custom Dependency Injection

Your MCP element handlers can use constructor dependency injection to access services like databases, APIs, or other business logic. When handlers have constructor dependencies, you must provide a pre-configured PSR-11 container that contains those dependencies.

By default, the server uses a `BasicContainer` - a simple implementation that attempts to auto-wire dependencies by instantiating classes with parameterless constructors. For dependencies that require configuration (like database connections), you can either manually add them to the BasicContainer or use a more advanced PSR-11 container like PHP-DI or Laravel's container.

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

### Resource Subscriptions

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

### Resumability and Event Store

For production deployments using `StreamableHttpServerTransport`, you can implement resumability with event sourcing by providing a custom event store:

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

### Custom Session Handlers

Implement custom session storage by creating a class that implements `SessionHandlerInterface`:

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

### Middleware Support

Both `HttpServerTransport` and `StreamableHttpServerTransport` support PSR-7 compatible middleware for intercepting and modifying HTTP requests and responses. Middleware allows you to extract common functionality like authentication, logging, CORS handling, and request validation into reusable components.

Middleware must be a valid PHP callable that accepts a PSR-7 `ServerRequestInterface` as the first argument and a `callable` as the second argument.

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

**Important Considerations:**

- **Response Handling**: Middleware must handle both synchronous `ResponseInterface` and asynchronous `PromiseInterface` returns from `$next($request)`, since ReactPHP operates asynchronously
- **Invokable Pattern**: The recommended pattern is to use invokable classes with a separate `handle()` method to process responses, making the async logic reusable
- **Execution Order**: Middleware executes in the order provided, with the last middleware being closest to your MCP handlers

### SSL Context Configuration

For HTTPS deployments of `StreamableHttpServerTransport`, configure SSL context options:

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

> **SSL Context Reference**: For complete SSL context options, see the [PHP SSL Context Options documentation](https://www.php.net/manual/en/context.ssl.php).
## 🔍 Error Handling & Debugging

The server provides comprehensive error handling and debugging capabilities:

### Exception Handling

Tool handlers can throw any PHP exception when errors occur. The server automatically converts these exceptions into proper JSON-RPC error responses for MCP clients.

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

The server will convert these exceptions into appropriate JSON-RPC error responses that MCP clients can understand and display to users.

### Logging and Debugging

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

## 🚀 Production Deployment

Since `$server->listen()` runs a persistent process, you can deploy it using any strategy that suits your infrastructure needs. The server can be deployed on VPS, cloud instances, containers, or any environment that supports long-running processes.

Here are two popular deployment approaches to consider:

### Option 1: VPS with Supervisor + Nginx (Recommended)

**Best for**: Most production deployments, cost-effective, full control

```bash
# 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
```

**Supervisor Configuration:**
```ini
[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
```

**Nginx Configuration with SSL:**
```nginx
# /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;
}
```

**Start Services:**
```bash
# 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
```

**Client Configuration:**
```json
{
  "mcpServers": {
    "my-server": {
      "url": "https://mcp.yourdomain.com/mcp"
    }
  }
}
```

### Option 2: Docker Deployment

**Best for**: Containerized environments, Kubernetes, cloud platforms

**Production Dockerfile:**
```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"]
```

**docker-compose.yml:**
```yaml
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:
```

### Security Best Practices

1. **Firewall Configuration:**
```bash
# 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
```

2. **SSL/TLS Setup:**
```bash
# Install Certbot for Let's Encrypt
sudo apt install certbot python3-certbot-nginx

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

## 📚 Examples & Use Cases

Explore comprehensive examples in the [`examples/`](./examples/) directory:

### Available Examples

- **`01-discovery-stdio-calculator/`** - Basic stdio calculator with attribute discovery
- **`02-discovery-http-userprofile/`** - HTTP server with user profile management  
- **`03-manual-registration-stdio/`** - Manual element registration patterns
- **`04-combined-registration-http/`** - Combining manual and discovered elements
- **`05-stdio-env-variables/`** - Environment variable handling
- **`06-custom-dependencies-stdio/`** - Dependency injection with task management
- **`07-complex-tool-schema-http/`** - Advanced schema validation examples
- **`08-schema-showcase-streamable/`** - Comprehensive schema feature showcase

### Running Examples

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

## 🚧 Migration from v2.x

If migrating from version 2.x, note these key changes:

### Schema Updates
- Uses `php-mcp/schema` package for DTOs instead of internal classes
- Content types moved to `PhpMcp\Schema\Content\*` namespace
- Updated method signatures for better type safety

### Session Management
- New session management with multiple backends
- Use `->withSession()` or `->withSessionHandler()` for configuration
- Sessions are now persistent across reconnections (with cache backend)

### Transport Changes
- New `StreamableHttpServerTransport` with resumability
- Enhanced error handling and event sourcing
- Better batch request processing

## 🧪 Testing

```bash
# 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
```

## 🤝 Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## 📄 License

The MIT License (MIT). See [LICENSE](LICENSE) for details.

## 🙏 Acknowledgments

- Built on the [Model Context Protocol](https://modelcontextprotocol.io/) specification
- Powered by [ReactPHP](https://reactphp.org/) for async operations
- Uses [PSR standards](https://www.php-fig.org/) for maximum interoperability

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to php-mcp/server

First off, thank you for considering contributing to `php-mcp/server`! We appreciate your time and effort. This project aims to provide a robust and easy-to-use PHP server for the Model Context Protocol.

Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open-source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.

## How Can I Contribute?

There are several ways you can contribute:

*   **Reporting Bugs:** If you find a bug, please open an issue on the GitHub repository. Include steps to reproduce, expected behavior, and actual behavior. Specify your PHP version, operating system, and relevant package versions.
*   **Suggesting Enhancements:** Open an issue to suggest new features or improvements to existing functionality. Explain the use case and why the enhancement would be valuable.
*   **Improving Documentation:** If you find errors, omissions, or areas that could be clearer in the README or code comments, please submit a pull request or open an issue.
*   **Writing Code:** Submit pull requests to fix bugs or add new features.

## Development Setup

1.  **Fork the repository:** Click the "Fork" button on the [php-mcp/server GitHub page](https://github.com/php-mcp/server).
2.  **Clone your fork:** `git clone [email protected]:YOUR_USERNAME/server.git`
3.  **Navigate into the directory:** `cd server`
4.  **Install dependencies:** `composer install` (This installs runtime and development dependencies).

## Submitting Changes (Pull Requests)

1.  **Create a new branch:** `git checkout -b feature/your-feature-name` or `git checkout -b fix/issue-number`.
2.  **Make your changes:** Write your code and accompanying tests.
3.  **Ensure Code Style:** Run the code style fixer (if configured, e.g., PHP CS Fixer):
    ```bash
    composer lint # Or ./vendor/bin/php-cs-fixer fix
    ```
    Adhere to PSR-12 coding standards.
4.  **Run Tests:** Ensure all tests pass:
    ```bash
    composer test # Or ./vendor/bin/pest
    ```
    Consider adding new tests for your changes. Aim for good test coverage.
5.  **Update Documentation:** If your changes affect the public API or usage, update the `README.md` and relevant PHPDoc blocks.
6.  **Commit your changes:** Use clear and descriptive commit messages. `git commit -m "feat: Add support for resource subscriptions"` or `git commit -m "fix: Correct handling of transport errors"`
7.  **Push to your fork:** `git push origin feature/your-feature-name`
8.  **Open a Pull Request:** Go to the original `php-mcp/server` repository on GitHub and open a pull request from your branch to the `main` branch (or the appropriate development branch).
9.  **Describe your changes:** Provide a clear description of the problem and solution in the pull request. Link to any relevant issues (`Closes #123`).

## Coding Standards

*   Follow **PSR-12** coding standards.
*   Use **strict types:** `declare(strict_types=1);` at the top of PHP files.
*   Use **PHP 8.1+ features** where appropriate (readonly properties, enums, etc.).
*   Add **PHPDoc blocks** for all public classes, methods, and properties.
*   Write clear and concise code. Add comments only where necessary to explain complex logic.

## Reporting Issues

*   Use the GitHub issue tracker.
*   Check if the issue already exists.
*   Provide a clear title and description.
*   Include steps to reproduce the issue, code examples, error messages, and stack traces if applicable.
*   Specify relevant environment details (PHP version, OS, package version).

Thank you for contributing!
```

--------------------------------------------------------------------------------
/tests/Fixtures/Enums/UnitEnum.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Enums;

enum UnitEnum
{
    case Yes;
    case No;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Enums/BackedIntEnum.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Enums;

enum BackedIntEnum: int
{
    case First = 1;
    case Second = 2;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Enums/BackedStringEnum.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Enums;

enum BackedStringEnum: string
{
    case OptionA = 'A';
    case OptionB = 'B';
}

```

--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    //
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Enums/PriorityEnum.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Enums;

enum PriorityEnum: int
{
    case LOW = 1;
    case MEDIUM = 2;
    case HIGH = 3;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Enums/StatusEnum.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Enums;

enum StatusEnum: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/SubDir/HiddenTool.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery\SubDir;

use PhpMcp\Server\Attributes\McpTool;

class HiddenTool
{
    #[McpTool(name: 'hidden_subdir_tool')]
    public function run()
    {
    }
}

```

--------------------------------------------------------------------------------
/src/Exception/DiscoveryException.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Exception;

/**
 * Exception related to errors during the attribute discovery process.
 */
class DiscoveryException extends McpServerException
{
    // No specific JSON-RPC code, internal server issue.
}

```

--------------------------------------------------------------------------------
/src/Defaults/SystemClock.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Defaults;

use DateTimeImmutable;
use Psr\Clock\ClockInterface;

class SystemClock implements ClockInterface
{
    public function now(): \DateTimeImmutable
    {
        return new \DateTimeImmutable();
    }
}

```

--------------------------------------------------------------------------------
/examples/07-complex-tool-schema-http/EventTypes.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\ComplexSchemaHttpExample\Model;

enum EventType: string
{
    case Meeting = 'meeting';
    case Reminder = 'reminder';
    case Call = 'call';
    case Other = 'other';
}

enum EventPriority: int
{
    case Low = 0;
    case Normal = 1;
    case High = 2;
}

```

--------------------------------------------------------------------------------
/src/Context.php:
--------------------------------------------------------------------------------

```php
<?php declare(strict_types = 1);
namespace PhpMcp\Server;

use PhpMcp\Server\Contracts\SessionInterface;
use Psr\Http\Message\ServerRequestInterface;

final class Context
{
    public function __construct(
        public readonly SessionInterface $session,
        public readonly ?ServerRequestInterface $request = null,
    )
    {
    }
}
```

--------------------------------------------------------------------------------
/src/Exception/ConfigurationException.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Exception;

/**
 * Exception related to invalid server configuration.
 *
 * Typically thrown during ServerBuilder::build().
 */
class ConfigurationException extends McpServerException
{
    // No specific JSON-RPC code, usually an internal setup issue.
    // Code 0 is appropriate.
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/InvocableResourceFixture.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpResource;

#[McpResource(uri: "invokable://config/status", name: "invokable_app_status")]
class InvocableResourceFixture
{
    public function __invoke(): array
    {
        return ["status" => "OK", "load" => rand(1, 100) / 100.0];
    }
}

```

--------------------------------------------------------------------------------
/src/Contracts/LoggerAwareInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

use Psr\Log\LoggerInterface;

/**
 * Interface for components that can accept a PSR-3 Logger instance.
 *
 * Primarily used for injecting the configured logger into transport implementations.
 */
interface LoggerAwareInterface
{
    public function setLogger(LoggerInterface $logger): void;
}

```

--------------------------------------------------------------------------------
/src/Contracts/LoopAwareInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

use React\EventLoop\LoopInterface;

/**
 * Interface for components that require a ReactPHP event loop instance.
 *
 * Primarily used for injecting the configured loop into transport implementations.
 */
interface LoopAwareInterface
{
    public function setLoop(LoopInterface $loop): void;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/NonDiscoverableClass.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

class NonDiscoverableClass
{
    public function someMethod(): string
    {
        return "Just a regular method.";
    }
}

interface MyDiscoverableInterface
{
}

trait MyDiscoverableTrait
{
    public function traitMethod()
    {
    }
}

enum MyDiscoverableEnum
{
    case Alpha;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/RequestAttributeMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

use Psr\Http\Message\ServerRequestInterface;

class RequestAttributeMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $request = $request->withAttribute('middleware-attr', 'middleware-value');
        return $next($request);
    }
}

```

--------------------------------------------------------------------------------
/src/Defaults/DefaultUuidSessionIdGenerator.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Defaults;

use PhpMcp\Server\Contracts\SessionIdGeneratorInterface;

class DefaultUuidSessionIdGenerator implements SessionIdGeneratorInterface
{
    public function generateId(): string
    {
        return bin2hex(random_bytes(16));
    }

    public function onSessionInitialized(string $sessionId): void
    {
        // no-op
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/ErrorMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

use Psr\Http\Message\ServerRequestInterface;

class ErrorMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        if (str_contains($request->getUri()->getPath(), '/error-middleware')) {
            throw new \Exception('Middleware error');
        }
        return $next($request);
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/InvocablePromptFixture.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpPrompt;

#[McpPrompt(name: "InvokableGreeterPrompt")]
class InvocablePromptFixture
{
    /**
     * @param string $personName
     * @return array
     */
    public function __invoke(string $personName): array
    {
        return [['role' => 'user', 'content' => "Generate a short greeting for {$personName}."]];
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/General/InvokableHandlerFixture.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

class InvokableHandlerFixture
{
    public string $type;
    public array $argsReceived;

    public function __construct(string $type = "default")
    {
        $this->type = $type;
    }

    public function __invoke(string $arg1, int $arg2 = 0): array
    {
        $this->argsReceived = func_get_args();
        return ['invoked' => $this->type, 'arg1' => $arg1, 'arg2' => $arg2];
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/InvocableResourceTemplateFixture.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpResourceTemplate;

#[McpResourceTemplate(uriTemplate: "invokable://user-profile/{userId}")]
class InvocableResourceTemplateFixture
{
    /**
     * @param string $userId
     * @return array
     */
    public function __invoke(string $userId): array
    {
        return ["id" => $userId, "email" => "user{$userId}@example-invokable.com"];
    }
}

```

--------------------------------------------------------------------------------
/src/Contracts/CompletionProviderInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

interface CompletionProviderInterface
{
    /**
     * Get completions for a given current value.
     *
     * @param  string  $currentValue  The current value to get completions for.
     * @param  SessionInterface  $session  The session to get completions for.
     * @return array  The completions.
     */
    public function getCompletions(string $currentValue, SessionInterface $session): array;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/ShortCircuitMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;

class ShortCircuitMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        if (str_contains($request->getUri()->getPath(), '/short-circuit')) {
            return new Response(418, [], 'Short-circuited by middleware');
        }
        return $next($request);
    }
}

```

--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Test Suite">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>

```

--------------------------------------------------------------------------------
/tests/Fixtures/General/RequestAttributeChecker.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Server\Context;

class RequestAttributeChecker
{
    public function checkAttribute(Context $context): TextContent
    {
        $attribute = $context->request->getAttribute('middleware-attr');
        if ($attribute === 'middleware-value') {
            return TextContent::make('middleware-value-found: ' . $attribute);
        }

        return TextContent::make('middleware-value-not-found: ' . $attribute);
    }
}

```

--------------------------------------------------------------------------------
/examples/02-discovery-http-userprofile/UserIdCompletionProvider.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace Mcp\HttpUserProfileExample;

use PhpMcp\Server\Contracts\CompletionProviderInterface;
use PhpMcp\Server\Contracts\SessionInterface;

class UserIdCompletionProvider implements CompletionProviderInterface
{
    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        $availableUserIds = ['101', '102', '103'];
        $filteredUserIds = array_filter($availableUserIds, fn(string $userId) => str_contains($userId, $currentValue));

        return $filteredUserIds;
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/InvocableToolFixture.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

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

#[McpTool(name: "InvokableCalculator", description: "An invokable calculator tool.")]
class InvocableToolFixture
{
    /**
     * Adds two numbers.
     * @param int $a
     * @param int $b
     * @return int
     */
    public function __invoke(int $a, int $b): int
    {
        return $a + $b;
    }
}

```

--------------------------------------------------------------------------------
/src/Exception/TransportException.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Exception;

use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;

/**
 * Exception related to errors in the underlying transport layer
 * (e.g., socket errors, process management issues, SSE stream errors).
 */
class TransportException extends McpServerException
{
    public function toJsonRpcError(string|int $id): JsonRpcError
    {
        return new JsonRpcError(
            jsonrpc: '2.0',
            id: $id,
            code: JsonRpcError::CODE_INTERNAL_ERROR,
            message: 'Transport layer error: ' . $this->getMessage(),
            data: null
        );
    }
}

```

--------------------------------------------------------------------------------
/src/Defaults/ListCompletionProvider.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Defaults;

use PhpMcp\Server\Contracts\CompletionProviderInterface;
use PhpMcp\Server\Contracts\SessionInterface;

class ListCompletionProvider implements CompletionProviderInterface
{
    public function __construct(private array $values) {}

    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        if (empty($currentValue)) {
            return $this->values;
        }

        return array_values(array_filter(
            $this->values,
            fn(string $value) => str_starts_with($value, $currentValue)
        ));
    }
}

```

--------------------------------------------------------------------------------
/src/Attributes/McpPrompt.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Attributes;

use Attribute;

/**
 * Marks a PHP method as an MCP Prompt generator.
 * The method should return the prompt messages, potentially using arguments for templating.
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
final class McpPrompt
{
    /**
     * @param  ?string  $name  Overrides the prompt name (defaults to method name).
     * @param  ?string  $description  Optional description of the prompt. Defaults to method DocBlock summary.
     */
    public function __construct(
        public ?string $name = null,
        public ?string $description = null,
    ) {
    }
}

```

--------------------------------------------------------------------------------
/src/Attributes/McpTool.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Attributes;

use Attribute;
use PhpMcp\Schema\ToolAnnotations;

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class McpTool
{
    /**
     * @param  string|null  $name  The name of the tool (defaults to the method name)
     * @param  string|null  $description  The description of the tool (defaults to the DocBlock/inferred)
     * @param  ToolAnnotations|null  $annotations  Optional annotations describing tool behavior
     */
    public function __construct(
        public ?string $name = null,
        public ?string $description = null,
        public ?ToolAnnotations $annotations = null,
    ) {
    }
}

```

--------------------------------------------------------------------------------
/.github/workflows/changelog.yml:
--------------------------------------------------------------------------------

```yaml
name: "Update Changelog"

on:
  release:
    types: [released]

permissions:
  contents: write

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Update Changelog
        uses: stefanzweifel/changelog-updater-action@v1
        with:
          latest-version: ${{ github.event.release.name }}
          release-notes: ${{ github.event.release.body }}

      - name: Commit updated CHANGELOG
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          branch: main
          commit_message: Update CHANGELOG
          file_pattern: CHANGELOG.md
```

--------------------------------------------------------------------------------
/tests/Fixtures/General/CompletionProviderFixture.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Server\Contracts\CompletionProviderInterface;
use PhpMcp\Server\Contracts\SessionInterface;

class CompletionProviderFixture implements CompletionProviderInterface
{
    public static array $completions = ['alpha', 'beta', 'gamma'];
    public static string $lastCurrentValue = '';
    public static ?SessionInterface $lastSession = null;

    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        self::$lastCurrentValue = $currentValue;
        self::$lastSession = $session;

        return array_filter(self::$completions, fn ($item) => str_starts_with($item, $currentValue));
    }
}

```

--------------------------------------------------------------------------------
/src/Exception/ProtocolException.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Exception;

use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;

/**
 * Exception related to violations of the JSON-RPC 2.0 or MCP structure
 * in incoming messages or outgoing responses (e.g., missing required fields,
 * invalid types within the protocol itself).
 */
class ProtocolException extends McpServerException
{
    public function toJsonRpcError(string|int $id): JsonRpcError
    {
        $code = ($this->code >= -32700 && $this->code <= -32600) ? $this->code : self::CODE_INVALID_REQUEST;

        return new JsonRpcError(
            jsonrpc: '2.0',
            id: $id,
            code: $code,
            message: $this->getMessage(),
            data: $this->getData()
        );
    }
}

```

--------------------------------------------------------------------------------
/examples/04-combined-registration-http/DiscoveredElements.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\CombinedHttpExample\Discovered;

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

class DiscoveredElements
{
    /**
     * A tool discovered via attributes.
     *
     * @return string A status message.
     */
    #[McpTool(name: 'discovered_status_check')]
    public function checkSystemStatus(): string
    {
        return 'System status: OK (discovered)';
    }

    /**
     * A resource discovered via attributes.
     * This will be overridden by a manual registration with the same URI.
     *
     * @return string Content.
     */
    #[McpResource(uri: 'config://priority', name: 'priority_config_discovered')]
    public function getPriorityConfigDiscovered(): string
    {
        return 'Discovered Priority Config: Low';
    }
}

```

--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------

```yaml
name: Tests

on: ['push', 'pull_request']

jobs:
  ci:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        php: [8.1, 8.2, 8.3, 8.4]
      max-parallel: 2

    name: Tests PHP${{ matrix.php }}

    steps:

      - name: Checkout
        uses: actions/checkout@v4

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache/files
          key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none

      - name: Install Composer dependencies
        run: composer update --no-interaction --prefer-dist

      - name: Run Tests
        run: composer test

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/HeaderMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

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

class HeaderMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $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-Test-Middleware', 'header-added')
            : $response;
    }
}

```

--------------------------------------------------------------------------------
/src/Attributes/CompletionProvider.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Attributes;

use Attribute;
use PhpMcp\Server\Contracts\CompletionProviderInterface;

#[Attribute(Attribute::TARGET_PARAMETER)]
class CompletionProvider
{
    /**
     * @param class-string<CompletionProviderInterface>|null $providerClass 
     * @param class-string<CompletionProviderInterface>|CompletionProviderInterface|null $provider If a class-string, it will be resolved from the container at the point of use.
     */
    public function __construct(
        public ?string $providerClass = null,
        public string|CompletionProviderInterface|null $provider = null,
        public ?array $values = null,
        public ?string $enum = null,
    ) {
        if (count(array_filter([$provider, $values, $enum])) !== 1) {
            throw new \InvalidArgumentException('Only one of provider, values, or enum can be set');
        }
    }
}

```

--------------------------------------------------------------------------------
/examples/04-combined-registration-http/ManualHandlers.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\CombinedHttpExample\Manual;

use Psr\Log\LoggerInterface;

class ManualHandlers
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * A manually registered tool.
     *
     * @param  string  $user  The user to greet.
     * @return string Greeting.
     */
    public function manualGreeter(string $user): string
    {
        $this->logger->info("Manual tool 'manual_greeter' called for {$user}");

        return "Hello {$user}, from manual registration!";
    }

    /**
     * Manually registered resource that overrides a discovered one.
     *
     * @return string Content.
     */
    public function getPriorityConfigManual(): string
    {
        $this->logger->info("Manual resource 'config://priority' read.");

        return 'Manual Priority Config: HIGH (overrides discovered)';
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/DiscoverableResourceHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Schema\Annotations;
use PhpMcp\Server\Attributes\McpResource;

class DiscoverableResourceHandler
{
    /**
     * Provides the application's current version.
     * @return string The version string.
     */
    #[McpResource(
        uri: "app://info/version",
        name: "app_version",
        description: "The current version of the application.",
        mimeType: "text/plain",
        size: 10
    )]
    public function getAppVersion(): string
    {
        return "1.2.3-discovered";
    }

    #[McpResource(
        uri: "config://settings/ui",
        name: "ui_settings_discovered",
        mimeType: "application/json",
        annotations: new Annotations(priority: 0.5)
    )]
    public function getUiSettings(): array
    {
        return ["theme" => "dark", "fontSize" => 14];
    }

    public function someOtherMethod(): void
    {
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/FirstMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

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

class FirstMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $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)
    {
        if ($response instanceof ResponseInterface) {
            $existing = $response->getHeaderLine('X-Middleware-Order');
            $new = $existing ? $existing . ',first' : 'first';
            return $response->withHeader('X-Middleware-Order', $new);
        }
        return $response;
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/ThirdMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

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

class ThirdMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $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)
    {
        if ($response instanceof ResponseInterface) {
            $existing = $response->getHeaderLine('X-Middleware-Order');
            $new = $existing ? $existing . ',third' : 'third';
            return $response->withHeader('X-Middleware-Order', $new);
        }
        return $response;
    }
}

```

--------------------------------------------------------------------------------
/src/Contracts/SessionHandlerInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

interface SessionHandlerInterface
{
    /**
     * Read session data
     *
     * Returns an encoded string of the read data.
     * If nothing was read, it must return false.
     * @param string $id The session id to read data for.
     */
    public function read(string $id): string|false;

    /**
     * Write session data
     * @param string $id The session id.
     * @param string $data The encoded session data.
     */
    public function write(string $id, string $data): bool;

    /**
     * Destroy a session
     * @param string $id The session ID being destroyed.
     * The return value (usually TRUE on success, FALSE on failure).
     */
    public function destroy(string $id): bool;

    /**
     * Cleanup old sessions
     * Sessions that have not updated for
     * the last maxlifetime seconds will be removed.
     */
    public function gc(int $maxLifetime): array;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Middlewares/SecondMiddleware.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Middlewares;

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

class SecondMiddleware
{
    public function __invoke(ServerRequestInterface $request, callable $next)
    {
        $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)
    {
        if ($response instanceof ResponseInterface) {
            $existing = $response->getHeaderLine('X-Middleware-Order');
            $new = $existing ? $existing . ',second' : 'second';
            return $response->withHeader('X-Middleware-Order', $new);
        }
        return $response;
    }
}

```

--------------------------------------------------------------------------------
/tests/Unit/Attributes/McpToolTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Attributes;

use PhpMcp\Server\Attributes\McpTool;

it('instantiates with correct properties', function () {
    // Arrange
    $name = 'test-tool-name';
    $description = 'This is a test description.';

    // Act
    $attribute = new McpTool(name: $name, description: $description);

    // Assert
    expect($attribute->name)->toBe($name);
    expect($attribute->description)->toBe($description);
});

it('instantiates with null values for name and description', function () {
    // Arrange & Act
    $attribute = new McpTool(name: null, description: null);

    // Assert
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
});

it('instantiates with missing optional arguments', function () {
    // Arrange & Act
    $attribute = new McpTool(); // Use default constructor values

    // Assert
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
});

```

--------------------------------------------------------------------------------
/src/Defaults/EnumCompletionProvider.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Defaults;

use PhpMcp\Server\Contracts\CompletionProviderInterface;
use PhpMcp\Server\Contracts\SessionInterface;

class EnumCompletionProvider implements CompletionProviderInterface
{
    private array $values;

    public function __construct(string $enumClass)
    {
        if (!enum_exists($enumClass)) {
            throw new \InvalidArgumentException("Class {$enumClass} is not an enum");
        }

        $this->values = array_map(
            fn($case) => isset($case->value) && is_string($case->value) ? $case->value : $case->name,
            $enumClass::cases()
        );
    }

    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        if (empty($currentValue)) {
            return $this->values;
        }

        return array_values(array_filter(
            $this->values,
            fn(string $value) => str_starts_with($value, $currentValue)
        ));
    }
}

```

--------------------------------------------------------------------------------
/tests/Unit/Attributes/McpPromptTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Attributes;

use PhpMcp\Server\Attributes\McpPrompt;

it('instantiates with name and description', function () {
    // Arrange
    $name = 'test-prompt-name';
    $description = 'This is a test prompt description.';

    // Act
    $attribute = new McpPrompt(name: $name, description: $description);

    // Assert
    expect($attribute->name)->toBe($name);
    expect($attribute->description)->toBe($description);
});

it('instantiates with null values for name and description', function () {
    // Arrange & Act
    $attribute = new McpPrompt(name: null, description: null);

    // Assert
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
});

it('instantiates with missing optional arguments', function () {
    // Arrange & Act
    $attribute = new McpPrompt(); // Use default constructor values

    // Assert
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
});

```

--------------------------------------------------------------------------------
/src/Contracts/EventStoreInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

/**
 * Interface for resumability support via event storage
 */
interface EventStoreInterface
{
    /**
     * Stores a message associated with a specific stream and returns a unique event ID.
     *
     * @param string $streamId The ID of the stream the event belongs to.
     * @param string $message The framed JSON-RPC message to store.
     * @return string The generated event ID for the stored event
     */
    public function storeEvent(string $streamId, string $message): string;

    /**
     * Replays events for a given stream that occurred after a specific event ID.
     *
     * @param string $lastEventId The last event ID the client received for this specific stream.
     * @param callable $sendCallback A function to call for each replayed message.
     *                           The callback will receive: `function(string $eventId, Message $message): void`
     */
    public function replayEventsAfter(string $lastEventId, callable $sendCallback): void;
}

```

--------------------------------------------------------------------------------
/src/Attributes/McpResourceTemplate.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Attributes;

use Attribute;
use PhpMcp\Schema\Annotations;

/**
 * Marks a PHP class definition as representing an MCP Resource Template.
 * This is informational, used for 'resources/templates/list'.
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
final class McpResourceTemplate
{
    /**
     * @param  string  $uriTemplate  The URI template string (RFC 6570).
     * @param  ?string  $name  A human-readable name for the template type.  If null, a default might be generated from the method name.
     * @param  ?string  $description  Optional description. Defaults to class DocBlock summary.
     * @param  ?string  $mimeType  Optional default MIME type for matching resources.
     * @param  ?Annotations  $annotations  Optional annotations describing the resource template.
     */
    public function __construct(
        public string $uriTemplate,
        public ?string $name = null,
        public ?string $description = null,
        public ?string $mimeType = null,
        public ?Annotations $annotations = null,
    ) {
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/DiscoverablePromptHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpPrompt;
use PhpMcp\Server\Attributes\CompletionProvider;
use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;

class DiscoverablePromptHandler
{
    /**
     * Generates a creative story prompt.
     * @param string $genre The genre of the story.
     * @param int $lengthWords Approximate length in words.
     * @return array The prompt messages.
     */
    #[McpPrompt(name: "creative_story_prompt")]
    public function generateStoryPrompt(
        #[CompletionProvider(provider: CompletionProviderFixture::class)]
        string $genre,
        int $lengthWords = 200
    ): array {
        return [
            ["role" => "user", "content" => "Write a {$genre} story about a lost robot, approximately {$lengthWords} words long."]
        ];
    }

    #[McpPrompt]
    public function simpleQuestionPrompt(string $question): array
    {
        return [
            ["role" => "user", "content" => $question],
            ["role" => "assistant", "content" => "I will try to answer that."]
        ];
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Utils/AttributeFixtures.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Utils;

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
class TestAttributeOne
{
    public function __construct(public string $value)
    {
    }
}

#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
class TestAttributeTwo
{
    public function __construct(public int $number)
    {
    }
}

#[\Attribute(\Attribute::TARGET_CLASS)]
class TestClassOnlyAttribute
{
}


// --- Test Class ---

#[TestClassOnlyAttribute]
#[TestAttributeOne(value: 'class-level')]
class AttributeFixtures
{
    #[TestAttributeOne(value: 'prop-level')]
    public string $propertyOne = 'default';

    #[TestAttributeOne(value: 'method-one')]
    public function methodOne(
        #[TestAttributeOne(value: 'param-one')]
        #[TestAttributeTwo(number: 1)]
        string $param1
    ): void {
    }

    #[TestAttributeOne(value: 'method-two')]
    #[TestAttributeTwo(number: 2)]
    public function methodTwo(
        #[TestAttributeTwo(number: 3)]
        int $paramA
    ): void {
    }

    // Method with no attributes
    public function methodThree(string $unattributedParam): void
    {
    }
}

```

--------------------------------------------------------------------------------
/src/Attributes/McpResource.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Attributes;

use Attribute;
use PhpMcp\Schema\Annotations;

/**
 * Marks a PHP class as representing or handling a specific MCP Resource instance.
 * Used primarily for the 'resources/list' discovery.
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
final class McpResource
{
    /**
     * @param  string  $uri  The specific URI identifying this resource instance. Must be unique within the server.
     * @param  ?string  $name  A human-readable name for this resource. If null, a default might be generated from the method name.
     * @param  ?string  $description  An optional description of the resource. Defaults to class DocBlock summary.
     * @param  ?string  $mimeType  The MIME type, if known and constant for this resource.
     * @param  ?int  $size  The size in bytes, if known and constant.
     * @param  Annotations|null  $annotations  Optional annotations describing the resource.
     */
    public function __construct(
        public string $uri,
        public ?string $name = null,
        public ?string $description = null,
        public ?string $mimeType = null,
        public ?int $size = null,
        public ?Annotations $annotations = null,
    ) {
    }
}

```

--------------------------------------------------------------------------------
/examples/05-stdio-env-variables/EnvToolHandler.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\EnvExample;

use PhpMcp\Server\Attributes\McpTool;

class EnvToolHandler
{
    public function __construct()
    {
    }

    /**
     * Performs an action that can be modified by an environment variable.
     * The MCP client should set 'APP_MODE' in its 'env' config for this server.
     *
     * @param  string  $input  Some input data.
     * @return array The result, varying by APP_MODE.
     */
    #[McpTool(name: 'process_data_by_mode')]
    public function processData(string $input): array
    {
        $appMode = getenv('APP_MODE'); // Read from environment

        if ($appMode === 'debug') {
            return [
                'mode' => 'debug',
                'processed_input' => strtoupper($input),
                'message' => 'Processed in DEBUG mode.',
            ];
        } elseif ($appMode === 'production') {
            return [
                'mode' => 'production',
                'processed_input_length' => strlen($input),
                'message' => 'Processed in PRODUCTION mode (summary only).',
            ];
        } else {
            return [
                'mode' => $appMode ?: 'default',
                'original_input' => $input,
                'message' => 'Processed in default mode (APP_MODE not recognized or not set).',
            ];
        }
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/DiscoverableTemplateHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpResourceTemplate;
use PhpMcp\Server\Attributes\CompletionProvider;
use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;

class DiscoverableTemplateHandler
{
    /**
     * Retrieves product details based on ID and region.
     * @param string $productId The ID of the product.
     * @param string $region The sales region.
     * @return array Product details.
     */
    #[McpResourceTemplate(
        uriTemplate: "product://{region}/details/{productId}",
        name: "product_details_template",
        mimeType: "application/json"
    )]
    public function getProductDetails(
        string $productId,
        #[CompletionProvider(provider: CompletionProviderFixture::class)]
        string $region
    ): array {
        return [
            "id" => $productId,
            "name" => "Product " . $productId,
            "region" => $region,
            "price" => ($region === "EU" ? "€" : "$") . (hexdec(substr(md5($productId), 0, 4)) / 100)
        ];
    }

    #[McpResourceTemplate(uriTemplate: "file://{path}/{filename}.{extension}")]
    public function getFileContent(string $path, string $filename, string $extension): string
    {
        return "Content of {$path}/{$filename}.{$extension}";
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/EnhancedCompletionHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Server\Attributes\McpPrompt;
use PhpMcp\Server\Attributes\McpResourceTemplate;
use PhpMcp\Server\Attributes\CompletionProvider;
use PhpMcp\Server\Tests\Fixtures\Enums\StatusEnum;
use PhpMcp\Server\Tests\Fixtures\Enums\PriorityEnum;

class EnhancedCompletionHandler
{
    /**
     * Create content with list and enum completion providers.
     */
    #[McpPrompt(name: 'content_creator')]
    public function createContent(
        #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide'])]
        string $type,
        #[CompletionProvider(enum: StatusEnum::class)]
        string $status,
        #[CompletionProvider(enum: PriorityEnum::class)]
        string $priority
    ): array {
        return [
            ['role' => 'user', 'content' => "Create a {$type} with status {$status} and priority {$priority}"]
        ];
    }

    /**
     * Resource template with list completion for categories.
     */
    #[McpResourceTemplate(
        uriTemplate: 'content://{category}/{slug}',
        name: 'content_template'
    )]
    public function getContent(
        #[CompletionProvider(values: ['news', 'blog', 'docs', 'api'])]
        string $category,
        string $slug
    ): array {
        return [
            'category' => $category,
            'slug' => $slug,
            'url' => "https://example.com/{$category}/{$slug}"
        ];
    }
}

```

--------------------------------------------------------------------------------
/tests/Unit/Attributes/McpResourceTemplateTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Attributes;

use PhpMcp\Server\Attributes\McpResourceTemplate;

it('instantiates with correct properties', function () {
    // Arrange
    $uriTemplate = 'file:///{path}/data';
    $name = 'test-template-name';
    $description = 'This is a test template description.';
    $mimeType = 'application/json';

    // Act
    $attribute = new McpResourceTemplate(
        uriTemplate: $uriTemplate,
        name: $name,
        description: $description,
        mimeType: $mimeType,
    );

    // Assert
    expect($attribute->uriTemplate)->toBe($uriTemplate);
    expect($attribute->name)->toBe($name);
    expect($attribute->description)->toBe($description);
    expect($attribute->mimeType)->toBe($mimeType);
});

it('instantiates with null values for name and description', function () {
    // Arrange & Act
    $attribute = new McpResourceTemplate(
        uriTemplate: 'test://{id}', // uriTemplate is required
        name: null,
        description: null,
        mimeType: null,
    );

    // Assert
    expect($attribute->uriTemplate)->toBe('test://{id}');
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
    expect($attribute->mimeType)->toBeNull();
});

it('instantiates with missing optional arguments', function () {
    // Arrange & Act
    $uriTemplate = 'tmpl://{key}';
    $attribute = new McpResourceTemplate(uriTemplate: $uriTemplate);

    // Assert
    expect($attribute->uriTemplate)->toBe($uriTemplate);
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
    expect($attribute->mimeType)->toBeNull();
});

```

--------------------------------------------------------------------------------
/tests/Fixtures/ServerScripts/StdioTestServer.php:
--------------------------------------------------------------------------------

```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;
use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\PromptHandlerFixture;
use Psr\Log\AbstractLogger;
use Psr\Log\NullLogger;

class StdErrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s] SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new NullLogger();
    $logger->info('StdioTestServer listener starting.');

    $server = Server::make()
        ->withServerInfo('StdioIntegrationTestServer', '0.1.0')
        ->withLogger($logger)
        ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_stdio_tool')
        ->withTool([ToolHandlerFixture::class, 'toolReadsContext'], 'tool_reads_context') // for Context testing
        ->withResource([ResourceHandlerFixture::class, 'getStaticText'], 'test://stdio/static', 'static_stdio_resource')
        ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_stdio_prompt')
        ->build();

    $transport = new StdioServerTransport();
    $server->listen($transport);

    $logger->info('StdioTestServer listener stopped.');
    exit(0);
} catch (\Throwable $e) {
    fwrite(STDERR, "[STDIO_SERVER_CRITICAL_ERROR]\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/src/Configuration.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server;

use PhpMcp\Schema\Implementation;
use PhpMcp\Schema\ServerCapabilities;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use React\EventLoop\LoopInterface;

/**
 * Value Object holding core configuration and shared dependencies for the MCP Server instance.
 *
 * This object is typically assembled by the ServerBuilder and passed to the Server constructor.
 */
class Configuration
{
    /**
     * @param  Implementation  $serverInfo  Info about this MCP server application.
     * @param  ServerCapabilities  $capabilities  Capabilities of this MCP server application.
     * @param  LoggerInterface  $logger  PSR-3 Logger instance.
     * @param  LoopInterface  $loop  ReactPHP Event Loop instance.
     * @param  CacheInterface|null  $cache  Optional PSR-16 Cache instance for registry/state.
     * @param  ContainerInterface  $container  PSR-11 DI Container for resolving handlers/dependencies.
     * @param  int  $paginationLimit  Maximum number of items to return for list methods.
     * @param  string|null  $instructions  Instructions describing how to use the server and its features.
     */
    public function __construct(
        public readonly Implementation $serverInfo,
        public readonly ServerCapabilities $capabilities,
        public readonly LoggerInterface $logger,
        public readonly LoopInterface $loop,
        public readonly ?CacheInterface $cache,
        public readonly ContainerInterface $container,
        public readonly int $paginationLimit = 50,
        public readonly ?string $instructions = null,
    ) {}
}

```

--------------------------------------------------------------------------------
/tests/Unit/Attributes/McpResourceTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Attributes;

use PhpMcp\Server\Attributes\McpResource;

it('instantiates with correct properties', function () {
    // Arrange
    $uri = 'file:///test/resource';
    $name = 'test-resource-name';
    $description = 'This is a test resource description.';
    $mimeType = 'text/plain';
    $size = 1024;

    // Act
    $attribute = new McpResource(
        uri: $uri,
        name: $name,
        description: $description,
        mimeType: $mimeType,
        size: $size,
    );

    // Assert
    expect($attribute->uri)->toBe($uri);
    expect($attribute->name)->toBe($name);
    expect($attribute->description)->toBe($description);
    expect($attribute->mimeType)->toBe($mimeType);
    expect($attribute->size)->toBe($size);
});

it('instantiates with null values for name and description', function () {
    // Arrange & Act
    $attribute = new McpResource(
        uri: 'file:///test', // URI is required
        name: null,
        description: null,
        mimeType: null,
        size: null,
    );

    // Assert
    expect($attribute->uri)->toBe('file:///test');
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
    expect($attribute->mimeType)->toBeNull();
    expect($attribute->size)->toBeNull();
});

it('instantiates with missing optional arguments', function () {
    // Arrange & Act
    $uri = 'file:///only-uri';
    $attribute = new McpResource(uri: $uri);

    // Assert
    expect($attribute->uri)->toBe($uri);
    expect($attribute->name)->toBeNull();
    expect($attribute->description)->toBeNull();
    expect($attribute->mimeType)->toBeNull();
    expect($attribute->size)->toBeNull();
});

```

--------------------------------------------------------------------------------
/tests/Fixtures/Discovery/DiscoverableToolHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Fixtures\Discovery;

use PhpMcp\Schema\ToolAnnotations;
use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;

class DiscoverableToolHandler
{
    /**
     * A basic discoverable tool.
     * @param string $name The name to greet.
     * @return string The greeting.
     */
    #[McpTool(name: "greet_user", description: "Greets a user by name.")]
    public function greet(string $name): string
    {
        return "Hello, {$name}!";
    }

    /**
     * A tool with more complex parameters and inferred name/description.
     * @param int $count The number of times to repeat.
     * @param bool $loudly Should it be loud?
     * @param BackedStringEnum $mode The mode of operation.
     * @return array An array with results.
     */
    #[McpTool(annotations: new ToolAnnotations(readOnlyHint: true))]
    public function repeatAction(int $count, bool $loudly = false, BackedStringEnum $mode = BackedStringEnum::OptionA): array
    {
        return ['count' => $count, 'loudly' => $loudly, 'mode' => $mode->value, 'message' => "Action repeated."];
    }

    // This method should NOT be discovered as a tool
    public function internalHelperMethod(int $value): int
    {
        return $value * 2;
    }

    #[McpTool(name: "private_tool_should_be_ignored")] // On private method
    private function aPrivateTool(): void
    {
    }

    #[McpTool(name: "protected_tool_should_be_ignored")] // On protected method
    protected function aProtectedTool(): void
    {
    }

    #[McpTool(name: "static_tool_should_be_ignored")] // On static method
    public static function aStaticTool(): void
    {
    }
}

```

--------------------------------------------------------------------------------
/src/Session/ArraySessionHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Session;

use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Defaults\SystemClock;
use Psr\Clock\ClockInterface;

class ArraySessionHandler implements SessionHandlerInterface
{
    /**
     * @var array<string, array{ data: array, timestamp: int }>
     */
    protected array $store = [];

    private ClockInterface $clock;

    public function __construct(
        public readonly int $ttl = 3600,
        ?ClockInterface $clock = null
    ) {
        $this->clock = $clock ?? new SystemClock();
    }

    public function read(string $sessionId): string|false
    {
        $session = $this->store[$sessionId] ?? '';
        if ($session === '') {
            return false;
        }

        $currentTimestamp = $this->clock->now()->getTimestamp();

        if ($currentTimestamp - $session['timestamp'] > $this->ttl) {
            unset($this->store[$sessionId]);
            return false;
        }

        return $session['data'];
    }

    public function write(string $sessionId, string $data): bool
    {
        $this->store[$sessionId] = [
            'data' => $data,
            'timestamp' => $this->clock->now()->getTimestamp(),
        ];

        return true;
    }

    public function destroy(string $sessionId): bool
    {
        if (isset($this->store[$sessionId])) {
            unset($this->store[$sessionId]);
        }

        return true;
    }

    public function gc(int $maxLifetime): array
    {
        $currentTimestamp = $this->clock->now()->getTimestamp();
        $deletedSessions = [];

        foreach ($this->store as $sessionId => $session) {
            if ($currentTimestamp - $session['timestamp'] > $maxLifetime) {
                unset($this->store[$sessionId]);
                $deletedSessions[] = $sessionId;
            }
        }

        return $deletedSessions;
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/General/DocBlockTestFixture.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

/**
 * A stub class for testing DocBlock parsing.
 */
class DocBlockTestFixture
{
    /**
     * Simple summary line.
     */
    public function methodWithSummaryOnly(): void
    {
    }

    /**
     * Summary line here.
     *
     * This is a longer description spanning
     * multiple lines.
     * It might contain *markdown* or `code`.
     *
     * @since 1.0
     */
    public function methodWithSummaryAndDescription(): void
    {
    }

    /**
     * Method with various parameter tags.
     *
     * @param string $param1 Description for string param.
     * @param int|null $param2 Description for nullable int param.
     * @param bool $param3
     * @param $param4 Missing type.
     * @param array<string, mixed> $param5 Array description.
     * @param \stdClass $param6 Object param.
     */
    public function methodWithParams(string $param1, ?int $param2, bool $param3, $param4, array $param5, \stdClass $param6): void
    {
    }

    /**
     * Method with return tag.
     *
     * @return string The result of the operation.
     */
    public function methodWithReturn(): string
    {
        return '';
    }

    /**
     * Method with multiple tags.
     *
     * @param float $value The value to process.
     * @return bool Status of the operation.
     * @throws \RuntimeException If processing fails.
     * @deprecated Use newMethod() instead.
     * @see \PhpMcp\Server\Tests\Fixtures\General\DocBlockTestFixture::newMethod()
     */
    public function methodWithMultipleTags(float $value): bool
    {
        return true;
    }

    /**
     * Malformed docblock - missing closing
     */
    public function methodWithMalformedDocBlock(): void
    {
    }

    public function methodWithNoDocBlock(): void
    {
    }

    // Some other method needed for a @see tag perhaps
    public function newMethod(): void
    {
    }
}

```

--------------------------------------------------------------------------------
/examples/03-manual-registration-stdio/SimpleHandlers.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\ManualStdioExample;

use Psr\Log\LoggerInterface;

class SimpleHandlers
{
    private LoggerInterface $logger;

    private string $appVersion = '1.0-manual';

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->logger->info('SimpleHandlers instantiated for manual registration example.');
    }

    /**
     * A manually registered tool to echo input.
     *
     * @param  string  $text  The text to echo.
     * @return string The echoed text.
     */
    public function echoText(string $text): string
    {
        $this->logger->info("Manual tool 'echo_text' called.", ['text' => $text]);

        return 'Echo: '.$text;
    }

    /**
     * A manually registered resource providing app version.
     *
     * @return string The application version.
     */
    public function getAppVersion(): string
    {
        $this->logger->info("Manual resource 'app://version' read.");

        return $this->appVersion;
    }

    /**
     * A manually registered prompt template.
     *
     * @param  string  $userName  The name of the user.
     * @return array The prompt messages.
     */
    public function greetingPrompt(string $userName): array
    {
        $this->logger->info("Manual prompt 'personalized_greeting' called.", ['userName' => $userName]);

        return [
            ['role' => 'user', 'content' => "Craft a personalized greeting for {$userName}."],
        ];
    }

    /**
     * A manually registered resource template.
     *
     * @param  string  $itemId  The ID of the item.
     * @return array Item details.
     */
    public function getItemDetails(string $itemId): array
    {
        $this->logger->info("Manual template 'item://{itemId}' resolved.", ['itemId' => $itemId]);

        return ['id' => $itemId, 'name' => "Item {$itemId}", 'description' => "Details for item {$itemId} from manual template."];
    }
}

```

--------------------------------------------------------------------------------
/tests/Mocks/Clock/FixedClock.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Mocks\Clock;

use DateTimeImmutable;
use DateTimeZone;
use Psr\Clock\ClockInterface;
use DateInterval;

class FixedClock implements ClockInterface
{
    private DateTimeImmutable $currentTime;

    public function __construct(string|DateTimeImmutable $initialTime = 'now', ?DateTimeZone $timezone = null)
    {
        if ($initialTime instanceof DateTimeImmutable) {
            $this->currentTime = $initialTime;
        } else {
            $this->currentTime = new DateTimeImmutable($initialTime, $timezone);
        }
    }

    public function now(): DateTimeImmutable
    {
        return $this->currentTime;
    }

    public function setCurrentTime(string|DateTimeImmutable $newTime, ?DateTimeZone $timezone = null): void
    {
        if ($newTime instanceof DateTimeImmutable) {
            $this->currentTime = $newTime;
        } else {
            $this->currentTime = new DateTimeImmutable($newTime, $timezone);
        }
    }

    public function advance(DateInterval $interval): void
    {
        $this->currentTime = $this->currentTime->add($interval);
    }

    public function rewind(DateInterval $interval): void
    {
        $this->currentTime = $this->currentTime->sub($interval);
    }

    public function addSecond(): void
    {
        $this->advance(new DateInterval("PT1S"));
    }

    public function addSeconds(int $seconds): void
    {
        $this->advance(new DateInterval("PT{$seconds}S"));
    }

    public function addMinutes(int $minutes): void
    {
        $this->advance(new DateInterval("PT{$minutes}M"));
    }

    public function addHours(int $hours): void
    {
        $this->advance(new DateInterval("PT{$hours}H"));
    }

    public function subSeconds(int $seconds): void
    {
        $this->rewind(new DateInterval("PT{$seconds}S"));
    }

    public function subMinutes(int $minutes): void
    {
        $this->rewind(new DateInterval("PT{$minutes}M"));
    }
}

```

--------------------------------------------------------------------------------
/tests/Unit/Defaults/ListCompletionProviderTest.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Unit\Defaults;

use PhpMcp\Server\Defaults\ListCompletionProvider;
use PhpMcp\Server\Contracts\SessionInterface;
use Mockery;

beforeEach(function () {
    $this->session = Mockery::mock(SessionInterface::class);
});

it('returns all values when current value is empty', function () {
    $values = ['apple', 'banana', 'cherry'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('', $this->session);

    expect($result)->toBe($values);
});

it('filters values based on current value prefix', function () {
    $values = ['apple', 'apricot', 'banana', 'cherry'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('ap', $this->session);

    expect($result)->toBe(['apple', 'apricot']);
});

it('returns empty array when no values match', function () {
    $values = ['apple', 'banana', 'cherry'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('xyz', $this->session);

    expect($result)->toBe([]);
});

it('works with single character prefix', function () {
    $values = ['apple', 'banana', 'cherry'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('a', $this->session);

    expect($result)->toBe(['apple']);
});

it('is case sensitive by default', function () {
    $values = ['Apple', 'apple', 'APPLE'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('A', $this->session);

    expect($result)->toEqual(['Apple', 'APPLE']);
});

it('handles empty values array', function () {
    $provider = new ListCompletionProvider([]);

    $result = $provider->getCompletions('test', $this->session);

    expect($result)->toBe([]);
});

it('preserves array order', function () {
    $values = ['zebra', 'apple', 'banana'];
    $provider = new ListCompletionProvider($values);

    $result = $provider->getCompletions('', $this->session);

    expect($result)->toBe(['zebra', 'apple', 'banana']);
});

```

--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------

```json
{
    "name": "php-mcp/server",
    "description": "PHP SDK for building Model Context Protocol (MCP) servers - Create MCP tools, resources, and prompts",
    "keywords": [
        "mcp",
        "model context protocol",
        "server",
        "php",
        "php mcp",
        "php mcp sdk",
        "php mcp server",
        "php mcp tools",
        "php mcp resources",
        "php mcp prompts",
        "php model context protocol"
    ],
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Kyrian Obikwelu",
            "email": "[email protected]"
        }
    ],
    "require": {
        "php": ">=8.1",
        "opis/json-schema": "^2.4",
        "php-mcp/schema": "^1.0",
        "phpdocumentor/reflection-docblock": "^5.6",
        "psr/clock": "^1.0",
        "psr/container": "^1.0 || ^2.0",
        "psr/log": "^1.0 || ^2.0 || ^3.0",
        "psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
        "react/event-loop": "^1.5",
        "react/http": "^1.11",
        "react/promise": "^3.0",
        "react/stream": "^1.4",
        "symfony/finder": "^6.4 || ^7.2"
    },
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.75",
        "mockery/mockery": "^1.6",
        "pestphp/pest": "^2.36.0|^3.5.0",
        "react/async": "^4.0",
        "react/child-process": "^0.6.6",
        "symfony/var-dumper": "^6.4.11|^7.1.5"
    },
    "suggest": {
        "ext-pcntl": "For signal handling support when using StdioServerTransport with StreamSelectLoop"
    },
    "autoload": {
        "psr-4": {
            "PhpMcp\\Server\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "PhpMcp\\Server\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "vendor/bin/pest",
        "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage",
        "lint": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php"
    },
    "config": {
        "sort-packages": true,
        "allow-plugins": {
            "pestphp/pest-plugin": true
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}
```

--------------------------------------------------------------------------------
/src/Contracts/SessionInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

use JsonSerializable;

interface SessionInterface extends JsonSerializable
{
    /**
     * Get the session ID.
     */
    public function getId(): string;

    /**
     * Save the session.
     */
    public function save(): void;

    /**
     * Get a specific attribute from the session.
     * Supports dot notation for nested access.
     */
    public function get(string $key, mixed $default = null): mixed;

    /**
     * Set a specific attribute in the session.
     * Supports dot notation for nested access.
     */
    public function set(string $key, mixed $value, bool $overwrite = true): void;

    /**
     * Check if an attribute exists in the session.
     * Supports dot notation for nested access.
     */
    public function has(string $key): bool;

    /**
     * Remove an attribute from the session.
     * Supports dot notation for nested access.
     */
    public function forget(string $key): void;

    /**
     * Remove all attributes from the session.
     */
    public function clear(): void;

    /**
     * Get an attribute's value and then remove it from the session.
     * Supports dot notation for nested access.
     */
    public function pull(string $key, mixed $default = null): mixed;

    /**
     * Get all attributes of the session.
     */
    public function all(): array;

    /**
     * Set all attributes of the session, typically for hydration.
     * This will overwrite existing attributes.
     */
    public function hydrate(array $attributes): void;

    /**
     * Add a message to the session's queue.
     */
    public function queueMessage(string $message): void;

    /**
     * Retrieve and remove all messages from the queue.
     * @return array<string>
     */
    public function dequeueMessages(): array;

    /**
     * Check if there are any messages in the queue.
     */
    public function hasQueuedMessages(): bool;

    /**
     * Get the session handler instance.
     *
     * @return SessionHandlerInterface
     */
    public function getHandler(): SessionHandlerInterface;
}

```

--------------------------------------------------------------------------------
/src/Defaults/InMemoryEventStore.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Defaults;

use PhpMcp\Server\Contracts\EventStoreInterface;

/**
 * Simple in-memory implementation of the EventStore interface for resumability
 * This is primarily intended for examples and testing, not for production use
 * where a persistent storage solution would be more appropriate.
 */
class InMemoryEventStore implements EventStoreInterface
{
    public const DEFAULT_MAX_EVENTS_PER_STREAM = 1000;

    /**
     * @var array<string, array{streamId: string, message: string}>
     * Example: [eventId1 => ['streamId' => 'abc', 'message' => '...']]
     */
    private array $events = [];

    private function generateEventId(string $streamId): string
    {
        return $streamId . '_' . (int)(microtime(true) * 1000) . '_' . bin2hex(random_bytes(4));
    }

    private function getStreamIdFromEventId(string $eventId): ?string
    {
        $parts = explode('_', $eventId);
        return $parts[0] ?? null;
    }

    public function storeEvent(string $streamId, string $message): string
    {
        $eventId = $this->generateEventId($streamId);

        $this->events[$eventId] = [
            'streamId' => $streamId,
            'message' => $message,
        ];

        return $eventId;
    }

    public function replayEventsAfter(string $lastEventId, callable $sendCallback): void
    {
        if (!isset($this->events[$lastEventId])) {
            return;
        }

        $streamId = $this->getStreamIdFromEventId($lastEventId);
        if ($streamId === null) {
            return;
        }

        $foundLastEvent = false;

        // Sort by eventId for deterministic ordering
        ksort($this->events);

        foreach ($this->events as $eventId => ['streamId' => $eventStreamId, 'message' => $message]) {
            if ($eventStreamId !== $streamId) {
                continue;
            }

            if ($eventId === $lastEventId) {
                $foundLastEvent = true;
                continue;
            }

            if ($foundLastEvent) {
                $sendCallback($eventId, $message);
            }
        }
    }
}

```

--------------------------------------------------------------------------------
/src/Contracts/ServerTransportInterface.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

use Evenement\EventEmitterInterface;
use PhpMcp\Server\Exception\TransportException;
use PhpMcp\Schema\JsonRpc\Message;
use React\Promise\PromiseInterface;

/**
 * Interface for server-side MCP transports.
 *
 * Implementations handle listening for connections/data and sending raw messages.
 * MUST emit events for lifecycle and messages.
 *
 * --- Expected Emitted Events ---
 * 'ready': () - Optional: Fired when listening starts successfully.
 * 'client_connected': (string $sessionId) - New client connection
 * 'message': (Message $message, string $sessionId, array $context) - Complete message received from a client.
 * 'client_disconnected': (string $sessionId, ?string $reason) - Client connection closed.
 * 'error': (Throwable $error, ?string $sessionId) - Error occurred (general transport error if sessionId is null).
 * 'close': (?string $reason) - Transport listener stopped completely.
 */
interface ServerTransportInterface extends EventEmitterInterface
{
    /**
     * Starts the transport listener (e.g., listens on STDIN, starts HTTP server).
     * Does NOT run the event loop itself. Prepares transport to emit events when loop runs.
     *
     * @throws TransportException on immediate setup failure (e.g., port binding).
     */
    public function listen(): void;

    /**
     * Sends a message to a connected client session with optional context.
     *
     * @param  Message  $message  Message to send.
     * @param  string  $sessionId  Target session identifier.
     * @param  array  $context  Optional context for the message. Eg. streamId for SSE.
     * @return PromiseInterface<void> Resolves on successful send/queue, rejects on specific send error.
     */
    public function sendMessage(Message $message, string $sessionId, array $context = []): PromiseInterface;

    /**
     * Stops the transport listener gracefully and closes all active connections.
     * MUST eventually emit a 'close' event for the transport itself.
     * Individual client disconnects should emit 'client_disconnected' events.
     */
    public function close(): void;
}

```

--------------------------------------------------------------------------------
/examples/05-stdio-env-variables/server.php:
--------------------------------------------------------------------------------

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

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once './EnvToolHandler.php';

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
use Psr\Log\AbstractLogger;

/*
    |--------------------------------------------------------------------------
    | MCP Stdio Environment Variable Example Server
    |--------------------------------------------------------------------------
    |
    | This server demonstrates how to use environment variables to modify tool
    | behavior. The MCP client can set the APP_MODE environment variable to
    | control the server's behavior.
    |
    | Configure your MCP Client (eg. Cursor) for this server like this:
    |
    | {
    |     "mcpServers": {
    |         "my-php-env-server": {
    |             "command": "php",
    |             "args": ["/full/path/to/examples/05-stdio-env-variables/server.php"],
    |             "env": {
    |                 "APP_MODE": "debug" // or "production", or leave it out
    |             }
    |         }
    |     }
    | }
    |
    | The server will read the APP_MODE environment variable and use it to
    | modify the behavior of the tools.
    |
    | If the APP_MODE environment variable is not set, the server will use the
    | default behavior.
    |
*/

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Stdio Environment Variable Example Server...');

    $server = Server::make()
        ->withServerInfo('Env Var Server', '1.0.0')
        ->withLogger($logger)
        ->build();

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

    $transport = new StdioServerTransport();
    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);

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

```

--------------------------------------------------------------------------------
/examples/08-schema-showcase-streamable/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP Schema Showcase Server (Attribute Discovery)
    |--------------------------------------------------------------------------
    |
    | This server demonstrates various ways to use the Schema attribute to
    | validate tool inputs. It showcases string constraints, numeric validation,
    | object schemas, array handling, enums, and format validation.
    |
    | To Use:
    | 1. Ensure 'SchemaShowcaseElements.php' defines classes with MCP attributes.
    | 2. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-schema-showcase": {
    |             "command": "php",
    |             "args": ["/full/path/to/examples/08-schema-showcase-stdio/server.php"]
    |         }
    |     }
    | }
    |
    | This example focuses specifically on demonstrating different Schema
    | attribute capabilities for robust input validation.
    |
*/

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once 'SchemaShowcaseElements.php';

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StreamableHttpServerTransport;
use Psr\Log\AbstractLogger;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf(
            "[%s] %s %s\n",
            strtoupper($level),
            $message,
            empty($context) ? '' : json_encode($context)
        ));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Schema Showcase Server...');

    $server = Server::make()
        ->withServerInfo('Schema Showcase', '1.0.0')
        ->withLogger($logger)
        ->build();

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

    $transport = new StreamableHttpServerTransport('127.0.0.1', 8080, 'mcp');

    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);
} catch (\Throwable $e) {
    fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
    fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
    fwrite(STDERR, 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n");
    fwrite(STDERR, $e->getTraceAsString() . "\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/tests/Unit/Attributes/CompletionProviderTest.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Unit\Attributes;

use PhpMcp\Server\Attributes\CompletionProvider;
use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
use PhpMcp\Server\Defaults\ListCompletionProvider;
use PhpMcp\Server\Defaults\EnumCompletionProvider;
use PhpMcp\Server\Tests\Fixtures\Enums\StatusEnum;

it('can be constructed with provider class', function () {
    $attribute = new CompletionProvider(provider: CompletionProviderFixture::class);

    expect($attribute->provider)->toBe(CompletionProviderFixture::class);
    expect($attribute->values)->toBeNull();
    expect($attribute->enum)->toBeNull();
});

it('can be constructed with provider instance', function () {
    $instance = new CompletionProviderFixture();
    $attribute = new CompletionProvider(provider: $instance);

    expect($attribute->provider)->toBe($instance);
    expect($attribute->values)->toBeNull();
    expect($attribute->enum)->toBeNull();
});

it('can be constructed with values array', function () {
    $values = ['draft', 'published', 'archived'];
    $attribute = new CompletionProvider(values: $values);

    expect($attribute->provider)->toBeNull();
    expect($attribute->values)->toBe($values);
    expect($attribute->enum)->toBeNull();
});

it('can be constructed with enum class', function () {
    $attribute = new CompletionProvider(enum: StatusEnum::class);

    expect($attribute->provider)->toBeNull();
    expect($attribute->values)->toBeNull();
    expect($attribute->enum)->toBe(StatusEnum::class);
});

it('throws exception when no parameters provided', function () {
    new CompletionProvider();
})->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');

it('throws exception when multiple parameters provided', function () {
    new CompletionProvider(
        provider: CompletionProviderFixture::class,
        values: ['test']
    );
})->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');

it('throws exception when all parameters provided', function () {
    new CompletionProvider(
        provider: CompletionProviderFixture::class,
        values: ['test'],
        enum: StatusEnum::class
    );
})->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');

```

--------------------------------------------------------------------------------
/tests/Unit/Defaults/EnumCompletionProviderTest.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Unit\Defaults;

use PhpMcp\Server\Defaults\EnumCompletionProvider;
use PhpMcp\Server\Contracts\SessionInterface;
use Mockery;

enum StringEnum: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
}

enum IntEnum: int
{
    case LOW = 1;
    case MEDIUM = 2;
    case HIGH = 3;
}

enum UnitEnum
{
    case ALPHA;
    case BETA;
    case GAMMA;
}

beforeEach(function () {
    $this->session = Mockery::mock(SessionInterface::class);
});

it('creates provider from string-backed enum', function () {
    $provider = new EnumCompletionProvider(StringEnum::class);

    $result = $provider->getCompletions('', $this->session);

    expect($result)->toBe(['draft', 'published', 'archived']);
});

it('creates provider from int-backed enum using names', function () {
    $provider = new EnumCompletionProvider(IntEnum::class);

    $result = $provider->getCompletions('', $this->session);

    expect($result)->toBe(['LOW', 'MEDIUM', 'HIGH']);
});

it('creates provider from unit enum using names', function () {
    $provider = new EnumCompletionProvider(UnitEnum::class);

    $result = $provider->getCompletions('', $this->session);

    expect($result)->toBe(['ALPHA', 'BETA', 'GAMMA']);
});

it('filters string enum values by prefix', function () {
    $provider = new EnumCompletionProvider(StringEnum::class);

    $result = $provider->getCompletions('ar', $this->session);

    expect($result)->toEqual(['archived']);
});

it('filters unit enum values by prefix', function () {
    $provider = new EnumCompletionProvider(UnitEnum::class);

    $result = $provider->getCompletions('A', $this->session);

    expect($result)->toBe(['ALPHA']);
});

it('returns empty array when no values match prefix', function () {
    $provider = new EnumCompletionProvider(StringEnum::class);

    $result = $provider->getCompletions('xyz', $this->session);

    expect($result)->toBe([]);
});

it('throws exception for non-enum class', function () {
    new EnumCompletionProvider(\stdClass::class);
})->throws(\InvalidArgumentException::class, 'Class stdClass is not an enum');

it('throws exception for non-existent class', function () {
    new EnumCompletionProvider('NonExistentClass');
})->throws(\InvalidArgumentException::class, 'Class NonExistentClass is not an enum');

```

--------------------------------------------------------------------------------
/examples/07-complex-tool-schema-http/McpEventScheduler.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\ComplexSchemaHttpExample;

use Mcp\ComplexSchemaHttpExample\Model\EventPriority;
use Mcp\ComplexSchemaHttpExample\Model\EventType;
use PhpMcp\Server\Attributes\McpTool;
use Psr\Log\LoggerInterface;

class McpEventScheduler
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Schedules a new event.
     * The inputSchema for this tool will reflect all parameter types and defaults.
     *
     * @param  string  $title  The title of the event.
     * @param  string  $date  The date of the event (YYYY-MM-DD).
     * @param  EventType  $type  The type of event.
     * @param  string|null  $time  The time of the event (HH:MM), optional.
     * @param  EventPriority  $priority  The priority of the event. Defaults to Normal.
     * @param  string[]|null  $attendees  An optional list of attendee email addresses.
     * @param  bool  $sendInvites  Send calendar invites to attendees? Defaults to true if attendees are provided.
     * @return array Confirmation of the scheduled event.
     */
    #[McpTool(name: 'schedule_event')]
    public function scheduleEvent(
        string $title,
        string $date,
        EventType $type,
        ?string $time = null, // Optional, nullable
        EventPriority $priority = EventPriority::Normal, // Optional with enum default
        ?array $attendees = null, // Optional array of strings, nullable
        bool $sendInvites = true   // Optional with default
    ): array {
        $this->logger->info("Tool 'schedule_event' called", compact('title', 'date', 'type', 'time', 'priority', 'attendees', 'sendInvites'));

        // Simulate scheduling logic
        $eventDetails = [
            'title' => $title,
            'date' => $date,
            'type' => $type->value, // Use enum value
            'time' => $time ?? 'All day',
            'priority' => $priority->name, // Use enum name
            'attendees' => $attendees ?? [],
            'invites_will_be_sent' => ($attendees && $sendInvites),
        ];

        // In a real app, this would interact with a calendar service
        $this->logger->info('Event scheduled', ['details' => $eventDetails]);

        return [
            'success' => true,
            'message' => "Event '{$title}' scheduled successfully for {$date}.",
            'event_details' => $eventDetails,
        ];
    }
}

```

--------------------------------------------------------------------------------
/src/Defaults/ArrayCache.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Defaults;

use DateInterval;
use DateTime;
use Psr\SimpleCache\CacheInterface;

/**
 * Very basic PSR-16 array cache implementation (not for production).
 */
class ArrayCache implements CacheInterface
{
    private array $store = [];

    private array $expiries = [];

    public function get(string $key, mixed $default = null): mixed
    {
        if (! $this->has($key)) {
            return $default;
        }

        return $this->store[$key];
    }

    public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
    {
        $this->store[$key] = $value;
        $this->expiries[$key] = $this->calculateExpiry($ttl);

        return true;
    }

    public function delete(string $key): bool
    {
        unset($this->store[$key], $this->expiries[$key]);

        return true;
    }

    public function clear(): bool
    {
        $this->store = [];
        $this->expiries = [];

        return true;
    }

    public function getMultiple(iterable $keys, mixed $default = null): iterable
    {
        $result = [];
        foreach ($keys as $key) {
            $result[$key] = $this->get($key, $default);
        }

        return $result;
    }

    public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool
    {
        $expiry = $this->calculateExpiry($ttl);
        foreach ($values as $key => $value) {
            $this->store[$key] = $value;
            $this->expiries[$key] = $expiry;
        }

        return true;
    }

    public function deleteMultiple(iterable $keys): bool
    {
        foreach ($keys as $key) {
            unset($this->store[$key], $this->expiries[$key]);
        }

        return true;
    }

    public function has(string $key): bool
    {
        if (! isset($this->store[$key])) {
            return false;
        }
        // Check expiry
        if (isset($this->expiries[$key]) && $this->expiries[$key] !== null && time() >= $this->expiries[$key]) {
            $this->delete($key);

            return false;
        }

        return true;
    }

    private function calculateExpiry(DateInterval|int|null $ttl): ?int
    {
        if ($ttl === null) {
            return null; // No expiry
        }
        if (is_int($ttl)) {
            return time() + $ttl;
        }
        if ($ttl instanceof DateInterval) {
            return (new DateTime())->add($ttl)->getTimestamp();
        }

        // Invalid TTL type, treat as no expiry
        return null;
    }
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/ServerScripts/HttpTestServer.php:
--------------------------------------------------------------------------------

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

declare(strict_types=1);

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

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\HttpServerTransport;
use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\PromptHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\RequestAttributeChecker;
use PhpMcp\Server\Tests\Fixtures\Middlewares\HeaderMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\RequestAttributeMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ShortCircuitMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\FirstMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\SecondMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ThirdMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ErrorMiddleware;
use Psr\Log\AbstractLogger;
use Psr\Log\NullLogger;

class StdErrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s] HTTP_SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
    }
}

$host = $argv[1] ?? '127.0.0.1';
$port = (int)($argv[2] ?? 8990);
$mcpPathPrefix = $argv[3] ?? 'mcp_http_test';

try {
    $logger = new NullLogger();

    $server = Server::make()
        ->withServerInfo('HttpIntegrationTestServer', '0.1.0')
        ->withLogger($logger)
        ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_http_tool')
        ->withTool([RequestAttributeChecker::class, 'checkAttribute'], 'check_request_attribute_tool')
        ->withResource([ResourceHandlerFixture::class, 'getStaticText'], "test://http/static", 'static_http_resource')
        ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_http_prompt')
        ->build();

    $middlewares = [
        new HeaderMiddleware(),
        new RequestAttributeMiddleware(),
        new ShortCircuitMiddleware(),
        new FirstMiddleware(),
        new SecondMiddleware(),
        new ThirdMiddleware(),
        new ErrorMiddleware()
    ];

    $transport = new HttpServerTransport($host, $port, $mcpPathPrefix, null, $middlewares);
    $server->listen($transport);

    exit(0);
} catch (\Throwable $e) {
    fwrite(STDERR, "[HTTP_SERVER_CRITICAL_ERROR]\nHost:{$host} Port:{$port} Prefix:{$mcpPathPrefix}\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/examples/01-discovery-stdio-calculator/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP Stdio Calculator Server (Attribute Discovery)
    |--------------------------------------------------------------------------
    |
    | This server demonstrates using attribute-based discovery to find MCP
    | elements (Tools, Resources) in the 'McpElements.php' file within this
    | directory. It runs via the STDIO transport.
    |
    | To Use:
    | 1. Ensure 'McpElements.php' defines classes with MCP attributes.
    | 2. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-stdio-calculator": {
    |             "command": "php",
    |             "args": ["/full/path/to/examples/01-discovery-stdio-calculator/server.php"]
    |         }
    |     }
    | }
    |
    | The ServerBuilder builds the server instance, then $server->discover()
    | scans the current directory (specified by basePath: __DIR__, scanDirs: ['.'])
    | to find and register elements before listening on STDIN/STDOUT.
    |
    | If you provided a `CacheInterface` implementation to the ServerBuilder,
    | the discovery process will be cached, so you can comment out the
    | discovery call after the first run to speed up subsequent runs.
    |
*/
declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once 'McpElements.php';

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
use Psr\Log\AbstractLogger;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf(
            "[%s] %s %s\n",
            strtoupper($level),
            $message,
            empty($context) ? '' : json_encode($context)
        ));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Stdio Calculator Server...');

    $server = Server::make()
        ->withServerInfo('Stdio Calculator', '1.1.0')
        ->withLogger($logger)
        ->build();

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

    $transport = new StdioServerTransport();

    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);

} catch (\Throwable $e) {
    fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
    fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
    fwrite(STDERR, 'File: '.$e->getFile().':'.$e->getLine()."\n");
    fwrite(STDERR, $e->getTraceAsString()."\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------

```php
<?php

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use React\Socket\SocketServer;

function getPrivateProperty(object $object, string $propertyName)
{
    $reflector = new ReflectionClass($object);
    $property = $reflector->getProperty($propertyName);
    $property->setAccessible(true);
    return $property->getValue($object);
}

function delay($time, ?LoopInterface $loop = null)
{
    if ($loop === null) {
        $loop = Loop::get();
    }

    /** @var TimerInterface $timer */
    $timer = null;
    return new Promise(function ($resolve) use ($loop, $time, &$timer) {
        $timer = $loop->addTimer($time, function () use ($resolve) {
            $resolve(null);
        });
    }, function () use (&$timer, $loop) {
        $loop->cancelTimer($timer);
        $timer = null;

        throw new \RuntimeException('Timer cancelled');
    });
}

function timeout(PromiseInterface $promise, $time, ?LoopInterface $loop = null)
{
    $canceller = null;
    if (\method_exists($promise, 'cancel')) {
        $canceller = function () use (&$promise) {
            $promise->cancel();
            $promise = null;
        };
    }

    if ($loop === null) {
        $loop = Loop::get();
    }

    return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
        $timer = null;
        $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
            if ($timer) {
                $loop->cancelTimer($timer);
            }
            $timer = false;
            $resolve($v);
        }, function ($v) use (&$timer, $loop, $reject) {
            if ($timer) {
                $loop->cancelTimer($timer);
            }
            $timer = false;
            $reject($v);
        });

        if ($timer === false) {
            return;
        }

        // start timeout timer which will cancel the input promise
        $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject) {
            $reject(new \RuntimeException('Timed out after ' . $time . ' seconds'));

            if (\method_exists($promise, 'cancel')) {
                $promise->cancel();
            }
            $promise = null;
        });
    }, $canceller);
}

function findFreePort()
{
    $server = new SocketServer('127.0.0.1:0');
    $address = $server->getAddress();
    $port = $address ? parse_url($address, PHP_URL_PORT) : null;
    $server->close();
    if (!$port) {
        throw new \RuntimeException("Could not find a free port for testing.");
    }
    return (int)$port;
}

```

--------------------------------------------------------------------------------
/tests/Fixtures/Utils/DockBlockParserFixture.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Utils;

/**
 * Test stub for DocBlock array type parsing
 */
class DockBlockParserFixture
{
    /**
     * Method with simple array[] syntax
     *
     * @param string[] $strings Array of strings using [] syntax
     * @param int[] $integers Array of integers using [] syntax
     * @param bool[] $booleans Array of booleans using [] syntax
     * @param float[] $floats Array of floats using [] syntax
     * @param object[] $objects Array of objects using [] syntax
     * @param \DateTime[] $dateTimeInstances Array of DateTime objects
     */
    public function simpleArraySyntax(
        array $strings,
        array $integers,
        array $booleans,
        array $floats,
        array $objects,
        array $dateTimeInstances
    ): void {
    }

    /**
     * Method with array<T> generic syntax
     *
     * @param array<string> $strings Array of strings using generic syntax
     * @param array<int> $integers Array of integers using generic syntax
     * @param array<bool> $booleans Array of booleans using generic syntax
     * @param array<float> $floats Array of floats using generic syntax
     * @param array<object> $objects Array of objects using generic syntax
     * @param array<\DateTime> $dateTimeInstances Array of DateTime objects using generic syntax
     */
    public function genericArraySyntax(
        array $strings,
        array $integers,
        array $booleans,
        array $floats,
        array $objects,
        array $dateTimeInstances
    ): void {
    }

    /**
     * Method with nested array syntax
     *
     * @param array<array<string>> $nestedStringArrays Array of arrays of strings
     * @param array<array<int>> $nestedIntArrays Array of arrays of integers
     * @param string[][] $doubleStringArrays Array of arrays of strings using double []
     * @param int[][] $doubleIntArrays Array of arrays of integers using double []
     */
    public function nestedArraySyntax(
        array $nestedStringArrays,
        array $nestedIntArrays,
        array $doubleStringArrays,
        array $doubleIntArrays
    ): void {
    }

    /**
     * Method with object-like array syntax
     *
     * @param array{name: string, age: int} $person Simple object array with name and age
     * @param array{id: int, title: string, tags: string[]} $article Article with array of tags
     * @param array{user: array{id: int, name: string}, items: array<int>} $order Order with nested user object and array of item IDs
     */
    public function objectArraySyntax(
        array $person,
        array $article,
        array $order
    ): void {
    }
}

```

--------------------------------------------------------------------------------
/src/Session/CacheSessionHandler.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Session;

use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Defaults\SystemClock;
use Psr\SimpleCache\CacheInterface;
use Psr\Clock\ClockInterface;

class CacheSessionHandler implements SessionHandlerInterface
{
    private const SESSION_INDEX_KEY = 'mcp_session_index';
    private array $sessionIndex = [];
    private ClockInterface $clock;

    public function __construct(
        public readonly CacheInterface $cache,
        public readonly int $ttl = 3600,
        ?ClockInterface $clock = null
    ) {
        $this->sessionIndex = $this->cache->get(self::SESSION_INDEX_KEY, []);
        $this->clock = $clock ?? new SystemClock();
    }

    public function read(string $sessionId): string|false
    {
        $session = $this->cache->get($sessionId, false);
        if ($session === false) {
            if (isset($this->sessionIndex[$sessionId])) {
                unset($this->sessionIndex[$sessionId]);
                $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
            }
            return false;
        }

        if (!isset($this->sessionIndex[$sessionId])) {
            $this->sessionIndex[$sessionId] = $this->clock->now()->getTimestamp();
            $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
            return $session;
        }

        if ($this->clock->now()->getTimestamp() - $this->sessionIndex[$sessionId] > $this->ttl) {
            $this->cache->delete($sessionId);
            return false;
        }

        return $session;
    }

    public function write(string $sessionId, string $data): bool
    {
        $this->sessionIndex[$sessionId] = $this->clock->now()->getTimestamp();
        $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
        return $this->cache->set($sessionId, $data);
    }

    public function destroy(string $sessionId): bool
    {
        unset($this->sessionIndex[$sessionId]);
        $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
        return $this->cache->delete($sessionId);
    }

    public function gc(int $maxLifetime): array
    {
        $currentTime = $this->clock->now()->getTimestamp();
        $deletedSessions = [];

        foreach ($this->sessionIndex as $sessionId => $timestamp) {
            if ($currentTime - $timestamp > $maxLifetime) {
                $this->cache->delete($sessionId);
                unset($this->sessionIndex[$sessionId]);
                $deletedSessions[] = $sessionId;
            }
        }

        $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);

        return $deletedSessions;
    }
}

```

--------------------------------------------------------------------------------
/examples/06-custom-dependencies-stdio/McpTaskHandlers.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\DependenciesStdioExample;

use Mcp\DependenciesStdioExample\Services\StatsServiceInterface;
use Mcp\DependenciesStdioExample\Services\TaskRepositoryInterface;
use PhpMcp\Server\Attributes\McpResource;
use PhpMcp\Server\Attributes\McpTool;
use Psr\Log\LoggerInterface;

class McpTaskHandlers
{
    private TaskRepositoryInterface $taskRepo;

    private StatsServiceInterface $statsService;

    private LoggerInterface $logger;

    // Dependencies injected by the DI container
    public function __construct(
        TaskRepositoryInterface $taskRepo,
        StatsServiceInterface $statsService,
        LoggerInterface $logger
    ) {
        $this->taskRepo = $taskRepo;
        $this->statsService = $statsService;
        $this->logger = $logger;
        $this->logger->info('McpTaskHandlers instantiated with dependencies.');
    }

    /**
     * Adds a new task for a given user.
     *
     * @param  string  $userId  The ID of the user.
     * @param  string  $description  The task description.
     * @return array The created task details.
     */
    #[McpTool(name: 'add_task')]
    public function addTask(string $userId, string $description): array
    {
        $this->logger->info("Tool 'add_task' invoked", ['userId' => $userId]);

        return $this->taskRepo->addTask($userId, $description);
    }

    /**
     * Lists pending tasks for a specific user.
     *
     * @param  string  $userId  The ID of the user.
     * @return array A list of tasks.
     */
    #[McpTool(name: 'list_user_tasks')]
    public function listUserTasks(string $userId): array
    {
        $this->logger->info("Tool 'list_user_tasks' invoked", ['userId' => $userId]);

        return $this->taskRepo->getTasksForUser($userId);
    }

    /**
     * Marks a task as complete.
     *
     * @param  int  $taskId  The ID of the task to complete.
     * @return array Status of the operation.
     */
    #[McpTool(name: 'complete_task')]
    public function completeTask(int $taskId): array
    {
        $this->logger->info("Tool 'complete_task' invoked", ['taskId' => $taskId]);
        $success = $this->taskRepo->completeTask($taskId);

        return ['success' => $success, 'message' => $success ? "Task {$taskId} completed." : "Task {$taskId} not found."];
    }

    /**
     * Provides current system statistics.
     *
     * @return array System statistics.
     */
    #[McpResource(uri: 'stats://system/overview', name: 'system_stats', mimeType: 'application/json')]
    public function getSystemStatistics(): array
    {
        $this->logger->info("Resource 'stats://system/overview' invoked");

        return $this->statsService->getSystemStats();
    }
}

```

--------------------------------------------------------------------------------
/src/Session/SubscriptionManager.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Session;

use Psr\Log\LoggerInterface;

class SubscriptionManager
{
    /** @var array<string, array<string, true>>  Key: URI, Value: array of session IDs */
    private array $resourceSubscribers = [];

    /** @var array<string, array<string, true>>  Key: Session ID, Value: array of URIs */
    private array $sessionSubscriptions = [];

    public function __construct(
        private readonly LoggerInterface $logger
    ) {
    }

    /**
     * Subscribe a session to a resource
     */
    public function subscribe(string $sessionId, string $uri): void
    {
        // Add to both mappings for efficient lookup
        $this->resourceSubscribers[$uri][$sessionId] = true;
        $this->sessionSubscriptions[$sessionId][$uri] = true;

        $this->logger->debug('Session subscribed to resource', [
            'sessionId' => $sessionId,
            'uri' => $uri
        ]);
    }

    /**
     * Unsubscribe a session from a resource
     */
    public function unsubscribe(string $sessionId, string $uri): void
    {
        unset($this->resourceSubscribers[$uri][$sessionId]);
        unset($this->sessionSubscriptions[$sessionId][$uri]);

        // Clean up empty arrays
        if (empty($this->resourceSubscribers[$uri])) {
            unset($this->resourceSubscribers[$uri]);
        }

        $this->logger->debug('Session unsubscribed from resource', [
            'sessionId' => $sessionId,
            'uri' => $uri
        ]);
    }

    /**
     * Get all sessions subscribed to a resource
     */
    public function getSubscribers(string $uri): array
    {
        return array_keys($this->resourceSubscribers[$uri] ?? []);
    }

    /**
     * Check if a session is subscribed to a resource
     */
    public function isSubscribed(string $sessionId, string $uri): bool
    {
        return isset($this->sessionSubscriptions[$sessionId][$uri]);
    }

    /**
     * Clean up all subscriptions for a session
     */
    public function cleanupSession(string $sessionId): void
    {
        if (!isset($this->sessionSubscriptions[$sessionId])) {
            return;
        }

        $uris = array_keys($this->sessionSubscriptions[$sessionId]);
        foreach ($uris as $uri) {
            unset($this->resourceSubscribers[$uri][$sessionId]);

            // Clean up empty arrays
            if (empty($this->resourceSubscribers[$uri])) {
                unset($this->resourceSubscribers[$uri]);
            }
        }

        unset($this->sessionSubscriptions[$sessionId]);

        $this->logger->debug('Cleaned up all subscriptions for session', [
            'sessionId' => $sessionId,
            'count' => count($uris)
        ]);
    }
}

```

--------------------------------------------------------------------------------
/examples/06-custom-dependencies-stdio/Services.php:
--------------------------------------------------------------------------------

```php
<?php

namespace Mcp\DependenciesStdioExample\Services;

use Psr\Log\LoggerInterface;

// --- Mock Services ---

interface TaskRepositoryInterface
{
    public function addTask(string $userId, string $description): array;

    public function getTasksForUser(string $userId): array;

    public function getAllTasks(): array;

    public function completeTask(int $taskId): bool;
}

class InMemoryTaskRepository implements TaskRepositoryInterface
{
    private array $tasks = [];

    private int $nextTaskId = 1;

    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        // Add some initial tasks
        $this->addTask('user1', 'Buy groceries');
        $this->addTask('user1', 'Write MCP example');
        $this->addTask('user2', 'Review PR');
    }

    public function addTask(string $userId, string $description): array
    {
        $task = [
            'id' => $this->nextTaskId++,
            'userId' => $userId,
            'description' => $description,
            'completed' => false,
            'createdAt' => date('c'),
        ];
        $this->tasks[$task['id']] = $task;
        $this->logger->info('Task added', ['id' => $task['id'], 'user' => $userId]);

        return $task;
    }

    public function getTasksForUser(string $userId): array
    {
        return array_values(array_filter($this->tasks, fn ($task) => $task['userId'] === $userId && ! $task['completed']));
    }

    public function getAllTasks(): array
    {
        return array_values($this->tasks);
    }

    public function completeTask(int $taskId): bool
    {
        if (isset($this->tasks[$taskId])) {
            $this->tasks[$taskId]['completed'] = true;
            $this->logger->info('Task completed', ['id' => $taskId]);

            return true;
        }

        return false;
    }
}

interface StatsServiceInterface
{
    public function getSystemStats(): array;
}

class SystemStatsService implements StatsServiceInterface
{
    private TaskRepositoryInterface $taskRepository;

    public function __construct(TaskRepositoryInterface $taskRepository)
    {
        $this->taskRepository = $taskRepository;
    }

    public function getSystemStats(): array
    {
        $allTasks = $this->taskRepository->getAllTasks();
        $completed = count(array_filter($allTasks, fn ($task) => $task['completed']));
        $pending = count($allTasks) - $completed;

        return [
            'total_tasks' => count($allTasks),
            'completed_tasks' => $completed,
            'pending_tasks' => $pending,
            'server_uptime_seconds' => time() - $_SERVER['REQUEST_TIME_FLOAT'], // Approx uptime for CLI script
        ];
    }
}

```

--------------------------------------------------------------------------------
/examples/03-manual-registration-stdio/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP Stdio Server (Manual Element Registration)
    |--------------------------------------------------------------------------
    |
    | This server demonstrates how to manually register all MCP elements
    | (Tools, Resources, Prompts, ResourceTemplates) using the ServerBuilder's
    | fluent `withTool()`, `withResource()`, etc., methods.
    | It does NOT use attribute discovery. Handlers are in 'SimpleHandlers.php'.
    | It runs via the STDIO transport.
    |
    | To Use:
    | 1. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-stdio-manual": {
    |             "command": "php",
    |             "args": ["/full/path/to/examples/03-manual-registration-stdio/server.php"]
    |         }
    |     }
    | }
    |
    | All elements are explicitly defined during the ServerBuilder chain.
    | The $server->discover() method is NOT called.
    |
*/

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once './SimpleHandlers.php';

use Mcp\ManualStdioExample\SimpleHandlers;
use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Manual Registration (Stdio) Server...');

    $container = new BasicContainer();
    $container->set(LoggerInterface::class, $logger);

    $server = Server::make()
        ->withServerInfo('Manual Reg Server', '1.0.0')
        ->withLogger($logger)
        ->withContainer($container)
        ->withTool([SimpleHandlers::class, 'echoText'], 'echo_text')
        ->withResource([SimpleHandlers::class, 'getAppVersion'], 'app://version', 'application_version', mimeType: 'text/plain')
        ->withPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting')
        ->withResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
        ->build();

    $transport = new StdioServerTransport();
    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);

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

```

--------------------------------------------------------------------------------
/tests/Unit/ConfigurationTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit;

use Mockery;
use PhpMcp\Schema\Implementation;
use PhpMcp\Schema\ServerCapabilities;
use PhpMcp\Server\Configuration;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use React\EventLoop\LoopInterface;

beforeEach(function () {
    $this->serverInfo = Implementation::make('TestServer', '1.1.0');
    $this->logger = Mockery::mock(LoggerInterface::class);
    $this->loop = Mockery::mock(LoopInterface::class);
    $this->cache = Mockery::mock(CacheInterface::class);
    $this->container = Mockery::mock(ContainerInterface::class);
    $this->capabilities = ServerCapabilities::make();
});

afterEach(function () {
    Mockery::close();
});

it('constructs configuration object with all properties', function () {
    $paginationLimit = 100;
    $config = new Configuration(
        serverInfo: $this->serverInfo,
        capabilities: $this->capabilities,
        logger: $this->logger,
        loop: $this->loop,
        cache: $this->cache,
        container: $this->container,
        paginationLimit: $paginationLimit
    );

    expect($config->serverInfo)->toBe($this->serverInfo);
    expect($config->capabilities)->toBe($this->capabilities);
    expect($config->logger)->toBe($this->logger);
    expect($config->loop)->toBe($this->loop);
    expect($config->cache)->toBe($this->cache);
    expect($config->container)->toBe($this->container);
    expect($config->paginationLimit)->toBe($paginationLimit);
});

it('constructs configuration object with default pagination limit', function () {
    $config = new Configuration(
        serverInfo: $this->serverInfo,
        capabilities: $this->capabilities,
        logger: $this->logger,
        loop: $this->loop,
        cache: $this->cache,
        container: $this->container
    );

    expect($config->paginationLimit)->toBe(50); // Default value
});

it('constructs configuration object with null cache', function () {
    $config = new Configuration(
        serverInfo: $this->serverInfo,
        capabilities: $this->capabilities,
        logger: $this->logger,
        loop: $this->loop,
        cache: null,
        container: $this->container
    );

    expect($config->cache)->toBeNull();
});

it('constructs configuration object with specific capabilities', function () {
    $customCaps = ServerCapabilities::make(
        resourcesSubscribe: true,
        logging: true,
    );

    $config = new Configuration(
        serverInfo: $this->serverInfo,
        capabilities: $customCaps,
        logger: $this->logger,
        loop: $this->loop,
        cache: null,
        container: $this->container
    );

    expect($config->capabilities)->toBe($customCaps);
    expect($config->capabilities->resourcesSubscribe)->toBeTrue();
    expect($config->capabilities->logging)->toBeTrue();
});

```

--------------------------------------------------------------------------------
/examples/04-combined-registration-http/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP HTTP Server (Combined Manual & Discovered Elements)
    |--------------------------------------------------------------------------
    |
    | This server demonstrates a combination of manual element registration
    | via the ServerBuilder and attribute-based discovery.
    | - Manually registered elements are defined in 'ManualHandlers.php'.
    | - Discoverable elements are in 'DiscoveredElements.php'.
    |
    | It runs via the HTTP transport.
    |
    | This example also shows precedence: if a manually registered element
    | has the same identifier (e.g., URI for a resource, or name for a tool)
    | as a discovered one, the manual registration takes priority.
    |
    | To Use:
    | 1. Run this script from your CLI: `php server.php`
    |    The server will listen on http://127.0.0.1:8081 by default.
    | 2. Configure your MCP Client (e.g., Cursor):
    |
    | {
    |     "mcpServers": {
    |         "php-http-combined": {
    |             "url": "http://127.0.0.1:8081/mcp_combined/sse" // Note the prefix
    |         }
    |     }
    | }
    |
    | Manual elements are registered during ServerBuilder->build().
    | Then, $server->discover() scans for attributed elements.
    |
*/

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once './DiscoveredElements.php';
require_once './ManualHandlers.php';

use Mcp\CombinedHttpExample\Manual\ManualHandlers;
use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\HttpServerTransport;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Combined Registration (HTTP) Server...');

    $container = new BasicContainer();
    $container->set(LoggerInterface::class, $logger); // ManualHandlers needs LoggerInterface

    $server = Server::make()
        ->withServerInfo('Combined HTTP Server', '1.0.0')
        ->withLogger($logger)
        ->withContainer($container)
        ->withTool([ManualHandlers::class, 'manualGreeter'])
        ->withResource(
            [ManualHandlers::class, 'getPriorityConfigManual'],
            'config://priority',
            'priority_config_manual',
        )
        ->build();

    // Now, run discovery. Discovered elements will be added.
    // If 'config://priority' was discovered, the manual one takes precedence.
    $server->discover(__DIR__, scanDirs: ['.']);

    $transport = new HttpServerTransport('127.0.0.1', 8081, 'mcp_combined');

    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);

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

```

--------------------------------------------------------------------------------
/tests/Fixtures/General/ToolHandlerFixture.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Schema\Content\ImageContent;
use PhpMcp\Schema\Content\AudioContent;
use PhpMcp\Server\Context;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
use Psr\Log\LoggerInterface;

class ToolHandlerFixture
{
    public function __construct()
    {
    }

    public function greet(string $name): string
    {
        return "Hello, {$name}!";
    }

    public function sum(int $a, int $b): int
    {
        return $a + $b;
    }

    public function optionalParamsTool(string $required, ?string $optional = "default_val"): string
    {
        return "{$required} and {$optional}";
    }

    public function noParamsTool(): array
    {
        return ['status' => 'ok', 'timestamp' => time()];
    }

    public function processBackedEnum(BackedStringEnum $status): string
    {
        return "Status processed: " . $status->value;
    }

    public function returnString(): string
    {
        return "This is a string result.";
    }

    public function returnInteger(): int
    {
        return 12345;
    }

    public function returnFloat(): float
    {
        return 67.89;
    }

    public function returnBooleanTrue(): bool
    {
        return true;
    }

    public function returnBooleanFalse(): bool
    {
        return false;
    }

    public function returnNull(): ?string
    {
        return null;
    }

    public function returnArray(): array
    {
        return ['message' => 'Array result', 'data' => [1, 2, 3]];
    }

    public function returnStdClass(): \stdClass
    {
        $obj = new \stdClass();
        $obj->property = "value";
        return $obj;
    }

    public function returnTextContent(): TextContent
    {
        return TextContent::make("Pre-formatted TextContent.");
    }

    public function returnImageContent(): ImageContent
    {
        return ImageContent::make("base64data==", "image/png");
    }

    public function returnAudioContent(): AudioContent
    {
        return AudioContent::make("base64audio==", "audio/mp3");
    }

    public function returnArrayOfContent(): array
    {
        return [
            TextContent::make("Part 1"),
            ImageContent::make("imgdata", "image/jpeg")
        ];
    }

    public function returnMixedArray(): array
    {
        return [
            "A raw string",
            TextContent::make("A TextContent object"),
            123,
            true,
            null,
            ['nested_key' => 'nested_value', 'sub_array' => [4, 5]],
            ImageContent::make("img_data_mixed", "image/gif"),
            (object)['obj_prop' => 'obj_val']
        ];
    }

    public function returnEmptyArray(): array
    {
        return [];
    }

    public function toolThatThrows(): void
    {
        throw new \InvalidArgumentException("Something went wrong in the tool.");
    }

    public function toolUnencodableResult()
    {
        return fopen('php://memory', 'r');
    }

    public function toolReadsContext(Context $context): string
    {
        if (!$context->request) {
            return "No request instance present";
        }

        return $context->request->getHeaderLine('X-Test-Header') ?: "No X-Test-Header";
    }
}

```

--------------------------------------------------------------------------------
/examples/07-complex-tool-schema-http/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP HTTP Server with Complex Tool Schema (Event Scheduler)
    |--------------------------------------------------------------------------
    |
    | This example demonstrates how to define an MCP Tool with a more complex
    | input schema, utilizing various PHP types, optional parameters, default
    | values, and backed Enums. The server automatically generates the
    | corresponding JSON Schema for the tool's input.
    |
    | Scenario:
    | An "Event Scheduler" tool that allows scheduling events with details like
    | title, date, time (optional), type (enum), priority (enum with default),
    | attendees (optional list), and invite preferences (boolean with default).
    |
    | Key Points:
    |   - The `schedule_event` tool in `McpEventScheduler.php` showcases:
    |       - Required string parameters (`title`, `date`).
    |       - A required backed string enum parameter (`EventType $type`).
    |       - Optional nullable string (`?string $time = null`).
    |       - Optional backed integer enum with a default value (`EventPriority $priority = EventPriority::Normal`).
    |       - Optional nullable array of strings (`?array $attendees = null`).
    |       - Optional boolean with a default value (`bool $sendInvites = true`).
    |   - PHP type hints and default values are used by `SchemaGenerator` (internal)
    |     to create the `inputSchema` for the tool.
    |   - This example uses attribute-based discovery and the HTTP transport.
    |
    | To Use:
    | 1. Run this script: `php server.php` (from this directory)
    |    The server will listen on http://127.0.0.1:8082 by default.
    | 2. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-http-complex-scheduler": {
    |             "url": "http://127.0.0.1:8082/mcp_scheduler/sse" // Note the prefix
    |         }
    |     }
    | }
    |
    | Connect your client, list tools, and inspect the 'inputSchema' for the
    | 'schedule_event' tool. Prompt your LLM with question to test the tool.
    |
*/

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once './EventTypes.php'; // Include enums
require_once './McpEventScheduler.php';

use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\HttpServerTransport;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Complex Schema HTTP Server...');

    $container = new BasicContainer();
    $container->set(LoggerInterface::class, $logger);

    $server = Server::make()
        ->withServerInfo('Event Scheduler Server', '1.0.0')
        ->withLogger($logger)
        ->withContainer($container)
        ->build();

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

    $transport = new HttpServerTransport('127.0.0.1', 8082, 'mcp_scheduler');
    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);

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

```

--------------------------------------------------------------------------------
/tests/Fixtures/ServerScripts/StreamableHttpTestServer.php:
--------------------------------------------------------------------------------

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

declare(strict_types=1);

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

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StreamableHttpServerTransport;
use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\PromptHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\RequestAttributeChecker;
use PhpMcp\Server\Tests\Fixtures\Middlewares\HeaderMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\RequestAttributeMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ShortCircuitMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\FirstMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\SecondMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ThirdMiddleware;
use PhpMcp\Server\Tests\Fixtures\Middlewares\ErrorMiddleware;
use PhpMcp\Server\Defaults\InMemoryEventStore;
use Psr\Log\AbstractLogger;
use Psr\Log\NullLogger;

class StdErrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s] SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
    }
}

$host = $argv[1] ?? '127.0.0.1';
$port = (int)($argv[2] ?? 8992);
$mcpPath = $argv[3] ?? 'mcp_streamable_test';
$enableJsonResponse = filter_var($argv[4] ?? 'true', FILTER_VALIDATE_BOOLEAN);
$useEventStore = filter_var($argv[5] ?? 'false', FILTER_VALIDATE_BOOLEAN);
$stateless = filter_var($argv[6] ?? 'false', FILTER_VALIDATE_BOOLEAN);

try {
    $logger = new NullLogger();
    $logger->info("Starting StreamableHttpTestServer on {$host}:{$port}/{$mcpPath}, JSON Mode: " . ($enableJsonResponse ? 'ON' : 'OFF') . ", Stateless: " . ($stateless ? 'ON' : 'OFF'));

    $eventStore = $useEventStore ? new InMemoryEventStore() : null;

    $server = Server::make()
        ->withServerInfo('StreamableHttpIntegrationServer', '0.1.0')
        ->withLogger($logger)
        ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_streamable_tool')
        ->withTool([ToolHandlerFixture::class, 'sum'], 'sum_streamable_tool') // For batch testing
        ->withTool([ToolHandlerFixture::class, 'toolReadsContext'], 'tool_reads_context') // for Context testing
        ->withTool([RequestAttributeChecker::class, 'checkAttribute'], 'check_request_attribute_tool')
        ->withResource([ResourceHandlerFixture::class, 'getStaticText'], "test://streamable/static", 'static_streamable_resource')
        ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_streamable_prompt')
        ->build();

    $middlewares = [
        new HeaderMiddleware(),
        new RequestAttributeMiddleware(),
        new ShortCircuitMiddleware(),
        new FirstMiddleware(),
        new SecondMiddleware(),
        new ThirdMiddleware(),
        new ErrorMiddleware()
    ];

    $transport = new StreamableHttpServerTransport(
        host: $host,
        port: $port,
        mcpPath: $mcpPath,
        enableJsonResponse: $enableJsonResponse,
        stateless: $stateless,
        eventStore: $eventStore,
        middlewares: $middlewares
    );

    $server->listen($transport);

    $logger->info("StreamableHttpTestServer listener stopped on {$host}:{$port}.");
    exit(0);
} catch (\Throwable $e) {
    fwrite(STDERR, "[STREAMABLE_HTTP_SERVER_CRITICAL_ERROR]\nHost:{$host} Port:{$port} Prefix:{$mcpPath}\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/src/Utils/HandlerResolver.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Utils;

use InvalidArgumentException;
use ReflectionMethod;
use ReflectionException;

/**
 * Utility class to validate and resolve MCP element handlers.
 */
class HandlerResolver
{
    /**
     * Validates and resolves a handler to a ReflectionMethod or ReflectionFunction instance.
     *
     * A handler can be:
     * - A Closure: function() { ... }
     * - An array: [ClassName::class, 'methodName'] (instance method)
     * - An array: [ClassName::class, 'staticMethod'] (static method, if callable)
     * - A string: InvokableClassName::class (which will resolve to its '__invoke' method)
     *
     * @param \Closure|array|string $handler The handler to resolve.
     * @return \ReflectionMethod|\ReflectionFunction
     *
     * @throws InvalidArgumentException If the handler format is invalid, the class/method doesn't exist,
     *                                  or the method is unsuitable (e.g., private, abstract).
     */
    public static function resolve(\Closure|array|string $handler): \ReflectionMethod|\ReflectionFunction
    {
        // Handle Closures
        if ($handler instanceof \Closure) {
            return new \ReflectionFunction($handler);
        }

        $className = null;
        $methodName = null;

        if (is_array($handler)) {
            if (count($handler) !== 2 || !isset($handler[0]) || !isset($handler[1]) || !is_string($handler[0]) || !is_string($handler[1])) {
                throw new InvalidArgumentException('Invalid array handler format. Expected [ClassName::class, \'methodName\'].');
            }
            [$className, $methodName] = $handler;
            if (!class_exists($className)) {
                throw new InvalidArgumentException("Handler class '{$className}' not found for array handler.");
            }
            if (!method_exists($className, $methodName)) {
                throw new InvalidArgumentException("Handler method '{$methodName}' not found in class '{$className}' for array handler.");
            }
        } elseif (is_string($handler) && class_exists($handler)) {
            $className = $handler;
            $methodName = '__invoke';
            if (!method_exists($className, $methodName)) {
                throw new InvalidArgumentException("Invokable handler class '{$className}' must have a public '__invoke' method.");
            }
        } else {
            throw new InvalidArgumentException('Invalid handler format. Expected Closure, [ClassName::class, \'methodName\'] or InvokableClassName::class string.');
        }

        try {
            $reflectionMethod = new ReflectionMethod($className, $methodName);

            // For discovered elements (non-manual), still reject static methods
            // For manual elements, we'll allow static methods since they're callable
            if (!$reflectionMethod->isPublic()) {
                throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' must be public.");
            }
            if ($reflectionMethod->isAbstract()) {
                throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' cannot be abstract.");
            }
            if ($reflectionMethod->isConstructor() || $reflectionMethod->isDestructor()) {
                throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' cannot be a constructor or destructor.");
            }

            return $reflectionMethod;
        } catch (ReflectionException $e) {
            // This typically occurs if class_exists passed but ReflectionMethod still fails (rare)
            throw new InvalidArgumentException("Reflection error for handler '{$className}::{$methodName}': {$e->getMessage()}", 0, $e);
        }
    }
}

```

--------------------------------------------------------------------------------
/examples/06-custom-dependencies-stdio/server.php:
--------------------------------------------------------------------------------

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

/*
    |--------------------------------------------------------------------------
    | MCP Stdio Server with Custom Dependencies (Task Manager)
    |--------------------------------------------------------------------------
    |
    | This example demonstrates how to use a PSR-11 Dependency Injection (DI)
    | container (PhpMcp\Server\Defaults\BasicContainer in this case) to inject
    | custom services (like a TaskRepositoryInterface or StatsServiceInterface)
    | into your MCP element handler classes.
    |
    | Scenario:
    | A simple Task Management system where:
    |   - Tools allow adding tasks, listing tasks for a user, and completing tasks.
    |   - A Resource provides system statistics (total tasks, pending, etc.).
    |   - Handlers in 'McpTaskHandlers.php' depend on service interfaces.
    |   - Concrete service implementations are in 'Services.php'.
    |
    | Key Points:
    |   - The `ServerBuilder` is configured with `->withContainer($container)`.
    |   - The DI container is set up with bindings for service interfaces to
    |     their concrete implementations (e.g., TaskRepositoryInterface -> InMemoryTaskRepository).
    |   - The `McpTaskHandlers` class receives its dependencies (TaskRepositoryInterface,
    |     StatsServiceInterface, LoggerInterface) via constructor injection, resolved by
    |     the DI container when the Processor needs an instance of McpTaskHandlers.
    |   - This example uses attribute-based discovery via `$server->discover()`.
    |   - It runs using the STDIO transport.
    |
    | To Use:
    | 1. Run this script: `php server.php` (from this directory)
    | 2. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-stdio-deps-taskmgr": {
    |             "command": "php",
    |             "args": ["/full/path/to/examples/06-custom-dependencies-stdio/server.php"]
    |         }
    |     }
    | }
    |
    | Interact with tools like 'add_task', 'list_user_tasks', 'complete_task'
    | and read the resource 'stats://system/overview'.
    |
*/

declare(strict_types=1);

chdir(__DIR__);
require_once '../../vendor/autoload.php';
require_once './Services.php';
require_once './McpTaskHandlers.php';

use Mcp\DependenciesStdioExample\Services;
use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class StderrLogger extends AbstractLogger
{
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
    }
}

try {
    $logger = new StderrLogger();
    $logger->info('Starting MCP Custom Dependencies (Stdio) Server...');

    $container = new BasicContainer();
    $container->set(LoggerInterface::class, $logger);

    $taskRepo = new Services\InMemoryTaskRepository($logger);
    $container->set(Services\TaskRepositoryInterface::class, $taskRepo);

    $statsService = new Services\SystemStatsService($taskRepo);
    $container->set(Services\StatsServiceInterface::class, $statsService);

    $server = Server::make()
        ->withServerInfo('Task Manager Server', '1.0.0')
        ->withLogger($logger)
        ->withContainer($container)
        ->build();

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

    $transport = new StdioServerTransport();
    $server->listen($transport);

    $logger->info('Server listener stopped gracefully.');
    exit(0);
} catch (\Throwable $e) {
    fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
    fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
    fwrite(STDERR, 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n");
    fwrite(STDERR, $e->getTraceAsString() . "\n");
    exit(1);
}

```

--------------------------------------------------------------------------------
/src/Session/SessionManager.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Session;

use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Contracts\SessionInterface;
use Psr\Log\LoggerInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;

class SessionManager implements EventEmitterInterface
{
    use EventEmitterTrait;

    protected ?TimerInterface $gcTimer = null;

    public function __construct(
        protected SessionHandlerInterface $handler,
        protected LoggerInterface $logger,
        protected ?LoopInterface $loop = null,
        protected int $ttl = 3600,
        protected int|float $gcInterval = 300
    ) {
        $this->loop ??= Loop::get();
    }

    /**
     * Start the garbage collection timer
     */
    public function startGcTimer(): void
    {
        if ($this->gcTimer !== null) {
            return;
        }

        $this->gcTimer = $this->loop->addPeriodicTimer($this->gcInterval, [$this, 'gc']);
    }

    public function gc(): array
    {
        $deletedSessions = $this->handler->gc($this->ttl);

        foreach ($deletedSessions as $sessionId) {
            $this->emit('session_deleted', [$sessionId]);
        }

        if (count($deletedSessions) > 0) {
            $this->logger->debug('Session garbage collection complete', [
                'purged_sessions' => count($deletedSessions),
            ]);
        }

        return $deletedSessions;
    }

    /**
     * Stop the garbage collection timer
     */
    public function stopGcTimer(): void
    {
        if ($this->gcTimer !== null) {
            $this->loop->cancelTimer($this->gcTimer);
            $this->gcTimer = null;
        }
    }

    /**
     * Create a new session
     */
    public function createSession(string $sessionId): SessionInterface
    {
        $session = new Session($this->handler, $sessionId);

        $session->hydrate([
            'initialized' => false,
            'client_info' => null,
            'protocol_version' => null,
            'subscriptions' => [],      // [uri => true]
            'message_queue' => [],      // string[] (raw JSON-RPC frames)
            'log_level' => null,
        ]);

        $session->save();

        $this->logger->info('Session created', ['sessionId' => $sessionId]);
        $this->emit('session_created', [$sessionId, $session]);

        return $session;
    }

    /**
     * Get an existing session
     */
    public function getSession(string $sessionId): ?SessionInterface
    {
        return Session::retrieve($sessionId, $this->handler);
    }

    public function hasSession(string $sessionId): bool
    {
        return $this->getSession($sessionId) !== null;
    }

    /**
     * Delete a session completely
     */
    public function deleteSession(string $sessionId): bool
    {
        $success = $this->handler->destroy($sessionId);

        if ($success) {
            $this->emit('session_deleted', [$sessionId]);
            $this->logger->info('Session deleted', ['sessionId' => $sessionId]);
        } else {
            $this->logger->warning('Failed to delete session', ['sessionId' => $sessionId]);
        }

        return $success;
    }

    public function queueMessage(string $sessionId, string $message): void
    {
        $session = $this->getSession($sessionId);
        if ($session === null) {
            return;
        }

        $session->queueMessage($message);
        $session->save();
    }

    public function dequeueMessages(string $sessionId): array
    {
        $session = $this->getSession($sessionId);
        if ($session === null) {
            return [];
        }

        $messages = $session->dequeueMessages();
        $session->save();

        return $messages;
    }

    public function hasQueuedMessages(string $sessionId): bool
    {
        $session = $this->getSession($sessionId, true);
        if ($session === null) {
            return false;
        }

        return $session->hasQueuedMessages();
    }
}

```

--------------------------------------------------------------------------------
/src/Exception/McpServerException.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Exception;

use Exception;
use PhpMcp\Schema\Constants;
use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;
use Throwable;

/**
 * Base exception for all MCP Server library errors.
 */
class McpServerException extends Exception
{
    // MCP reserved range: -32000 to -32099 (Server error)
    // Add specific server-side codes if needed later, e.g.:
    // public const CODE_RESOURCE_ACTION_FAILED = -32000;
    // public const CODE_TOOL_EXECUTION_FAILED = -32001;

    /**
     * Additional data associated with the error, suitable for JSON-RPC 'data' field.
     *
     * @var mixed|null
     */
    protected mixed $data = null;

    /**
     * @param  string  $message  Error message.
     * @param  int  $code  Error code (use constants or appropriate HTTP status codes if applicable).
     * @param  mixed|null  $data  Additional data.
     * @param  ?Throwable  $previous  Previous exception.
     */
    public function __construct(
        string $message = '',
        int $code = 0,
        mixed $data = null,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
        $this->data = $data;
    }

    /**
     * Get additional error data.
     *
     * @return mixed|null
     */
    public function getData(): mixed
    {
        return $this->data;
    }

    /**
     * Formats the exception into a JSON-RPC 2.0 error object structure.
     * Specific exceptions should override this or provide factories with correct codes.
     */
    public function toJsonRpcError(string|int $id): JsonRpcError
    {
        $code = ($this->code >= -32768 && $this->code <= -32000) ? $this->code : Constants::INTERNAL_ERROR;

        return new JsonRpcError(
            jsonrpc: '2.0',
            id: $id,
            code: $code,
            message: $this->getMessage(),
            data: $this->getData()
        );
    }

    public static function parseError(string $details, ?Throwable $previous = null): self
    {
        return new ProtocolException('Parse error: ' . $details, Constants::PARSE_ERROR, null, $previous);
    }

    public static function invalidRequest(?string $details = 'Invalid Request', ?Throwable $previous = null): self
    {
        return new ProtocolException($details, Constants::INVALID_REQUEST, null, $previous);
    }

    public static function methodNotFound(string $methodName, ?string $message = null, ?Throwable $previous = null): self
    {
        return new ProtocolException($message ?? "Method not found: {$methodName}", Constants::METHOD_NOT_FOUND, null, $previous);
    }

    public static function invalidParams(string $message = 'Invalid params', $data = null, ?Throwable $previous = null): self
    {
        // Pass data (e.g., validation errors) through
        return new ProtocolException($message, Constants::INVALID_PARAMS, $data, $previous);
    }

    public static function internalError(?string $details = 'Internal server error', ?Throwable $previous = null): self
    {
        $message = 'Internal error';
        if ($details && is_string($details)) {
            $message .= ': ' . $details;
        } elseif ($previous && $details === null) {
            $message .= ' (See server logs)';
        }

        return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
    }

    public static function toolExecutionFailed(string $toolName, ?Throwable $previous = null): self
    {
        $message = "Execution failed for tool '{$toolName}'";
        if ($previous) {
            $message .= ': ' . $previous->getMessage();
        }

        return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
    }

    public static function resourceReadFailed(string $uri, ?Throwable $previous = null): self
    {
        $message = "Failed to read resource '{$uri}'";
        if ($previous) {
            $message .= ': ' . $previous->getMessage();
        }

        return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
    }

    public static function promptGenerationFailed(string $promptName, ?Throwable $previous = null): self
    {
        $message = "Failed to generate prompt '{$promptName}'";
        if ($previous) {
            $message .= ': ' . $previous->getMessage();
        }

        return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
    }
}

```

--------------------------------------------------------------------------------
/src/Elements/RegisteredTool.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Elements;

use PhpMcp\Schema\Content\Content;
use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Server\Context;
use Psr\Container\ContainerInterface;
use PhpMcp\Schema\Tool;
use Throwable;

class RegisteredTool extends RegisteredElement
{
    public function __construct(
        public readonly Tool $schema,
        callable|array|string $handler,
        bool $isManual = false,
    ) {
        parent::__construct($handler, $isManual);
    }

    public static function make(Tool $schema, callable|array|string $handler, bool $isManual = false): self
    {
        return new self($schema, $handler, $isManual);
    }

    /**
     * Calls the underlying handler for this tool.
     *
     * @return Content[] The content items for CallToolResult.
     */
    public function call(ContainerInterface $container, array $arguments, Context $context): array
    {
        $result = $this->handle($container, $arguments, $context);

        return $this->formatResult($result);
    }

    /**
     * Formats the result of a tool execution into an array of MCP Content items.
     *
     * - If the result is already a Content object, it's wrapped in an array.
     * - If the result is an array:
     *   - If all elements are Content objects, the array is returned as is.
     *   - If it's a mixed array (Content and non-Content items), non-Content items are
     *     individually formatted (scalars to TextContent, others to JSON TextContent).
     *   - If it's an array with no Content items, the entire array is JSON-encoded into a single TextContent.
     * - Scalars (string, int, float, bool) are wrapped in TextContent.
     * - null is represented as TextContent('(null)').
     * - Other objects are JSON-encoded and wrapped in TextContent.
     *
     * @param  mixed  $toolExecutionResult  The raw value returned by the tool's PHP method.
     * @return Content[] The content items for CallToolResult.
     * @throws JsonException if JSON encoding fails for non-Content array/object results.
     */
    protected function formatResult(mixed $toolExecutionResult): array
    {
        if ($toolExecutionResult instanceof Content) {
            return [$toolExecutionResult];
        }

        if (is_array($toolExecutionResult)) {
            if (empty($toolExecutionResult)) {
                return [TextContent::make('[]')];
            }

            $allAreContent = true;
            $hasContent = false;

            foreach ($toolExecutionResult as $item) {
                if ($item instanceof Content) {
                    $hasContent = true;
                } else {
                    $allAreContent = false;
                }
            }

            if ($allAreContent && $hasContent) {
                return $toolExecutionResult;
            }

            if ($hasContent) {
                $result = [];
                foreach ($toolExecutionResult as $item) {
                    if ($item instanceof Content) {
                        $result[] = $item;
                    } else {
                        $result = array_merge($result, $this->formatResult($item));
                    }
                }
                return $result;
            }
        }

        if ($toolExecutionResult === null) {
            return [TextContent::make('(null)')];
        }

        if (is_bool($toolExecutionResult)) {
            return [TextContent::make($toolExecutionResult ? 'true' : 'false')];
        }

        if (is_scalar($toolExecutionResult)) {
            return [TextContent::make($toolExecutionResult)];
        }

        $jsonResult = json_encode(
            $toolExecutionResult,
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE
        );

        return [TextContent::make($jsonResult)];
    }

    public function toArray(): array
    {
        return [
            'schema' => $this->schema->toArray(),
            ...parent::toArray(),
        ];
    }

    public static function fromArray(array $data): self|false
    {
        try {
            if (! isset($data['schema']) || ! isset($data['handler'])) {
                return false;
            }

            return new self(
                Tool::fromArray($data['schema']),
                $data['handler'],
                $data['isManual'] ?? false,
            );
        } catch (Throwable $e) {
            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/src/Utils/DocBlockParser.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Utils;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\DocBlockFactory;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;

/**
 * Parses DocBlocks using phpdocumentor/reflection-docblock.
 */
class DocBlockParser
{
    private DocBlockFactory $docBlockFactory;
    private LoggerInterface $logger;

    public function __construct(?LoggerInterface $logger = null)
    {
        $this->docBlockFactory = DocBlockFactory::createInstance();
        $this->logger = $logger ?? new NullLogger();
    }

    /**
     * Safely parses a DocComment string into a DocBlock object.
     */
    public function parseDocBlock(string|null|false $docComment): ?DocBlock
    {
        if ($docComment === false || $docComment === null || empty($docComment)) {
            return null;
        }
        try {
            return $this->docBlockFactory->create($docComment);
        } catch (Throwable $e) {
            // Log error or handle gracefully if invalid DocBlock syntax is encountered
            $this->logger->warning('Failed to parse DocBlock', [
                'error' => $e->getMessage(),
                'exception_trace' => $e->getTraceAsString(),
            ]);

            return null;
        }
    }

    /**
     * Gets the summary line from a DocBlock.
     */
    public function getSummary(?DocBlock $docBlock): ?string
    {
        if (! $docBlock) {
            return null;
        }
        $summary = trim($docBlock->getSummary());

        return $summary ?: null; // Return null if empty after trimming
    }

    /**
     * Gets the description from a DocBlock (summary + description body).
     */
    public function getDescription(?DocBlock $docBlock): ?string
    {
        if (! $docBlock) {
            return null;
        }
        $summary = trim($docBlock->getSummary());
        $descriptionBody = trim((string) $docBlock->getDescription());

        if ($summary && $descriptionBody) {
            return $summary . "\n\n" . $descriptionBody;
        }
        if ($summary) {
            return $summary;
        }
        if ($descriptionBody) {
            return $descriptionBody;
        }

        return null;
    }

    /**
     * Extracts @param tag information from a DocBlock, keyed by variable name (e.g., '$paramName').
     *
     * @return array<string, Param>
     */
    public function getParamTags(?DocBlock $docBlock): array
    {
        if (! $docBlock) {
            return [];
        }

        /** @var array<string, Param> $paramTags */
        $paramTags = [];
        foreach ($docBlock->getTagsByName('param') as $tag) {
            if ($tag instanceof Param && $tag->getVariableName()) {
                $paramTags['$' . $tag->getVariableName()] = $tag;
            }
        }

        return $paramTags;
    }

    /**
     * Gets the @return tag information from a DocBlock.
     */
    public function getReturnTag(?DocBlock $docBlock): ?Return_
    {
        if (! $docBlock) {
            return null;
        }
        /** @var Return_|null $returnTag */
        $returnTag = $docBlock->getTagsByName('return')[0] ?? null;

        return $returnTag;
    }

    /**
     * Gets the description string from a Param tag.
     */
    public function getParamDescription(?Param $paramTag): ?string
    {
        return $paramTag ? (trim((string) $paramTag->getDescription()) ?: null) : null;
    }

    /**
     * Gets the type string from a Param tag.
     */
    public function getParamTypeString(?Param $paramTag): ?string
    {
        if ($paramTag && $paramTag->getType()) {
            $typeFromTag = trim((string) $paramTag->getType());
            if (! empty($typeFromTag)) {
                return ltrim($typeFromTag, '\\');
            }
        }

        return null;
    }

    /**
     * Gets the description string from a Return_ tag.
     */
    public function getReturnDescription(?Return_ $returnTag): ?string
    {
        return $returnTag ? (trim((string) $returnTag->getDescription()) ?: null) : null;
    }

    /**
     * Gets the type string from a Return_ tag.
     */
    public function getReturnTypeString(?Return_ $returnTag): ?string
    {
        if ($returnTag && $returnTag->getType()) {
            $typeFromTag = trim((string) $returnTag->getType());
            if (! empty($typeFromTag)) {
                return ltrim($typeFromTag, '\\');
            }
        }

        return null;
    }
}

```
Page 1/5FirstPrevNextLast