#
tokens: 49936/50000 84/154 files (page 1/7)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 7. Use http://codebase.md/php-mcp/server?lines=true&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
 1 | <?php
 2 | 
 3 | $finder = PhpCsFixer\Finder::create()
 4 |     ->exclude([
 5 |         'examples',
 6 |         'vendor',
 7 |         'tests/Mocks',
 8 |     ])
 9 |     ->in(__DIR__);
10 | 
11 | return (new PhpCsFixer\Config)
12 |     ->setRules([
13 |         '@PSR12' => true,
14 |         'array_syntax' => ['syntax' => 'short'],
15 |     ])
16 |     ->setFinder($finder);
17 | 
```

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

```
 1 | # Composer dependencies
 2 | /vendor/
 3 | /composer.lock
 4 | 
 5 | # PHPUnit
 6 | .phpunit.result.cache
 7 | 
 8 | # PHP CS Fixer
 9 | /.php-cs-fixer.cache
10 | 
11 | # Editor directories and files
12 | /.idea
13 | /.vscode
14 | *.sublime-project
15 | *.sublime-workspace
16 | 
17 | # Operating system files
18 | .DS_Store
19 | Thumbs.db
20 | 
21 | # Local environment files
22 | /.env
23 | /.env.backup
24 | /.env.local
25 | 
26 | # PHP CodeSniffer
27 | /.phpcs.xml
28 | /.phpcs.xml.dist
29 | /phpcs.xml
30 | /phpcs.xml.dist
31 | 
32 | # PHPStan
33 | /phpstan.neon
34 | /phpstan.neon.dist
35 | 
36 | # Local development tools
37 | /.php_cs
38 | /.php_cs.cache
39 | /.php_cs.dist
40 | /_ide_helper.php
41 | 
42 | # Build artifacts
43 | /build/
44 | /coverage/
45 | 
46 | # PHPUnit coverage reports
47 | /clover.xml
48 | /coverage.xml
49 | /coverage/
50 | 
51 | # Laravel generated files
52 | bootstrap/cache/
53 | .phpunit.result.cache
54 | 
55 | # Local Composer dependencies
56 | composer.phar
57 | 
58 | workbench
59 | playground
60 | 
61 | # Log files
62 | *.log
63 | 
64 | # Cache files
65 | cache
```

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

```markdown
   1 | # PHP MCP Server SDK
   2 | 
   3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/php-mcp/server.svg?style=flat-square)](https://packagist.org/packages/php-mcp/server)
   4 | [![Total Downloads](https://img.shields.io/packagist/dt/php-mcp/server.svg?style=flat-square)](https://packagist.org/packages/php-mcp/server)
   5 | [![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)
   6 | [![License](https://img.shields.io/packagist/l/php-mcp/server.svg?style=flat-square)](LICENSE)
   7 | 
   8 | **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.**
   9 | 
  10 | 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.
  11 | 
  12 | ## 🚀 Key Features
  13 | 
  14 | - **🏗️ Modern Architecture**: Built with PHP 8.1+ features, PSR standards, and modular design
  15 | - **📡 Multiple Transports**: Supports `stdio`, `http+sse`, and new **streamable HTTP** with resumability
  16 | - **🎯 Attribute-Based Definition**: Use PHP 8 Attributes (`#[McpTool]`, `#[McpResource]`, etc.) for zero-config element registration
  17 | - **🔧 Flexible Handlers**: Support for closures, class methods, static methods, and invokable classes
  18 | - **📝 Smart Schema Generation**: Automatic JSON schema generation from method signatures with optional `#[Schema]` attribute enhancements
  19 | - **⚡ Session Management**: Advanced session handling with multiple storage backends
  20 | - **🔄 Event-Driven**: ReactPHP-based for high concurrency and non-blocking operations  
  21 | - **📊 Batch Processing**: Full support for JSON-RPC batch requests
  22 | - **💾 Smart Caching**: Intelligent caching of discovered elements with manual override precedence
  23 | - **🧪 Completion Providers**: Built-in support for argument completion in tools and prompts
  24 | - **🔌 Dependency Injection**: Full PSR-11 container support with auto-wiring
  25 | - **📋 Comprehensive Testing**: Extensive test suite with integration tests for all transports
  26 | 
  27 | This package supports the **2025-03-26** version of the Model Context Protocol with backward compatibility.
  28 | 
  29 | ## 📋 Requirements
  30 | 
  31 | - **PHP** >= 8.1
  32 | - **Composer**
  33 | - **For HTTP Transport**: An event-driven PHP environment (CLI recommended)
  34 | - **Extensions**: `json`, `mbstring`, `pcre` (typically enabled by default)
  35 | 
  36 | ## 📦 Installation
  37 | 
  38 | ```bash
  39 | composer require php-mcp/server
  40 | ```
  41 | 
  42 | > **💡 Laravel Users**: Consider using [`php-mcp/laravel`](https://github.com/php-mcp/laravel) for enhanced framework integration, configuration management, and Artisan commands.
  43 | 
  44 | ## ⚡ Quick Start: Stdio Server with Discovery
  45 | 
  46 | This example demonstrates the most common usage pattern - a `stdio` server using attribute discovery.
  47 | 
  48 | **1. Define Your MCP Elements**
  49 | 
  50 | Create `src/CalculatorElements.php`:
  51 | 
  52 | ```php
  53 | <?php
  54 | 
  55 | namespace App;
  56 | 
  57 | use PhpMcp\Server\Attributes\McpTool;
  58 | use PhpMcp\Server\Attributes\Schema;
  59 | 
  60 | class CalculatorElements
  61 | {
  62 |     /**
  63 |      * Adds two numbers together.
  64 |      * 
  65 |      * @param int $a The first number
  66 |      * @param int $b The second number  
  67 |      * @return int The sum of the two numbers
  68 |      */
  69 |     #[McpTool(name: 'add_numbers')]
  70 |     public function add(int $a, int $b): int
  71 |     {
  72 |         return $a + $b;
  73 |     }
  74 | 
  75 |     /**
  76 |      * Calculates power with validation.
  77 |      */
  78 |     #[McpTool(name: 'calculate_power')]
  79 |     public function power(
  80 |         #[Schema(type: 'number', minimum: 0, maximum: 1000)]
  81 |         float $base,
  82 |         
  83 |         #[Schema(type: 'integer', minimum: 0, maximum: 10)]
  84 |         int $exponent
  85 |     ): float {
  86 |         return pow($base, $exponent);
  87 |     }
  88 | }
  89 | ```
  90 | 
  91 | **2. Create the Server Script**
  92 | 
  93 | Create `mcp-server.php`:
  94 | 
  95 | ```php
  96 | #!/usr/bin/env php
  97 | <?php
  98 | 
  99 | declare(strict_types=1);
 100 | 
 101 | require_once __DIR__ . '/vendor/autoload.php';
 102 | 
 103 | use PhpMcp\Server\Server;
 104 | use PhpMcp\Server\Transports\StdioServerTransport;
 105 | 
 106 | try {
 107 |     // Build server configuration
 108 |     $server = Server::make()
 109 |         ->withServerInfo('PHP Calculator Server', '1.0.0') 
 110 |         ->build();
 111 | 
 112 |     // Discover MCP elements via attributes
 113 |     $server->discover(
 114 |         basePath: __DIR__,
 115 |         scanDirs: ['src']
 116 |     );
 117 | 
 118 |     // Start listening via stdio transport
 119 |     $transport = new StdioServerTransport();
 120 |     $server->listen($transport);
 121 | 
 122 | } catch (\Throwable $e) {
 123 |     fwrite(STDERR, "[CRITICAL ERROR] " . $e->getMessage() . "\n");
 124 |     exit(1);
 125 | }
 126 | ```
 127 | 
 128 | **3. Configure Your MCP Client**
 129 | 
 130 | Add to your client configuration (e.g., `.cursor/mcp.json`):
 131 | 
 132 | ```json
 133 | {
 134 |     "mcpServers": {
 135 |         "php-calculator": {
 136 |             "command": "php",
 137 |             "args": ["/absolute/path/to/your/mcp-server.php"]
 138 |         }
 139 |     }
 140 | }
 141 | ```
 142 | 
 143 | **4. Test the Server**
 144 | 
 145 | Your AI assistant can now call:
 146 | - `add_numbers` - Add two integers
 147 | - `calculate_power` - Calculate power with validation constraints
 148 | 
 149 | ## 🏗️ Architecture Overview
 150 | 
 151 | The PHP MCP Server uses a modern, decoupled architecture:
 152 | 
 153 | ```
 154 | ┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
 155 | │   MCP Client    │◄──►│   Transport      │◄──►│   Protocol      │
 156 | │  (Claude, etc.) │    │ (Stdio/HTTP/SSE) │    │   (JSON-RPC)    │
 157 | └─────────────────┘    └──────────────────┘    └─────────────────┘
 158 |                                                          │
 159 |                        ┌─────────────────┐               │
 160 |                        │ Session Manager │◄──────────────┤
 161 |                        │ (Multi-backend) │               │
 162 |                        └─────────────────┘               │
 163 |                                                          │
 164 | ┌─────────────────┐    ┌──────────────────┐              │
 165 | │   Dispatcher    │◄───│   Server Core    │◄─────────────┤
 166 | │ (Method Router) │    │   Configuration  │              │
 167 | └─────────────────┘    └──────────────────┘              │
 168 |          │                                               │
 169 |          ▼                                               │
 170 | ┌─────────────────┐    ┌──────────────────┐              │
 171 | │    Registry     │    │   Elements       │◄─────────────┘
 172 | │  (Element Store)│◄──►│ (Tools/Resources │
 173 | └─────────────────┘    │  Prompts/etc.)   │
 174 |                        └──────────────────┘
 175 | ```
 176 | 
 177 | ### Core Components
 178 | 
 179 | - **`ServerBuilder`**: Fluent configuration interface (`Server::make()->...->build()`)
 180 | - **`Server`**: Central coordinator containing all configured components
 181 | - **`Protocol`**: JSON-RPC 2.0 handler bridging transports and core logic
 182 | - **`SessionManager`**: Multi-backend session storage (array, cache, custom)
 183 | - **`Dispatcher`**: Method routing and request processing
 184 | - **`Registry`**: Element storage with smart caching and precedence rules
 185 | - **`Elements`**: Registered MCP components (Tools, Resources, Prompts, Templates)
 186 | 
 187 | ### Transport Options
 188 | 
 189 | 1. **`StdioServerTransport`**: Standard I/O for direct client launches
 190 | 2. **`HttpServerTransport`**: HTTP + Server-Sent Events for web integration  
 191 | 3. **`StreamableHttpServerTransport`**: Enhanced HTTP with resumability and event sourcing
 192 | 
 193 | ## ⚙️ Server Configuration
 194 | 
 195 | ### Basic Configuration
 196 | 
 197 | ```php
 198 | use PhpMcp\Server\Server;
 199 | use PhpMcp\Schema\ServerCapabilities;
 200 | 
 201 | $server = Server::make()
 202 |     ->withServerInfo('My App Server', '2.1.0')
 203 |     ->withCapabilities(ServerCapabilities::make(
 204 |         resources: true,
 205 |         resourcesSubscribe: true,
 206 |         prompts: true,
 207 |         tools: true
 208 |     ))
 209 |     ->withPaginationLimit(100)
 210 |     ->build();
 211 | ```
 212 | 
 213 | ### Advanced Configuration with Dependencies
 214 | 
 215 | ```php
 216 | use Psr\Log\Logger;
 217 | use Psr\SimpleCache\CacheInterface;
 218 | use Psr\Container\ContainerInterface;
 219 | 
 220 | $server = Server::make()
 221 |     ->withServerInfo('Production Server', '1.0.0')
 222 |     ->withLogger($myPsrLogger)                    // PSR-3 Logger
 223 |     ->withCache($myPsrCache)                      // PSR-16 Cache  
 224 |     ->withContainer($myPsrContainer)              // PSR-11 Container
 225 |     ->withSession('cache', 7200)                  // Cache-backed sessions, 2hr TTL
 226 |     ->withPaginationLimit(50)                     // Limit list responses
 227 |     ->build();
 228 | ```
 229 | 
 230 | ### Session Management Options
 231 | 
 232 | ```php
 233 | // In-memory sessions (default, not persistent)
 234 | ->withSession('array', 3600)
 235 | 
 236 | // Cache-backed sessions (persistent across restarts)  
 237 | ->withSession('cache', 7200)
 238 | 
 239 | // Custom session handler (implement SessionHandlerInterface)
 240 | ->withSessionHandler(new MyCustomSessionHandler(), 1800)
 241 | ```
 242 | 
 243 | ## 🎯 Defining MCP Elements
 244 | 
 245 | 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.
 246 | 
 247 | ### Element Types
 248 | 
 249 | - **🔧 Tools**: Executable functions/actions (e.g., `calculate`, `send_email`, `query_database`)
 250 | - **📄 Resources**: Static content/data (e.g., `config://settings`, `file://readme.txt`)
 251 | - **📋 Resource Templates**: Dynamic resources with URI patterns (e.g., `user://{id}/profile`)  
 252 | - **💬 Prompts**: Conversation starters/templates (e.g., `summarize`, `translate`)
 253 | 
 254 | ### 1. 🏷️ Attribute-Based Discovery (Recommended)
 255 | 
 256 | Use PHP 8 attributes to mark methods or invokable classes as MCP elements. The server will discover them via filesystem scanning.
 257 | 
 258 | ```php
 259 | use PhpMcp\Server\Attributes\{McpTool, McpResource, McpResourceTemplate, McpPrompt};
 260 | 
 261 | class UserManager
 262 | {
 263 |     /**
 264 |      * Creates a new user account.
 265 |      */
 266 |     #[McpTool(name: 'create_user')]
 267 |     public function createUser(string $email, string $password, string $role = 'user'): array
 268 |     {
 269 |         // Create user logic
 270 |         return ['id' => 123, 'email' => $email, 'role' => $role];
 271 |     }
 272 | 
 273 |     /**
 274 |      * Get user configuration.
 275 |      */
 276 |     #[McpResource(
 277 |         uri: 'config://user/settings',
 278 |         mimeType: 'application/json'
 279 |     )]
 280 |     public function getUserConfig(): array
 281 |     {
 282 |         return ['theme' => 'dark', 'notifications' => true];
 283 |     }
 284 | 
 285 |     /**
 286 |      * Get user profile by ID.
 287 |      */
 288 |     #[McpResourceTemplate(
 289 |         uriTemplate: 'user://{userId}/profile',
 290 |         mimeType: 'application/json'
 291 |     )]
 292 |     public function getUserProfile(string $userId): array
 293 |     {
 294 |         return ['id' => $userId, 'name' => 'John Doe'];
 295 |     }
 296 | 
 297 |     /**
 298 |      * Generate welcome message prompt.
 299 |      */
 300 |     #[McpPrompt(name: 'welcome_user')]
 301 |     public function welcomeUserPrompt(string $username, string $role): array
 302 |     {
 303 |         return [
 304 |             ['role' => 'user', 'content' => "Create a welcome message for {$username} with role {$role}"]
 305 |         ];
 306 |     }
 307 | }
 308 | ```
 309 | 
 310 | **Discovery Process:**
 311 | 
 312 | ```php
 313 | // Build server first
 314 | $server = Server::make()
 315 |     ->withServerInfo('My App Server', '1.0.0')
 316 |     ->build();
 317 | 
 318 | // Then discover elements
 319 | $server->discover(
 320 |     basePath: __DIR__,
 321 |     scanDirs: ['src/Handlers', 'src/Services'],  // Directories to scan
 322 |     excludeDirs: ['src/Tests'],                  // Directories to skip
 323 |     saveToCache: true                            // Cache results (default: true)
 324 | );
 325 | ```
 326 | 
 327 | **Available Attributes:**
 328 | 
 329 | - **`#[McpTool]`**: Executable actions
 330 | - **`#[McpResource]`**: Static content accessible via URI
 331 | - **`#[McpResourceTemplate]`**: Dynamic resources with URI templates  
 332 | - **`#[McpPrompt]`**: Conversation templates and prompt generators
 333 | 
 334 | ### 2. 🔧 Manual Registration 
 335 | 
 336 | Register elements programmatically using the `ServerBuilder` before calling `build()`. Useful for dynamic registration, closures, or when you prefer explicit control.
 337 | 
 338 | ```php
 339 | use App\Handlers\{EmailHandler, ConfigHandler, UserHandler, PromptHandler};
 340 | use PhpMcp\Schema\{ToolAnnotations, Annotations};
 341 | 
 342 | $server = Server::make()
 343 |     ->withServerInfo('Manual Registration Server', '1.0.0')
 344 |     
 345 |     // Register a tool with handler method
 346 |     ->withTool(
 347 |         [EmailHandler::class, 'sendEmail'],     // Handler: [class, method]
 348 |         name: 'send_email',                     // Tool name (optional)
 349 |         description: 'Send email to user',     // Description (optional)
 350 |         annotations: ToolAnnotations::make(     // Annotations (optional)
 351 |             title: 'Send Email Tool'
 352 |         )
 353 |     )
 354 |     
 355 |     // Register invokable class as tool
 356 |     ->withTool(UserHandler::class)             // Handler: Invokable class
 357 |     
 358 |     // Register a closure as tool
 359 |     ->withTool(
 360 |         function(int $a, int $b): int {         // Handler: Closure
 361 |             return $a + $b;
 362 |         },
 363 |         name: 'add_numbers',
 364 |         description: 'Add two numbers together'
 365 |     )
 366 |     
 367 |     // Register a resource with closure
 368 |     ->withResource(
 369 |         function(): array {                     // Handler: Closure
 370 |             return ['timestamp' => time(), 'server' => 'php-mcp'];
 371 |         },
 372 |         uri: 'config://runtime/status',         // URI (required)
 373 |         mimeType: 'application/json'           // MIME type (optional)
 374 |     )
 375 |     
 376 |     // Register a resource template
 377 |     ->withResourceTemplate(
 378 |         [UserHandler::class, 'getUserProfile'],
 379 |         uriTemplate: 'user://{userId}/profile'  // URI template (required)
 380 |     )
 381 |     
 382 |     // Register a prompt with closure
 383 |     ->withPrompt(
 384 |         function(string $topic, string $tone = 'professional'): array {
 385 |             return [
 386 |                 ['role' => 'user', 'content' => "Write about {$topic} in a {$tone} tone"]
 387 |             ];
 388 |         },
 389 |         name: 'writing_prompt'                  // Prompt name (optional)
 390 |     )
 391 |     
 392 |     ->build();
 393 | ```
 394 | 
 395 | 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.
 396 | 
 397 | > [!IMPORTANT]
 398 | > 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:
 399 | > 
 400 | > - Use the [`#[Schema]` attribute](#-schema-generation-and-validation) for enhanced schema generation
 401 | > - Provide a custom `$inputSchema` parameter when registering tools with `->withTool()`
 402 | 
 403 | ### 🏆 Element Precedence & Discovery
 404 | 
 405 | **Precedence Rules:**
 406 | - Manual registrations **always** override discovered/cached elements with the same identifier
 407 | - Discovered elements are cached for performance (configurable)
 408 | - Cache is automatically invalidated on fresh discovery runs
 409 | 
 410 | **Discovery Process:**
 411 | 
 412 | ```php
 413 | $server->discover(
 414 |     basePath: __DIR__,
 415 |     scanDirs: ['src/Handlers', 'src/Services'],  // Scan these directories
 416 |     excludeDirs: ['tests', 'vendor'],            // Skip these directories
 417 |     force: false,                                // Force re-scan (default: false)
 418 |     saveToCache: true                            // Save to cache (default: true)
 419 | );
 420 | ```
 421 | 
 422 | **Caching Behavior:**
 423 | - Only **discovered** elements are cached (never manual registrations)
 424 | - Cache loaded automatically during `build()` if available
 425 | - Fresh `discover()` calls clear and rebuild cache
 426 | - Use `force: true` to bypass discovery-already-ran check
 427 | 
 428 | ## 🚀 Running the Server (Transports)
 429 | 
 430 | The server core is transport-agnostic. Choose a transport based on your deployment needs:
 431 | 
 432 | ### 1. 📟 Stdio Transport
 433 | 
 434 | **Best for**: Direct client execution, command-line tools, simple deployments
 435 | 
 436 | ```php
 437 | use PhpMcp\Server\Transports\StdioServerTransport;
 438 | 
 439 | $server = Server::make()
 440 |     ->withServerInfo('Stdio Server', '1.0.0')
 441 |     ->build();
 442 | 
 443 | $server->discover(__DIR__, ['src']);
 444 | 
 445 | // Create stdio transport (uses STDIN/STDOUT by default)
 446 | $transport = new StdioServerTransport();
 447 | 
 448 | // Start listening (blocking call)
 449 | $server->listen($transport);
 450 | ```
 451 | 
 452 | **Client Configuration:**
 453 | ```json
 454 | {
 455 |     "mcpServers": {
 456 |         "my-php-server": {
 457 |             "command": "php",
 458 |             "args": ["/absolute/path/to/server.php"]
 459 |         }
 460 |     }
 461 | }
 462 | ```
 463 | 
 464 | > ⚠️ **Important**: When using stdio transport, **never** write to `STDOUT` in your handlers (use `STDERR` for debugging). `STDOUT` is reserved for JSON-RPC communication.
 465 | 
 466 | ### 2. 🌐 HTTP + Server-Sent Events Transport (Deprecated)
 467 | 
 468 | > ⚠️ **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.
 469 | 
 470 | **Best for**: Legacy applications requiring backwards compatibility
 471 | 
 472 | ```php
 473 | use PhpMcp\Server\Transports\HttpServerTransport;
 474 | 
 475 | $server = Server::make()
 476 |     ->withServerInfo('HTTP Server', '1.0.0')
 477 |     ->withLogger($logger)  // Recommended for HTTP
 478 |     ->build();
 479 | 
 480 | $server->discover(__DIR__, ['src']);
 481 | 
 482 | // Create HTTP transport
 483 | $transport = new HttpServerTransport(
 484 |     host: '127.0.0.1',      // MCP protocol prohibits 0.0.0.0
 485 |     port: 8080,             // Port number
 486 |     mcpPathPrefix: 'mcp'    // URL prefix (/mcp/sse, /mcp/message)
 487 | );
 488 | 
 489 | $server->listen($transport);
 490 | ```
 491 | 
 492 | **Client Configuration:**
 493 | ```json
 494 | {
 495 |     "mcpServers": {
 496 |         "my-http-server": {
 497 |             "url": "http://localhost:8080/mcp/sse"
 498 |         }
 499 |     }
 500 | }
 501 | ```
 502 | 
 503 | **Endpoints:**
 504 | - **SSE Connection**: `GET /mcp/sse`
 505 | - **Message Sending**: `POST /mcp/message?clientId={clientId}`
 506 | 
 507 | ### 3. 🔄 Streamable HTTP Transport (Recommended)
 508 | 
 509 | **Best for**: Production deployments, remote MCP servers, multiple clients, resumable connections
 510 | 
 511 | ```php
 512 | use PhpMcp\Server\Transports\StreamableHttpServerTransport;
 513 | 
 514 | $server = Server::make()
 515 |     ->withServerInfo('Streamable Server', '1.0.0')
 516 |     ->withLogger($logger)
 517 |     ->withCache($cache)     // Required for resumability
 518 |     ->build();
 519 | 
 520 | $server->discover(__DIR__, ['src']);
 521 | 
 522 | // Create streamable transport with resumability
 523 | $transport = new StreamableHttpServerTransport(
 524 |     host: '127.0.0.1',      // MCP protocol prohibits 0.0.0.0
 525 |     port: 8080,
 526 |     mcpPathPrefix: 'mcp',
 527 |     enableJsonResponse: false,  // Use SSE streaming (default)
 528 |     stateless: false            // Enable stateless mode for session-less clients
 529 | );
 530 | 
 531 | $server->listen($transport);
 532 | ```
 533 | 
 534 | **JSON Response Mode:**
 535 | 
 536 | The `enableJsonResponse` option controls how responses are delivered:
 537 | 
 538 | - **`false` (default)**: Uses Server-Sent Events (SSE) streams for responses. Best for tools that may take time to process.
 539 | - **`true`**: Returns immediate JSON responses without opening SSE streams. Use this when your tools execute quickly and don't need streaming.
 540 | 
 541 | ```php
 542 | // For fast-executing tools, enable JSON mode
 543 | $transport = new StreamableHttpServerTransport(
 544 |     host: '127.0.0.1',
 545 |     port: 8080,
 546 |     enableJsonResponse: true  // Immediate JSON responses
 547 | );
 548 | ```
 549 | 
 550 | **Stateless Mode:**
 551 | 
 552 | For clients that have issues with session management, enable stateless mode:
 553 | 
 554 | ```php
 555 | $transport = new StreamableHttpServerTransport(
 556 |     host: '127.0.0.1',
 557 |     port: 8080,
 558 |     stateless: true  // Each request is independent
 559 | );
 560 | ```
 561 | 
 562 | In stateless mode, session IDs are generated internally but not exposed to clients, and each request is treated as independent without persistent session state.
 563 | 
 564 | **Features:**
 565 | - **Resumable connections** - clients can reconnect and replay missed events
 566 | - **Event sourcing** - all events are stored for replay
 567 | - **JSON mode** - optional JSON-only responses for fast tools
 568 | - **Enhanced session management** - persistent session state
 569 | - **Multiple client support** - designed for concurrent clients
 570 | - **Stateless mode** - session-less operation for simple clients
 571 | 
 572 | ## 📋 Schema Generation and Validation
 573 | 
 574 | 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.
 575 | 
 576 | ### Schema Generation Priority
 577 | 
 578 | The server follows this order of precedence when generating schemas:
 579 | 
 580 | 1. **`#[Schema]` attribute with `definition`** - Complete schema override (highest precedence)
 581 | 2. **Parameter-level `#[Schema]` attribute** - Parameter-specific schema enhancements
 582 | 3. **Method-level `#[Schema]` attribute** - Method-wide schema configuration
 583 | 4. **PHP type hints + docblocks** - Automatic inference from code (lowest precedence)
 584 | 
 585 | When a `definition` is provided in the Schema attribute, all automatic inference is bypassed and the complete definition is used as-is.
 586 | 
 587 | ### Parameter-Level Schema Attributes
 588 | 
 589 | ```php
 590 | use PhpMcp\Server\Attributes\{McpTool, Schema};
 591 | 
 592 | #[McpTool(name: 'validate_user')]
 593 | public function validateUser(
 594 |     #[Schema(format: 'email')]              // PHP already knows it's string
 595 |     string $email,
 596 |     
 597 |     #[Schema(
 598 |         pattern: '^[A-Z][a-z]+$',
 599 |         description: 'Capitalized name'
 600 |     )]
 601 |     string $name,
 602 |     
 603 |     #[Schema(minimum: 18, maximum: 120)]    // PHP already knows it's integer
 604 |     int $age
 605 | ): bool {
 606 |     return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
 607 | }
 608 | ```
 609 | 
 610 | ### Method-Level Schema
 611 | 
 612 | ```php
 613 | /**
 614 |  * Process user data with nested validation.
 615 |  */
 616 | #[McpTool(name: 'create_user')]
 617 | #[Schema(
 618 |     properties: [
 619 |         'profile' => [
 620 |             'type' => 'object',
 621 |             'properties' => [
 622 |                 'name' => ['type' => 'string', 'minLength' => 2],
 623 |                 'age' => ['type' => 'integer', 'minimum' => 18],
 624 |                 'email' => ['type' => 'string', 'format' => 'email']
 625 |             ],
 626 |             'required' => ['name', 'email']
 627 |         ]
 628 |     ],
 629 |     required: ['profile']
 630 | )]
 631 | public function createUser(array $userData): array
 632 | {
 633 |     // PHP type hint provides base 'array' type
 634 |     // Method-level Schema adds object structure validation
 635 |     return ['id' => 123, 'status' => 'created'];
 636 | }
 637 | ```
 638 | 
 639 | ### Complete Schema Override (Method-Level Only)
 640 | 
 641 | ```php
 642 | #[McpTool(name: 'process_api_request')]
 643 | #[Schema(definition: [
 644 |     'type' => 'object',
 645 |     'properties' => [
 646 |         'endpoint' => ['type' => 'string', 'format' => 'uri'],
 647 |         'method' => ['type' => 'string', 'enum' => ['GET', 'POST', 'PUT', 'DELETE']],
 648 |         'headers' => [
 649 |             'type' => 'object',
 650 |             'patternProperties' => [
 651 |                 '^[A-Za-z0-9-]+$' => ['type' => 'string']
 652 |             ]
 653 |         ]
 654 |     ],
 655 |     'required' => ['endpoint', 'method']
 656 | ])]
 657 | public function processApiRequest(string $endpoint, string $method, array $headers): array
 658 | {
 659 |     // PHP type hints are completely ignored when definition is provided
 660 |     // The schema definition above takes full precedence
 661 |     return ['status' => 'processed', 'endpoint' => $endpoint];
 662 | }
 663 | ```
 664 | 
 665 | > ⚠️ **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.
 666 | 
 667 | ## 🎨 Return Value Formatting
 668 | 
 669 | The server automatically formats return values from your handlers into appropriate MCP content types:
 670 | 
 671 | ### Automatic Formatting
 672 | 
 673 | ```php
 674 | // Simple values are auto-wrapped in TextContent
 675 | public function getString(): string { return "Hello World"; }           // → TextContent
 676 | public function getNumber(): int { return 42; }                        // → TextContent  
 677 | public function getBool(): bool { return true; }                       // → TextContent
 678 | public function getArray(): array { return ['key' => 'value']; }       // → TextContent (JSON)
 679 | 
 680 | // Null handling
 681 | public function getNull(): ?string { return null; }                    // → TextContent("(null)")
 682 | public function returnVoid(): void { /* no return */ }                 // → Empty content
 683 | ```
 684 | 
 685 | ### Advanced Content Types
 686 | 
 687 | ```php
 688 | use PhpMcp\Schema\Content\{TextContent, ImageContent, AudioContent, ResourceContent};
 689 | 
 690 | public function getFormattedCode(): TextContent
 691 | {
 692 |     return TextContent::code('<?php echo "Hello";', 'php');
 693 | }
 694 | 
 695 | public function getMarkdown(): TextContent  
 696 | {
 697 |     return TextContent::make('# Title\n\nContent here');
 698 | }
 699 | 
 700 | public function getImage(): ImageContent
 701 | {
 702 |     return ImageContent::make(
 703 |         data: base64_encode(file_get_contents('image.png')),
 704 |         mimeType: 'image/png'
 705 |     );
 706 | }
 707 | 
 708 | public function getAudio(): AudioContent
 709 | {
 710 |     return AudioContent::make(
 711 |         data: base64_encode(file_get_contents('audio.mp3')),
 712 |         mimeType: 'audio/mpeg'
 713 |     );
 714 | }
 715 | ```
 716 | 
 717 | ### File and Stream Handling
 718 | 
 719 | ```php
 720 | // File objects are automatically read and formatted
 721 | public function getFileContent(): \SplFileInfo
 722 | {
 723 |     return new \SplFileInfo('/path/to/file.txt');  // Auto-detects MIME type
 724 | }
 725 | 
 726 | // Stream resources are read completely
 727 | public function getStreamContent()
 728 | {
 729 |     $stream = fopen('/path/to/data.json', 'r');
 730 |     return $stream;  // Will be read and closed automatically
 731 | }
 732 | 
 733 | // Structured resource responses
 734 | public function getStructuredResource(): array
 735 | {
 736 |     return [
 737 |         'text' => 'File content here',
 738 |         'mimeType' => 'text/plain'
 739 |     ];
 740 |     
 741 |     // Or for binary data:
 742 |     // return [
 743 |     //     'blob' => base64_encode($binaryData),
 744 |     //     'mimeType' => 'application/octet-stream'
 745 |     // ];
 746 | }
 747 | ```
 748 | 
 749 | ## 🔄 Batch Processing
 750 | 
 751 | The server automatically handles JSON-RPC batch requests:
 752 | 
 753 | ```php
 754 | // Client can send multiple requests in a single HTTP call:
 755 | [
 756 |     {"jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": {...}},
 757 |     {"jsonrpc": "2.0", "method": "notifications/ping"},              // notification
 758 |     {"jsonrpc": "2.0", "id": "2", "method": "tools/call", "params": {...}}
 759 | ]
 760 | 
 761 | // Server returns batch response (excluding notifications):
 762 | [
 763 |     {"jsonrpc": "2.0", "id": "1", "result": {...}},
 764 |     {"jsonrpc": "2.0", "id": "2", "result": {...}}
 765 | ]
 766 | ```
 767 | 
 768 | ## 🔧 Advanced Features
 769 | 
 770 | ### Completion Providers
 771 | 
 772 | 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.
 773 | 
 774 | > **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.
 775 | 
 776 | The `#[CompletionProvider]` attribute supports three types of completion sources:
 777 | 
 778 | #### 1. Custom Provider Classes
 779 | 
 780 | For complex completion logic, implement the `CompletionProviderInterface`:
 781 | 
 782 | ```php
 783 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 784 | use PhpMcp\Server\Contracts\SessionInterface;
 785 | use PhpMcp\Server\Attributes\{McpResourceTemplate, CompletionProvider};
 786 | 
 787 | class UserIdCompletionProvider implements CompletionProviderInterface
 788 | {
 789 |     public function __construct(private DatabaseService $db) {}
 790 | 
 791 |     public function getCompletions(string $currentValue, SessionInterface $session): array
 792 |     {
 793 |         // Dynamic completion from database
 794 |         return $this->db->searchUsers($currentValue);
 795 |     }
 796 | }
 797 | 
 798 | class UserService
 799 | {
 800 |     #[McpResourceTemplate(uriTemplate: 'user://{userId}/profile')]
 801 |     public function getUserProfile(
 802 |         #[CompletionProvider(provider: UserIdCompletionProvider::class)]  // Class string - resolved from container
 803 |         string $userId
 804 |     ): array {
 805 |         return ['id' => $userId, 'name' => 'John Doe'];
 806 |     }
 807 | }
 808 | ```
 809 | 
 810 | You can also pass pre-configured provider instances:
 811 | 
 812 | ```php
 813 | class DocumentService  
 814 | {
 815 |     #[McpPrompt(name: 'document_prompt')]
 816 |     public function generatePrompt(
 817 |         #[CompletionProvider(provider: new UserIdCompletionProvider($database))]  // Pre-configured instance
 818 |         string $userId,
 819 |         
 820 |         #[CompletionProvider(provider: $this->categoryProvider)]  // Instance from property
 821 |         string $category
 822 |     ): array {
 823 |         return [['role' => 'user', 'content' => "Generate document for user {$userId} in {$category}"]];
 824 |     }
 825 | }
 826 | ```
 827 | 
 828 | #### 2. Simple List Completions
 829 | 
 830 | For static completion lists, use the `values` parameter:
 831 | 
 832 | ```php
 833 | use PhpMcp\Server\Attributes\{McpPrompt, CompletionProvider};
 834 | 
 835 | class ContentService
 836 | {
 837 |     #[McpPrompt(name: 'content_generator')]
 838 |     public function generateContent(
 839 |         #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide', 'documentation'])]
 840 |         string $contentType,
 841 |         
 842 |         #[CompletionProvider(values: ['beginner', 'intermediate', 'advanced', 'expert'])]
 843 |         string $difficulty
 844 |     ): array {
 845 |         return [['role' => 'user', 'content' => "Create a {$difficulty} level {$contentType}"]];
 846 |     }
 847 | }
 848 | ```
 849 | 
 850 | #### 3. Enum-Based Completions
 851 | 
 852 | For enum classes, use the `enum` parameter:
 853 | 
 854 | ```php
 855 | enum Priority: string
 856 | {
 857 |     case LOW = 'low';
 858 |     case MEDIUM = 'medium';
 859 |     case HIGH = 'high';
 860 |     case CRITICAL = 'critical';
 861 | }
 862 | 
 863 | enum Status  // Unit enum (no backing values)
 864 | {
 865 |     case DRAFT;
 866 |     case PUBLISHED;
 867 |     case ARCHIVED;
 868 | }
 869 | 
 870 | class TaskService
 871 | {
 872 |     #[McpTool(name: 'create_task')]
 873 |     public function createTask(
 874 |         string $title,
 875 |         
 876 |         #[CompletionProvider(enum: Priority::class)]  // String-backed enum uses values
 877 |         string $priority,
 878 |         
 879 |         #[CompletionProvider(enum: Status::class)]    // Unit enum uses case names
 880 |         string $status
 881 |     ): array {
 882 |         return ['id' => 123, 'title' => $title, 'priority' => $priority, 'status' => $status];
 883 |     }
 884 | }
 885 | ```
 886 | 
 887 | #### Manual Registration with Completion Providers
 888 | 
 889 | ```php
 890 | $server = Server::make()
 891 |     ->withServerInfo('Completion Demo', '1.0.0')
 892 |     
 893 |     // Using provider class (resolved from container)
 894 |     ->withPrompt(
 895 |         [DocumentHandler::class, 'generateReport'],
 896 |         name: 'document_report'
 897 |         // Completion providers are auto-discovered from method attributes
 898 |     )
 899 |     
 900 |     // Using closure with inline completion providers
 901 |     ->withPrompt(
 902 |         function(
 903 |             #[CompletionProvider(values: ['json', 'xml', 'csv', 'yaml'])]
 904 |             string $format,
 905 |             
 906 |             #[CompletionProvider(enum: Priority::class)]
 907 |             string $priority
 908 |         ): array {
 909 |             return [['role' => 'user', 'content' => "Export data in {$format} format with {$priority} priority"]];
 910 |         },
 911 |         name: 'export_data'
 912 |     )
 913 |     
 914 |     ->build();
 915 | ```
 916 | 
 917 | #### Completion Provider Resolution
 918 | 
 919 | The server automatically handles provider resolution:
 920 | 
 921 | - **Class strings** (`MyProvider::class`) → Resolved from PSR-11 container with dependency injection
 922 | - **Instances** (`new MyProvider()`) → Used directly as-is
 923 | - **Values arrays** (`['a', 'b', 'c']`) → Automatically wrapped in `ListCompletionProvider`
 924 | - **Enum classes** (`MyEnum::class`) → Automatically wrapped in `EnumCompletionProvider`
 925 | 
 926 | > **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.
 927 | 
 928 | ### Custom Dependency Injection
 929 | 
 930 | 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.
 931 | 
 932 | 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.
 933 | 
 934 | ```php
 935 | use Psr\Container\ContainerInterface;
 936 | 
 937 | class DatabaseService
 938 | {
 939 |     public function __construct(private \PDO $pdo) {}
 940 |     
 941 |     #[McpTool(name: 'query_users')]
 942 |     public function queryUsers(): array
 943 |     {
 944 |         $stmt = $this->pdo->query('SELECT * FROM users');
 945 |         return $stmt->fetchAll();
 946 |     }
 947 | }
 948 | 
 949 | // Option 1: Use the basic container and manually add dependencies
 950 | $basicContainer = new \PhpMcp\Server\Defaults\BasicContainer();
 951 | $basicContainer->set(\PDO::class, new \PDO('sqlite::memory:'));
 952 | 
 953 | // Option 2: Use any PSR-11 compatible container (PHP-DI, Laravel, etc.)
 954 | $container = new \DI\Container();
 955 | $container->set(\PDO::class, new \PDO('mysql:host=localhost;dbname=app', $user, $pass));
 956 | 
 957 | $server = Server::make()
 958 |     ->withContainer($basicContainer)  // Handlers get dependencies auto-injected
 959 |     ->build();
 960 | ```
 961 | 
 962 | ### Resource Subscriptions
 963 | 
 964 | ```php
 965 | use PhpMcp\Schema\ServerCapabilities;
 966 | 
 967 | $server = Server::make()
 968 |     ->withCapabilities(ServerCapabilities::make(
 969 |         resourcesSubscribe: true,  // Enable resource subscriptions
 970 |         prompts: true,
 971 |         tools: true
 972 |     ))
 973 |     ->build();
 974 | 
 975 | // In your resource handler, you can notify clients of changes:
 976 | #[McpResource(uri: 'file://config.json')]
 977 | public function getConfig(): array
 978 | {
 979 |     // When config changes, notify subscribers
 980 |     $this->notifyResourceChange('file://config.json');
 981 |     return ['setting' => 'value'];
 982 | }
 983 | ```
 984 | 
 985 | ### Resumability and Event Store
 986 | 
 987 | For production deployments using `StreamableHttpServerTransport`, you can implement resumability with event sourcing by providing a custom event store:
 988 | 
 989 | ```php
 990 | use PhpMcp\Server\Contracts\EventStoreInterface;
 991 | use PhpMcp\Server\Defaults\InMemoryEventStore;
 992 | use PhpMcp\Server\Transports\StreamableHttpServerTransport;
 993 | 
 994 | // Use the built-in in-memory event store (for development/testing)
 995 | $eventStore = new InMemoryEventStore();
 996 | 
 997 | // Or implement your own persistent event store
 998 | class DatabaseEventStore implements EventStoreInterface
 999 | {
1000 |     public function storeEvent(string $streamId, string $message): string
1001 |     {
1002 |         // Store event in database and return unique event ID
1003 |         return $this->database->insert('events', [
1004 |             'stream_id' => $streamId,
1005 |             'message' => $message,
1006 |             'created_at' => now()
1007 |         ]);
1008 |     }
1009 | 
1010 |     public function replayEventsAfter(string $lastEventId, callable $sendCallback): void
1011 |     {
1012 |         // Replay events for resumability
1013 |         $events = $this->database->getEventsAfter($lastEventId);
1014 |         foreach ($events as $event) {
1015 |             $sendCallback($event['id'], $event['message']);
1016 |         }
1017 |     }
1018 | }
1019 | 
1020 | // Configure transport with event store
1021 | $transport = new StreamableHttpServerTransport(
1022 |     host: '127.0.0.1',
1023 |     port: 8080,
1024 |     eventStore: new DatabaseEventStore()  // Enable resumability
1025 | );
1026 | ```
1027 | 
1028 | ### Custom Session Handlers
1029 | 
1030 | Implement custom session storage by creating a class that implements `SessionHandlerInterface`:
1031 | 
1032 | ```php
1033 | use PhpMcp\Server\Contracts\SessionHandlerInterface;
1034 | 
1035 | class DatabaseSessionHandler implements SessionHandlerInterface
1036 | {
1037 |     public function __construct(private \PDO $db) {}
1038 | 
1039 |     public function read(string $id): string|false
1040 |     {
1041 |         $stmt = $this->db->prepare('SELECT data FROM sessions WHERE id = ?');
1042 |         $stmt->execute([$id]);
1043 |         $session = $stmt->fetch(\PDO::FETCH_ASSOC);
1044 |         return $session ? $session['data'] : false;
1045 |     }
1046 | 
1047 |     public function write(string $id, string $data): bool
1048 |     {
1049 |         $stmt = $this->db->prepare(
1050 |             'INSERT OR REPLACE INTO sessions (id, data, updated_at) VALUES (?, ?, ?)'
1051 |         );
1052 |         return $stmt->execute([$id, $data, time()]);
1053 |     }
1054 | 
1055 |     public function destroy(string $id): bool
1056 |     {
1057 |         $stmt = $this->db->prepare('DELETE FROM sessions WHERE id = ?');
1058 |         return $stmt->execute([$id]);
1059 |     }
1060 | 
1061 |     public function gc(int $maxLifetime): array
1062 |     {
1063 |         $cutoff = time() - $maxLifetime;
1064 |         $stmt = $this->db->prepare('DELETE FROM sessions WHERE updated_at < ?');
1065 |         $stmt->execute([$cutoff]);
1066 |         return []; // Return array of cleaned session IDs if needed
1067 |     }
1068 | }
1069 | 
1070 | // Use custom session handler
1071 | $server = Server::make()
1072 |     ->withSessionHandler(new DatabaseSessionHandler(), 3600)
1073 |     ->build();
1074 | ```
1075 | 
1076 | ### Middleware Support
1077 | 
1078 | 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.
1079 | 
1080 | Middleware must be a valid PHP callable that accepts a PSR-7 `ServerRequestInterface` as the first argument and a `callable` as the second argument.
1081 | 
1082 | ```php
1083 | use Psr\Http\Message\ServerRequestInterface;
1084 | use Psr\Http\Message\ResponseInterface;
1085 | use React\Promise\PromiseInterface;
1086 | 
1087 | class AuthMiddleware
1088 | {
1089 |     public function __invoke(ServerRequestInterface $request, callable $next)
1090 |     {
1091 |         $apiKey = $request->getHeaderLine('Authorization');
1092 |         if (empty($apiKey)) {
1093 |             return new Response(401, [], 'Authorization required');
1094 |         }
1095 |         
1096 |         $request = $request->withAttribute('user_id', $this->validateApiKey($apiKey));
1097 |         $result = $next($request);
1098 |         
1099 |         return match (true) {
1100 |             $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
1101 |             $result instanceof ResponseInterface => $this->handle($result),
1102 |             default => $result
1103 |         };
1104 |     }
1105 |     
1106 |     private function handle($response)
1107 |     {
1108 |         return $response instanceof ResponseInterface
1109 |             ? $response->withHeader('X-Auth-Provider', 'mcp-server')
1110 |             : $response;
1111 |     }
1112 | }
1113 | 
1114 | $middlewares = [
1115 |     new AuthMiddleware(),
1116 |     new LoggingMiddleware(),
1117 |     function(ServerRequestInterface $request, callable $next) {
1118 |         $result = $next($request);
1119 |         return match (true) {
1120 |             $result instanceof PromiseInterface => $result->then(function($response) {
1121 |                 return $response instanceof ResponseInterface 
1122 |                     ? $response->withHeader('Access-Control-Allow-Origin', '*')
1123 |                     : $response;
1124 |             }),
1125 |             $result instanceof ResponseInterface => $result->withHeader('Access-Control-Allow-Origin', '*'),
1126 |             default => $result
1127 |         };
1128 |     }
1129 | ];
1130 | 
1131 | $transport = new StreamableHttpServerTransport(
1132 |     host: '127.0.0.1',
1133 |     port: 8080,
1134 |     middlewares: $middlewares
1135 | );
1136 | ```
1137 | 
1138 | **Important Considerations:**
1139 | 
1140 | - **Response Handling**: Middleware must handle both synchronous `ResponseInterface` and asynchronous `PromiseInterface` returns from `$next($request)`, since ReactPHP operates asynchronously
1141 | - **Invokable Pattern**: The recommended pattern is to use invokable classes with a separate `handle()` method to process responses, making the async logic reusable
1142 | - **Execution Order**: Middleware executes in the order provided, with the last middleware being closest to your MCP handlers
1143 | 
1144 | ### SSL Context Configuration
1145 | 
1146 | For HTTPS deployments of `StreamableHttpServerTransport`, configure SSL context options:
1147 | 
1148 | ```php
1149 | $sslContext = [
1150 |     'ssl' => [
1151 |         'local_cert' => '/path/to/certificate.pem',
1152 |         'local_pk' => '/path/to/private-key.pem',
1153 |         'verify_peer' => false,
1154 |         'allow_self_signed' => true,
1155 |     ]
1156 | ];
1157 | 
1158 | $transport = new StreamableHttpServerTransport(
1159 |     host: '0.0.0.0',
1160 |     port: 8443,
1161 |     sslContext: $sslContext
1162 | );
1163 | ```
1164 | 
1165 | > **SSL Context Reference**: For complete SSL context options, see the [PHP SSL Context Options documentation](https://www.php.net/manual/en/context.ssl.php).
1166 | ## 🔍 Error Handling & Debugging
1167 | 
1168 | The server provides comprehensive error handling and debugging capabilities:
1169 | 
1170 | ### Exception Handling
1171 | 
1172 | 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.
1173 | 
1174 | ```php
1175 | #[McpTool(name: 'divide_numbers')]
1176 | public function divideNumbers(float $dividend, float $divisor): float
1177 | {
1178 |     if ($divisor === 0.0) {
1179 |         // Any exception with descriptive message will be sent to client
1180 |         throw new \InvalidArgumentException('Division by zero is not allowed');
1181 |     }
1182 |     
1183 |     return $dividend / $divisor;
1184 | }
1185 | 
1186 | #[McpTool(name: 'calculate_factorial')]
1187 | public function calculateFactorial(int $number): int
1188 | {
1189 |     if ($number < 0) {
1190 |         throw new \InvalidArgumentException('Factorial is not defined for negative numbers');
1191 |     }
1192 |     
1193 |     if ($number > 20) {
1194 |         throw new \OverflowException('Number too large, factorial would cause overflow');
1195 |     }
1196 |     
1197 |     // Implementation continues...
1198 |     return $this->factorial($number);
1199 | }
1200 | ```
1201 | 
1202 | The server will convert these exceptions into appropriate JSON-RPC error responses that MCP clients can understand and display to users.
1203 | 
1204 | ### Logging and Debugging
1205 | 
1206 | ```php
1207 | use Psr\Log\LoggerInterface;
1208 | 
1209 | class DebugAwareHandler
1210 | {
1211 |     public function __construct(private LoggerInterface $logger) {}
1212 |     
1213 |     #[McpTool(name: 'debug_tool')]
1214 |     public function debugTool(string $data): array
1215 |     {
1216 |         $this->logger->info('Processing debug tool', ['input' => $data]);
1217 |         
1218 |         // For stdio transport, use STDERR for debug output
1219 |         fwrite(STDERR, "Debug: Processing data length: " . strlen($data) . "\n");
1220 |         
1221 |         return ['processed' => true];
1222 |     }
1223 | }
1224 | ```
1225 | 
1226 | ## 🚀 Production Deployment
1227 | 
1228 | 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.
1229 | 
1230 | Here are two popular deployment approaches to consider:
1231 | 
1232 | ### Option 1: VPS with Supervisor + Nginx (Recommended)
1233 | 
1234 | **Best for**: Most production deployments, cost-effective, full control
1235 | 
1236 | ```bash
1237 | # 1. Install your application on VPS
1238 | git clone https://github.com/yourorg/your-mcp-server.git /var/www/mcp-server
1239 | cd /var/www/mcp-server
1240 | composer install --no-dev --optimize-autoloader
1241 | 
1242 | # 2. Install Supervisor
1243 | sudo apt-get install supervisor
1244 | 
1245 | # 3. Create Supervisor configuration
1246 | sudo nano /etc/supervisor/conf.d/mcp-server.conf
1247 | ```
1248 | 
1249 | **Supervisor Configuration:**
1250 | ```ini
1251 | [program:mcp-server]
1252 | process_name=%(program_name)s_%(process_num)02d
1253 | command=php /var/www/mcp-server/server.php --transport=http --host=127.0.0.1 --port=8080
1254 | autostart=true
1255 | autorestart=true
1256 | stopasgroup=true
1257 | killasgroup=true
1258 | user=www-data
1259 | numprocs=1
1260 | redirect_stderr=true
1261 | stdout_logfile=/var/log/mcp-server.log
1262 | stdout_logfile_maxbytes=10MB
1263 | stdout_logfile_backups=3
1264 | ```
1265 | 
1266 | **Nginx Configuration with SSL:**
1267 | ```nginx
1268 | # /etc/nginx/sites-available/mcp-server
1269 | server {
1270 |     listen 443 ssl http2;
1271 |     listen [::]:443 ssl http2;
1272 |     server_name mcp.yourdomain.com;
1273 | 
1274 |     # SSL configuration
1275 |     ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
1276 |     ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
1277 |     
1278 |     # Security headers
1279 |     add_header X-Frame-Options "SAMEORIGIN" always;
1280 |     add_header X-Content-Type-Options "nosniff" always;
1281 |     add_header X-XSS-Protection "1; mode=block" always;
1282 | 
1283 |     # MCP Server proxy
1284 |     location / {
1285 |         proxy_http_version 1.1;
1286 |         proxy_set_header Host $http_host;
1287 |         proxy_set_header X-Real-IP $remote_addr;
1288 |         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1289 |         proxy_set_header X-Forwarded-Proto $scheme;
1290 |         proxy_set_header Upgrade $http_upgrade;
1291 |         proxy_set_header Connection "upgrade";
1292 |         
1293 |         # Important for SSE connections
1294 |         proxy_buffering off;
1295 |         proxy_cache off;
1296 |         
1297 |         proxy_pass http://127.0.0.1:8080/;
1298 |     }
1299 | }
1300 | 
1301 | # Redirect HTTP to HTTPS
1302 | server {
1303 |     listen 80;
1304 |     listen [::]:80;
1305 |     server_name mcp.yourdomain.com;
1306 |     return 301 https://$server_name$request_uri;
1307 | }
1308 | ```
1309 | 
1310 | **Start Services:**
1311 | ```bash
1312 | # Enable and start supervisor
1313 | sudo supervisorctl reread
1314 | sudo supervisorctl update
1315 | sudo supervisorctl start mcp-server:*
1316 | 
1317 | # Enable and start nginx
1318 | sudo systemctl enable nginx
1319 | sudo systemctl restart nginx
1320 | 
1321 | # Check status
1322 | sudo supervisorctl status
1323 | ```
1324 | 
1325 | **Client Configuration:**
1326 | ```json
1327 | {
1328 |   "mcpServers": {
1329 |     "my-server": {
1330 |       "url": "https://mcp.yourdomain.com/mcp"
1331 |     }
1332 |   }
1333 | }
1334 | ```
1335 | 
1336 | ### Option 2: Docker Deployment
1337 | 
1338 | **Best for**: Containerized environments, Kubernetes, cloud platforms
1339 | 
1340 | **Production Dockerfile:**
1341 | ```dockerfile
1342 | FROM php:8.3-fpm-alpine
1343 | 
1344 | # Install system dependencies
1345 | RUN apk --no-cache add \
1346 |     nginx \
1347 |     supervisor \
1348 |     && docker-php-ext-enable opcache
1349 | 
1350 | # Install PHP extensions for MCP
1351 | RUN docker-php-ext-install pdo_mysql pdo_sqlite opcache
1352 | 
1353 | # Create application directory
1354 | WORKDIR /var/www/mcp
1355 | 
1356 | # Copy application code
1357 | COPY . /var/www/mcp
1358 | COPY docker/nginx.conf /etc/nginx/nginx.conf
1359 | COPY docker/supervisord.conf /etc/supervisord.conf
1360 | COPY docker/php.ini /usr/local/etc/php/conf.d/production.ini
1361 | 
1362 | # Install Composer dependencies
1363 | RUN composer install --no-dev --optimize-autoloader --no-interaction
1364 | 
1365 | # Set permissions
1366 | RUN chown -R www-data:www-data /var/www/mcp
1367 | 
1368 | # Expose port
1369 | EXPOSE 80
1370 | 
1371 | # Start supervisor
1372 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
1373 | ```
1374 | 
1375 | **docker-compose.yml:**
1376 | ```yaml
1377 | services:
1378 |   mcp-server:
1379 |     build: .
1380 |     ports:
1381 |       - "8080:80"
1382 |     environment:
1383 |       - MCP_ENV=production
1384 |       - MCP_LOG_LEVEL=info
1385 |     volumes:
1386 |       - ./storage:/var/www/mcp/storage
1387 |     restart: unless-stopped
1388 |     healthcheck:
1389 |       test: ["CMD", "curl", "-f", "http://localhost/health"]
1390 |       interval: 30s
1391 |       timeout: 10s
1392 |       retries: 3
1393 | 
1394 |   # Optional: Add database if needed
1395 |   database:
1396 |     image: mysql:8.0
1397 |     environment:
1398 |       MYSQL_ROOT_PASSWORD: secure_password
1399 |       MYSQL_DATABASE: mcp_server
1400 |     volumes:
1401 |       - mysql_data:/var/lib/mysql
1402 |     restart: unless-stopped
1403 | 
1404 | volumes:
1405 |   mysql_data:
1406 | ```
1407 | 
1408 | ### Security Best Practices
1409 | 
1410 | 1. **Firewall Configuration:**
1411 | ```bash
1412 | # Only allow necessary ports
1413 | sudo ufw allow ssh
1414 | sudo ufw allow 80
1415 | sudo ufw allow 443
1416 | sudo ufw deny 8080  # MCP port should not be publicly accessible
1417 | sudo ufw enable
1418 | ```
1419 | 
1420 | 2. **SSL/TLS Setup:**
1421 | ```bash
1422 | # Install Certbot for Let's Encrypt
1423 | sudo apt install certbot python3-certbot-nginx
1424 | 
1425 | # Generate SSL certificate
1426 | sudo certbot --nginx -d mcp.yourdomain.com
1427 | ```
1428 | 
1429 | ## 📚 Examples & Use Cases
1430 | 
1431 | Explore comprehensive examples in the [`examples/`](./examples/) directory:
1432 | 
1433 | ### Available Examples
1434 | 
1435 | - **`01-discovery-stdio-calculator/`** - Basic stdio calculator with attribute discovery
1436 | - **`02-discovery-http-userprofile/`** - HTTP server with user profile management  
1437 | - **`03-manual-registration-stdio/`** - Manual element registration patterns
1438 | - **`04-combined-registration-http/`** - Combining manual and discovered elements
1439 | - **`05-stdio-env-variables/`** - Environment variable handling
1440 | - **`06-custom-dependencies-stdio/`** - Dependency injection with task management
1441 | - **`07-complex-tool-schema-http/`** - Advanced schema validation examples
1442 | - **`08-schema-showcase-streamable/`** - Comprehensive schema feature showcase
1443 | 
1444 | ### Running Examples
1445 | 
1446 | ```bash
1447 | # Navigate to an example directory
1448 | cd examples/01-discovery-stdio-calculator/
1449 | 
1450 | # Make the server executable
1451 | chmod +x server.php
1452 | 
1453 | # Run the server (or configure it in your MCP client)
1454 | ./server.php
1455 | ```
1456 | 
1457 | ## 🚧 Migration from v2.x
1458 | 
1459 | If migrating from version 2.x, note these key changes:
1460 | 
1461 | ### Schema Updates
1462 | - Uses `php-mcp/schema` package for DTOs instead of internal classes
1463 | - Content types moved to `PhpMcp\Schema\Content\*` namespace
1464 | - Updated method signatures for better type safety
1465 | 
1466 | ### Session Management
1467 | - New session management with multiple backends
1468 | - Use `->withSession()` or `->withSessionHandler()` for configuration
1469 | - Sessions are now persistent across reconnections (with cache backend)
1470 | 
1471 | ### Transport Changes
1472 | - New `StreamableHttpServerTransport` with resumability
1473 | - Enhanced error handling and event sourcing
1474 | - Better batch request processing
1475 | 
1476 | ## 🧪 Testing
1477 | 
1478 | ```bash
1479 | # Install development dependencies
1480 | composer install --dev
1481 | 
1482 | # Run the test suite
1483 | composer test
1484 | 
1485 | # Run tests with coverage (requires Xdebug)
1486 | composer test:coverage
1487 | 
1488 | # Run code style checks
1489 | composer lint
1490 | ```
1491 | 
1492 | ## 🤝 Contributing
1493 | 
1494 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
1495 | 
1496 | ## 📄 License
1497 | 
1498 | The MIT License (MIT). See [LICENSE](LICENSE) for details.
1499 | 
1500 | ## 🙏 Acknowledgments
1501 | 
1502 | - Built on the [Model Context Protocol](https://modelcontextprotocol.io/) specification
1503 | - Powered by [ReactPHP](https://reactphp.org/) for async operations
1504 | - Uses [PSR standards](https://www.php-fig.org/) for maximum interoperability
1505 | 
```

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

```markdown
 1 | # Contributing to php-mcp/server
 2 | 
 3 | 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.
 4 | 
 5 | 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.
 6 | 
 7 | ## How Can I Contribute?
 8 | 
 9 | There are several ways you can contribute:
10 | 
11 | *   **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.
12 | *   **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.
13 | *   **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.
14 | *   **Writing Code:** Submit pull requests to fix bugs or add new features.
15 | 
16 | ## Development Setup
17 | 
18 | 1.  **Fork the repository:** Click the "Fork" button on the [php-mcp/server GitHub page](https://github.com/php-mcp/server).
19 | 2.  **Clone your fork:** `git clone [email protected]:YOUR_USERNAME/server.git`
20 | 3.  **Navigate into the directory:** `cd server`
21 | 4.  **Install dependencies:** `composer install` (This installs runtime and development dependencies).
22 | 
23 | ## Submitting Changes (Pull Requests)
24 | 
25 | 1.  **Create a new branch:** `git checkout -b feature/your-feature-name` or `git checkout -b fix/issue-number`.
26 | 2.  **Make your changes:** Write your code and accompanying tests.
27 | 3.  **Ensure Code Style:** Run the code style fixer (if configured, e.g., PHP CS Fixer):
28 |     ```bash
29 |     composer lint # Or ./vendor/bin/php-cs-fixer fix
30 |     ```
31 |     Adhere to PSR-12 coding standards.
32 | 4.  **Run Tests:** Ensure all tests pass:
33 |     ```bash
34 |     composer test # Or ./vendor/bin/pest
35 |     ```
36 |     Consider adding new tests for your changes. Aim for good test coverage.
37 | 5.  **Update Documentation:** If your changes affect the public API or usage, update the `README.md` and relevant PHPDoc blocks.
38 | 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"`
39 | 7.  **Push to your fork:** `git push origin feature/your-feature-name`
40 | 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).
41 | 9.  **Describe your changes:** Provide a clear description of the problem and solution in the pull request. Link to any relevant issues (`Closes #123`).
42 | 
43 | ## Coding Standards
44 | 
45 | *   Follow **PSR-12** coding standards.
46 | *   Use **strict types:** `declare(strict_types=1);` at the top of PHP files.
47 | *   Use **PHP 8.1+ features** where appropriate (readonly properties, enums, etc.).
48 | *   Add **PHPDoc blocks** for all public classes, methods, and properties.
49 | *   Write clear and concise code. Add comments only where necessary to explain complex logic.
50 | 
51 | ## Reporting Issues
52 | 
53 | *   Use the GitHub issue tracker.
54 | *   Check if the issue already exists.
55 | *   Provide a clear title and description.
56 | *   Include steps to reproduce the issue, code examples, error messages, and stack traces if applicable.
57 | *   Specify relevant environment details (PHP version, OS, package version).
58 | 
59 | Thank you for contributing!
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\Enums;
 4 | 
 5 | enum UnitEnum
 6 | {
 7 |     case Yes;
 8 |     case No;
 9 | }
10 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\Enums;
 4 | 
 5 | enum BackedIntEnum: int
 6 | {
 7 |     case First = 1;
 8 |     case Second = 2;
 9 | }
10 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\Enums;
 4 | 
 5 | enum BackedStringEnum: string
 6 | {
 7 |     case OptionA = 'A';
 8 |     case OptionB = 'B';
 9 | }
10 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests;
 4 | 
 5 | use PHPUnit\Framework\TestCase as BaseTestCase;
 6 | 
 7 | abstract class TestCase extends BaseTestCase
 8 | {
 9 |     //
10 | }
11 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Enums;
 6 | 
 7 | enum PriorityEnum: int
 8 | {
 9 |     case LOW = 1;
10 |     case MEDIUM = 2;
11 |     case HIGH = 3;
12 | }
13 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Enums;
 6 | 
 7 | enum StatusEnum: string
 8 | {
 9 |     case DRAFT = 'draft';
10 |     case PUBLISHED = 'published';
11 |     case ARCHIVED = 'archived';
12 | }
13 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery\SubDir;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpTool;
 8 | 
 9 | class HiddenTool
10 | {
11 |     #[McpTool(name: 'hidden_subdir_tool')]
12 |     public function run()
13 |     {
14 |     }
15 | }
16 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Exception;
 6 | 
 7 | /**
 8 |  * Exception related to errors during the attribute discovery process.
 9 |  */
10 | class DiscoveryException extends McpServerException
11 | {
12 |     // No specific JSON-RPC code, internal server issue.
13 | }
14 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Defaults;
 6 | 
 7 | use DateTimeImmutable;
 8 | use Psr\Clock\ClockInterface;
 9 | 
10 | class SystemClock implements ClockInterface
11 | {
12 |     public function now(): \DateTimeImmutable
13 |     {
14 |         return new \DateTimeImmutable();
15 |     }
16 | }
17 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\ComplexSchemaHttpExample\Model;
 4 | 
 5 | enum EventType: string
 6 | {
 7 |     case Meeting = 'meeting';
 8 |     case Reminder = 'reminder';
 9 |     case Call = 'call';
10 |     case Other = 'other';
11 | }
12 | 
13 | enum EventPriority: int
14 | {
15 |     case Low = 0;
16 |     case Normal = 1;
17 |     case High = 2;
18 | }
19 | 
```

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

```php
 1 | <?php declare(strict_types = 1);
 2 | namespace PhpMcp\Server;
 3 | 
 4 | use PhpMcp\Server\Contracts\SessionInterface;
 5 | use Psr\Http\Message\ServerRequestInterface;
 6 | 
 7 | final class Context
 8 | {
 9 |     public function __construct(
10 |         public readonly SessionInterface $session,
11 |         public readonly ?ServerRequestInterface $request = null,
12 |     )
13 |     {
14 |     }
15 | }
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Exception;
 6 | 
 7 | /**
 8 |  * Exception related to invalid server configuration.
 9 |  *
10 |  * Typically thrown during ServerBuilder::build().
11 |  */
12 | class ConfigurationException extends McpServerException
13 | {
14 |     // No specific JSON-RPC code, usually an internal setup issue.
15 |     // Code 0 is appropriate.
16 | }
17 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpResource;
 8 | 
 9 | #[McpResource(uri: "invokable://config/status", name: "invokable_app_status")]
10 | class InvocableResourceFixture
11 | {
12 |     public function __invoke(): array
13 |     {
14 |         return ["status" => "OK", "load" => rand(1, 100) / 100.0];
15 |     }
16 | }
17 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | use Psr\Log\LoggerInterface;
 8 | 
 9 | /**
10 |  * Interface for components that can accept a PSR-3 Logger instance.
11 |  *
12 |  * Primarily used for injecting the configured logger into transport implementations.
13 |  */
14 | interface LoggerAwareInterface
15 | {
16 |     public function setLogger(LoggerInterface $logger): void;
17 | }
18 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | use React\EventLoop\LoopInterface;
 8 | 
 9 | /**
10 |  * Interface for components that require a ReactPHP event loop instance.
11 |  *
12 |  * Primarily used for injecting the configured loop into transport implementations.
13 |  */
14 | interface LoopAwareInterface
15 | {
16 |     public function setLoop(LoopInterface $loop): void;
17 | }
18 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | class NonDiscoverableClass
 8 | {
 9 |     public function someMethod(): string
10 |     {
11 |         return "Just a regular method.";
12 |     }
13 | }
14 | 
15 | interface MyDiscoverableInterface
16 | {
17 | }
18 | 
19 | trait MyDiscoverableTrait
20 | {
21 |     public function traitMethod()
22 |     {
23 |     }
24 | }
25 | 
26 | enum MyDiscoverableEnum
27 | {
28 |     case Alpha;
29 | }
30 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | 
 9 | class RequestAttributeMiddleware
10 | {
11 |     public function __invoke(ServerRequestInterface $request, callable $next)
12 |     {
13 |         $request = $request->withAttribute('middleware-attr', 'middleware-value');
14 |         return $next($request);
15 |     }
16 | }
17 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Defaults;
 6 | 
 7 | use PhpMcp\Server\Contracts\SessionIdGeneratorInterface;
 8 | 
 9 | class DefaultUuidSessionIdGenerator implements SessionIdGeneratorInterface
10 | {
11 |     public function generateId(): string
12 |     {
13 |         return bin2hex(random_bytes(16));
14 |     }
15 | 
16 |     public function onSessionInitialized(string $sessionId): void
17 |     {
18 |         // no-op
19 |     }
20 | }
21 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | 
 9 | class ErrorMiddleware
10 | {
11 |     public function __invoke(ServerRequestInterface $request, callable $next)
12 |     {
13 |         if (str_contains($request->getUri()->getPath(), '/error-middleware')) {
14 |             throw new \Exception('Middleware error');
15 |         }
16 |         return $next($request);
17 |     }
18 | }
19 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpPrompt;
 8 | 
 9 | #[McpPrompt(name: "InvokableGreeterPrompt")]
10 | class InvocablePromptFixture
11 | {
12 |     /**
13 |      * @param string $personName
14 |      * @return array
15 |      */
16 |     public function __invoke(string $personName): array
17 |     {
18 |         return [['role' => 'user', 'content' => "Generate a short greeting for {$personName}."]];
19 |     }
20 | }
21 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\General;
 4 | 
 5 | class InvokableHandlerFixture
 6 | {
 7 |     public string $type;
 8 |     public array $argsReceived;
 9 | 
10 |     public function __construct(string $type = "default")
11 |     {
12 |         $this->type = $type;
13 |     }
14 | 
15 |     public function __invoke(string $arg1, int $arg2 = 0): array
16 |     {
17 |         $this->argsReceived = func_get_args();
18 |         return ['invoked' => $this->type, 'arg1' => $arg1, 'arg2' => $arg2];
19 |     }
20 | }
21 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpResourceTemplate;
 8 | 
 9 | #[McpResourceTemplate(uriTemplate: "invokable://user-profile/{userId}")]
10 | class InvocableResourceTemplateFixture
11 | {
12 |     /**
13 |      * @param string $userId
14 |      * @return array
15 |      */
16 |     public function __invoke(string $userId): array
17 |     {
18 |         return ["id" => $userId, "email" => "user{$userId}@example-invokable.com"];
19 |     }
20 | }
21 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | interface CompletionProviderInterface
 8 | {
 9 |     /**
10 |      * Get completions for a given current value.
11 |      *
12 |      * @param  string  $currentValue  The current value to get completions for.
13 |      * @param  SessionInterface  $session  The session to get completions for.
14 |      * @return array  The completions.
15 |      */
16 |     public function getCompletions(string $currentValue, SessionInterface $session): array;
17 | }
18 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | use React\Http\Message\Response;
 9 | 
10 | class ShortCircuitMiddleware
11 | {
12 |     public function __invoke(ServerRequestInterface $request, callable $next)
13 |     {
14 |         if (str_contains($request->getUri()->getPath(), '/short-circuit')) {
15 |             return new Response(418, [], 'Short-circuited by middleware');
16 |         }
17 |         return $next($request);
18 |     }
19 | }
20 | 
```

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

```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3 |          xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
 4 |          bootstrap="vendor/autoload.php"
 5 |          colors="true"
 6 | >
 7 |     <testsuites>
 8 |         <testsuite name="Test Suite">
 9 |             <directory suffix="Test.php">./tests</directory>
10 |         </testsuite>
11 |     </testsuites>
12 |     <source>
13 |         <include>
14 |             <directory>src</directory>
15 |         </include>
16 |     </source>
17 | </phpunit>
18 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\General;
 6 | 
 7 | use PhpMcp\Schema\Content\TextContent;
 8 | use PhpMcp\Server\Context;
 9 | 
10 | class RequestAttributeChecker
11 | {
12 |     public function checkAttribute(Context $context): TextContent
13 |     {
14 |         $attribute = $context->request->getAttribute('middleware-attr');
15 |         if ($attribute === 'middleware-value') {
16 |             return TextContent::make('middleware-value-found: ' . $attribute);
17 |         }
18 | 
19 |         return TextContent::make('middleware-value-not-found: ' . $attribute);
20 |     }
21 | }
22 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace Mcp\HttpUserProfileExample;
 6 | 
 7 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 8 | use PhpMcp\Server\Contracts\SessionInterface;
 9 | 
10 | class UserIdCompletionProvider implements CompletionProviderInterface
11 | {
12 |     public function getCompletions(string $currentValue, SessionInterface $session): array
13 |     {
14 |         $availableUserIds = ['101', '102', '103'];
15 |         $filteredUserIds = array_filter($availableUserIds, fn(string $userId) => str_contains($userId, $currentValue));
16 | 
17 |         return $filteredUserIds;
18 |     }
19 | }
20 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpTool;
 8 | use PhpMcp\Server\Attributes\McpResource;
 9 | use PhpMcp\Server\Attributes\McpPrompt;
10 | use PhpMcp\Server\Attributes\McpResourceTemplate;
11 | 
12 | #[McpTool(name: "InvokableCalculator", description: "An invokable calculator tool.")]
13 | class InvocableToolFixture
14 | {
15 |     /**
16 |      * Adds two numbers.
17 |      * @param int $a
18 |      * @param int $b
19 |      * @return int
20 |      */
21 |     public function __invoke(int $a, int $b): int
22 |     {
23 |         return $a + $b;
24 |     }
25 | }
26 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Exception;
 6 | 
 7 | use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;
 8 | 
 9 | /**
10 |  * Exception related to errors in the underlying transport layer
11 |  * (e.g., socket errors, process management issues, SSE stream errors).
12 |  */
13 | class TransportException extends McpServerException
14 | {
15 |     public function toJsonRpcError(string|int $id): JsonRpcError
16 |     {
17 |         return new JsonRpcError(
18 |             jsonrpc: '2.0',
19 |             id: $id,
20 |             code: JsonRpcError::CODE_INTERNAL_ERROR,
21 |             message: 'Transport layer error: ' . $this->getMessage(),
22 |             data: null
23 |         );
24 |     }
25 | }
26 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Defaults;
 6 | 
 7 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 8 | use PhpMcp\Server\Contracts\SessionInterface;
 9 | 
10 | class ListCompletionProvider implements CompletionProviderInterface
11 | {
12 |     public function __construct(private array $values) {}
13 | 
14 |     public function getCompletions(string $currentValue, SessionInterface $session): array
15 |     {
16 |         if (empty($currentValue)) {
17 |             return $this->values;
18 |         }
19 | 
20 |         return array_values(array_filter(
21 |             $this->values,
22 |             fn(string $value) => str_starts_with($value, $currentValue)
23 |         ));
24 |     }
25 | }
26 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Attributes;
 4 | 
 5 | use Attribute;
 6 | 
 7 | /**
 8 |  * Marks a PHP method as an MCP Prompt generator.
 9 |  * The method should return the prompt messages, potentially using arguments for templating.
10 |  */
11 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
12 | final class McpPrompt
13 | {
14 |     /**
15 |      * @param  ?string  $name  Overrides the prompt name (defaults to method name).
16 |      * @param  ?string  $description  Optional description of the prompt. Defaults to method DocBlock summary.
17 |      */
18 |     public function __construct(
19 |         public ?string $name = null,
20 |         public ?string $description = null,
21 |     ) {
22 |     }
23 | }
24 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Attributes;
 4 | 
 5 | use Attribute;
 6 | use PhpMcp\Schema\ToolAnnotations;
 7 | 
 8 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
 9 | class McpTool
10 | {
11 |     /**
12 |      * @param  string|null  $name  The name of the tool (defaults to the method name)
13 |      * @param  string|null  $description  The description of the tool (defaults to the DocBlock/inferred)
14 |      * @param  ToolAnnotations|null  $annotations  Optional annotations describing tool behavior
15 |      */
16 |     public function __construct(
17 |         public ?string $name = null,
18 |         public ?string $description = null,
19 |         public ?ToolAnnotations $annotations = null,
20 |     ) {
21 |     }
22 | }
23 | 
```

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

```yaml
 1 | name: "Update Changelog"
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [released]
 6 | 
 7 | permissions:
 8 |   contents: write
 9 | 
10 | jobs:
11 |   update:
12 |     runs-on: ubuntu-latest
13 | 
14 |     steps:
15 |       - name: Checkout code
16 |         uses: actions/checkout@v4
17 |         with:
18 |           ref: main
19 | 
20 |       - name: Update Changelog
21 |         uses: stefanzweifel/changelog-updater-action@v1
22 |         with:
23 |           latest-version: ${{ github.event.release.name }}
24 |           release-notes: ${{ github.event.release.body }}
25 | 
26 |       - name: Commit updated CHANGELOG
27 |         uses: stefanzweifel/git-auto-commit-action@v5
28 |         with:
29 |           branch: main
30 |           commit_message: Update CHANGELOG
31 |           file_pattern: CHANGELOG.md
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\General;
 4 | 
 5 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 6 | use PhpMcp\Server\Contracts\SessionInterface;
 7 | 
 8 | class CompletionProviderFixture implements CompletionProviderInterface
 9 | {
10 |     public static array $completions = ['alpha', 'beta', 'gamma'];
11 |     public static string $lastCurrentValue = '';
12 |     public static ?SessionInterface $lastSession = null;
13 | 
14 |     public function getCompletions(string $currentValue, SessionInterface $session): array
15 |     {
16 |         self::$lastCurrentValue = $currentValue;
17 |         self::$lastSession = $session;
18 | 
19 |         return array_filter(self::$completions, fn ($item) => str_starts_with($item, $currentValue));
20 |     }
21 | }
22 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Exception;
 6 | 
 7 | use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;
 8 | 
 9 | /**
10 |  * Exception related to violations of the JSON-RPC 2.0 or MCP structure
11 |  * in incoming messages or outgoing responses (e.g., missing required fields,
12 |  * invalid types within the protocol itself).
13 |  */
14 | class ProtocolException extends McpServerException
15 | {
16 |     public function toJsonRpcError(string|int $id): JsonRpcError
17 |     {
18 |         $code = ($this->code >= -32700 && $this->code <= -32600) ? $this->code : self::CODE_INVALID_REQUEST;
19 | 
20 |         return new JsonRpcError(
21 |             jsonrpc: '2.0',
22 |             id: $id,
23 |             code: $code,
24 |             message: $this->getMessage(),
25 |             data: $this->getData()
26 |         );
27 |     }
28 | }
29 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\CombinedHttpExample\Discovered;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpResource;
 6 | use PhpMcp\Server\Attributes\McpTool;
 7 | 
 8 | class DiscoveredElements
 9 | {
10 |     /**
11 |      * A tool discovered via attributes.
12 |      *
13 |      * @return string A status message.
14 |      */
15 |     #[McpTool(name: 'discovered_status_check')]
16 |     public function checkSystemStatus(): string
17 |     {
18 |         return 'System status: OK (discovered)';
19 |     }
20 | 
21 |     /**
22 |      * A resource discovered via attributes.
23 |      * This will be overridden by a manual registration with the same URI.
24 |      *
25 |      * @return string Content.
26 |      */
27 |     #[McpResource(uri: 'config://priority', name: 'priority_config_discovered')]
28 |     public function getPriorityConfigDiscovered(): string
29 |     {
30 |         return 'Discovered Priority Config: Low';
31 |     }
32 | }
33 | 
```

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

```yaml
 1 | name: Tests
 2 | 
 3 | on: ['push', 'pull_request']
 4 | 
 5 | jobs:
 6 |   ci:
 7 |     runs-on: ubuntu-latest
 8 | 
 9 |     strategy:
10 |       fail-fast: false
11 |       matrix:
12 |         php: [8.1, 8.2, 8.3, 8.4]
13 |       max-parallel: 2
14 | 
15 |     name: Tests PHP${{ matrix.php }}
16 | 
17 |     steps:
18 | 
19 |       - name: Checkout
20 |         uses: actions/checkout@v4
21 | 
22 |       - name: Cache dependencies
23 |         uses: actions/cache@v4
24 |         with:
25 |           path: ~/.composer/cache/files
26 |           key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
27 | 
28 |       - name: Setup PHP
29 |         uses: shivammathur/setup-php@v2
30 |         with:
31 |           php-version: ${{ matrix.php }}
32 |           coverage: none
33 | 
34 |       - name: Install Composer dependencies
35 |         run: composer update --no-interaction --prefer-dist
36 | 
37 |       - name: Run Tests
38 |         run: composer test
39 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | use Psr\Http\Message\ResponseInterface;
 9 | use React\Promise\PromiseInterface;
10 | 
11 | class HeaderMiddleware
12 | {
13 |     public function __invoke(ServerRequestInterface $request, callable $next)
14 |     {
15 |         $result = $next($request);
16 | 
17 |         return match (true) {
18 |             $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
19 |             $result instanceof ResponseInterface => $this->handle($result),
20 |             default => $result
21 |         };
22 |     }
23 | 
24 |     private function handle($response)
25 |     {
26 |         return $response instanceof ResponseInterface
27 |             ? $response->withHeader('X-Test-Middleware', 'header-added')
28 |             : $response;
29 |     }
30 | }
31 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Attributes;
 6 | 
 7 | use Attribute;
 8 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 9 | 
10 | #[Attribute(Attribute::TARGET_PARAMETER)]
11 | class CompletionProvider
12 | {
13 |     /**
14 |      * @param class-string<CompletionProviderInterface>|null $providerClass 
15 |      * @param class-string<CompletionProviderInterface>|CompletionProviderInterface|null $provider If a class-string, it will be resolved from the container at the point of use.
16 |      */
17 |     public function __construct(
18 |         public ?string $providerClass = null,
19 |         public string|CompletionProviderInterface|null $provider = null,
20 |         public ?array $values = null,
21 |         public ?string $enum = null,
22 |     ) {
23 |         if (count(array_filter([$provider, $values, $enum])) !== 1) {
24 |             throw new \InvalidArgumentException('Only one of provider, values, or enum can be set');
25 |         }
26 |     }
27 | }
28 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\CombinedHttpExample\Manual;
 4 | 
 5 | use Psr\Log\LoggerInterface;
 6 | 
 7 | class ManualHandlers
 8 | {
 9 |     private LoggerInterface $logger;
10 | 
11 |     public function __construct(LoggerInterface $logger)
12 |     {
13 |         $this->logger = $logger;
14 |     }
15 | 
16 |     /**
17 |      * A manually registered tool.
18 |      *
19 |      * @param  string  $user  The user to greet.
20 |      * @return string Greeting.
21 |      */
22 |     public function manualGreeter(string $user): string
23 |     {
24 |         $this->logger->info("Manual tool 'manual_greeter' called for {$user}");
25 | 
26 |         return "Hello {$user}, from manual registration!";
27 |     }
28 | 
29 |     /**
30 |      * Manually registered resource that overrides a discovered one.
31 |      *
32 |      * @return string Content.
33 |      */
34 |     public function getPriorityConfigManual(): string
35 |     {
36 |         $this->logger->info("Manual resource 'config://priority' read.");
37 | 
38 |         return 'Manual Priority Config: HIGH (overrides discovered)';
39 |     }
40 | }
41 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Schema\Annotations;
 8 | use PhpMcp\Server\Attributes\McpResource;
 9 | 
10 | class DiscoverableResourceHandler
11 | {
12 |     /**
13 |      * Provides the application's current version.
14 |      * @return string The version string.
15 |      */
16 |     #[McpResource(
17 |         uri: "app://info/version",
18 |         name: "app_version",
19 |         description: "The current version of the application.",
20 |         mimeType: "text/plain",
21 |         size: 10
22 |     )]
23 |     public function getAppVersion(): string
24 |     {
25 |         return "1.2.3-discovered";
26 |     }
27 | 
28 |     #[McpResource(
29 |         uri: "config://settings/ui",
30 |         name: "ui_settings_discovered",
31 |         mimeType: "application/json",
32 |         annotations: new Annotations(priority: 0.5)
33 |     )]
34 |     public function getUiSettings(): array
35 |     {
36 |         return ["theme" => "dark", "fontSize" => 14];
37 |     }
38 | 
39 |     public function someOtherMethod(): void
40 |     {
41 |     }
42 | }
43 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | use Psr\Http\Message\ResponseInterface;
 9 | use React\Promise\PromiseInterface;
10 | 
11 | class FirstMiddleware
12 | {
13 |     public function __invoke(ServerRequestInterface $request, callable $next)
14 |     {
15 |         $result = $next($request);
16 | 
17 |         return match (true) {
18 |             $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
19 |             $result instanceof ResponseInterface => $this->handle($result),
20 |             default => $result
21 |         };
22 |     }
23 | 
24 |     private function handle($response)
25 |     {
26 |         if ($response instanceof ResponseInterface) {
27 |             $existing = $response->getHeaderLine('X-Middleware-Order');
28 |             $new = $existing ? $existing . ',first' : 'first';
29 |             return $response->withHeader('X-Middleware-Order', $new);
30 |         }
31 |         return $response;
32 |     }
33 | }
34 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | use Psr\Http\Message\ResponseInterface;
 9 | use React\Promise\PromiseInterface;
10 | 
11 | class ThirdMiddleware
12 | {
13 |     public function __invoke(ServerRequestInterface $request, callable $next)
14 |     {
15 |         $result = $next($request);
16 | 
17 |         return match (true) {
18 |             $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
19 |             $result instanceof ResponseInterface => $this->handle($result),
20 |             default => $result
21 |         };
22 |     }
23 | 
24 |     private function handle($response)
25 |     {
26 |         if ($response instanceof ResponseInterface) {
27 |             $existing = $response->getHeaderLine('X-Middleware-Order');
28 |             $new = $existing ? $existing . ',third' : 'third';
29 |             return $response->withHeader('X-Middleware-Order', $new);
30 |         }
31 |         return $response;
32 |     }
33 | }
34 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | interface SessionHandlerInterface
 8 | {
 9 |     /**
10 |      * Read session data
11 |      *
12 |      * Returns an encoded string of the read data.
13 |      * If nothing was read, it must return false.
14 |      * @param string $id The session id to read data for.
15 |      */
16 |     public function read(string $id): string|false;
17 | 
18 |     /**
19 |      * Write session data
20 |      * @param string $id The session id.
21 |      * @param string $data The encoded session data.
22 |      */
23 |     public function write(string $id, string $data): bool;
24 | 
25 |     /**
26 |      * Destroy a session
27 |      * @param string $id The session ID being destroyed.
28 |      * The return value (usually TRUE on success, FALSE on failure).
29 |      */
30 |     public function destroy(string $id): bool;
31 | 
32 |     /**
33 |      * Cleanup old sessions
34 |      * Sessions that have not updated for
35 |      * the last maxlifetime seconds will be removed.
36 |      */
37 |     public function gc(int $maxLifetime): array;
38 | }
39 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Middlewares;
 6 | 
 7 | use Psr\Http\Message\ServerRequestInterface;
 8 | use Psr\Http\Message\ResponseInterface;
 9 | use React\Promise\PromiseInterface;
10 | 
11 | class SecondMiddleware
12 | {
13 |     public function __invoke(ServerRequestInterface $request, callable $next)
14 |     {
15 |         $result = $next($request);
16 | 
17 |         return match (true) {
18 |             $result instanceof PromiseInterface => $result->then(fn($response) => $this->handle($response)),
19 |             $result instanceof ResponseInterface => $this->handle($result),
20 |             default => $result
21 |         };
22 |     }
23 | 
24 |     private function handle($response)
25 |     {
26 |         if ($response instanceof ResponseInterface) {
27 |             $existing = $response->getHeaderLine('X-Middleware-Order');
28 |             $new = $existing ? $existing . ',second' : 'second';
29 |             return $response->withHeader('X-Middleware-Order', $new);
30 |         }
31 |         return $response;
32 |     }
33 | }
34 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Unit\Attributes;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpTool;
 6 | 
 7 | it('instantiates with correct properties', function () {
 8 |     // Arrange
 9 |     $name = 'test-tool-name';
10 |     $description = 'This is a test description.';
11 | 
12 |     // Act
13 |     $attribute = new McpTool(name: $name, description: $description);
14 | 
15 |     // Assert
16 |     expect($attribute->name)->toBe($name);
17 |     expect($attribute->description)->toBe($description);
18 | });
19 | 
20 | it('instantiates with null values for name and description', function () {
21 |     // Arrange & Act
22 |     $attribute = new McpTool(name: null, description: null);
23 | 
24 |     // Assert
25 |     expect($attribute->name)->toBeNull();
26 |     expect($attribute->description)->toBeNull();
27 | });
28 | 
29 | it('instantiates with missing optional arguments', function () {
30 |     // Arrange & Act
31 |     $attribute = new McpTool(); // Use default constructor values
32 | 
33 |     // Assert
34 |     expect($attribute->name)->toBeNull();
35 |     expect($attribute->description)->toBeNull();
36 | });
37 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Defaults;
 6 | 
 7 | use PhpMcp\Server\Contracts\CompletionProviderInterface;
 8 | use PhpMcp\Server\Contracts\SessionInterface;
 9 | 
10 | class EnumCompletionProvider implements CompletionProviderInterface
11 | {
12 |     private array $values;
13 | 
14 |     public function __construct(string $enumClass)
15 |     {
16 |         if (!enum_exists($enumClass)) {
17 |             throw new \InvalidArgumentException("Class {$enumClass} is not an enum");
18 |         }
19 | 
20 |         $this->values = array_map(
21 |             fn($case) => isset($case->value) && is_string($case->value) ? $case->value : $case->name,
22 |             $enumClass::cases()
23 |         );
24 |     }
25 | 
26 |     public function getCompletions(string $currentValue, SessionInterface $session): array
27 |     {
28 |         if (empty($currentValue)) {
29 |             return $this->values;
30 |         }
31 | 
32 |         return array_values(array_filter(
33 |             $this->values,
34 |             fn(string $value) => str_starts_with($value, $currentValue)
35 |         ));
36 |     }
37 | }
38 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Unit\Attributes;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpPrompt;
 6 | 
 7 | it('instantiates with name and description', function () {
 8 |     // Arrange
 9 |     $name = 'test-prompt-name';
10 |     $description = 'This is a test prompt description.';
11 | 
12 |     // Act
13 |     $attribute = new McpPrompt(name: $name, description: $description);
14 | 
15 |     // Assert
16 |     expect($attribute->name)->toBe($name);
17 |     expect($attribute->description)->toBe($description);
18 | });
19 | 
20 | it('instantiates with null values for name and description', function () {
21 |     // Arrange & Act
22 |     $attribute = new McpPrompt(name: null, description: null);
23 | 
24 |     // Assert
25 |     expect($attribute->name)->toBeNull();
26 |     expect($attribute->description)->toBeNull();
27 | });
28 | 
29 | it('instantiates with missing optional arguments', function () {
30 |     // Arrange & Act
31 |     $attribute = new McpPrompt(); // Use default constructor values
32 | 
33 |     // Assert
34 |     expect($attribute->name)->toBeNull();
35 |     expect($attribute->description)->toBeNull();
36 | });
37 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | /**
 8 |  * Interface for resumability support via event storage
 9 |  */
10 | interface EventStoreInterface
11 | {
12 |     /**
13 |      * Stores a message associated with a specific stream and returns a unique event ID.
14 |      *
15 |      * @param string $streamId The ID of the stream the event belongs to.
16 |      * @param string $message The framed JSON-RPC message to store.
17 |      * @return string The generated event ID for the stored event
18 |      */
19 |     public function storeEvent(string $streamId, string $message): string;
20 | 
21 |     /**
22 |      * Replays events for a given stream that occurred after a specific event ID.
23 |      *
24 |      * @param string $lastEventId The last event ID the client received for this specific stream.
25 |      * @param callable $sendCallback A function to call for each replayed message.
26 |      *                           The callback will receive: `function(string $eventId, Message $message): void`
27 |      */
28 |     public function replayEventsAfter(string $lastEventId, callable $sendCallback): void;
29 | }
30 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Attributes;
 4 | 
 5 | use Attribute;
 6 | use PhpMcp\Schema\Annotations;
 7 | 
 8 | /**
 9 |  * Marks a PHP class definition as representing an MCP Resource Template.
10 |  * This is informational, used for 'resources/templates/list'.
11 |  */
12 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
13 | final class McpResourceTemplate
14 | {
15 |     /**
16 |      * @param  string  $uriTemplate  The URI template string (RFC 6570).
17 |      * @param  ?string  $name  A human-readable name for the template type.  If null, a default might be generated from the method name.
18 |      * @param  ?string  $description  Optional description. Defaults to class DocBlock summary.
19 |      * @param  ?string  $mimeType  Optional default MIME type for matching resources.
20 |      * @param  ?Annotations  $annotations  Optional annotations describing the resource template.
21 |      */
22 |     public function __construct(
23 |         public string $uriTemplate,
24 |         public ?string $name = null,
25 |         public ?string $description = null,
26 |         public ?string $mimeType = null,
27 |         public ?Annotations $annotations = null,
28 |     ) {
29 |     }
30 | }
31 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpPrompt;
 8 | use PhpMcp\Server\Attributes\CompletionProvider;
 9 | use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
10 | 
11 | class DiscoverablePromptHandler
12 | {
13 |     /**
14 |      * Generates a creative story prompt.
15 |      * @param string $genre The genre of the story.
16 |      * @param int $lengthWords Approximate length in words.
17 |      * @return array The prompt messages.
18 |      */
19 |     #[McpPrompt(name: "creative_story_prompt")]
20 |     public function generateStoryPrompt(
21 |         #[CompletionProvider(provider: CompletionProviderFixture::class)]
22 |         string $genre,
23 |         int $lengthWords = 200
24 |     ): array {
25 |         return [
26 |             ["role" => "user", "content" => "Write a {$genre} story about a lost robot, approximately {$lengthWords} words long."]
27 |         ];
28 |     }
29 | 
30 |     #[McpPrompt]
31 |     public function simpleQuestionPrompt(string $question): array
32 |     {
33 |         return [
34 |             ["role" => "user", "content" => $question],
35 |             ["role" => "assistant", "content" => "I will try to answer that."]
36 |         ];
37 |     }
38 | }
39 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\Utils;
 4 | 
 5 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
 6 | class TestAttributeOne
 7 | {
 8 |     public function __construct(public string $value)
 9 |     {
10 |     }
11 | }
12 | 
13 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
14 | class TestAttributeTwo
15 | {
16 |     public function __construct(public int $number)
17 |     {
18 |     }
19 | }
20 | 
21 | #[\Attribute(\Attribute::TARGET_CLASS)]
22 | class TestClassOnlyAttribute
23 | {
24 | }
25 | 
26 | 
27 | // --- Test Class ---
28 | 
29 | #[TestClassOnlyAttribute]
30 | #[TestAttributeOne(value: 'class-level')]
31 | class AttributeFixtures
32 | {
33 |     #[TestAttributeOne(value: 'prop-level')]
34 |     public string $propertyOne = 'default';
35 | 
36 |     #[TestAttributeOne(value: 'method-one')]
37 |     public function methodOne(
38 |         #[TestAttributeOne(value: 'param-one')]
39 |         #[TestAttributeTwo(number: 1)]
40 |         string $param1
41 |     ): void {
42 |     }
43 | 
44 |     #[TestAttributeOne(value: 'method-two')]
45 |     #[TestAttributeTwo(number: 2)]
46 |     public function methodTwo(
47 |         #[TestAttributeTwo(number: 3)]
48 |         int $paramA
49 |     ): void {
50 |     }
51 | 
52 |     // Method with no attributes
53 |     public function methodThree(string $unattributedParam): void
54 |     {
55 |     }
56 | }
57 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Attributes;
 4 | 
 5 | use Attribute;
 6 | use PhpMcp\Schema\Annotations;
 7 | 
 8 | /**
 9 |  * Marks a PHP class as representing or handling a specific MCP Resource instance.
10 |  * Used primarily for the 'resources/list' discovery.
11 |  */
12 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
13 | final class McpResource
14 | {
15 |     /**
16 |      * @param  string  $uri  The specific URI identifying this resource instance. Must be unique within the server.
17 |      * @param  ?string  $name  A human-readable name for this resource. If null, a default might be generated from the method name.
18 |      * @param  ?string  $description  An optional description of the resource. Defaults to class DocBlock summary.
19 |      * @param  ?string  $mimeType  The MIME type, if known and constant for this resource.
20 |      * @param  ?int  $size  The size in bytes, if known and constant.
21 |      * @param  Annotations|null  $annotations  Optional annotations describing the resource.
22 |      */
23 |     public function __construct(
24 |         public string $uri,
25 |         public ?string $name = null,
26 |         public ?string $description = null,
27 |         public ?string $mimeType = null,
28 |         public ?int $size = null,
29 |         public ?Annotations $annotations = null,
30 |     ) {
31 |     }
32 | }
33 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\EnvExample;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpTool;
 6 | 
 7 | class EnvToolHandler
 8 | {
 9 |     public function __construct()
10 |     {
11 |     }
12 | 
13 |     /**
14 |      * Performs an action that can be modified by an environment variable.
15 |      * The MCP client should set 'APP_MODE' in its 'env' config for this server.
16 |      *
17 |      * @param  string  $input  Some input data.
18 |      * @return array The result, varying by APP_MODE.
19 |      */
20 |     #[McpTool(name: 'process_data_by_mode')]
21 |     public function processData(string $input): array
22 |     {
23 |         $appMode = getenv('APP_MODE'); // Read from environment
24 | 
25 |         if ($appMode === 'debug') {
26 |             return [
27 |                 'mode' => 'debug',
28 |                 'processed_input' => strtoupper($input),
29 |                 'message' => 'Processed in DEBUG mode.',
30 |             ];
31 |         } elseif ($appMode === 'production') {
32 |             return [
33 |                 'mode' => 'production',
34 |                 'processed_input_length' => strlen($input),
35 |                 'message' => 'Processed in PRODUCTION mode (summary only).',
36 |             ];
37 |         } else {
38 |             return [
39 |                 'mode' => $appMode ?: 'default',
40 |                 'original_input' => $input,
41 |                 'message' => 'Processed in default mode (APP_MODE not recognized or not set).',
42 |             ];
43 |         }
44 |     }
45 | }
46 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpResourceTemplate;
 8 | use PhpMcp\Server\Attributes\CompletionProvider;
 9 | use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
10 | 
11 | class DiscoverableTemplateHandler
12 | {
13 |     /**
14 |      * Retrieves product details based on ID and region.
15 |      * @param string $productId The ID of the product.
16 |      * @param string $region The sales region.
17 |      * @return array Product details.
18 |      */
19 |     #[McpResourceTemplate(
20 |         uriTemplate: "product://{region}/details/{productId}",
21 |         name: "product_details_template",
22 |         mimeType: "application/json"
23 |     )]
24 |     public function getProductDetails(
25 |         string $productId,
26 |         #[CompletionProvider(provider: CompletionProviderFixture::class)]
27 |         string $region
28 |     ): array {
29 |         return [
30 |             "id" => $productId,
31 |             "name" => "Product " . $productId,
32 |             "region" => $region,
33 |             "price" => ($region === "EU" ? "€" : "$") . (hexdec(substr(md5($productId), 0, 4)) / 100)
34 |         ];
35 |     }
36 | 
37 |     #[McpResourceTemplate(uriTemplate: "file://{path}/{filename}.{extension}")]
38 |     public function getFileContent(string $path, string $filename, string $extension): string
39 |     {
40 |         return "Content of {$path}/{$filename}.{$extension}";
41 |     }
42 | }
43 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Server\Attributes\McpPrompt;
 8 | use PhpMcp\Server\Attributes\McpResourceTemplate;
 9 | use PhpMcp\Server\Attributes\CompletionProvider;
10 | use PhpMcp\Server\Tests\Fixtures\Enums\StatusEnum;
11 | use PhpMcp\Server\Tests\Fixtures\Enums\PriorityEnum;
12 | 
13 | class EnhancedCompletionHandler
14 | {
15 |     /**
16 |      * Create content with list and enum completion providers.
17 |      */
18 |     #[McpPrompt(name: 'content_creator')]
19 |     public function createContent(
20 |         #[CompletionProvider(values: ['blog', 'article', 'tutorial', 'guide'])]
21 |         string $type,
22 |         #[CompletionProvider(enum: StatusEnum::class)]
23 |         string $status,
24 |         #[CompletionProvider(enum: PriorityEnum::class)]
25 |         string $priority
26 |     ): array {
27 |         return [
28 |             ['role' => 'user', 'content' => "Create a {$type} with status {$status} and priority {$priority}"]
29 |         ];
30 |     }
31 | 
32 |     /**
33 |      * Resource template with list completion for categories.
34 |      */
35 |     #[McpResourceTemplate(
36 |         uriTemplate: 'content://{category}/{slug}',
37 |         name: 'content_template'
38 |     )]
39 |     public function getContent(
40 |         #[CompletionProvider(values: ['news', 'blog', 'docs', 'api'])]
41 |         string $category,
42 |         string $slug
43 |     ): array {
44 |         return [
45 |             'category' => $category,
46 |             'slug' => $slug,
47 |             'url' => "https://example.com/{$category}/{$slug}"
48 |         ];
49 |     }
50 | }
51 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Unit\Attributes;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpResourceTemplate;
 6 | 
 7 | it('instantiates with correct properties', function () {
 8 |     // Arrange
 9 |     $uriTemplate = 'file:///{path}/data';
10 |     $name = 'test-template-name';
11 |     $description = 'This is a test template description.';
12 |     $mimeType = 'application/json';
13 | 
14 |     // Act
15 |     $attribute = new McpResourceTemplate(
16 |         uriTemplate: $uriTemplate,
17 |         name: $name,
18 |         description: $description,
19 |         mimeType: $mimeType,
20 |     );
21 | 
22 |     // Assert
23 |     expect($attribute->uriTemplate)->toBe($uriTemplate);
24 |     expect($attribute->name)->toBe($name);
25 |     expect($attribute->description)->toBe($description);
26 |     expect($attribute->mimeType)->toBe($mimeType);
27 | });
28 | 
29 | it('instantiates with null values for name and description', function () {
30 |     // Arrange & Act
31 |     $attribute = new McpResourceTemplate(
32 |         uriTemplate: 'test://{id}', // uriTemplate is required
33 |         name: null,
34 |         description: null,
35 |         mimeType: null,
36 |     );
37 | 
38 |     // Assert
39 |     expect($attribute->uriTemplate)->toBe('test://{id}');
40 |     expect($attribute->name)->toBeNull();
41 |     expect($attribute->description)->toBeNull();
42 |     expect($attribute->mimeType)->toBeNull();
43 | });
44 | 
45 | it('instantiates with missing optional arguments', function () {
46 |     // Arrange & Act
47 |     $uriTemplate = 'tmpl://{key}';
48 |     $attribute = new McpResourceTemplate(uriTemplate: $uriTemplate);
49 | 
50 |     // Assert
51 |     expect($attribute->uriTemplate)->toBe($uriTemplate);
52 |     expect($attribute->name)->toBeNull();
53 |     expect($attribute->description)->toBeNull();
54 |     expect($attribute->mimeType)->toBeNull();
55 | });
56 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | declare(strict_types=1);
 5 | 
 6 | require_once __DIR__ . '/../../../vendor/autoload.php';
 7 | 
 8 | use PhpMcp\Server\Server;
 9 | use PhpMcp\Server\Transports\StdioServerTransport;
10 | use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
11 | use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
12 | use PhpMcp\Server\Tests\Fixtures\General\PromptHandlerFixture;
13 | use Psr\Log\AbstractLogger;
14 | use Psr\Log\NullLogger;
15 | 
16 | class StdErrLogger extends AbstractLogger
17 | {
18 |     public function log($level, \Stringable|string $message, array $context = []): void
19 |     {
20 |         fwrite(STDERR, sprintf("[%s] SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
21 |     }
22 | }
23 | 
24 | try {
25 |     $logger = new NullLogger();
26 |     $logger->info('StdioTestServer listener starting.');
27 | 
28 |     $server = Server::make()
29 |         ->withServerInfo('StdioIntegrationTestServer', '0.1.0')
30 |         ->withLogger($logger)
31 |         ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_stdio_tool')
32 |         ->withTool([ToolHandlerFixture::class, 'toolReadsContext'], 'tool_reads_context') // for Context testing
33 |         ->withResource([ResourceHandlerFixture::class, 'getStaticText'], 'test://stdio/static', 'static_stdio_resource')
34 |         ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_stdio_prompt')
35 |         ->build();
36 | 
37 |     $transport = new StdioServerTransport();
38 |     $server->listen($transport);
39 | 
40 |     $logger->info('StdioTestServer listener stopped.');
41 |     exit(0);
42 | } catch (\Throwable $e) {
43 |     fwrite(STDERR, "[STDIO_SERVER_CRITICAL_ERROR]\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
44 |     exit(1);
45 | }
46 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server;
 6 | 
 7 | use PhpMcp\Schema\Implementation;
 8 | use PhpMcp\Schema\ServerCapabilities;
 9 | use Psr\Container\ContainerInterface;
10 | use Psr\Log\LoggerInterface;
11 | use Psr\SimpleCache\CacheInterface;
12 | use React\EventLoop\LoopInterface;
13 | 
14 | /**
15 |  * Value Object holding core configuration and shared dependencies for the MCP Server instance.
16 |  *
17 |  * This object is typically assembled by the ServerBuilder and passed to the Server constructor.
18 |  */
19 | class Configuration
20 | {
21 |     /**
22 |      * @param  Implementation  $serverInfo  Info about this MCP server application.
23 |      * @param  ServerCapabilities  $capabilities  Capabilities of this MCP server application.
24 |      * @param  LoggerInterface  $logger  PSR-3 Logger instance.
25 |      * @param  LoopInterface  $loop  ReactPHP Event Loop instance.
26 |      * @param  CacheInterface|null  $cache  Optional PSR-16 Cache instance for registry/state.
27 |      * @param  ContainerInterface  $container  PSR-11 DI Container for resolving handlers/dependencies.
28 |      * @param  int  $paginationLimit  Maximum number of items to return for list methods.
29 |      * @param  string|null  $instructions  Instructions describing how to use the server and its features.
30 |      */
31 |     public function __construct(
32 |         public readonly Implementation $serverInfo,
33 |         public readonly ServerCapabilities $capabilities,
34 |         public readonly LoggerInterface $logger,
35 |         public readonly LoopInterface $loop,
36 |         public readonly ?CacheInterface $cache,
37 |         public readonly ContainerInterface $container,
38 |         public readonly int $paginationLimit = 50,
39 |         public readonly ?string $instructions = null,
40 |     ) {}
41 | }
42 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Unit\Attributes;
 4 | 
 5 | use PhpMcp\Server\Attributes\McpResource;
 6 | 
 7 | it('instantiates with correct properties', function () {
 8 |     // Arrange
 9 |     $uri = 'file:///test/resource';
10 |     $name = 'test-resource-name';
11 |     $description = 'This is a test resource description.';
12 |     $mimeType = 'text/plain';
13 |     $size = 1024;
14 | 
15 |     // Act
16 |     $attribute = new McpResource(
17 |         uri: $uri,
18 |         name: $name,
19 |         description: $description,
20 |         mimeType: $mimeType,
21 |         size: $size,
22 |     );
23 | 
24 |     // Assert
25 |     expect($attribute->uri)->toBe($uri);
26 |     expect($attribute->name)->toBe($name);
27 |     expect($attribute->description)->toBe($description);
28 |     expect($attribute->mimeType)->toBe($mimeType);
29 |     expect($attribute->size)->toBe($size);
30 | });
31 | 
32 | it('instantiates with null values for name and description', function () {
33 |     // Arrange & Act
34 |     $attribute = new McpResource(
35 |         uri: 'file:///test', // URI is required
36 |         name: null,
37 |         description: null,
38 |         mimeType: null,
39 |         size: null,
40 |     );
41 | 
42 |     // Assert
43 |     expect($attribute->uri)->toBe('file:///test');
44 |     expect($attribute->name)->toBeNull();
45 |     expect($attribute->description)->toBeNull();
46 |     expect($attribute->mimeType)->toBeNull();
47 |     expect($attribute->size)->toBeNull();
48 | });
49 | 
50 | it('instantiates with missing optional arguments', function () {
51 |     // Arrange & Act
52 |     $uri = 'file:///only-uri';
53 |     $attribute = new McpResource(uri: $uri);
54 | 
55 |     // Assert
56 |     expect($attribute->uri)->toBe($uri);
57 |     expect($attribute->name)->toBeNull();
58 |     expect($attribute->description)->toBeNull();
59 |     expect($attribute->mimeType)->toBeNull();
60 |     expect($attribute->size)->toBeNull();
61 | });
62 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Fixtures\Discovery;
 6 | 
 7 | use PhpMcp\Schema\ToolAnnotations;
 8 | use PhpMcp\Server\Attributes\McpTool;
 9 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
10 | 
11 | class DiscoverableToolHandler
12 | {
13 |     /**
14 |      * A basic discoverable tool.
15 |      * @param string $name The name to greet.
16 |      * @return string The greeting.
17 |      */
18 |     #[McpTool(name: "greet_user", description: "Greets a user by name.")]
19 |     public function greet(string $name): string
20 |     {
21 |         return "Hello, {$name}!";
22 |     }
23 | 
24 |     /**
25 |      * A tool with more complex parameters and inferred name/description.
26 |      * @param int $count The number of times to repeat.
27 |      * @param bool $loudly Should it be loud?
28 |      * @param BackedStringEnum $mode The mode of operation.
29 |      * @return array An array with results.
30 |      */
31 |     #[McpTool(annotations: new ToolAnnotations(readOnlyHint: true))]
32 |     public function repeatAction(int $count, bool $loudly = false, BackedStringEnum $mode = BackedStringEnum::OptionA): array
33 |     {
34 |         return ['count' => $count, 'loudly' => $loudly, 'mode' => $mode->value, 'message' => "Action repeated."];
35 |     }
36 | 
37 |     // This method should NOT be discovered as a tool
38 |     public function internalHelperMethod(int $value): int
39 |     {
40 |         return $value * 2;
41 |     }
42 | 
43 |     #[McpTool(name: "private_tool_should_be_ignored")] // On private method
44 |     private function aPrivateTool(): void
45 |     {
46 |     }
47 | 
48 |     #[McpTool(name: "protected_tool_should_be_ignored")] // On protected method
49 |     protected function aProtectedTool(): void
50 |     {
51 |     }
52 | 
53 |     #[McpTool(name: "static_tool_should_be_ignored")] // On static method
54 |     public static function aStaticTool(): void
55 |     {
56 |     }
57 | }
58 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Session;
 6 | 
 7 | use PhpMcp\Server\Contracts\SessionHandlerInterface;
 8 | use PhpMcp\Server\Defaults\SystemClock;
 9 | use Psr\Clock\ClockInterface;
10 | 
11 | class ArraySessionHandler implements SessionHandlerInterface
12 | {
13 |     /**
14 |      * @var array<string, array{ data: array, timestamp: int }>
15 |      */
16 |     protected array $store = [];
17 | 
18 |     private ClockInterface $clock;
19 | 
20 |     public function __construct(
21 |         public readonly int $ttl = 3600,
22 |         ?ClockInterface $clock = null
23 |     ) {
24 |         $this->clock = $clock ?? new SystemClock();
25 |     }
26 | 
27 |     public function read(string $sessionId): string|false
28 |     {
29 |         $session = $this->store[$sessionId] ?? '';
30 |         if ($session === '') {
31 |             return false;
32 |         }
33 | 
34 |         $currentTimestamp = $this->clock->now()->getTimestamp();
35 | 
36 |         if ($currentTimestamp - $session['timestamp'] > $this->ttl) {
37 |             unset($this->store[$sessionId]);
38 |             return false;
39 |         }
40 | 
41 |         return $session['data'];
42 |     }
43 | 
44 |     public function write(string $sessionId, string $data): bool
45 |     {
46 |         $this->store[$sessionId] = [
47 |             'data' => $data,
48 |             'timestamp' => $this->clock->now()->getTimestamp(),
49 |         ];
50 | 
51 |         return true;
52 |     }
53 | 
54 |     public function destroy(string $sessionId): bool
55 |     {
56 |         if (isset($this->store[$sessionId])) {
57 |             unset($this->store[$sessionId]);
58 |         }
59 | 
60 |         return true;
61 |     }
62 | 
63 |     public function gc(int $maxLifetime): array
64 |     {
65 |         $currentTimestamp = $this->clock->now()->getTimestamp();
66 |         $deletedSessions = [];
67 | 
68 |         foreach ($this->store as $sessionId => $session) {
69 |             if ($currentTimestamp - $session['timestamp'] > $maxLifetime) {
70 |                 unset($this->store[$sessionId]);
71 |                 $deletedSessions[] = $sessionId;
72 |             }
73 |         }
74 | 
75 |         return $deletedSessions;
76 |     }
77 | }
78 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\General;
 4 | 
 5 | /**
 6 |  * A stub class for testing DocBlock parsing.
 7 |  */
 8 | class DocBlockTestFixture
 9 | {
10 |     /**
11 |      * Simple summary line.
12 |      */
13 |     public function methodWithSummaryOnly(): void
14 |     {
15 |     }
16 | 
17 |     /**
18 |      * Summary line here.
19 |      *
20 |      * This is a longer description spanning
21 |      * multiple lines.
22 |      * It might contain *markdown* or `code`.
23 |      *
24 |      * @since 1.0
25 |      */
26 |     public function methodWithSummaryAndDescription(): void
27 |     {
28 |     }
29 | 
30 |     /**
31 |      * Method with various parameter tags.
32 |      *
33 |      * @param string $param1 Description for string param.
34 |      * @param int|null $param2 Description for nullable int param.
35 |      * @param bool $param3
36 |      * @param $param4 Missing type.
37 |      * @param array<string, mixed> $param5 Array description.
38 |      * @param \stdClass $param6 Object param.
39 |      */
40 |     public function methodWithParams(string $param1, ?int $param2, bool $param3, $param4, array $param5, \stdClass $param6): void
41 |     {
42 |     }
43 | 
44 |     /**
45 |      * Method with return tag.
46 |      *
47 |      * @return string The result of the operation.
48 |      */
49 |     public function methodWithReturn(): string
50 |     {
51 |         return '';
52 |     }
53 | 
54 |     /**
55 |      * Method with multiple tags.
56 |      *
57 |      * @param float $value The value to process.
58 |      * @return bool Status of the operation.
59 |      * @throws \RuntimeException If processing fails.
60 |      * @deprecated Use newMethod() instead.
61 |      * @see \PhpMcp\Server\Tests\Fixtures\General\DocBlockTestFixture::newMethod()
62 |      */
63 |     public function methodWithMultipleTags(float $value): bool
64 |     {
65 |         return true;
66 |     }
67 | 
68 |     /**
69 |      * Malformed docblock - missing closing
70 |      */
71 |     public function methodWithMalformedDocBlock(): void
72 |     {
73 |     }
74 | 
75 |     public function methodWithNoDocBlock(): void
76 |     {
77 |     }
78 | 
79 |     // Some other method needed for a @see tag perhaps
80 |     public function newMethod(): void
81 |     {
82 |     }
83 | }
84 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\ManualStdioExample;
 4 | 
 5 | use Psr\Log\LoggerInterface;
 6 | 
 7 | class SimpleHandlers
 8 | {
 9 |     private LoggerInterface $logger;
10 | 
11 |     private string $appVersion = '1.0-manual';
12 | 
13 |     public function __construct(LoggerInterface $logger)
14 |     {
15 |         $this->logger = $logger;
16 |         $this->logger->info('SimpleHandlers instantiated for manual registration example.');
17 |     }
18 | 
19 |     /**
20 |      * A manually registered tool to echo input.
21 |      *
22 |      * @param  string  $text  The text to echo.
23 |      * @return string The echoed text.
24 |      */
25 |     public function echoText(string $text): string
26 |     {
27 |         $this->logger->info("Manual tool 'echo_text' called.", ['text' => $text]);
28 | 
29 |         return 'Echo: '.$text;
30 |     }
31 | 
32 |     /**
33 |      * A manually registered resource providing app version.
34 |      *
35 |      * @return string The application version.
36 |      */
37 |     public function getAppVersion(): string
38 |     {
39 |         $this->logger->info("Manual resource 'app://version' read.");
40 | 
41 |         return $this->appVersion;
42 |     }
43 | 
44 |     /**
45 |      * A manually registered prompt template.
46 |      *
47 |      * @param  string  $userName  The name of the user.
48 |      * @return array The prompt messages.
49 |      */
50 |     public function greetingPrompt(string $userName): array
51 |     {
52 |         $this->logger->info("Manual prompt 'personalized_greeting' called.", ['userName' => $userName]);
53 | 
54 |         return [
55 |             ['role' => 'user', 'content' => "Craft a personalized greeting for {$userName}."],
56 |         ];
57 |     }
58 | 
59 |     /**
60 |      * A manually registered resource template.
61 |      *
62 |      * @param  string  $itemId  The ID of the item.
63 |      * @return array Item details.
64 |      */
65 |     public function getItemDetails(string $itemId): array
66 |     {
67 |         $this->logger->info("Manual template 'item://{itemId}' resolved.", ['itemId' => $itemId]);
68 | 
69 |         return ['id' => $itemId, 'name' => "Item {$itemId}", 'description' => "Details for item {$itemId} from manual template."];
70 |     }
71 | }
72 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Mocks\Clock;
 6 | 
 7 | use DateTimeImmutable;
 8 | use DateTimeZone;
 9 | use Psr\Clock\ClockInterface;
10 | use DateInterval;
11 | 
12 | class FixedClock implements ClockInterface
13 | {
14 |     private DateTimeImmutable $currentTime;
15 | 
16 |     public function __construct(string|DateTimeImmutable $initialTime = 'now', ?DateTimeZone $timezone = null)
17 |     {
18 |         if ($initialTime instanceof DateTimeImmutable) {
19 |             $this->currentTime = $initialTime;
20 |         } else {
21 |             $this->currentTime = new DateTimeImmutable($initialTime, $timezone);
22 |         }
23 |     }
24 | 
25 |     public function now(): DateTimeImmutable
26 |     {
27 |         return $this->currentTime;
28 |     }
29 | 
30 |     public function setCurrentTime(string|DateTimeImmutable $newTime, ?DateTimeZone $timezone = null): void
31 |     {
32 |         if ($newTime instanceof DateTimeImmutable) {
33 |             $this->currentTime = $newTime;
34 |         } else {
35 |             $this->currentTime = new DateTimeImmutable($newTime, $timezone);
36 |         }
37 |     }
38 | 
39 |     public function advance(DateInterval $interval): void
40 |     {
41 |         $this->currentTime = $this->currentTime->add($interval);
42 |     }
43 | 
44 |     public function rewind(DateInterval $interval): void
45 |     {
46 |         $this->currentTime = $this->currentTime->sub($interval);
47 |     }
48 | 
49 |     public function addSecond(): void
50 |     {
51 |         $this->advance(new DateInterval("PT1S"));
52 |     }
53 | 
54 |     public function addSeconds(int $seconds): void
55 |     {
56 |         $this->advance(new DateInterval("PT{$seconds}S"));
57 |     }
58 | 
59 |     public function addMinutes(int $minutes): void
60 |     {
61 |         $this->advance(new DateInterval("PT{$minutes}M"));
62 |     }
63 | 
64 |     public function addHours(int $hours): void
65 |     {
66 |         $this->advance(new DateInterval("PT{$hours}H"));
67 |     }
68 | 
69 |     public function subSeconds(int $seconds): void
70 |     {
71 |         $this->rewind(new DateInterval("PT{$seconds}S"));
72 |     }
73 | 
74 |     public function subMinutes(int $minutes): void
75 |     {
76 |         $this->rewind(new DateInterval("PT{$minutes}M"));
77 |     }
78 | }
79 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Unit\Defaults;
 6 | 
 7 | use PhpMcp\Server\Defaults\ListCompletionProvider;
 8 | use PhpMcp\Server\Contracts\SessionInterface;
 9 | use Mockery;
10 | 
11 | beforeEach(function () {
12 |     $this->session = Mockery::mock(SessionInterface::class);
13 | });
14 | 
15 | it('returns all values when current value is empty', function () {
16 |     $values = ['apple', 'banana', 'cherry'];
17 |     $provider = new ListCompletionProvider($values);
18 | 
19 |     $result = $provider->getCompletions('', $this->session);
20 | 
21 |     expect($result)->toBe($values);
22 | });
23 | 
24 | it('filters values based on current value prefix', function () {
25 |     $values = ['apple', 'apricot', 'banana', 'cherry'];
26 |     $provider = new ListCompletionProvider($values);
27 | 
28 |     $result = $provider->getCompletions('ap', $this->session);
29 | 
30 |     expect($result)->toBe(['apple', 'apricot']);
31 | });
32 | 
33 | it('returns empty array when no values match', function () {
34 |     $values = ['apple', 'banana', 'cherry'];
35 |     $provider = new ListCompletionProvider($values);
36 | 
37 |     $result = $provider->getCompletions('xyz', $this->session);
38 | 
39 |     expect($result)->toBe([]);
40 | });
41 | 
42 | it('works with single character prefix', function () {
43 |     $values = ['apple', 'banana', 'cherry'];
44 |     $provider = new ListCompletionProvider($values);
45 | 
46 |     $result = $provider->getCompletions('a', $this->session);
47 | 
48 |     expect($result)->toBe(['apple']);
49 | });
50 | 
51 | it('is case sensitive by default', function () {
52 |     $values = ['Apple', 'apple', 'APPLE'];
53 |     $provider = new ListCompletionProvider($values);
54 | 
55 |     $result = $provider->getCompletions('A', $this->session);
56 | 
57 |     expect($result)->toEqual(['Apple', 'APPLE']);
58 | });
59 | 
60 | it('handles empty values array', function () {
61 |     $provider = new ListCompletionProvider([]);
62 | 
63 |     $result = $provider->getCompletions('test', $this->session);
64 | 
65 |     expect($result)->toBe([]);
66 | });
67 | 
68 | it('preserves array order', function () {
69 |     $values = ['zebra', 'apple', 'banana'];
70 |     $provider = new ListCompletionProvider($values);
71 | 
72 |     $result = $provider->getCompletions('', $this->session);
73 | 
74 |     expect($result)->toBe(['zebra', 'apple', 'banana']);
75 | });
76 | 
```

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

```json
 1 | {
 2 |     "name": "php-mcp/server",
 3 |     "description": "PHP SDK for building Model Context Protocol (MCP) servers - Create MCP tools, resources, and prompts",
 4 |     "keywords": [
 5 |         "mcp",
 6 |         "model context protocol",
 7 |         "server",
 8 |         "php",
 9 |         "php mcp",
10 |         "php mcp sdk",
11 |         "php mcp server",
12 |         "php mcp tools",
13 |         "php mcp resources",
14 |         "php mcp prompts",
15 |         "php model context protocol"
16 |     ],
17 |     "type": "library",
18 |     "license": "MIT",
19 |     "authors": [
20 |         {
21 |             "name": "Kyrian Obikwelu",
22 |             "email": "[email protected]"
23 |         }
24 |     ],
25 |     "require": {
26 |         "php": ">=8.1",
27 |         "opis/json-schema": "^2.4",
28 |         "php-mcp/schema": "^1.0",
29 |         "phpdocumentor/reflection-docblock": "^5.6",
30 |         "psr/clock": "^1.0",
31 |         "psr/container": "^1.0 || ^2.0",
32 |         "psr/log": "^1.0 || ^2.0 || ^3.0",
33 |         "psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
34 |         "react/event-loop": "^1.5",
35 |         "react/http": "^1.11",
36 |         "react/promise": "^3.0",
37 |         "react/stream": "^1.4",
38 |         "symfony/finder": "^6.4 || ^7.2"
39 |     },
40 |     "require-dev": {
41 |         "friendsofphp/php-cs-fixer": "^3.75",
42 |         "mockery/mockery": "^1.6",
43 |         "pestphp/pest": "^2.36.0|^3.5.0",
44 |         "react/async": "^4.0",
45 |         "react/child-process": "^0.6.6",
46 |         "symfony/var-dumper": "^6.4.11|^7.1.5"
47 |     },
48 |     "suggest": {
49 |         "ext-pcntl": "For signal handling support when using StdioServerTransport with StreamSelectLoop"
50 |     },
51 |     "autoload": {
52 |         "psr-4": {
53 |             "PhpMcp\\Server\\": "src/"
54 |         }
55 |     },
56 |     "autoload-dev": {
57 |         "psr-4": {
58 |             "PhpMcp\\Server\\Tests\\": "tests/"
59 |         }
60 |     },
61 |     "scripts": {
62 |         "test": "vendor/bin/pest",
63 |         "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage",
64 |         "lint": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php"
65 |     },
66 |     "config": {
67 |         "sort-packages": true,
68 |         "allow-plugins": {
69 |             "pestphp/pest-plugin": true
70 |         }
71 |     },
72 |     "minimum-stability": "dev",
73 |     "prefer-stable": true
74 | }
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | use JsonSerializable;
 8 | 
 9 | interface SessionInterface extends JsonSerializable
10 | {
11 |     /**
12 |      * Get the session ID.
13 |      */
14 |     public function getId(): string;
15 | 
16 |     /**
17 |      * Save the session.
18 |      */
19 |     public function save(): void;
20 | 
21 |     /**
22 |      * Get a specific attribute from the session.
23 |      * Supports dot notation for nested access.
24 |      */
25 |     public function get(string $key, mixed $default = null): mixed;
26 | 
27 |     /**
28 |      * Set a specific attribute in the session.
29 |      * Supports dot notation for nested access.
30 |      */
31 |     public function set(string $key, mixed $value, bool $overwrite = true): void;
32 | 
33 |     /**
34 |      * Check if an attribute exists in the session.
35 |      * Supports dot notation for nested access.
36 |      */
37 |     public function has(string $key): bool;
38 | 
39 |     /**
40 |      * Remove an attribute from the session.
41 |      * Supports dot notation for nested access.
42 |      */
43 |     public function forget(string $key): void;
44 | 
45 |     /**
46 |      * Remove all attributes from the session.
47 |      */
48 |     public function clear(): void;
49 | 
50 |     /**
51 |      * Get an attribute's value and then remove it from the session.
52 |      * Supports dot notation for nested access.
53 |      */
54 |     public function pull(string $key, mixed $default = null): mixed;
55 | 
56 |     /**
57 |      * Get all attributes of the session.
58 |      */
59 |     public function all(): array;
60 | 
61 |     /**
62 |      * Set all attributes of the session, typically for hydration.
63 |      * This will overwrite existing attributes.
64 |      */
65 |     public function hydrate(array $attributes): void;
66 | 
67 |     /**
68 |      * Add a message to the session's queue.
69 |      */
70 |     public function queueMessage(string $message): void;
71 | 
72 |     /**
73 |      * Retrieve and remove all messages from the queue.
74 |      * @return array<string>
75 |      */
76 |     public function dequeueMessages(): array;
77 | 
78 |     /**
79 |      * Check if there are any messages in the queue.
80 |      */
81 |     public function hasQueuedMessages(): bool;
82 | 
83 |     /**
84 |      * Get the session handler instance.
85 |      *
86 |      * @return SessionHandlerInterface
87 |      */
88 |     public function getHandler(): SessionHandlerInterface;
89 | }
90 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Defaults;
 6 | 
 7 | use PhpMcp\Server\Contracts\EventStoreInterface;
 8 | 
 9 | /**
10 |  * Simple in-memory implementation of the EventStore interface for resumability
11 |  * This is primarily intended for examples and testing, not for production use
12 |  * where a persistent storage solution would be more appropriate.
13 |  */
14 | class InMemoryEventStore implements EventStoreInterface
15 | {
16 |     public const DEFAULT_MAX_EVENTS_PER_STREAM = 1000;
17 | 
18 |     /**
19 |      * @var array<string, array{streamId: string, message: string}>
20 |      * Example: [eventId1 => ['streamId' => 'abc', 'message' => '...']]
21 |      */
22 |     private array $events = [];
23 | 
24 |     private function generateEventId(string $streamId): string
25 |     {
26 |         return $streamId . '_' . (int)(microtime(true) * 1000) . '_' . bin2hex(random_bytes(4));
27 |     }
28 | 
29 |     private function getStreamIdFromEventId(string $eventId): ?string
30 |     {
31 |         $parts = explode('_', $eventId);
32 |         return $parts[0] ?? null;
33 |     }
34 | 
35 |     public function storeEvent(string $streamId, string $message): string
36 |     {
37 |         $eventId = $this->generateEventId($streamId);
38 | 
39 |         $this->events[$eventId] = [
40 |             'streamId' => $streamId,
41 |             'message' => $message,
42 |         ];
43 | 
44 |         return $eventId;
45 |     }
46 | 
47 |     public function replayEventsAfter(string $lastEventId, callable $sendCallback): void
48 |     {
49 |         if (!isset($this->events[$lastEventId])) {
50 |             return;
51 |         }
52 | 
53 |         $streamId = $this->getStreamIdFromEventId($lastEventId);
54 |         if ($streamId === null) {
55 |             return;
56 |         }
57 | 
58 |         $foundLastEvent = false;
59 | 
60 |         // Sort by eventId for deterministic ordering
61 |         ksort($this->events);
62 | 
63 |         foreach ($this->events as $eventId => ['streamId' => $eventStreamId, 'message' => $message]) {
64 |             if ($eventStreamId !== $streamId) {
65 |                 continue;
66 |             }
67 | 
68 |             if ($eventId === $lastEventId) {
69 |                 $foundLastEvent = true;
70 |                 continue;
71 |             }
72 | 
73 |             if ($foundLastEvent) {
74 |                 $sendCallback($eventId, $message);
75 |             }
76 |         }
77 |     }
78 | }
79 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Contracts;
 6 | 
 7 | use Evenement\EventEmitterInterface;
 8 | use PhpMcp\Server\Exception\TransportException;
 9 | use PhpMcp\Schema\JsonRpc\Message;
10 | use React\Promise\PromiseInterface;
11 | 
12 | /**
13 |  * Interface for server-side MCP transports.
14 |  *
15 |  * Implementations handle listening for connections/data and sending raw messages.
16 |  * MUST emit events for lifecycle and messages.
17 |  *
18 |  * --- Expected Emitted Events ---
19 |  * 'ready': () - Optional: Fired when listening starts successfully.
20 |  * 'client_connected': (string $sessionId) - New client connection
21 |  * 'message': (Message $message, string $sessionId, array $context) - Complete message received from a client.
22 |  * 'client_disconnected': (string $sessionId, ?string $reason) - Client connection closed.
23 |  * 'error': (Throwable $error, ?string $sessionId) - Error occurred (general transport error if sessionId is null).
24 |  * 'close': (?string $reason) - Transport listener stopped completely.
25 |  */
26 | interface ServerTransportInterface extends EventEmitterInterface
27 | {
28 |     /**
29 |      * Starts the transport listener (e.g., listens on STDIN, starts HTTP server).
30 |      * Does NOT run the event loop itself. Prepares transport to emit events when loop runs.
31 |      *
32 |      * @throws TransportException on immediate setup failure (e.g., port binding).
33 |      */
34 |     public function listen(): void;
35 | 
36 |     /**
37 |      * Sends a message to a connected client session with optional context.
38 |      *
39 |      * @param  Message  $message  Message to send.
40 |      * @param  string  $sessionId  Target session identifier.
41 |      * @param  array  $context  Optional context for the message. Eg. streamId for SSE.
42 |      * @return PromiseInterface<void> Resolves on successful send/queue, rejects on specific send error.
43 |      */
44 |     public function sendMessage(Message $message, string $sessionId, array $context = []): PromiseInterface;
45 | 
46 |     /**
47 |      * Stops the transport listener gracefully and closes all active connections.
48 |      * MUST eventually emit a 'close' event for the transport itself.
49 |      * Individual client disconnects should emit 'client_disconnected' events.
50 |      */
51 |     public function close(): void;
52 | }
53 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | declare(strict_types=1);
 5 | 
 6 | chdir(__DIR__);
 7 | require_once '../../vendor/autoload.php';
 8 | require_once './EnvToolHandler.php';
 9 | 
10 | use PhpMcp\Server\Server;
11 | use PhpMcp\Server\Transports\StdioServerTransport;
12 | use Psr\Log\AbstractLogger;
13 | 
14 | /*
15 |     |--------------------------------------------------------------------------
16 |     | MCP Stdio Environment Variable Example Server
17 |     |--------------------------------------------------------------------------
18 |     |
19 |     | This server demonstrates how to use environment variables to modify tool
20 |     | behavior. The MCP client can set the APP_MODE environment variable to
21 |     | control the server's behavior.
22 |     |
23 |     | Configure your MCP Client (eg. Cursor) for this server like this:
24 |     |
25 |     | {
26 |     |     "mcpServers": {
27 |     |         "my-php-env-server": {
28 |     |             "command": "php",
29 |     |             "args": ["/full/path/to/examples/05-stdio-env-variables/server.php"],
30 |     |             "env": {
31 |     |                 "APP_MODE": "debug" // or "production", or leave it out
32 |     |             }
33 |     |         }
34 |     |     }
35 |     | }
36 |     |
37 |     | The server will read the APP_MODE environment variable and use it to
38 |     | modify the behavior of the tools.
39 |     |
40 |     | If the APP_MODE environment variable is not set, the server will use the
41 |     | default behavior.
42 |     |
43 | */
44 | 
45 | class StderrLogger extends AbstractLogger
46 | {
47 |     public function log($level, \Stringable|string $message, array $context = []): void
48 |     {
49 |         fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
50 |     }
51 | }
52 | 
53 | try {
54 |     $logger = new StderrLogger();
55 |     $logger->info('Starting MCP Stdio Environment Variable Example Server...');
56 | 
57 |     $server = Server::make()
58 |         ->withServerInfo('Env Var Server', '1.0.0')
59 |         ->withLogger($logger)
60 |         ->build();
61 | 
62 |     $server->discover(__DIR__, ['.']);
63 | 
64 |     $transport = new StdioServerTransport();
65 |     $server->listen($transport);
66 | 
67 |     $logger->info('Server listener stopped gracefully.');
68 |     exit(0);
69 | 
70 | } catch (\Throwable $e) {
71 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n".$e."\n");
72 |     exit(1);
73 | }
74 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | /*
 5 |     |--------------------------------------------------------------------------
 6 |     | MCP Schema Showcase Server (Attribute Discovery)
 7 |     |--------------------------------------------------------------------------
 8 |     |
 9 |     | This server demonstrates various ways to use the Schema attribute to
10 |     | validate tool inputs. It showcases string constraints, numeric validation,
11 |     | object schemas, array handling, enums, and format validation.
12 |     |
13 |     | To Use:
14 |     | 1. Ensure 'SchemaShowcaseElements.php' defines classes with MCP attributes.
15 |     | 2. Configure your MCP Client (e.g., Cursor) for this server:
16 |     |
17 |     | {
18 |     |     "mcpServers": {
19 |     |         "php-schema-showcase": {
20 |     |             "command": "php",
21 |     |             "args": ["/full/path/to/examples/08-schema-showcase-stdio/server.php"]
22 |     |         }
23 |     |     }
24 |     | }
25 |     |
26 |     | This example focuses specifically on demonstrating different Schema
27 |     | attribute capabilities for robust input validation.
28 |     |
29 | */
30 | 
31 | declare(strict_types=1);
32 | 
33 | chdir(__DIR__);
34 | require_once '../../vendor/autoload.php';
35 | require_once 'SchemaShowcaseElements.php';
36 | 
37 | use PhpMcp\Server\Server;
38 | use PhpMcp\Server\Transports\StreamableHttpServerTransport;
39 | use Psr\Log\AbstractLogger;
40 | 
41 | class StderrLogger extends AbstractLogger
42 | {
43 |     public function log($level, \Stringable|string $message, array $context = []): void
44 |     {
45 |         fwrite(STDERR, sprintf(
46 |             "[%s] %s %s\n",
47 |             strtoupper($level),
48 |             $message,
49 |             empty($context) ? '' : json_encode($context)
50 |         ));
51 |     }
52 | }
53 | 
54 | try {
55 |     $logger = new StderrLogger();
56 |     $logger->info('Starting MCP Schema Showcase Server...');
57 | 
58 |     $server = Server::make()
59 |         ->withServerInfo('Schema Showcase', '1.0.0')
60 |         ->withLogger($logger)
61 |         ->build();
62 | 
63 |     $server->discover(__DIR__, ['.']);
64 | 
65 |     $transport = new StreamableHttpServerTransport('127.0.0.1', 8080, 'mcp');
66 | 
67 |     $server->listen($transport);
68 | 
69 |     $logger->info('Server listener stopped gracefully.');
70 |     exit(0);
71 | } catch (\Throwable $e) {
72 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
73 |     fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
74 |     fwrite(STDERR, 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n");
75 |     fwrite(STDERR, $e->getTraceAsString() . "\n");
76 |     exit(1);
77 | }
78 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Unit\Attributes;
 6 | 
 7 | use PhpMcp\Server\Attributes\CompletionProvider;
 8 | use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
 9 | use PhpMcp\Server\Defaults\ListCompletionProvider;
10 | use PhpMcp\Server\Defaults\EnumCompletionProvider;
11 | use PhpMcp\Server\Tests\Fixtures\Enums\StatusEnum;
12 | 
13 | it('can be constructed with provider class', function () {
14 |     $attribute = new CompletionProvider(provider: CompletionProviderFixture::class);
15 | 
16 |     expect($attribute->provider)->toBe(CompletionProviderFixture::class);
17 |     expect($attribute->values)->toBeNull();
18 |     expect($attribute->enum)->toBeNull();
19 | });
20 | 
21 | it('can be constructed with provider instance', function () {
22 |     $instance = new CompletionProviderFixture();
23 |     $attribute = new CompletionProvider(provider: $instance);
24 | 
25 |     expect($attribute->provider)->toBe($instance);
26 |     expect($attribute->values)->toBeNull();
27 |     expect($attribute->enum)->toBeNull();
28 | });
29 | 
30 | it('can be constructed with values array', function () {
31 |     $values = ['draft', 'published', 'archived'];
32 |     $attribute = new CompletionProvider(values: $values);
33 | 
34 |     expect($attribute->provider)->toBeNull();
35 |     expect($attribute->values)->toBe($values);
36 |     expect($attribute->enum)->toBeNull();
37 | });
38 | 
39 | it('can be constructed with enum class', function () {
40 |     $attribute = new CompletionProvider(enum: StatusEnum::class);
41 | 
42 |     expect($attribute->provider)->toBeNull();
43 |     expect($attribute->values)->toBeNull();
44 |     expect($attribute->enum)->toBe(StatusEnum::class);
45 | });
46 | 
47 | it('throws exception when no parameters provided', function () {
48 |     new CompletionProvider();
49 | })->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');
50 | 
51 | it('throws exception when multiple parameters provided', function () {
52 |     new CompletionProvider(
53 |         provider: CompletionProviderFixture::class,
54 |         values: ['test']
55 |     );
56 | })->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');
57 | 
58 | it('throws exception when all parameters provided', function () {
59 |     new CompletionProvider(
60 |         provider: CompletionProviderFixture::class,
61 |         values: ['test'],
62 |         enum: StatusEnum::class
63 |     );
64 | })->throws(\InvalidArgumentException::class, 'Only one of provider, values, or enum can be set');
65 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Tests\Unit\Defaults;
 6 | 
 7 | use PhpMcp\Server\Defaults\EnumCompletionProvider;
 8 | use PhpMcp\Server\Contracts\SessionInterface;
 9 | use Mockery;
10 | 
11 | enum StringEnum: string
12 | {
13 |     case DRAFT = 'draft';
14 |     case PUBLISHED = 'published';
15 |     case ARCHIVED = 'archived';
16 | }
17 | 
18 | enum IntEnum: int
19 | {
20 |     case LOW = 1;
21 |     case MEDIUM = 2;
22 |     case HIGH = 3;
23 | }
24 | 
25 | enum UnitEnum
26 | {
27 |     case ALPHA;
28 |     case BETA;
29 |     case GAMMA;
30 | }
31 | 
32 | beforeEach(function () {
33 |     $this->session = Mockery::mock(SessionInterface::class);
34 | });
35 | 
36 | it('creates provider from string-backed enum', function () {
37 |     $provider = new EnumCompletionProvider(StringEnum::class);
38 | 
39 |     $result = $provider->getCompletions('', $this->session);
40 | 
41 |     expect($result)->toBe(['draft', 'published', 'archived']);
42 | });
43 | 
44 | it('creates provider from int-backed enum using names', function () {
45 |     $provider = new EnumCompletionProvider(IntEnum::class);
46 | 
47 |     $result = $provider->getCompletions('', $this->session);
48 | 
49 |     expect($result)->toBe(['LOW', 'MEDIUM', 'HIGH']);
50 | });
51 | 
52 | it('creates provider from unit enum using names', function () {
53 |     $provider = new EnumCompletionProvider(UnitEnum::class);
54 | 
55 |     $result = $provider->getCompletions('', $this->session);
56 | 
57 |     expect($result)->toBe(['ALPHA', 'BETA', 'GAMMA']);
58 | });
59 | 
60 | it('filters string enum values by prefix', function () {
61 |     $provider = new EnumCompletionProvider(StringEnum::class);
62 | 
63 |     $result = $provider->getCompletions('ar', $this->session);
64 | 
65 |     expect($result)->toEqual(['archived']);
66 | });
67 | 
68 | it('filters unit enum values by prefix', function () {
69 |     $provider = new EnumCompletionProvider(UnitEnum::class);
70 | 
71 |     $result = $provider->getCompletions('A', $this->session);
72 | 
73 |     expect($result)->toBe(['ALPHA']);
74 | });
75 | 
76 | it('returns empty array when no values match prefix', function () {
77 |     $provider = new EnumCompletionProvider(StringEnum::class);
78 | 
79 |     $result = $provider->getCompletions('xyz', $this->session);
80 | 
81 |     expect($result)->toBe([]);
82 | });
83 | 
84 | it('throws exception for non-enum class', function () {
85 |     new EnumCompletionProvider(\stdClass::class);
86 | })->throws(\InvalidArgumentException::class, 'Class stdClass is not an enum');
87 | 
88 | it('throws exception for non-existent class', function () {
89 |     new EnumCompletionProvider('NonExistentClass');
90 | })->throws(\InvalidArgumentException::class, 'Class NonExistentClass is not an enum');
91 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\ComplexSchemaHttpExample;
 4 | 
 5 | use Mcp\ComplexSchemaHttpExample\Model\EventPriority;
 6 | use Mcp\ComplexSchemaHttpExample\Model\EventType;
 7 | use PhpMcp\Server\Attributes\McpTool;
 8 | use Psr\Log\LoggerInterface;
 9 | 
10 | class McpEventScheduler
11 | {
12 |     private LoggerInterface $logger;
13 | 
14 |     public function __construct(LoggerInterface $logger)
15 |     {
16 |         $this->logger = $logger;
17 |     }
18 | 
19 |     /**
20 |      * Schedules a new event.
21 |      * The inputSchema for this tool will reflect all parameter types and defaults.
22 |      *
23 |      * @param  string  $title  The title of the event.
24 |      * @param  string  $date  The date of the event (YYYY-MM-DD).
25 |      * @param  EventType  $type  The type of event.
26 |      * @param  string|null  $time  The time of the event (HH:MM), optional.
27 |      * @param  EventPriority  $priority  The priority of the event. Defaults to Normal.
28 |      * @param  string[]|null  $attendees  An optional list of attendee email addresses.
29 |      * @param  bool  $sendInvites  Send calendar invites to attendees? Defaults to true if attendees are provided.
30 |      * @return array Confirmation of the scheduled event.
31 |      */
32 |     #[McpTool(name: 'schedule_event')]
33 |     public function scheduleEvent(
34 |         string $title,
35 |         string $date,
36 |         EventType $type,
37 |         ?string $time = null, // Optional, nullable
38 |         EventPriority $priority = EventPriority::Normal, // Optional with enum default
39 |         ?array $attendees = null, // Optional array of strings, nullable
40 |         bool $sendInvites = true   // Optional with default
41 |     ): array {
42 |         $this->logger->info("Tool 'schedule_event' called", compact('title', 'date', 'type', 'time', 'priority', 'attendees', 'sendInvites'));
43 | 
44 |         // Simulate scheduling logic
45 |         $eventDetails = [
46 |             'title' => $title,
47 |             'date' => $date,
48 |             'type' => $type->value, // Use enum value
49 |             'time' => $time ?? 'All day',
50 |             'priority' => $priority->name, // Use enum name
51 |             'attendees' => $attendees ?? [],
52 |             'invites_will_be_sent' => ($attendees && $sendInvites),
53 |         ];
54 | 
55 |         // In a real app, this would interact with a calendar service
56 |         $this->logger->info('Event scheduled', ['details' => $eventDetails]);
57 | 
58 |         return [
59 |             'success' => true,
60 |             'message' => "Event '{$title}' scheduled successfully for {$date}.",
61 |             'event_details' => $eventDetails,
62 |         ];
63 |     }
64 | }
65 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Defaults;
  4 | 
  5 | use DateInterval;
  6 | use DateTime;
  7 | use Psr\SimpleCache\CacheInterface;
  8 | 
  9 | /**
 10 |  * Very basic PSR-16 array cache implementation (not for production).
 11 |  */
 12 | class ArrayCache implements CacheInterface
 13 | {
 14 |     private array $store = [];
 15 | 
 16 |     private array $expiries = [];
 17 | 
 18 |     public function get(string $key, mixed $default = null): mixed
 19 |     {
 20 |         if (! $this->has($key)) {
 21 |             return $default;
 22 |         }
 23 | 
 24 |         return $this->store[$key];
 25 |     }
 26 | 
 27 |     public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
 28 |     {
 29 |         $this->store[$key] = $value;
 30 |         $this->expiries[$key] = $this->calculateExpiry($ttl);
 31 | 
 32 |         return true;
 33 |     }
 34 | 
 35 |     public function delete(string $key): bool
 36 |     {
 37 |         unset($this->store[$key], $this->expiries[$key]);
 38 | 
 39 |         return true;
 40 |     }
 41 | 
 42 |     public function clear(): bool
 43 |     {
 44 |         $this->store = [];
 45 |         $this->expiries = [];
 46 | 
 47 |         return true;
 48 |     }
 49 | 
 50 |     public function getMultiple(iterable $keys, mixed $default = null): iterable
 51 |     {
 52 |         $result = [];
 53 |         foreach ($keys as $key) {
 54 |             $result[$key] = $this->get($key, $default);
 55 |         }
 56 | 
 57 |         return $result;
 58 |     }
 59 | 
 60 |     public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool
 61 |     {
 62 |         $expiry = $this->calculateExpiry($ttl);
 63 |         foreach ($values as $key => $value) {
 64 |             $this->store[$key] = $value;
 65 |             $this->expiries[$key] = $expiry;
 66 |         }
 67 | 
 68 |         return true;
 69 |     }
 70 | 
 71 |     public function deleteMultiple(iterable $keys): bool
 72 |     {
 73 |         foreach ($keys as $key) {
 74 |             unset($this->store[$key], $this->expiries[$key]);
 75 |         }
 76 | 
 77 |         return true;
 78 |     }
 79 | 
 80 |     public function has(string $key): bool
 81 |     {
 82 |         if (! isset($this->store[$key])) {
 83 |             return false;
 84 |         }
 85 |         // Check expiry
 86 |         if (isset($this->expiries[$key]) && $this->expiries[$key] !== null && time() >= $this->expiries[$key]) {
 87 |             $this->delete($key);
 88 | 
 89 |             return false;
 90 |         }
 91 | 
 92 |         return true;
 93 |     }
 94 | 
 95 |     private function calculateExpiry(DateInterval|int|null $ttl): ?int
 96 |     {
 97 |         if ($ttl === null) {
 98 |             return null; // No expiry
 99 |         }
100 |         if (is_int($ttl)) {
101 |             return time() + $ttl;
102 |         }
103 |         if ($ttl instanceof DateInterval) {
104 |             return (new DateTime())->add($ttl)->getTimestamp();
105 |         }
106 | 
107 |         // Invalid TTL type, treat as no expiry
108 |         return null;
109 |     }
110 | }
111 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | declare(strict_types=1);
 5 | 
 6 | require_once __DIR__ . '/../../../vendor/autoload.php';
 7 | 
 8 | use PhpMcp\Server\Server;
 9 | use PhpMcp\Server\Transports\HttpServerTransport;
10 | use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
11 | use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
12 | use PhpMcp\Server\Tests\Fixtures\General\PromptHandlerFixture;
13 | use PhpMcp\Server\Tests\Fixtures\General\RequestAttributeChecker;
14 | use PhpMcp\Server\Tests\Fixtures\Middlewares\HeaderMiddleware;
15 | use PhpMcp\Server\Tests\Fixtures\Middlewares\RequestAttributeMiddleware;
16 | use PhpMcp\Server\Tests\Fixtures\Middlewares\ShortCircuitMiddleware;
17 | use PhpMcp\Server\Tests\Fixtures\Middlewares\FirstMiddleware;
18 | use PhpMcp\Server\Tests\Fixtures\Middlewares\SecondMiddleware;
19 | use PhpMcp\Server\Tests\Fixtures\Middlewares\ThirdMiddleware;
20 | use PhpMcp\Server\Tests\Fixtures\Middlewares\ErrorMiddleware;
21 | use Psr\Log\AbstractLogger;
22 | use Psr\Log\NullLogger;
23 | 
24 | class StdErrLogger extends AbstractLogger
25 | {
26 |     public function log($level, \Stringable|string $message, array $context = []): void
27 |     {
28 |         fwrite(STDERR, sprintf("[%s] HTTP_SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
29 |     }
30 | }
31 | 
32 | $host = $argv[1] ?? '127.0.0.1';
33 | $port = (int)($argv[2] ?? 8990);
34 | $mcpPathPrefix = $argv[3] ?? 'mcp_http_test';
35 | 
36 | try {
37 |     $logger = new NullLogger();
38 | 
39 |     $server = Server::make()
40 |         ->withServerInfo('HttpIntegrationTestServer', '0.1.0')
41 |         ->withLogger($logger)
42 |         ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_http_tool')
43 |         ->withTool([RequestAttributeChecker::class, 'checkAttribute'], 'check_request_attribute_tool')
44 |         ->withResource([ResourceHandlerFixture::class, 'getStaticText'], "test://http/static", 'static_http_resource')
45 |         ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_http_prompt')
46 |         ->build();
47 | 
48 |     $middlewares = [
49 |         new HeaderMiddleware(),
50 |         new RequestAttributeMiddleware(),
51 |         new ShortCircuitMiddleware(),
52 |         new FirstMiddleware(),
53 |         new SecondMiddleware(),
54 |         new ThirdMiddleware(),
55 |         new ErrorMiddleware()
56 |     ];
57 | 
58 |     $transport = new HttpServerTransport($host, $port, $mcpPathPrefix, null, $middlewares);
59 |     $server->listen($transport);
60 | 
61 |     exit(0);
62 | } catch (\Throwable $e) {
63 |     fwrite(STDERR, "[HTTP_SERVER_CRITICAL_ERROR]\nHost:{$host} Port:{$port} Prefix:{$mcpPathPrefix}\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
64 |     exit(1);
65 | }
66 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | /*
 5 |     |--------------------------------------------------------------------------
 6 |     | MCP Stdio Calculator Server (Attribute Discovery)
 7 |     |--------------------------------------------------------------------------
 8 |     |
 9 |     | This server demonstrates using attribute-based discovery to find MCP
10 |     | elements (Tools, Resources) in the 'McpElements.php' file within this
11 |     | directory. It runs via the STDIO transport.
12 |     |
13 |     | To Use:
14 |     | 1. Ensure 'McpElements.php' defines classes with MCP attributes.
15 |     | 2. Configure your MCP Client (e.g., Cursor) for this server:
16 |     |
17 |     | {
18 |     |     "mcpServers": {
19 |     |         "php-stdio-calculator": {
20 |     |             "command": "php",
21 |     |             "args": ["/full/path/to/examples/01-discovery-stdio-calculator/server.php"]
22 |     |         }
23 |     |     }
24 |     | }
25 |     |
26 |     | The ServerBuilder builds the server instance, then $server->discover()
27 |     | scans the current directory (specified by basePath: __DIR__, scanDirs: ['.'])
28 |     | to find and register elements before listening on STDIN/STDOUT.
29 |     |
30 |     | If you provided a `CacheInterface` implementation to the ServerBuilder,
31 |     | the discovery process will be cached, so you can comment out the
32 |     | discovery call after the first run to speed up subsequent runs.
33 |     |
34 | */
35 | declare(strict_types=1);
36 | 
37 | chdir(__DIR__);
38 | require_once '../../vendor/autoload.php';
39 | require_once 'McpElements.php';
40 | 
41 | use PhpMcp\Server\Server;
42 | use PhpMcp\Server\Transports\StdioServerTransport;
43 | use Psr\Log\AbstractLogger;
44 | 
45 | class StderrLogger extends AbstractLogger
46 | {
47 |     public function log($level, \Stringable|string $message, array $context = []): void
48 |     {
49 |         fwrite(STDERR, sprintf(
50 |             "[%s] %s %s\n",
51 |             strtoupper($level),
52 |             $message,
53 |             empty($context) ? '' : json_encode($context)
54 |         ));
55 |     }
56 | }
57 | 
58 | try {
59 |     $logger = new StderrLogger();
60 |     $logger->info('Starting MCP Stdio Calculator Server...');
61 | 
62 |     $server = Server::make()
63 |         ->withServerInfo('Stdio Calculator', '1.1.0')
64 |         ->withLogger($logger)
65 |         ->build();
66 | 
67 |     $server->discover(__DIR__, ['.']);
68 | 
69 |     $transport = new StdioServerTransport();
70 | 
71 |     $server->listen($transport);
72 | 
73 |     $logger->info('Server listener stopped gracefully.');
74 |     exit(0);
75 | 
76 | } catch (\Throwable $e) {
77 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
78 |     fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
79 |     fwrite(STDERR, 'File: '.$e->getFile().':'.$e->getLine()."\n");
80 |     fwrite(STDERR, $e->getTraceAsString()."\n");
81 |     exit(1);
82 | }
83 | 
```

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

```php
 1 | <?php
 2 | 
 3 | use React\EventLoop\Loop;
 4 | use React\EventLoop\LoopInterface;
 5 | use React\EventLoop\TimerInterface;
 6 | use React\Promise\Promise;
 7 | use React\Promise\PromiseInterface;
 8 | use React\Socket\SocketServer;
 9 | 
10 | function getPrivateProperty(object $object, string $propertyName)
11 | {
12 |     $reflector = new ReflectionClass($object);
13 |     $property = $reflector->getProperty($propertyName);
14 |     $property->setAccessible(true);
15 |     return $property->getValue($object);
16 | }
17 | 
18 | function delay($time, ?LoopInterface $loop = null)
19 | {
20 |     if ($loop === null) {
21 |         $loop = Loop::get();
22 |     }
23 | 
24 |     /** @var TimerInterface $timer */
25 |     $timer = null;
26 |     return new Promise(function ($resolve) use ($loop, $time, &$timer) {
27 |         $timer = $loop->addTimer($time, function () use ($resolve) {
28 |             $resolve(null);
29 |         });
30 |     }, function () use (&$timer, $loop) {
31 |         $loop->cancelTimer($timer);
32 |         $timer = null;
33 | 
34 |         throw new \RuntimeException('Timer cancelled');
35 |     });
36 | }
37 | 
38 | function timeout(PromiseInterface $promise, $time, ?LoopInterface $loop = null)
39 | {
40 |     $canceller = null;
41 |     if (\method_exists($promise, 'cancel')) {
42 |         $canceller = function () use (&$promise) {
43 |             $promise->cancel();
44 |             $promise = null;
45 |         };
46 |     }
47 | 
48 |     if ($loop === null) {
49 |         $loop = Loop::get();
50 |     }
51 | 
52 |     return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
53 |         $timer = null;
54 |         $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
55 |             if ($timer) {
56 |                 $loop->cancelTimer($timer);
57 |             }
58 |             $timer = false;
59 |             $resolve($v);
60 |         }, function ($v) use (&$timer, $loop, $reject) {
61 |             if ($timer) {
62 |                 $loop->cancelTimer($timer);
63 |             }
64 |             $timer = false;
65 |             $reject($v);
66 |         });
67 | 
68 |         if ($timer === false) {
69 |             return;
70 |         }
71 | 
72 |         // start timeout timer which will cancel the input promise
73 |         $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject) {
74 |             $reject(new \RuntimeException('Timed out after ' . $time . ' seconds'));
75 | 
76 |             if (\method_exists($promise, 'cancel')) {
77 |                 $promise->cancel();
78 |             }
79 |             $promise = null;
80 |         });
81 |     }, $canceller);
82 | }
83 | 
84 | function findFreePort()
85 | {
86 |     $server = new SocketServer('127.0.0.1:0');
87 |     $address = $server->getAddress();
88 |     $port = $address ? parse_url($address, PHP_URL_PORT) : null;
89 |     $server->close();
90 |     if (!$port) {
91 |         throw new \RuntimeException("Could not find a free port for testing.");
92 |     }
93 |     return (int)$port;
94 | }
95 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Fixtures\Utils;
 4 | 
 5 | /**
 6 |  * Test stub for DocBlock array type parsing
 7 |  */
 8 | class DockBlockParserFixture
 9 | {
10 |     /**
11 |      * Method with simple array[] syntax
12 |      *
13 |      * @param string[] $strings Array of strings using [] syntax
14 |      * @param int[] $integers Array of integers using [] syntax
15 |      * @param bool[] $booleans Array of booleans using [] syntax
16 |      * @param float[] $floats Array of floats using [] syntax
17 |      * @param object[] $objects Array of objects using [] syntax
18 |      * @param \DateTime[] $dateTimeInstances Array of DateTime objects
19 |      */
20 |     public function simpleArraySyntax(
21 |         array $strings,
22 |         array $integers,
23 |         array $booleans,
24 |         array $floats,
25 |         array $objects,
26 |         array $dateTimeInstances
27 |     ): void {
28 |     }
29 | 
30 |     /**
31 |      * Method with array<T> generic syntax
32 |      *
33 |      * @param array<string> $strings Array of strings using generic syntax
34 |      * @param array<int> $integers Array of integers using generic syntax
35 |      * @param array<bool> $booleans Array of booleans using generic syntax
36 |      * @param array<float> $floats Array of floats using generic syntax
37 |      * @param array<object> $objects Array of objects using generic syntax
38 |      * @param array<\DateTime> $dateTimeInstances Array of DateTime objects using generic syntax
39 |      */
40 |     public function genericArraySyntax(
41 |         array $strings,
42 |         array $integers,
43 |         array $booleans,
44 |         array $floats,
45 |         array $objects,
46 |         array $dateTimeInstances
47 |     ): void {
48 |     }
49 | 
50 |     /**
51 |      * Method with nested array syntax
52 |      *
53 |      * @param array<array<string>> $nestedStringArrays Array of arrays of strings
54 |      * @param array<array<int>> $nestedIntArrays Array of arrays of integers
55 |      * @param string[][] $doubleStringArrays Array of arrays of strings using double []
56 |      * @param int[][] $doubleIntArrays Array of arrays of integers using double []
57 |      */
58 |     public function nestedArraySyntax(
59 |         array $nestedStringArrays,
60 |         array $nestedIntArrays,
61 |         array $doubleStringArrays,
62 |         array $doubleIntArrays
63 |     ): void {
64 |     }
65 | 
66 |     /**
67 |      * Method with object-like array syntax
68 |      *
69 |      * @param array{name: string, age: int} $person Simple object array with name and age
70 |      * @param array{id: int, title: string, tags: string[]} $article Article with array of tags
71 |      * @param array{user: array{id: int, name: string}, items: array<int>} $order Order with nested user object and array of item IDs
72 |      */
73 |     public function objectArraySyntax(
74 |         array $person,
75 |         array $article,
76 |         array $order
77 |     ): void {
78 |     }
79 | }
80 | 
```
Page 1/7FirstPrevNextLast