#
tokens: 47753/50000 31/154 files (page 2/7)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

--------------------------------------------------------------------------------
/src/Session/CacheSessionHandler.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\SimpleCache\CacheInterface;
10 | use Psr\Clock\ClockInterface;
11 | 
12 | class CacheSessionHandler implements SessionHandlerInterface
13 | {
14 |     private const SESSION_INDEX_KEY = 'mcp_session_index';
15 |     private array $sessionIndex = [];
16 |     private ClockInterface $clock;
17 | 
18 |     public function __construct(
19 |         public readonly CacheInterface $cache,
20 |         public readonly int $ttl = 3600,
21 |         ?ClockInterface $clock = null
22 |     ) {
23 |         $this->sessionIndex = $this->cache->get(self::SESSION_INDEX_KEY, []);
24 |         $this->clock = $clock ?? new SystemClock();
25 |     }
26 | 
27 |     public function read(string $sessionId): string|false
28 |     {
29 |         $session = $this->cache->get($sessionId, false);
30 |         if ($session === false) {
31 |             if (isset($this->sessionIndex[$sessionId])) {
32 |                 unset($this->sessionIndex[$sessionId]);
33 |                 $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
34 |             }
35 |             return false;
36 |         }
37 | 
38 |         if (!isset($this->sessionIndex[$sessionId])) {
39 |             $this->sessionIndex[$sessionId] = $this->clock->now()->getTimestamp();
40 |             $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
41 |             return $session;
42 |         }
43 | 
44 |         if ($this->clock->now()->getTimestamp() - $this->sessionIndex[$sessionId] > $this->ttl) {
45 |             $this->cache->delete($sessionId);
46 |             return false;
47 |         }
48 | 
49 |         return $session;
50 |     }
51 | 
52 |     public function write(string $sessionId, string $data): bool
53 |     {
54 |         $this->sessionIndex[$sessionId] = $this->clock->now()->getTimestamp();
55 |         $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
56 |         return $this->cache->set($sessionId, $data);
57 |     }
58 | 
59 |     public function destroy(string $sessionId): bool
60 |     {
61 |         unset($this->sessionIndex[$sessionId]);
62 |         $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
63 |         return $this->cache->delete($sessionId);
64 |     }
65 | 
66 |     public function gc(int $maxLifetime): array
67 |     {
68 |         $currentTime = $this->clock->now()->getTimestamp();
69 |         $deletedSessions = [];
70 | 
71 |         foreach ($this->sessionIndex as $sessionId => $timestamp) {
72 |             if ($currentTime - $timestamp > $maxLifetime) {
73 |                 $this->cache->delete($sessionId);
74 |                 unset($this->sessionIndex[$sessionId]);
75 |                 $deletedSessions[] = $sessionId;
76 |             }
77 |         }
78 | 
79 |         $this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
80 | 
81 |         return $deletedSessions;
82 |     }
83 | }
84 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace Mcp\DependenciesStdioExample;
 4 | 
 5 | use Mcp\DependenciesStdioExample\Services\StatsServiceInterface;
 6 | use Mcp\DependenciesStdioExample\Services\TaskRepositoryInterface;
 7 | use PhpMcp\Server\Attributes\McpResource;
 8 | use PhpMcp\Server\Attributes\McpTool;
 9 | use Psr\Log\LoggerInterface;
10 | 
11 | class McpTaskHandlers
12 | {
13 |     private TaskRepositoryInterface $taskRepo;
14 | 
15 |     private StatsServiceInterface $statsService;
16 | 
17 |     private LoggerInterface $logger;
18 | 
19 |     // Dependencies injected by the DI container
20 |     public function __construct(
21 |         TaskRepositoryInterface $taskRepo,
22 |         StatsServiceInterface $statsService,
23 |         LoggerInterface $logger
24 |     ) {
25 |         $this->taskRepo = $taskRepo;
26 |         $this->statsService = $statsService;
27 |         $this->logger = $logger;
28 |         $this->logger->info('McpTaskHandlers instantiated with dependencies.');
29 |     }
30 | 
31 |     /**
32 |      * Adds a new task for a given user.
33 |      *
34 |      * @param  string  $userId  The ID of the user.
35 |      * @param  string  $description  The task description.
36 |      * @return array The created task details.
37 |      */
38 |     #[McpTool(name: 'add_task')]
39 |     public function addTask(string $userId, string $description): array
40 |     {
41 |         $this->logger->info("Tool 'add_task' invoked", ['userId' => $userId]);
42 | 
43 |         return $this->taskRepo->addTask($userId, $description);
44 |     }
45 | 
46 |     /**
47 |      * Lists pending tasks for a specific user.
48 |      *
49 |      * @param  string  $userId  The ID of the user.
50 |      * @return array A list of tasks.
51 |      */
52 |     #[McpTool(name: 'list_user_tasks')]
53 |     public function listUserTasks(string $userId): array
54 |     {
55 |         $this->logger->info("Tool 'list_user_tasks' invoked", ['userId' => $userId]);
56 | 
57 |         return $this->taskRepo->getTasksForUser($userId);
58 |     }
59 | 
60 |     /**
61 |      * Marks a task as complete.
62 |      *
63 |      * @param  int  $taskId  The ID of the task to complete.
64 |      * @return array Status of the operation.
65 |      */
66 |     #[McpTool(name: 'complete_task')]
67 |     public function completeTask(int $taskId): array
68 |     {
69 |         $this->logger->info("Tool 'complete_task' invoked", ['taskId' => $taskId]);
70 |         $success = $this->taskRepo->completeTask($taskId);
71 | 
72 |         return ['success' => $success, 'message' => $success ? "Task {$taskId} completed." : "Task {$taskId} not found."];
73 |     }
74 | 
75 |     /**
76 |      * Provides current system statistics.
77 |      *
78 |      * @return array System statistics.
79 |      */
80 |     #[McpResource(uri: 'stats://system/overview', name: 'system_stats', mimeType: 'application/json')]
81 |     public function getSystemStatistics(): array
82 |     {
83 |         $this->logger->info("Resource 'stats://system/overview' invoked");
84 | 
85 |         return $this->statsService->getSystemStats();
86 |     }
87 | }
88 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Session;
 4 | 
 5 | use Psr\Log\LoggerInterface;
 6 | 
 7 | class SubscriptionManager
 8 | {
 9 |     /** @var array<string, array<string, true>>  Key: URI, Value: array of session IDs */
10 |     private array $resourceSubscribers = [];
11 | 
12 |     /** @var array<string, array<string, true>>  Key: Session ID, Value: array of URIs */
13 |     private array $sessionSubscriptions = [];
14 | 
15 |     public function __construct(
16 |         private readonly LoggerInterface $logger
17 |     ) {
18 |     }
19 | 
20 |     /**
21 |      * Subscribe a session to a resource
22 |      */
23 |     public function subscribe(string $sessionId, string $uri): void
24 |     {
25 |         // Add to both mappings for efficient lookup
26 |         $this->resourceSubscribers[$uri][$sessionId] = true;
27 |         $this->sessionSubscriptions[$sessionId][$uri] = true;
28 | 
29 |         $this->logger->debug('Session subscribed to resource', [
30 |             'sessionId' => $sessionId,
31 |             'uri' => $uri
32 |         ]);
33 |     }
34 | 
35 |     /**
36 |      * Unsubscribe a session from a resource
37 |      */
38 |     public function unsubscribe(string $sessionId, string $uri): void
39 |     {
40 |         unset($this->resourceSubscribers[$uri][$sessionId]);
41 |         unset($this->sessionSubscriptions[$sessionId][$uri]);
42 | 
43 |         // Clean up empty arrays
44 |         if (empty($this->resourceSubscribers[$uri])) {
45 |             unset($this->resourceSubscribers[$uri]);
46 |         }
47 | 
48 |         $this->logger->debug('Session unsubscribed from resource', [
49 |             'sessionId' => $sessionId,
50 |             'uri' => $uri
51 |         ]);
52 |     }
53 | 
54 |     /**
55 |      * Get all sessions subscribed to a resource
56 |      */
57 |     public function getSubscribers(string $uri): array
58 |     {
59 |         return array_keys($this->resourceSubscribers[$uri] ?? []);
60 |     }
61 | 
62 |     /**
63 |      * Check if a session is subscribed to a resource
64 |      */
65 |     public function isSubscribed(string $sessionId, string $uri): bool
66 |     {
67 |         return isset($this->sessionSubscriptions[$sessionId][$uri]);
68 |     }
69 | 
70 |     /**
71 |      * Clean up all subscriptions for a session
72 |      */
73 |     public function cleanupSession(string $sessionId): void
74 |     {
75 |         if (!isset($this->sessionSubscriptions[$sessionId])) {
76 |             return;
77 |         }
78 | 
79 |         $uris = array_keys($this->sessionSubscriptions[$sessionId]);
80 |         foreach ($uris as $uri) {
81 |             unset($this->resourceSubscribers[$uri][$sessionId]);
82 | 
83 |             // Clean up empty arrays
84 |             if (empty($this->resourceSubscribers[$uri])) {
85 |                 unset($this->resourceSubscribers[$uri]);
86 |             }
87 |         }
88 | 
89 |         unset($this->sessionSubscriptions[$sessionId]);
90 | 
91 |         $this->logger->debug('Cleaned up all subscriptions for session', [
92 |             'sessionId' => $sessionId,
93 |             'count' => count($uris)
94 |         ]);
95 |     }
96 | }
97 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace Mcp\DependenciesStdioExample\Services;
  4 | 
  5 | use Psr\Log\LoggerInterface;
  6 | 
  7 | // --- Mock Services ---
  8 | 
  9 | interface TaskRepositoryInterface
 10 | {
 11 |     public function addTask(string $userId, string $description): array;
 12 | 
 13 |     public function getTasksForUser(string $userId): array;
 14 | 
 15 |     public function getAllTasks(): array;
 16 | 
 17 |     public function completeTask(int $taskId): bool;
 18 | }
 19 | 
 20 | class InMemoryTaskRepository implements TaskRepositoryInterface
 21 | {
 22 |     private array $tasks = [];
 23 | 
 24 |     private int $nextTaskId = 1;
 25 | 
 26 |     private LoggerInterface $logger;
 27 | 
 28 |     public function __construct(LoggerInterface $logger)
 29 |     {
 30 |         $this->logger = $logger;
 31 |         // Add some initial tasks
 32 |         $this->addTask('user1', 'Buy groceries');
 33 |         $this->addTask('user1', 'Write MCP example');
 34 |         $this->addTask('user2', 'Review PR');
 35 |     }
 36 | 
 37 |     public function addTask(string $userId, string $description): array
 38 |     {
 39 |         $task = [
 40 |             'id' => $this->nextTaskId++,
 41 |             'userId' => $userId,
 42 |             'description' => $description,
 43 |             'completed' => false,
 44 |             'createdAt' => date('c'),
 45 |         ];
 46 |         $this->tasks[$task['id']] = $task;
 47 |         $this->logger->info('Task added', ['id' => $task['id'], 'user' => $userId]);
 48 | 
 49 |         return $task;
 50 |     }
 51 | 
 52 |     public function getTasksForUser(string $userId): array
 53 |     {
 54 |         return array_values(array_filter($this->tasks, fn ($task) => $task['userId'] === $userId && ! $task['completed']));
 55 |     }
 56 | 
 57 |     public function getAllTasks(): array
 58 |     {
 59 |         return array_values($this->tasks);
 60 |     }
 61 | 
 62 |     public function completeTask(int $taskId): bool
 63 |     {
 64 |         if (isset($this->tasks[$taskId])) {
 65 |             $this->tasks[$taskId]['completed'] = true;
 66 |             $this->logger->info('Task completed', ['id' => $taskId]);
 67 | 
 68 |             return true;
 69 |         }
 70 | 
 71 |         return false;
 72 |     }
 73 | }
 74 | 
 75 | interface StatsServiceInterface
 76 | {
 77 |     public function getSystemStats(): array;
 78 | }
 79 | 
 80 | class SystemStatsService implements StatsServiceInterface
 81 | {
 82 |     private TaskRepositoryInterface $taskRepository;
 83 | 
 84 |     public function __construct(TaskRepositoryInterface $taskRepository)
 85 |     {
 86 |         $this->taskRepository = $taskRepository;
 87 |     }
 88 | 
 89 |     public function getSystemStats(): array
 90 |     {
 91 |         $allTasks = $this->taskRepository->getAllTasks();
 92 |         $completed = count(array_filter($allTasks, fn ($task) => $task['completed']));
 93 |         $pending = count($allTasks) - $completed;
 94 | 
 95 |         return [
 96 |             'total_tasks' => count($allTasks),
 97 |             'completed_tasks' => $completed,
 98 |             'pending_tasks' => $pending,
 99 |             'server_uptime_seconds' => time() - $_SERVER['REQUEST_TIME_FLOAT'], // Approx uptime for CLI script
100 |         ];
101 |     }
102 | }
103 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | /*
 5 |     |--------------------------------------------------------------------------
 6 |     | MCP Stdio Server (Manual Element Registration)
 7 |     |--------------------------------------------------------------------------
 8 |     |
 9 |     | This server demonstrates how to manually register all MCP elements
10 |     | (Tools, Resources, Prompts, ResourceTemplates) using the ServerBuilder's
11 |     | fluent `withTool()`, `withResource()`, etc., methods.
12 |     | It does NOT use attribute discovery. Handlers are in 'SimpleHandlers.php'.
13 |     | It runs via the STDIO transport.
14 |     |
15 |     | To Use:
16 |     | 1. Configure your MCP Client (e.g., Cursor) for this server:
17 |     |
18 |     | {
19 |     |     "mcpServers": {
20 |     |         "php-stdio-manual": {
21 |     |             "command": "php",
22 |     |             "args": ["/full/path/to/examples/03-manual-registration-stdio/server.php"]
23 |     |         }
24 |     |     }
25 |     | }
26 |     |
27 |     | All elements are explicitly defined during the ServerBuilder chain.
28 |     | The $server->discover() method is NOT called.
29 |     |
30 | */
31 | 
32 | declare(strict_types=1);
33 | 
34 | chdir(__DIR__);
35 | require_once '../../vendor/autoload.php';
36 | require_once './SimpleHandlers.php';
37 | 
38 | use Mcp\ManualStdioExample\SimpleHandlers;
39 | use PhpMcp\Server\Defaults\BasicContainer;
40 | use PhpMcp\Server\Server;
41 | use PhpMcp\Server\Transports\StdioServerTransport;
42 | use Psr\Log\AbstractLogger;
43 | use Psr\Log\LoggerInterface;
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 Manual Registration (Stdio) Server...');
56 | 
57 |     $container = new BasicContainer();
58 |     $container->set(LoggerInterface::class, $logger);
59 | 
60 |     $server = Server::make()
61 |         ->withServerInfo('Manual Reg Server', '1.0.0')
62 |         ->withLogger($logger)
63 |         ->withContainer($container)
64 |         ->withTool([SimpleHandlers::class, 'echoText'], 'echo_text')
65 |         ->withResource([SimpleHandlers::class, 'getAppVersion'], 'app://version', 'application_version', mimeType: 'text/plain')
66 |         ->withPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting')
67 |         ->withResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
68 |         ->build();
69 | 
70 |     $transport = new StdioServerTransport();
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".$e."\n");
78 |     exit(1);
79 | }
80 | 
```

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

```php
 1 | <?php
 2 | 
 3 | namespace PhpMcp\Server\Tests\Unit;
 4 | 
 5 | use Mockery;
 6 | use PhpMcp\Schema\Implementation;
 7 | use PhpMcp\Schema\ServerCapabilities;
 8 | use PhpMcp\Server\Configuration;
 9 | use Psr\Container\ContainerInterface;
10 | use Psr\Log\LoggerInterface;
11 | use Psr\SimpleCache\CacheInterface;
12 | use React\EventLoop\LoopInterface;
13 | 
14 | beforeEach(function () {
15 |     $this->serverInfo = Implementation::make('TestServer', '1.1.0');
16 |     $this->logger = Mockery::mock(LoggerInterface::class);
17 |     $this->loop = Mockery::mock(LoopInterface::class);
18 |     $this->cache = Mockery::mock(CacheInterface::class);
19 |     $this->container = Mockery::mock(ContainerInterface::class);
20 |     $this->capabilities = ServerCapabilities::make();
21 | });
22 | 
23 | afterEach(function () {
24 |     Mockery::close();
25 | });
26 | 
27 | it('constructs configuration object with all properties', function () {
28 |     $paginationLimit = 100;
29 |     $config = new Configuration(
30 |         serverInfo: $this->serverInfo,
31 |         capabilities: $this->capabilities,
32 |         logger: $this->logger,
33 |         loop: $this->loop,
34 |         cache: $this->cache,
35 |         container: $this->container,
36 |         paginationLimit: $paginationLimit
37 |     );
38 | 
39 |     expect($config->serverInfo)->toBe($this->serverInfo);
40 |     expect($config->capabilities)->toBe($this->capabilities);
41 |     expect($config->logger)->toBe($this->logger);
42 |     expect($config->loop)->toBe($this->loop);
43 |     expect($config->cache)->toBe($this->cache);
44 |     expect($config->container)->toBe($this->container);
45 |     expect($config->paginationLimit)->toBe($paginationLimit);
46 | });
47 | 
48 | it('constructs configuration object with default pagination limit', function () {
49 |     $config = new Configuration(
50 |         serverInfo: $this->serverInfo,
51 |         capabilities: $this->capabilities,
52 |         logger: $this->logger,
53 |         loop: $this->loop,
54 |         cache: $this->cache,
55 |         container: $this->container
56 |     );
57 | 
58 |     expect($config->paginationLimit)->toBe(50); // Default value
59 | });
60 | 
61 | it('constructs configuration object with null cache', function () {
62 |     $config = new Configuration(
63 |         serverInfo: $this->serverInfo,
64 |         capabilities: $this->capabilities,
65 |         logger: $this->logger,
66 |         loop: $this->loop,
67 |         cache: null,
68 |         container: $this->container
69 |     );
70 | 
71 |     expect($config->cache)->toBeNull();
72 | });
73 | 
74 | it('constructs configuration object with specific capabilities', function () {
75 |     $customCaps = ServerCapabilities::make(
76 |         resourcesSubscribe: true,
77 |         logging: true,
78 |     );
79 | 
80 |     $config = new Configuration(
81 |         serverInfo: $this->serverInfo,
82 |         capabilities: $customCaps,
83 |         logger: $this->logger,
84 |         loop: $this->loop,
85 |         cache: null,
86 |         container: $this->container
87 |     );
88 | 
89 |     expect($config->capabilities)->toBe($customCaps);
90 |     expect($config->capabilities->resourcesSubscribe)->toBeTrue();
91 |     expect($config->capabilities->logging)->toBeTrue();
92 | });
93 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | /*
 5 |     |--------------------------------------------------------------------------
 6 |     | MCP HTTP Server (Combined Manual & Discovered Elements)
 7 |     |--------------------------------------------------------------------------
 8 |     |
 9 |     | This server demonstrates a combination of manual element registration
10 |     | via the ServerBuilder and attribute-based discovery.
11 |     | - Manually registered elements are defined in 'ManualHandlers.php'.
12 |     | - Discoverable elements are in 'DiscoveredElements.php'.
13 |     |
14 |     | It runs via the HTTP transport.
15 |     |
16 |     | This example also shows precedence: if a manually registered element
17 |     | has the same identifier (e.g., URI for a resource, or name for a tool)
18 |     | as a discovered one, the manual registration takes priority.
19 |     |
20 |     | To Use:
21 |     | 1. Run this script from your CLI: `php server.php`
22 |     |    The server will listen on http://127.0.0.1:8081 by default.
23 |     | 2. Configure your MCP Client (e.g., Cursor):
24 |     |
25 |     | {
26 |     |     "mcpServers": {
27 |     |         "php-http-combined": {
28 |     |             "url": "http://127.0.0.1:8081/mcp_combined/sse" // Note the prefix
29 |     |         }
30 |     |     }
31 |     | }
32 |     |
33 |     | Manual elements are registered during ServerBuilder->build().
34 |     | Then, $server->discover() scans for attributed elements.
35 |     |
36 | */
37 | 
38 | declare(strict_types=1);
39 | 
40 | chdir(__DIR__);
41 | require_once '../../vendor/autoload.php';
42 | require_once './DiscoveredElements.php';
43 | require_once './ManualHandlers.php';
44 | 
45 | use Mcp\CombinedHttpExample\Manual\ManualHandlers;
46 | use PhpMcp\Server\Defaults\BasicContainer;
47 | use PhpMcp\Server\Server;
48 | use PhpMcp\Server\Transports\HttpServerTransport;
49 | use Psr\Log\AbstractLogger;
50 | use Psr\Log\LoggerInterface;
51 | 
52 | class StderrLogger extends AbstractLogger
53 | {
54 |     public function log($level, \Stringable|string $message, array $context = []): void
55 |     {
56 |         fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
57 |     }
58 | }
59 | 
60 | try {
61 |     $logger = new StderrLogger();
62 |     $logger->info('Starting MCP Combined Registration (HTTP) Server...');
63 | 
64 |     $container = new BasicContainer();
65 |     $container->set(LoggerInterface::class, $logger); // ManualHandlers needs LoggerInterface
66 | 
67 |     $server = Server::make()
68 |         ->withServerInfo('Combined HTTP Server', '1.0.0')
69 |         ->withLogger($logger)
70 |         ->withContainer($container)
71 |         ->withTool([ManualHandlers::class, 'manualGreeter'])
72 |         ->withResource(
73 |             [ManualHandlers::class, 'getPriorityConfigManual'],
74 |             'config://priority',
75 |             'priority_config_manual',
76 |         )
77 |         ->build();
78 | 
79 |     // Now, run discovery. Discovered elements will be added.
80 |     // If 'config://priority' was discovered, the manual one takes precedence.
81 |     $server->discover(__DIR__, scanDirs: ['.']);
82 | 
83 |     $transport = new HttpServerTransport('127.0.0.1', 8081, 'mcp_combined');
84 | 
85 |     $server->listen($transport);
86 | 
87 |     $logger->info('Server listener stopped gracefully.');
88 |     exit(0);
89 | 
90 | } catch (\Throwable $e) {
91 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n".$e."\n");
92 |     exit(1);
93 | }
94 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Fixtures\General;
  4 | 
  5 | use PhpMcp\Schema\Content\TextContent;
  6 | use PhpMcp\Schema\Content\ImageContent;
  7 | use PhpMcp\Schema\Content\AudioContent;
  8 | use PhpMcp\Server\Context;
  9 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
 10 | use Psr\Log\LoggerInterface;
 11 | 
 12 | class ToolHandlerFixture
 13 | {
 14 |     public function __construct()
 15 |     {
 16 |     }
 17 | 
 18 |     public function greet(string $name): string
 19 |     {
 20 |         return "Hello, {$name}!";
 21 |     }
 22 | 
 23 |     public function sum(int $a, int $b): int
 24 |     {
 25 |         return $a + $b;
 26 |     }
 27 | 
 28 |     public function optionalParamsTool(string $required, ?string $optional = "default_val"): string
 29 |     {
 30 |         return "{$required} and {$optional}";
 31 |     }
 32 | 
 33 |     public function noParamsTool(): array
 34 |     {
 35 |         return ['status' => 'ok', 'timestamp' => time()];
 36 |     }
 37 | 
 38 |     public function processBackedEnum(BackedStringEnum $status): string
 39 |     {
 40 |         return "Status processed: " . $status->value;
 41 |     }
 42 | 
 43 |     public function returnString(): string
 44 |     {
 45 |         return "This is a string result.";
 46 |     }
 47 | 
 48 |     public function returnInteger(): int
 49 |     {
 50 |         return 12345;
 51 |     }
 52 | 
 53 |     public function returnFloat(): float
 54 |     {
 55 |         return 67.89;
 56 |     }
 57 | 
 58 |     public function returnBooleanTrue(): bool
 59 |     {
 60 |         return true;
 61 |     }
 62 | 
 63 |     public function returnBooleanFalse(): bool
 64 |     {
 65 |         return false;
 66 |     }
 67 | 
 68 |     public function returnNull(): ?string
 69 |     {
 70 |         return null;
 71 |     }
 72 | 
 73 |     public function returnArray(): array
 74 |     {
 75 |         return ['message' => 'Array result', 'data' => [1, 2, 3]];
 76 |     }
 77 | 
 78 |     public function returnStdClass(): \stdClass
 79 |     {
 80 |         $obj = new \stdClass();
 81 |         $obj->property = "value";
 82 |         return $obj;
 83 |     }
 84 | 
 85 |     public function returnTextContent(): TextContent
 86 |     {
 87 |         return TextContent::make("Pre-formatted TextContent.");
 88 |     }
 89 | 
 90 |     public function returnImageContent(): ImageContent
 91 |     {
 92 |         return ImageContent::make("base64data==", "image/png");
 93 |     }
 94 | 
 95 |     public function returnAudioContent(): AudioContent
 96 |     {
 97 |         return AudioContent::make("base64audio==", "audio/mp3");
 98 |     }
 99 | 
100 |     public function returnArrayOfContent(): array
101 |     {
102 |         return [
103 |             TextContent::make("Part 1"),
104 |             ImageContent::make("imgdata", "image/jpeg")
105 |         ];
106 |     }
107 | 
108 |     public function returnMixedArray(): array
109 |     {
110 |         return [
111 |             "A raw string",
112 |             TextContent::make("A TextContent object"),
113 |             123,
114 |             true,
115 |             null,
116 |             ['nested_key' => 'nested_value', 'sub_array' => [4, 5]],
117 |             ImageContent::make("img_data_mixed", "image/gif"),
118 |             (object)['obj_prop' => 'obj_val']
119 |         ];
120 |     }
121 | 
122 |     public function returnEmptyArray(): array
123 |     {
124 |         return [];
125 |     }
126 | 
127 |     public function toolThatThrows(): void
128 |     {
129 |         throw new \InvalidArgumentException("Something went wrong in the tool.");
130 |     }
131 | 
132 |     public function toolUnencodableResult()
133 |     {
134 |         return fopen('php://memory', 'r');
135 |     }
136 | 
137 |     public function toolReadsContext(Context $context): string
138 |     {
139 |         if (!$context->request) {
140 |             return "No request instance present";
141 |         }
142 | 
143 |         return $context->request->getHeaderLine('X-Test-Header') ?: "No X-Test-Header";
144 |     }
145 | }
146 | 
```

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

```php
 1 | #!/usr/bin/env php
 2 | <?php
 3 | 
 4 | /*
 5 |     |--------------------------------------------------------------------------
 6 |     | MCP HTTP Server with Complex Tool Schema (Event Scheduler)
 7 |     |--------------------------------------------------------------------------
 8 |     |
 9 |     | This example demonstrates how to define an MCP Tool with a more complex
10 |     | input schema, utilizing various PHP types, optional parameters, default
11 |     | values, and backed Enums. The server automatically generates the
12 |     | corresponding JSON Schema for the tool's input.
13 |     |
14 |     | Scenario:
15 |     | An "Event Scheduler" tool that allows scheduling events with details like
16 |     | title, date, time (optional), type (enum), priority (enum with default),
17 |     | attendees (optional list), and invite preferences (boolean with default).
18 |     |
19 |     | Key Points:
20 |     |   - The `schedule_event` tool in `McpEventScheduler.php` showcases:
21 |     |       - Required string parameters (`title`, `date`).
22 |     |       - A required backed string enum parameter (`EventType $type`).
23 |     |       - Optional nullable string (`?string $time = null`).
24 |     |       - Optional backed integer enum with a default value (`EventPriority $priority = EventPriority::Normal`).
25 |     |       - Optional nullable array of strings (`?array $attendees = null`).
26 |     |       - Optional boolean with a default value (`bool $sendInvites = true`).
27 |     |   - PHP type hints and default values are used by `SchemaGenerator` (internal)
28 |     |     to create the `inputSchema` for the tool.
29 |     |   - This example uses attribute-based discovery and the HTTP transport.
30 |     |
31 |     | To Use:
32 |     | 1. Run this script: `php server.php` (from this directory)
33 |     |    The server will listen on http://127.0.0.1:8082 by default.
34 |     | 2. Configure your MCP Client (e.g., Cursor) for this server:
35 |     |
36 |     | {
37 |     |     "mcpServers": {
38 |     |         "php-http-complex-scheduler": {
39 |     |             "url": "http://127.0.0.1:8082/mcp_scheduler/sse" // Note the prefix
40 |     |         }
41 |     |     }
42 |     | }
43 |     |
44 |     | Connect your client, list tools, and inspect the 'inputSchema' for the
45 |     | 'schedule_event' tool. Prompt your LLM with question to test the tool.
46 |     |
47 | */
48 | 
49 | declare(strict_types=1);
50 | 
51 | chdir(__DIR__);
52 | require_once '../../vendor/autoload.php';
53 | require_once './EventTypes.php'; // Include enums
54 | require_once './McpEventScheduler.php';
55 | 
56 | use PhpMcp\Server\Defaults\BasicContainer;
57 | use PhpMcp\Server\Server;
58 | use PhpMcp\Server\Transports\HttpServerTransport;
59 | use Psr\Log\AbstractLogger;
60 | use Psr\Log\LoggerInterface;
61 | 
62 | class StderrLogger extends AbstractLogger
63 | {
64 |     public function log($level, \Stringable|string $message, array $context = []): void
65 |     {
66 |         fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
67 |     }
68 | }
69 | 
70 | try {
71 |     $logger = new StderrLogger();
72 |     $logger->info('Starting MCP Complex Schema HTTP Server...');
73 | 
74 |     $container = new BasicContainer();
75 |     $container->set(LoggerInterface::class, $logger);
76 | 
77 |     $server = Server::make()
78 |         ->withServerInfo('Event Scheduler Server', '1.0.0')
79 |         ->withLogger($logger)
80 |         ->withContainer($container)
81 |         ->build();
82 | 
83 |     $server->discover(__DIR__, ['.']);
84 | 
85 |     $transport = new HttpServerTransport('127.0.0.1', 8082, 'mcp_scheduler');
86 |     $server->listen($transport);
87 | 
88 |     $logger->info('Server listener stopped gracefully.');
89 |     exit(0);
90 | 
91 | } catch (\Throwable $e) {
92 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n".$e."\n");
93 |     exit(1);
94 | }
95 | 
```

--------------------------------------------------------------------------------
/tests/Fixtures/ServerScripts/StreamableHttpTestServer.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\StreamableHttpServerTransport;
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 PhpMcp\Server\Defaults\InMemoryEventStore;
22 | use Psr\Log\AbstractLogger;
23 | use Psr\Log\NullLogger;
24 | 
25 | class StdErrLogger extends AbstractLogger
26 | {
27 |     public function log($level, \Stringable|string $message, array $context = []): void
28 |     {
29 |         fwrite(STDERR, sprintf("[%s] SERVER_LOG: %s %s\n", strtoupper((string)$level), $message, empty($context) ? '' : json_encode($context)));
30 |     }
31 | }
32 | 
33 | $host = $argv[1] ?? '127.0.0.1';
34 | $port = (int)($argv[2] ?? 8992);
35 | $mcpPath = $argv[3] ?? 'mcp_streamable_test';
36 | $enableJsonResponse = filter_var($argv[4] ?? 'true', FILTER_VALIDATE_BOOLEAN);
37 | $useEventStore = filter_var($argv[5] ?? 'false', FILTER_VALIDATE_BOOLEAN);
38 | $stateless = filter_var($argv[6] ?? 'false', FILTER_VALIDATE_BOOLEAN);
39 | 
40 | try {
41 |     $logger = new NullLogger();
42 |     $logger->info("Starting StreamableHttpTestServer on {$host}:{$port}/{$mcpPath}, JSON Mode: " . ($enableJsonResponse ? 'ON' : 'OFF') . ", Stateless: " . ($stateless ? 'ON' : 'OFF'));
43 | 
44 |     $eventStore = $useEventStore ? new InMemoryEventStore() : null;
45 | 
46 |     $server = Server::make()
47 |         ->withServerInfo('StreamableHttpIntegrationServer', '0.1.0')
48 |         ->withLogger($logger)
49 |         ->withTool([ToolHandlerFixture::class, 'greet'], 'greet_streamable_tool')
50 |         ->withTool([ToolHandlerFixture::class, 'sum'], 'sum_streamable_tool') // For batch testing
51 |         ->withTool([ToolHandlerFixture::class, 'toolReadsContext'], 'tool_reads_context') // for Context testing
52 |         ->withTool([RequestAttributeChecker::class, 'checkAttribute'], 'check_request_attribute_tool')
53 |         ->withResource([ResourceHandlerFixture::class, 'getStaticText'], "test://streamable/static", 'static_streamable_resource')
54 |         ->withPrompt([PromptHandlerFixture::class, 'generateSimpleGreeting'], 'simple_streamable_prompt')
55 |         ->build();
56 | 
57 |     $middlewares = [
58 |         new HeaderMiddleware(),
59 |         new RequestAttributeMiddleware(),
60 |         new ShortCircuitMiddleware(),
61 |         new FirstMiddleware(),
62 |         new SecondMiddleware(),
63 |         new ThirdMiddleware(),
64 |         new ErrorMiddleware()
65 |     ];
66 | 
67 |     $transport = new StreamableHttpServerTransport(
68 |         host: $host,
69 |         port: $port,
70 |         mcpPath: $mcpPath,
71 |         enableJsonResponse: $enableJsonResponse,
72 |         stateless: $stateless,
73 |         eventStore: $eventStore,
74 |         middlewares: $middlewares
75 |     );
76 | 
77 |     $server->listen($transport);
78 | 
79 |     $logger->info("StreamableHttpTestServer listener stopped on {$host}:{$port}.");
80 |     exit(0);
81 | } catch (\Throwable $e) {
82 |     fwrite(STDERR, "[STREAMABLE_HTTP_SERVER_CRITICAL_ERROR]\nHost:{$host} Port:{$port} Prefix:{$mcpPath}\n" . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
83 |     exit(1);
84 | }
85 | 
```

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

```php
 1 | <?php
 2 | 
 3 | declare(strict_types=1);
 4 | 
 5 | namespace PhpMcp\Server\Utils;
 6 | 
 7 | use InvalidArgumentException;
 8 | use ReflectionMethod;
 9 | use ReflectionException;
10 | 
11 | /**
12 |  * Utility class to validate and resolve MCP element handlers.
13 |  */
14 | class HandlerResolver
15 | {
16 |     /**
17 |      * Validates and resolves a handler to a ReflectionMethod or ReflectionFunction instance.
18 |      *
19 |      * A handler can be:
20 |      * - A Closure: function() { ... }
21 |      * - An array: [ClassName::class, 'methodName'] (instance method)
22 |      * - An array: [ClassName::class, 'staticMethod'] (static method, if callable)
23 |      * - A string: InvokableClassName::class (which will resolve to its '__invoke' method)
24 |      *
25 |      * @param \Closure|array|string $handler The handler to resolve.
26 |      * @return \ReflectionMethod|\ReflectionFunction
27 |      *
28 |      * @throws InvalidArgumentException If the handler format is invalid, the class/method doesn't exist,
29 |      *                                  or the method is unsuitable (e.g., private, abstract).
30 |      */
31 |     public static function resolve(\Closure|array|string $handler): \ReflectionMethod|\ReflectionFunction
32 |     {
33 |         // Handle Closures
34 |         if ($handler instanceof \Closure) {
35 |             return new \ReflectionFunction($handler);
36 |         }
37 | 
38 |         $className = null;
39 |         $methodName = null;
40 | 
41 |         if (is_array($handler)) {
42 |             if (count($handler) !== 2 || !isset($handler[0]) || !isset($handler[1]) || !is_string($handler[0]) || !is_string($handler[1])) {
43 |                 throw new InvalidArgumentException('Invalid array handler format. Expected [ClassName::class, \'methodName\'].');
44 |             }
45 |             [$className, $methodName] = $handler;
46 |             if (!class_exists($className)) {
47 |                 throw new InvalidArgumentException("Handler class '{$className}' not found for array handler.");
48 |             }
49 |             if (!method_exists($className, $methodName)) {
50 |                 throw new InvalidArgumentException("Handler method '{$methodName}' not found in class '{$className}' for array handler.");
51 |             }
52 |         } elseif (is_string($handler) && class_exists($handler)) {
53 |             $className = $handler;
54 |             $methodName = '__invoke';
55 |             if (!method_exists($className, $methodName)) {
56 |                 throw new InvalidArgumentException("Invokable handler class '{$className}' must have a public '__invoke' method.");
57 |             }
58 |         } else {
59 |             throw new InvalidArgumentException('Invalid handler format. Expected Closure, [ClassName::class, \'methodName\'] or InvokableClassName::class string.');
60 |         }
61 | 
62 |         try {
63 |             $reflectionMethod = new ReflectionMethod($className, $methodName);
64 | 
65 |             // For discovered elements (non-manual), still reject static methods
66 |             // For manual elements, we'll allow static methods since they're callable
67 |             if (!$reflectionMethod->isPublic()) {
68 |                 throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' must be public.");
69 |             }
70 |             if ($reflectionMethod->isAbstract()) {
71 |                 throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' cannot be abstract.");
72 |             }
73 |             if ($reflectionMethod->isConstructor() || $reflectionMethod->isDestructor()) {
74 |                 throw new InvalidArgumentException("Handler method '{$className}::{$methodName}' cannot be a constructor or destructor.");
75 |             }
76 | 
77 |             return $reflectionMethod;
78 |         } catch (ReflectionException $e) {
79 |             // This typically occurs if class_exists passed but ReflectionMethod still fails (rare)
80 |             throw new InvalidArgumentException("Reflection error for handler '{$className}::{$methodName}': {$e->getMessage()}", 0, $e);
81 |         }
82 |     }
83 | }
84 | 
```

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

```php
  1 | #!/usr/bin/env php
  2 | <?php
  3 | 
  4 | /*
  5 |     |--------------------------------------------------------------------------
  6 |     | MCP Stdio Server with Custom Dependencies (Task Manager)
  7 |     |--------------------------------------------------------------------------
  8 |     |
  9 |     | This example demonstrates how to use a PSR-11 Dependency Injection (DI)
 10 |     | container (PhpMcp\Server\Defaults\BasicContainer in this case) to inject
 11 |     | custom services (like a TaskRepositoryInterface or StatsServiceInterface)
 12 |     | into your MCP element handler classes.
 13 |     |
 14 |     | Scenario:
 15 |     | A simple Task Management system where:
 16 |     |   - Tools allow adding tasks, listing tasks for a user, and completing tasks.
 17 |     |   - A Resource provides system statistics (total tasks, pending, etc.).
 18 |     |   - Handlers in 'McpTaskHandlers.php' depend on service interfaces.
 19 |     |   - Concrete service implementations are in 'Services.php'.
 20 |     |
 21 |     | Key Points:
 22 |     |   - The `ServerBuilder` is configured with `->withContainer($container)`.
 23 |     |   - The DI container is set up with bindings for service interfaces to
 24 |     |     their concrete implementations (e.g., TaskRepositoryInterface -> InMemoryTaskRepository).
 25 |     |   - The `McpTaskHandlers` class receives its dependencies (TaskRepositoryInterface,
 26 |     |     StatsServiceInterface, LoggerInterface) via constructor injection, resolved by
 27 |     |     the DI container when the Processor needs an instance of McpTaskHandlers.
 28 |     |   - This example uses attribute-based discovery via `$server->discover()`.
 29 |     |   - It runs using the STDIO transport.
 30 |     |
 31 |     | To Use:
 32 |     | 1. Run this script: `php server.php` (from this directory)
 33 |     | 2. Configure your MCP Client (e.g., Cursor) for this server:
 34 |     |
 35 |     | {
 36 |     |     "mcpServers": {
 37 |     |         "php-stdio-deps-taskmgr": {
 38 |     |             "command": "php",
 39 |     |             "args": ["/full/path/to/examples/06-custom-dependencies-stdio/server.php"]
 40 |     |         }
 41 |     |     }
 42 |     | }
 43 |     |
 44 |     | Interact with tools like 'add_task', 'list_user_tasks', 'complete_task'
 45 |     | and read the resource 'stats://system/overview'.
 46 |     |
 47 | */
 48 | 
 49 | declare(strict_types=1);
 50 | 
 51 | chdir(__DIR__);
 52 | require_once '../../vendor/autoload.php';
 53 | require_once './Services.php';
 54 | require_once './McpTaskHandlers.php';
 55 | 
 56 | use Mcp\DependenciesStdioExample\Services;
 57 | use PhpMcp\Server\Defaults\BasicContainer;
 58 | use PhpMcp\Server\Server;
 59 | use PhpMcp\Server\Transports\StdioServerTransport;
 60 | use Psr\Log\AbstractLogger;
 61 | use Psr\Log\LoggerInterface;
 62 | 
 63 | class StderrLogger extends AbstractLogger
 64 | {
 65 |     public function log($level, \Stringable|string $message, array $context = []): void
 66 |     {
 67 |         fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
 68 |     }
 69 | }
 70 | 
 71 | try {
 72 |     $logger = new StderrLogger();
 73 |     $logger->info('Starting MCP Custom Dependencies (Stdio) Server...');
 74 | 
 75 |     $container = new BasicContainer();
 76 |     $container->set(LoggerInterface::class, $logger);
 77 | 
 78 |     $taskRepo = new Services\InMemoryTaskRepository($logger);
 79 |     $container->set(Services\TaskRepositoryInterface::class, $taskRepo);
 80 | 
 81 |     $statsService = new Services\SystemStatsService($taskRepo);
 82 |     $container->set(Services\StatsServiceInterface::class, $statsService);
 83 | 
 84 |     $server = Server::make()
 85 |         ->withServerInfo('Task Manager Server', '1.0.0')
 86 |         ->withLogger($logger)
 87 |         ->withContainer($container)
 88 |         ->build();
 89 | 
 90 |     $server->discover(__DIR__, ['.']);
 91 | 
 92 |     $transport = new StdioServerTransport();
 93 |     $server->listen($transport);
 94 | 
 95 |     $logger->info('Server listener stopped gracefully.');
 96 |     exit(0);
 97 | } catch (\Throwable $e) {
 98 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
 99 |     fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
100 |     fwrite(STDERR, 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n");
101 |     fwrite(STDERR, $e->getTraceAsString() . "\n");
102 |     exit(1);
103 | }
104 | 
```

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

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server\Session;
  6 | 
  7 | use Evenement\EventEmitterInterface;
  8 | use Evenement\EventEmitterTrait;
  9 | use PhpMcp\Server\Contracts\SessionHandlerInterface;
 10 | use PhpMcp\Server\Contracts\SessionInterface;
 11 | use Psr\Log\LoggerInterface;
 12 | use React\EventLoop\Loop;
 13 | use React\EventLoop\LoopInterface;
 14 | use React\EventLoop\TimerInterface;
 15 | 
 16 | class SessionManager implements EventEmitterInterface
 17 | {
 18 |     use EventEmitterTrait;
 19 | 
 20 |     protected ?TimerInterface $gcTimer = null;
 21 | 
 22 |     public function __construct(
 23 |         protected SessionHandlerInterface $handler,
 24 |         protected LoggerInterface $logger,
 25 |         protected ?LoopInterface $loop = null,
 26 |         protected int $ttl = 3600,
 27 |         protected int|float $gcInterval = 300
 28 |     ) {
 29 |         $this->loop ??= Loop::get();
 30 |     }
 31 | 
 32 |     /**
 33 |      * Start the garbage collection timer
 34 |      */
 35 |     public function startGcTimer(): void
 36 |     {
 37 |         if ($this->gcTimer !== null) {
 38 |             return;
 39 |         }
 40 | 
 41 |         $this->gcTimer = $this->loop->addPeriodicTimer($this->gcInterval, [$this, 'gc']);
 42 |     }
 43 | 
 44 |     public function gc(): array
 45 |     {
 46 |         $deletedSessions = $this->handler->gc($this->ttl);
 47 | 
 48 |         foreach ($deletedSessions as $sessionId) {
 49 |             $this->emit('session_deleted', [$sessionId]);
 50 |         }
 51 | 
 52 |         if (count($deletedSessions) > 0) {
 53 |             $this->logger->debug('Session garbage collection complete', [
 54 |                 'purged_sessions' => count($deletedSessions),
 55 |             ]);
 56 |         }
 57 | 
 58 |         return $deletedSessions;
 59 |     }
 60 | 
 61 |     /**
 62 |      * Stop the garbage collection timer
 63 |      */
 64 |     public function stopGcTimer(): void
 65 |     {
 66 |         if ($this->gcTimer !== null) {
 67 |             $this->loop->cancelTimer($this->gcTimer);
 68 |             $this->gcTimer = null;
 69 |         }
 70 |     }
 71 | 
 72 |     /**
 73 |      * Create a new session
 74 |      */
 75 |     public function createSession(string $sessionId): SessionInterface
 76 |     {
 77 |         $session = new Session($this->handler, $sessionId);
 78 | 
 79 |         $session->hydrate([
 80 |             'initialized' => false,
 81 |             'client_info' => null,
 82 |             'protocol_version' => null,
 83 |             'subscriptions' => [],      // [uri => true]
 84 |             'message_queue' => [],      // string[] (raw JSON-RPC frames)
 85 |             'log_level' => null,
 86 |         ]);
 87 | 
 88 |         $session->save();
 89 | 
 90 |         $this->logger->info('Session created', ['sessionId' => $sessionId]);
 91 |         $this->emit('session_created', [$sessionId, $session]);
 92 | 
 93 |         return $session;
 94 |     }
 95 | 
 96 |     /**
 97 |      * Get an existing session
 98 |      */
 99 |     public function getSession(string $sessionId): ?SessionInterface
100 |     {
101 |         return Session::retrieve($sessionId, $this->handler);
102 |     }
103 | 
104 |     public function hasSession(string $sessionId): bool
105 |     {
106 |         return $this->getSession($sessionId) !== null;
107 |     }
108 | 
109 |     /**
110 |      * Delete a session completely
111 |      */
112 |     public function deleteSession(string $sessionId): bool
113 |     {
114 |         $success = $this->handler->destroy($sessionId);
115 | 
116 |         if ($success) {
117 |             $this->emit('session_deleted', [$sessionId]);
118 |             $this->logger->info('Session deleted', ['sessionId' => $sessionId]);
119 |         } else {
120 |             $this->logger->warning('Failed to delete session', ['sessionId' => $sessionId]);
121 |         }
122 | 
123 |         return $success;
124 |     }
125 | 
126 |     public function queueMessage(string $sessionId, string $message): void
127 |     {
128 |         $session = $this->getSession($sessionId);
129 |         if ($session === null) {
130 |             return;
131 |         }
132 | 
133 |         $session->queueMessage($message);
134 |         $session->save();
135 |     }
136 | 
137 |     public function dequeueMessages(string $sessionId): array
138 |     {
139 |         $session = $this->getSession($sessionId);
140 |         if ($session === null) {
141 |             return [];
142 |         }
143 | 
144 |         $messages = $session->dequeueMessages();
145 |         $session->save();
146 | 
147 |         return $messages;
148 |     }
149 | 
150 |     public function hasQueuedMessages(string $sessionId): bool
151 |     {
152 |         $session = $this->getSession($sessionId, true);
153 |         if ($session === null) {
154 |             return false;
155 |         }
156 | 
157 |         return $session->hasQueuedMessages();
158 |     }
159 | }
160 | 
```

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

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server\Exception;
  6 | 
  7 | use Exception;
  8 | use PhpMcp\Schema\Constants;
  9 | use PhpMcp\Schema\JsonRpc\Error as JsonRpcError;
 10 | use Throwable;
 11 | 
 12 | /**
 13 |  * Base exception for all MCP Server library errors.
 14 |  */
 15 | class McpServerException extends Exception
 16 | {
 17 |     // MCP reserved range: -32000 to -32099 (Server error)
 18 |     // Add specific server-side codes if needed later, e.g.:
 19 |     // public const CODE_RESOURCE_ACTION_FAILED = -32000;
 20 |     // public const CODE_TOOL_EXECUTION_FAILED = -32001;
 21 | 
 22 |     /**
 23 |      * Additional data associated with the error, suitable for JSON-RPC 'data' field.
 24 |      *
 25 |      * @var mixed|null
 26 |      */
 27 |     protected mixed $data = null;
 28 | 
 29 |     /**
 30 |      * @param  string  $message  Error message.
 31 |      * @param  int  $code  Error code (use constants or appropriate HTTP status codes if applicable).
 32 |      * @param  mixed|null  $data  Additional data.
 33 |      * @param  ?Throwable  $previous  Previous exception.
 34 |      */
 35 |     public function __construct(
 36 |         string $message = '',
 37 |         int $code = 0,
 38 |         mixed $data = null,
 39 |         ?Throwable $previous = null
 40 |     ) {
 41 |         parent::__construct($message, $code, $previous);
 42 |         $this->data = $data;
 43 |     }
 44 | 
 45 |     /**
 46 |      * Get additional error data.
 47 |      *
 48 |      * @return mixed|null
 49 |      */
 50 |     public function getData(): mixed
 51 |     {
 52 |         return $this->data;
 53 |     }
 54 | 
 55 |     /**
 56 |      * Formats the exception into a JSON-RPC 2.0 error object structure.
 57 |      * Specific exceptions should override this or provide factories with correct codes.
 58 |      */
 59 |     public function toJsonRpcError(string|int $id): JsonRpcError
 60 |     {
 61 |         $code = ($this->code >= -32768 && $this->code <= -32000) ? $this->code : Constants::INTERNAL_ERROR;
 62 | 
 63 |         return new JsonRpcError(
 64 |             jsonrpc: '2.0',
 65 |             id: $id,
 66 |             code: $code,
 67 |             message: $this->getMessage(),
 68 |             data: $this->getData()
 69 |         );
 70 |     }
 71 | 
 72 |     public static function parseError(string $details, ?Throwable $previous = null): self
 73 |     {
 74 |         return new ProtocolException('Parse error: ' . $details, Constants::PARSE_ERROR, null, $previous);
 75 |     }
 76 | 
 77 |     public static function invalidRequest(?string $details = 'Invalid Request', ?Throwable $previous = null): self
 78 |     {
 79 |         return new ProtocolException($details, Constants::INVALID_REQUEST, null, $previous);
 80 |     }
 81 | 
 82 |     public static function methodNotFound(string $methodName, ?string $message = null, ?Throwable $previous = null): self
 83 |     {
 84 |         return new ProtocolException($message ?? "Method not found: {$methodName}", Constants::METHOD_NOT_FOUND, null, $previous);
 85 |     }
 86 | 
 87 |     public static function invalidParams(string $message = 'Invalid params', $data = null, ?Throwable $previous = null): self
 88 |     {
 89 |         // Pass data (e.g., validation errors) through
 90 |         return new ProtocolException($message, Constants::INVALID_PARAMS, $data, $previous);
 91 |     }
 92 | 
 93 |     public static function internalError(?string $details = 'Internal server error', ?Throwable $previous = null): self
 94 |     {
 95 |         $message = 'Internal error';
 96 |         if ($details && is_string($details)) {
 97 |             $message .= ': ' . $details;
 98 |         } elseif ($previous && $details === null) {
 99 |             $message .= ' (See server logs)';
100 |         }
101 | 
102 |         return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
103 |     }
104 | 
105 |     public static function toolExecutionFailed(string $toolName, ?Throwable $previous = null): self
106 |     {
107 |         $message = "Execution failed for tool '{$toolName}'";
108 |         if ($previous) {
109 |             $message .= ': ' . $previous->getMessage();
110 |         }
111 | 
112 |         return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
113 |     }
114 | 
115 |     public static function resourceReadFailed(string $uri, ?Throwable $previous = null): self
116 |     {
117 |         $message = "Failed to read resource '{$uri}'";
118 |         if ($previous) {
119 |             $message .= ': ' . $previous->getMessage();
120 |         }
121 | 
122 |         return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
123 |     }
124 | 
125 |     public static function promptGenerationFailed(string $promptName, ?Throwable $previous = null): self
126 |     {
127 |         $message = "Failed to generate prompt '{$promptName}'";
128 |         if ($previous) {
129 |             $message .= ': ' . $previous->getMessage();
130 |         }
131 | 
132 |         return new McpServerException($message, Constants::INTERNAL_ERROR, null, $previous);
133 |     }
134 | }
135 | 
```

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

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server\Elements;
  6 | 
  7 | use PhpMcp\Schema\Content\Content;
  8 | use PhpMcp\Schema\Content\TextContent;
  9 | use PhpMcp\Server\Context;
 10 | use Psr\Container\ContainerInterface;
 11 | use PhpMcp\Schema\Tool;
 12 | use Throwable;
 13 | 
 14 | class RegisteredTool extends RegisteredElement
 15 | {
 16 |     public function __construct(
 17 |         public readonly Tool $schema,
 18 |         callable|array|string $handler,
 19 |         bool $isManual = false,
 20 |     ) {
 21 |         parent::__construct($handler, $isManual);
 22 |     }
 23 | 
 24 |     public static function make(Tool $schema, callable|array|string $handler, bool $isManual = false): self
 25 |     {
 26 |         return new self($schema, $handler, $isManual);
 27 |     }
 28 | 
 29 |     /**
 30 |      * Calls the underlying handler for this tool.
 31 |      *
 32 |      * @return Content[] The content items for CallToolResult.
 33 |      */
 34 |     public function call(ContainerInterface $container, array $arguments, Context $context): array
 35 |     {
 36 |         $result = $this->handle($container, $arguments, $context);
 37 | 
 38 |         return $this->formatResult($result);
 39 |     }
 40 | 
 41 |     /**
 42 |      * Formats the result of a tool execution into an array of MCP Content items.
 43 |      *
 44 |      * - If the result is already a Content object, it's wrapped in an array.
 45 |      * - If the result is an array:
 46 |      *   - If all elements are Content objects, the array is returned as is.
 47 |      *   - If it's a mixed array (Content and non-Content items), non-Content items are
 48 |      *     individually formatted (scalars to TextContent, others to JSON TextContent).
 49 |      *   - If it's an array with no Content items, the entire array is JSON-encoded into a single TextContent.
 50 |      * - Scalars (string, int, float, bool) are wrapped in TextContent.
 51 |      * - null is represented as TextContent('(null)').
 52 |      * - Other objects are JSON-encoded and wrapped in TextContent.
 53 |      *
 54 |      * @param  mixed  $toolExecutionResult  The raw value returned by the tool's PHP method.
 55 |      * @return Content[] The content items for CallToolResult.
 56 |      * @throws JsonException if JSON encoding fails for non-Content array/object results.
 57 |      */
 58 |     protected function formatResult(mixed $toolExecutionResult): array
 59 |     {
 60 |         if ($toolExecutionResult instanceof Content) {
 61 |             return [$toolExecutionResult];
 62 |         }
 63 | 
 64 |         if (is_array($toolExecutionResult)) {
 65 |             if (empty($toolExecutionResult)) {
 66 |                 return [TextContent::make('[]')];
 67 |             }
 68 | 
 69 |             $allAreContent = true;
 70 |             $hasContent = false;
 71 | 
 72 |             foreach ($toolExecutionResult as $item) {
 73 |                 if ($item instanceof Content) {
 74 |                     $hasContent = true;
 75 |                 } else {
 76 |                     $allAreContent = false;
 77 |                 }
 78 |             }
 79 | 
 80 |             if ($allAreContent && $hasContent) {
 81 |                 return $toolExecutionResult;
 82 |             }
 83 | 
 84 |             if ($hasContent) {
 85 |                 $result = [];
 86 |                 foreach ($toolExecutionResult as $item) {
 87 |                     if ($item instanceof Content) {
 88 |                         $result[] = $item;
 89 |                     } else {
 90 |                         $result = array_merge($result, $this->formatResult($item));
 91 |                     }
 92 |                 }
 93 |                 return $result;
 94 |             }
 95 |         }
 96 | 
 97 |         if ($toolExecutionResult === null) {
 98 |             return [TextContent::make('(null)')];
 99 |         }
100 | 
101 |         if (is_bool($toolExecutionResult)) {
102 |             return [TextContent::make($toolExecutionResult ? 'true' : 'false')];
103 |         }
104 | 
105 |         if (is_scalar($toolExecutionResult)) {
106 |             return [TextContent::make($toolExecutionResult)];
107 |         }
108 | 
109 |         $jsonResult = json_encode(
110 |             $toolExecutionResult,
111 |             JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE
112 |         );
113 | 
114 |         return [TextContent::make($jsonResult)];
115 |     }
116 | 
117 |     public function toArray(): array
118 |     {
119 |         return [
120 |             'schema' => $this->schema->toArray(),
121 |             ...parent::toArray(),
122 |         ];
123 |     }
124 | 
125 |     public static function fromArray(array $data): self|false
126 |     {
127 |         try {
128 |             if (! isset($data['schema']) || ! isset($data['handler'])) {
129 |                 return false;
130 |             }
131 | 
132 |             return new self(
133 |                 Tool::fromArray($data['schema']),
134 |                 $data['handler'],
135 |                 $data['isManual'] ?? false,
136 |             );
137 |         } catch (Throwable $e) {
138 |             return false;
139 |         }
140 |     }
141 | }
142 | 
```

--------------------------------------------------------------------------------
/src/Utils/DocBlockParser.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Utils;
  4 | 
  5 | use phpDocumentor\Reflection\DocBlock;
  6 | use phpDocumentor\Reflection\DocBlock\Tags\Param;
  7 | use phpDocumentor\Reflection\DocBlock\Tags\Return_;
  8 | use phpDocumentor\Reflection\DocBlockFactory;
  9 | use Psr\Log\LoggerInterface;
 10 | use Psr\Log\NullLogger;
 11 | use Throwable;
 12 | 
 13 | /**
 14 |  * Parses DocBlocks using phpdocumentor/reflection-docblock.
 15 |  */
 16 | class DocBlockParser
 17 | {
 18 |     private DocBlockFactory $docBlockFactory;
 19 |     private LoggerInterface $logger;
 20 | 
 21 |     public function __construct(?LoggerInterface $logger = null)
 22 |     {
 23 |         $this->docBlockFactory = DocBlockFactory::createInstance();
 24 |         $this->logger = $logger ?? new NullLogger();
 25 |     }
 26 | 
 27 |     /**
 28 |      * Safely parses a DocComment string into a DocBlock object.
 29 |      */
 30 |     public function parseDocBlock(string|null|false $docComment): ?DocBlock
 31 |     {
 32 |         if ($docComment === false || $docComment === null || empty($docComment)) {
 33 |             return null;
 34 |         }
 35 |         try {
 36 |             return $this->docBlockFactory->create($docComment);
 37 |         } catch (Throwable $e) {
 38 |             // Log error or handle gracefully if invalid DocBlock syntax is encountered
 39 |             $this->logger->warning('Failed to parse DocBlock', [
 40 |                 'error' => $e->getMessage(),
 41 |                 'exception_trace' => $e->getTraceAsString(),
 42 |             ]);
 43 | 
 44 |             return null;
 45 |         }
 46 |     }
 47 | 
 48 |     /**
 49 |      * Gets the summary line from a DocBlock.
 50 |      */
 51 |     public function getSummary(?DocBlock $docBlock): ?string
 52 |     {
 53 |         if (! $docBlock) {
 54 |             return null;
 55 |         }
 56 |         $summary = trim($docBlock->getSummary());
 57 | 
 58 |         return $summary ?: null; // Return null if empty after trimming
 59 |     }
 60 | 
 61 |     /**
 62 |      * Gets the description from a DocBlock (summary + description body).
 63 |      */
 64 |     public function getDescription(?DocBlock $docBlock): ?string
 65 |     {
 66 |         if (! $docBlock) {
 67 |             return null;
 68 |         }
 69 |         $summary = trim($docBlock->getSummary());
 70 |         $descriptionBody = trim((string) $docBlock->getDescription());
 71 | 
 72 |         if ($summary && $descriptionBody) {
 73 |             return $summary . "\n\n" . $descriptionBody;
 74 |         }
 75 |         if ($summary) {
 76 |             return $summary;
 77 |         }
 78 |         if ($descriptionBody) {
 79 |             return $descriptionBody;
 80 |         }
 81 | 
 82 |         return null;
 83 |     }
 84 | 
 85 |     /**
 86 |      * Extracts @param tag information from a DocBlock, keyed by variable name (e.g., '$paramName').
 87 |      *
 88 |      * @return array<string, Param>
 89 |      */
 90 |     public function getParamTags(?DocBlock $docBlock): array
 91 |     {
 92 |         if (! $docBlock) {
 93 |             return [];
 94 |         }
 95 | 
 96 |         /** @var array<string, Param> $paramTags */
 97 |         $paramTags = [];
 98 |         foreach ($docBlock->getTagsByName('param') as $tag) {
 99 |             if ($tag instanceof Param && $tag->getVariableName()) {
100 |                 $paramTags['$' . $tag->getVariableName()] = $tag;
101 |             }
102 |         }
103 | 
104 |         return $paramTags;
105 |     }
106 | 
107 |     /**
108 |      * Gets the @return tag information from a DocBlock.
109 |      */
110 |     public function getReturnTag(?DocBlock $docBlock): ?Return_
111 |     {
112 |         if (! $docBlock) {
113 |             return null;
114 |         }
115 |         /** @var Return_|null $returnTag */
116 |         $returnTag = $docBlock->getTagsByName('return')[0] ?? null;
117 | 
118 |         return $returnTag;
119 |     }
120 | 
121 |     /**
122 |      * Gets the description string from a Param tag.
123 |      */
124 |     public function getParamDescription(?Param $paramTag): ?string
125 |     {
126 |         return $paramTag ? (trim((string) $paramTag->getDescription()) ?: null) : null;
127 |     }
128 | 
129 |     /**
130 |      * Gets the type string from a Param tag.
131 |      */
132 |     public function getParamTypeString(?Param $paramTag): ?string
133 |     {
134 |         if ($paramTag && $paramTag->getType()) {
135 |             $typeFromTag = trim((string) $paramTag->getType());
136 |             if (! empty($typeFromTag)) {
137 |                 return ltrim($typeFromTag, '\\');
138 |             }
139 |         }
140 | 
141 |         return null;
142 |     }
143 | 
144 |     /**
145 |      * Gets the description string from a Return_ tag.
146 |      */
147 |     public function getReturnDescription(?Return_ $returnTag): ?string
148 |     {
149 |         return $returnTag ? (trim((string) $returnTag->getDescription()) ?: null) : null;
150 |     }
151 | 
152 |     /**
153 |      * Gets the type string from a Return_ tag.
154 |      */
155 |     public function getReturnTypeString(?Return_ $returnTag): ?string
156 |     {
157 |         if ($returnTag && $returnTag->getType()) {
158 |             $typeFromTag = trim((string) $returnTag->getType());
159 |             if (! empty($typeFromTag)) {
160 |                 return ltrim($typeFromTag, '\\');
161 |             }
162 |         }
163 | 
164 |         return null;
165 |     }
166 | }
167 | 
```

--------------------------------------------------------------------------------
/tests/Fixtures/Schema/SchemaGenerationTarget.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Fixtures\Schema;
  4 | 
  5 | use PhpMcp\Server\Attributes\Schema;
  6 | use PhpMcp\Server\Attributes\Schema\Format;
  7 | use PhpMcp\Server\Attributes\Schema\ArrayItems;
  8 | use PhpMcp\Server\Attributes\Schema\Property;
  9 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedIntEnum;
 10 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
 11 | use PhpMcp\Server\Tests\Fixtures\Enums\UnitEnum;
 12 | use stdClass;
 13 | 
 14 | class SchemaGenerationTarget
 15 | {
 16 |     public function noParamsMethod(): void
 17 |     {
 18 |     }
 19 | 
 20 |     /**
 21 |      * Method with simple required types.
 22 |      * @param string $pString String param
 23 |      * @param int $pInt Int param
 24 |      * @param bool $pBool Bool param
 25 |      * @param float $pFloat Float param
 26 |      * @param array $pArray Array param
 27 |      * @param stdClass $pObject Object param
 28 |      */
 29 |     public function simpleRequiredTypes(string $pString, int $pInt, bool $pBool, float $pFloat, array $pArray, stdClass $pObject): void
 30 |     {
 31 |     }
 32 | 
 33 |     /**
 34 |      * Method with simple optional types with default values.
 35 |      * @param string $pStringOpt String param with default
 36 |      * @param int $pIntOpt Int param with default
 37 |      * @param bool $pBoolOpt Bool param with default
 38 |      * @param ?float $pFloatOptNullable Float param with default, also nullable
 39 |      * @param array $pArrayOpt Array param with default
 40 |      * @param ?stdClass $pObjectOptNullable Object param with default null
 41 |      */
 42 |     public function optionalTypesWithDefaults(
 43 |         string $pStringOpt = "hello",
 44 |         int $pIntOpt = 123,
 45 |         bool $pBoolOpt = true,
 46 |         ?float $pFloatOptNullable = 1.23,
 47 |         array $pArrayOpt = ['a', 'b'],
 48 |         ?stdClass $pObjectOptNullable = null
 49 |     ): void {
 50 |     }
 51 | 
 52 |     /**
 53 |      * Nullable types without explicit defaults.
 54 |      * @param ?string $pNullableString Nullable string
 55 |      * @param int|null $pUnionNullableInt Union nullable int
 56 |      */
 57 |     public function nullableTypes(?string $pNullableString, ?int $pUnionNullableInt, ?BackedStringEnum $pNullableEnum): void
 58 |     {
 59 |     }
 60 | 
 61 |     /**
 62 |      * Union types.
 63 |      * @param string|int $pStringOrInt String or Int
 64 |      * @param bool|float|null $pBoolOrFloatOrNull Bool, Float or Null
 65 |      */
 66 |     public function unionTypes(string|int $pStringOrInt, $pBoolOrFloatOrNull): void
 67 |     {
 68 |     } // PHP 7.x style union in docblock usually
 69 | 
 70 |     /**
 71 |      * Various array type hints.
 72 |      * @param string[] $pStringArray Array of strings (docblock style)
 73 |      * @param array<int> $pIntArrayGeneric Array of integers (generic style)
 74 |      * @param array<string, mixed> $pAssocArray Associative array
 75 |      * @param BackedIntEnum[] $pEnumArray Array of enums
 76 |      * @param array{name: string, age: int} $pShapeArray Typed array shape
 77 |      * @param array<array{id:int, value:string}> $pArrayOfShapes Array of shapes
 78 |      */
 79 |     public function arrayTypes(
 80 |         array $pStringArray,
 81 |         array $pIntArrayGeneric,
 82 |         array $pAssocArray,
 83 |         array $pEnumArray,
 84 |         array $pShapeArray,
 85 |         array $pArrayOfShapes
 86 |     ): void {
 87 |     }
 88 | 
 89 |     /**
 90 |      * Enum types.
 91 |      * @param BackedStringEnum $pBackedStringEnum Backed string enum
 92 |      * @param BackedIntEnum $pBackedIntEnum Backed int enum
 93 |      * @param UnitEnum $pUnitEnum Unit enum
 94 |      */
 95 |     public function enumTypes(BackedStringEnum $pBackedStringEnum, BackedIntEnum $pBackedIntEnum, UnitEnum $pUnitEnum): void
 96 |     {
 97 |     }
 98 | 
 99 |     /**
100 |      * Variadic parameters.
101 |      * @param string ...$pVariadicStrings Variadic strings
102 |      */
103 |     public function variadicParams(string ...$pVariadicStrings): void
104 |     {
105 |     }
106 | 
107 |     /**
108 |      * Mixed type.
109 |      * @param mixed $pMixed Mixed type
110 |      */
111 |     public function mixedType(mixed $pMixed): void
112 |     {
113 |     }
114 | 
115 |     /**
116 |      * With #[Schema] attributes for enhanced validation.
117 |      * @param string $email With email format.
118 |      * @param int $quantity With numeric constraints.
119 |      * @param string[] $tags With array constraints.
120 |      * @param array $userProfile With object property constraints.
121 |      */
122 |     public function withSchemaAttributes(
123 |         #[Schema(format: Format::EMAIL)]
124 |         string $email,
125 |         #[Schema(minimum: 1, maximum: 100, multipleOf: 5)]
126 |         int $quantity,
127 |         #[Schema(minItems: 1, maxItems: 5, uniqueItems: true, items: new ArrayItems(minLength: 3))]
128 |         array $tags,
129 |         #[Schema(
130 |             properties: [
131 |                 new Property(name: 'id', minimum: 1),
132 |                 new Property(name: 'username', pattern: '^[a-z0-9_]{3,16}$'),
133 |             ],
134 |             required: ['id', 'username'],
135 |             additionalProperties: false
136 |         )]
137 |         array $userProfile
138 |     ): void {
139 |     }
140 | }
141 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace Mcp\HttpUserProfileExample;
  4 | 
  5 | use PhpMcp\Server\Attributes\CompletionProvider;
  6 | use PhpMcp\Server\Attributes\McpPrompt;
  7 | use PhpMcp\Server\Attributes\McpResource;
  8 | use PhpMcp\Server\Attributes\McpResourceTemplate;
  9 | use PhpMcp\Server\Attributes\McpTool;
 10 | use PhpMcp\Server\Exception\McpServerException;
 11 | use Psr\Log\LoggerInterface;
 12 | 
 13 | class McpElements
 14 | {
 15 |     // Simulate a simple user database
 16 |     private array $users = [
 17 |         '101' => ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'],
 18 |         '102' => ['name' => 'Bob', 'email' => '[email protected]', 'role' => 'user'],
 19 |         '103' => ['name' => 'Charlie', 'email' => '[email protected]', 'role' => 'user'],
 20 |     ];
 21 | 
 22 |     private LoggerInterface $logger;
 23 | 
 24 |     public function __construct(LoggerInterface $logger)
 25 |     {
 26 |         $this->logger = $logger;
 27 |         $this->logger->debug('HttpUserProfileExample McpElements instantiated.');
 28 |     }
 29 | 
 30 |     /**
 31 |      * Retrieves the profile data for a specific user.
 32 |      *
 33 |      * @param  string  $userId  The ID of the user (from URI).
 34 |      * @return array User profile data.
 35 |      *
 36 |      * @throws McpServerException If the user is not found.
 37 |      */
 38 |     #[McpResourceTemplate(
 39 |         uriTemplate: 'user://{userId}/profile',
 40 |         name: 'user_profile',
 41 |         description: 'Get profile information for a specific user ID.',
 42 |         mimeType: 'application/json'
 43 |     )]
 44 | 
 45 |     public function getUserProfile(
 46 |         #[CompletionProvider(values: ['101', '102', '103'])]
 47 |         string $userId
 48 |     ): array {
 49 |         $this->logger->info('Reading resource: user profile', ['userId' => $userId]);
 50 |         if (! isset($this->users[$userId])) {
 51 |             // Throwing an exception that Processor can turn into an error response
 52 |             throw McpServerException::invalidParams("User profile not found for ID: {$userId}");
 53 |         }
 54 | 
 55 |         return $this->users[$userId];
 56 |     }
 57 | 
 58 |     /**
 59 |      * Retrieves a list of all known user IDs.
 60 |      *
 61 |      * @return array List of user IDs.
 62 |      */
 63 |     #[McpResource(
 64 |         uri: 'user://list/ids',
 65 |         name: 'user_id_list',
 66 |         description: 'Provides a list of all available user IDs.',
 67 |         mimeType: 'application/json'
 68 |     )]
 69 |     public function listUserIds(): array
 70 |     {
 71 |         $this->logger->info('Reading resource: user ID list');
 72 | 
 73 |         return array_keys($this->users);
 74 |     }
 75 | 
 76 |     /**
 77 |      * Sends a welcome message to a user.
 78 |      * (This is a placeholder - in a real app, it might queue an email)
 79 |      *
 80 |      * @param  string  $userId  The ID of the user to message.
 81 |      * @param  string|null  $customMessage  An optional custom message part.
 82 |      * @return array Status of the operation.
 83 |      */
 84 |     #[McpTool(name: 'send_welcome')]
 85 |     public function sendWelcomeMessage(string $userId, ?string $customMessage = null): array
 86 |     {
 87 |         $this->logger->info('Executing tool: send_welcome', ['userId' => $userId]);
 88 |         if (! isset($this->users[$userId])) {
 89 |             return ['success' => false, 'error' => "User ID {$userId} not found."];
 90 |         }
 91 |         $user = $this->users[$userId];
 92 |         $message = "Welcome, {$user['name']}!";
 93 |         if ($customMessage) {
 94 |             $message .= ' ' . $customMessage;
 95 |         }
 96 |         // Simulate sending
 97 |         $this->logger->info("Simulated sending message to {$user['email']}: {$message}");
 98 | 
 99 |         return ['success' => true, 'message_sent' => $message];
100 |     }
101 | 
102 |     #[McpTool(name: 'test_tool_without_params')]
103 |     public function testToolWithoutParams()
104 |     {
105 |         return ['success' => true, 'message' => 'Test tool without params'];
106 |     }
107 | 
108 |     /**
109 |      * Generates a prompt to write a bio for a user.
110 |      *
111 |      * @param  string  $userId  The user ID to generate the bio for.
112 |      * @param  string  $tone  Desired tone (e.g., 'formal', 'casual').
113 |      * @return array Prompt messages.
114 |      *
115 |      * @throws McpServerException If user not found.
116 |      */
117 |     #[McpPrompt(name: 'generate_bio_prompt')]
118 |     public function generateBio(
119 |         #[CompletionProvider(provider: UserIdCompletionProvider::class)]
120 |         string $userId,
121 |         string $tone = 'professional'
122 |     ): array {
123 |         $this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
124 |         if (! isset($this->users[$userId])) {
125 |             throw McpServerException::invalidParams("User not found for bio prompt: {$userId}");
126 |         }
127 |         $user = $this->users[$userId];
128 | 
129 |         return [
130 |             ['role' => 'user', 'content' => "Write a short, {$tone} biography for {$user['name']} (Role: {$user['role']}, Email: {$user['email']}). Highlight their role within the system."],
131 |         ];
132 |     }
133 | }
134 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace Mcp\StdioCalculatorExample;
  4 | 
  5 | use PhpMcp\Server\Attributes\McpResource;
  6 | use PhpMcp\Server\Attributes\McpTool;
  7 | 
  8 | class McpElements
  9 | {
 10 |     private array $config = [
 11 |         'precision' => 2,
 12 |         'allow_negative' => true,
 13 |     ];
 14 | 
 15 |     /**
 16 |      * Performs a calculation based on the operation.
 17 |      *
 18 |      * Supports 'add', 'subtract', 'multiply', 'divide'.
 19 |      * Obeys the 'precision' and 'allow_negative' settings from the config resource.
 20 |      *
 21 |      * @param  float  $a  The first operand.
 22 |      * @param  float  $b  The second operand.
 23 |      * @param  string  $operation  The operation ('add', 'subtract', 'multiply', 'divide').
 24 |      * @return float|string The result of the calculation, or an error message string.
 25 |      */
 26 |     #[McpTool(name: 'calculate')]
 27 |     public function calculate(float $a, float $b, string $operation): float|string
 28 |     {
 29 |         // Use STDERR for logs
 30 |         fwrite(STDERR, "Calculate tool called: a=$a, b=$b, op=$operation\n");
 31 | 
 32 |         $op = strtolower($operation);
 33 |         $result = null;
 34 | 
 35 |         switch ($op) {
 36 |             case 'add':
 37 |                 $result = $a + $b;
 38 |                 break;
 39 |             case 'subtract':
 40 |                 $result = $a - $b;
 41 |                 break;
 42 |             case 'multiply':
 43 |                 $result = $a * $b;
 44 |                 break;
 45 |             case 'divide':
 46 |                 if ($b == 0) {
 47 |                     return 'Error: Division by zero.';
 48 |                 }
 49 |                 $result = $a / $b;
 50 |                 break;
 51 |             default:
 52 |                 return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
 53 |         }
 54 | 
 55 |         if (! $this->config['allow_negative'] && $result < 0) {
 56 |             return 'Error: Negative results are disabled.';
 57 |         }
 58 | 
 59 |         return round($result, $this->config['precision']);
 60 |     }
 61 | 
 62 |     /**
 63 |      * Provides the current calculator configuration.
 64 |      * Can be read by clients to understand precision etc.
 65 |      *
 66 |      * @return array The configuration array.
 67 |      */
 68 |     #[McpResource(
 69 |         uri: 'config://calculator/settings',
 70 |         name: 'calculator_config',
 71 |         description: 'Current settings for the calculator tool (precision, allow_negative).',
 72 |         mimeType: 'application/json' // Return as JSON
 73 |     )]
 74 |     public function getConfiguration(): array
 75 |     {
 76 |         fwrite(STDERR, "Resource config://calculator/settings read.\n");
 77 | 
 78 |         return $this->config;
 79 |     }
 80 | 
 81 |     /**
 82 |      * Updates a specific configuration setting.
 83 |      * Note: This requires more robust validation in a real app.
 84 |      *
 85 |      * @param  string  $setting  The setting key ('precision' or 'allow_negative').
 86 |      * @param  mixed  $value  The new value (int for precision, bool for allow_negative).
 87 |      * @return array Success message or error.
 88 |      */
 89 |     #[McpTool(name: 'update_setting')]
 90 |     public function updateSetting(string $setting, mixed $value): array
 91 |     {
 92 |         fwrite(STDERR, "Update Setting tool called: setting=$setting, value=".var_export($value, true)."\n");
 93 |         if (! array_key_exists($setting, $this->config)) {
 94 |             return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
 95 |         }
 96 | 
 97 |         if ($setting === 'precision') {
 98 |             if (! is_int($value) || $value < 0 || $value > 10) {
 99 |                 return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
100 |             }
101 |             $this->config['precision'] = $value;
102 | 
103 |             // In real app, notify subscribers of config://calculator/settings change
104 |             // $registry->notifyResourceChanged('config://calculator/settings');
105 |             return ['success' => true, 'message' => "Precision updated to {$value}."];
106 |         }
107 | 
108 |         if ($setting === 'allow_negative') {
109 |             if (! is_bool($value)) {
110 |                 // Attempt basic cast for flexibility
111 |                 if (in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
112 |                     $value = true;
113 |                 } elseif (in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
114 |                     $value = false;
115 |                 } else {
116 |                     return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
117 |                 }
118 |             }
119 |             $this->config['allow_negative'] = $value;
120 | 
121 |             // $registry->notifyResourceChanged('config://calculator/settings');
122 |             return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
123 |         }
124 | 
125 |         return ['success' => false, 'error' => 'Internal error handling setting.']; // Should not happen
126 |     }
127 | }
128 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Fixtures\General;
  4 | 
  5 | use PhpMcp\Schema\Content\EmbeddedResource;
  6 | use PhpMcp\Schema\Content\TextResourceContents;
  7 | use PhpMcp\Schema\Content\BlobResourceContents;
  8 | use Psr\Log\LoggerInterface;
  9 | use SplFileInfo;
 10 | 
 11 | class ResourceHandlerFixture
 12 | {
 13 |     public static string $staticTextContent = "Default static text content.";
 14 |     public array $dynamicContentStore = [];
 15 |     public static ?string $unlinkableSplFile = null;
 16 | 
 17 |     public function __construct()
 18 |     {
 19 |         $this->dynamicContentStore['dynamic://data/item1'] = "Content for item 1";
 20 |     }
 21 | 
 22 |     public function returnStringText(string $uri): string
 23 |     {
 24 |         return "Plain string content for {$uri}";
 25 |     }
 26 | 
 27 |     public function returnStringJson(string $uri): string
 28 |     {
 29 |         return json_encode(['uri_in_json' => $uri, 'data' => 'some json string']);
 30 |     }
 31 | 
 32 |     public function returnStringHtml(string $uri): string
 33 |     {
 34 |         return "<html><title>{$uri}</title><body>Content</body></html>";
 35 |     }
 36 | 
 37 |     public function returnArrayJson(string $uri): array
 38 |     {
 39 |         return ['uri_in_array' => $uri, 'message' => 'This is JSON data from array', 'timestamp' => time()];
 40 |     }
 41 | 
 42 |     public function returnEmptyArray(string $uri): array
 43 |     {
 44 |         return [];
 45 |     }
 46 | 
 47 |     public function returnStream(string $uri) // Returns a stream resource
 48 |     {
 49 |         $stream = fopen('php://memory', 'r+');
 50 |         fwrite($stream, "Streamed content for {$uri}");
 51 |         rewind($stream);
 52 |         return $stream;
 53 |     }
 54 | 
 55 |     public function returnSplFileInfo(string $uri): SplFileInfo
 56 |     {
 57 |         self::$unlinkableSplFile = tempnam(sys_get_temp_dir(), 'res_fixture_spl_');
 58 |         file_put_contents(self::$unlinkableSplFile, "Content from SplFileInfo for {$uri}");
 59 |         return new SplFileInfo(self::$unlinkableSplFile);
 60 |     }
 61 | 
 62 |     public function returnEmbeddedResource(string $uri): EmbeddedResource
 63 |     {
 64 |         return EmbeddedResource::make(
 65 |             TextResourceContents::make($uri, 'application/vnd.custom-embedded', 'Direct EmbeddedResource content')
 66 |         );
 67 |     }
 68 | 
 69 |     public function returnTextResourceContents(string $uri): TextResourceContents
 70 |     {
 71 |         return TextResourceContents::make($uri, 'text/special-contents', 'Direct TextResourceContents');
 72 |     }
 73 | 
 74 |     public function returnBlobResourceContents(string $uri): BlobResourceContents
 75 |     {
 76 |         return BlobResourceContents::make($uri, 'application/custom-blob-contents', base64_encode('blobbycontents'));
 77 |     }
 78 | 
 79 |     public function returnArrayForBlobSchema(string $uri): array
 80 |     {
 81 |         return ['blob' => base64_encode("Blob for {$uri} via array"), 'mimeType' => 'application/x-custom-blob-array'];
 82 |     }
 83 | 
 84 |     public function returnArrayForTextSchema(string $uri): array
 85 |     {
 86 |         return ['text' => "Text from array for {$uri} via array", 'mimeType' => 'text/vnd.custom-array-text'];
 87 |     }
 88 | 
 89 |     public function returnArrayOfResourceContents(string $uri): array
 90 |     {
 91 |         return [
 92 |             TextResourceContents::make($uri . "_part1", 'text/plain', 'Part 1 of many RC'),
 93 |             BlobResourceContents::make($uri . "_part2", 'image/png', base64_encode('pngdata')),
 94 |         ];
 95 |     }
 96 | 
 97 |     public function returnArrayOfEmbeddedResources(string $uri): array
 98 |     {
 99 |         return [
100 |             EmbeddedResource::make(TextResourceContents::make($uri . "_emb1", 'text/xml', '<doc1/>')),
101 |             EmbeddedResource::make(BlobResourceContents::make($uri . "_emb2", 'font/woff2', base64_encode('fontdata'))),
102 |         ];
103 |     }
104 | 
105 |     public function returnMixedArrayWithResourceTypes(string $uri): array
106 |     {
107 |         return [
108 |             "A raw string piece", // Will be formatted
109 |             TextResourceContents::make($uri . "_rc1", 'text/markdown', '**Markdown!**'), // Used as is
110 |             ['nested_array_data' => 'value', 'for_uri' => $uri], // Will be formatted (JSON)
111 |             EmbeddedResource::make(TextResourceContents::make($uri . "_emb1", 'text/csv', 'col1,col2')), // Extracted
112 |         ];
113 |     }
114 | 
115 |     public function handlerThrowsException(string $uri): void
116 |     {
117 |         throw new \DomainException("Cannot read resource {$uri} - handler error.");
118 |     }
119 | 
120 |     public function returnUnformattableType(string $uri)
121 |     {
122 |         return new \DateTimeImmutable();
123 |     }
124 | 
125 |     public function resourceHandlerNeedsUri(string $uri): string
126 |     {
127 |         return "Handler received URI: " . $uri;
128 |     }
129 | 
130 |     public function resourceHandlerDoesNotNeedUri(): string
131 |     {
132 |         return "Handler did not need or receive URI parameter.";
133 |     }
134 | 
135 |     public function getTemplatedContent(
136 |         string $category,
137 |         string $itemId,
138 |         string $format,
139 |     ): array {
140 |         return [
141 |             'message' => "Content for item {$itemId} in category {$category}, format {$format}.",
142 |             'category_received' => $category,
143 |             'itemId_received' => $itemId,
144 |             'format_received' => $format,
145 |         ];
146 |     }
147 | 
148 |     public function getStaticText(): string
149 |     {
150 |         return self::$staticTextContent;
151 |     }
152 | }
153 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Fixtures\General;
  4 | 
  5 | use PhpMcp\Server\Context;
  6 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedIntEnum;
  7 | use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
  8 | use PhpMcp\Server\Tests\Fixtures\Enums\UnitEnum;
  9 | use stdClass;
 10 | 
 11 | class VariousTypesHandler
 12 | {
 13 |     public function noArgsMethod(): array
 14 |     {
 15 |         return compact([]);
 16 |     }
 17 | 
 18 |     public function simpleRequiredArgs(string $pString, int $pInt, bool $pBool): array
 19 |     {
 20 |         return compact('pString', 'pInt', 'pBool');
 21 |     }
 22 | 
 23 |     public function optionalArgsWithDefaults(
 24 |         string $pString = 'default_string',
 25 |         int $pInt = 100,
 26 |         ?bool $pNullableBool = true,
 27 |         float $pFloat = 3.14
 28 |     ): array {
 29 |         return compact('pString', 'pInt', 'pNullableBool', 'pFloat');
 30 |     }
 31 | 
 32 |     public function nullableArgsWithoutDefaults(?string $pString, ?int $pInt, ?array $pArray): array
 33 |     {
 34 |         return compact('pString', 'pInt', 'pArray');
 35 |     }
 36 | 
 37 |     public function mixedTypeArg(mixed $pMixed): array
 38 |     {
 39 |         return compact('pMixed');
 40 |     }
 41 | 
 42 |     public function backedEnumArgs(
 43 |         BackedStringEnum $pBackedString,
 44 |         BackedIntEnum $pBackedInt,
 45 |         ?BackedStringEnum $pNullableBackedString = null,
 46 |         BackedIntEnum $pOptionalBackedInt = BackedIntEnum::First
 47 |     ): array {
 48 |         return compact('pBackedString', 'pBackedInt', 'pNullableBackedString', 'pOptionalBackedInt');
 49 |     }
 50 | 
 51 |     public function unitEnumArg(UnitEnum $pUnitEnum): array
 52 |     {
 53 |         return compact('pUnitEnum');
 54 |     }
 55 | 
 56 |     public function arrayArg(array $pArray): array
 57 |     {
 58 |         return compact('pArray');
 59 |     }
 60 | 
 61 |     public function objectArg(stdClass $pObject): array
 62 |     {
 63 |         return compact('pObject');
 64 |     }
 65 | 
 66 |     public function variadicArgs(string ...$items): array
 67 |     {
 68 |         return compact('items');
 69 |     }
 70 | 
 71 |     /**
 72 |      * A comprehensive method for testing various argument types and casting.
 73 |      * @param string $strParam A string.
 74 |      * @param int $intParam An integer.
 75 |      * @param bool $boolProp A boolean.
 76 |      * @param float $floatParam A float.
 77 |      * @param array $arrayParam An array.
 78 |      * @param BackedStringEnum $backedStringEnumParam A backed string enum.
 79 |      * @param BackedIntEnum $backedIntEnumParam A backed int enum.
 80 |      * @param UnitEnum $unitEnumParam A unit enum (passed as instance).
 81 |      * @param string|null $nullableStringParam A nullable string.
 82 |      * @param int $optionalIntWithDefaultParam An optional int with default.
 83 |      * @param mixed $mixedParam A mixed type.
 84 |      * @param stdClass $objectParam An object.
 85 |      * @param string $stringForIntCast String that should be cast to int.
 86 |      * @param string $stringForFloatCast String that should be cast to float.
 87 |      * @param string $stringForBoolTrueCast String that should be cast to bool true.
 88 |      * @param string $stringForBoolFalseCast String that should be cast to bool false.
 89 |      * @param int $intForStringCast Int that should be cast to string.
 90 |      * @param int $intForFloatCast Int that should be cast to float.
 91 |      * @param bool $boolForStringCast Bool that should be cast to string.
 92 |      * @param string $valueForBackedStringEnum String value for backed string enum.
 93 |      * @param int $valueForBackedIntEnum Int value for backed int enum.
 94 |      */
 95 |     public function comprehensiveArgumentTest(
 96 |         string $strParam,
 97 |         int $intParam,
 98 |         bool $boolProp,
 99 |         float $floatParam,
100 |         array $arrayParam,
101 |         BackedStringEnum $backedStringEnumParam,
102 |         BackedIntEnum $backedIntEnumParam,
103 |         UnitEnum $unitEnumParam,
104 |         ?string $nullableStringParam,
105 |         mixed $mixedParam,
106 |         stdClass $objectParam,
107 |         string $stringForIntCast,
108 |         string $stringForFloatCast,
109 |         string $stringForBoolTrueCast,
110 |         string $stringForBoolFalseCast,
111 |         int $intForStringCast,
112 |         int $intForFloatCast,
113 |         bool $boolForStringCast,
114 |         string $valueForBackedStringEnum,
115 |         int $valueForBackedIntEnum,
116 |         int $optionalIntWithDefaultParam = 999,
117 |     ): array {
118 |         return compact(
119 |             'strParam',
120 |             'intParam',
121 |             'boolProp',
122 |             'floatParam',
123 |             'arrayParam',
124 |             'backedStringEnumParam',
125 |             'backedIntEnumParam',
126 |             'unitEnumParam',
127 |             'nullableStringParam',
128 |             'optionalIntWithDefaultParam',
129 |             'mixedParam',
130 |             'objectParam',
131 |             'stringForIntCast',
132 |             'stringForFloatCast',
133 |             'stringForBoolTrueCast',
134 |             'stringForBoolFalseCast',
135 |             'intForStringCast',
136 |             'intForFloatCast',
137 |             'boolForStringCast',
138 |             'valueForBackedStringEnum',
139 |             'valueForBackedIntEnum'
140 |         );
141 |     }
142 | 
143 |     public function methodCausesTypeError(int $mustBeInt): void
144 |     {
145 |     }
146 | 
147 |     public function contextArg(Context $context): array {
148 |         return [
149 |             'session' => $context->session->get('testKey'),
150 |             'request' => $context->request->getHeaderLine('testHeader'),
151 |         ];
152 |     }
153 | }
154 | 
```

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

```php
  1 | #!/usr/bin/env php
  2 | <?php
  3 | 
  4 | /*
  5 |     |--------------------------------------------------------------------------
  6 |     | MCP HTTP User Profile Server (Attribute Discovery)
  7 |     |--------------------------------------------------------------------------
  8 |     |
  9 |     | This server demonstrates attribute-based discovery for MCP elements
 10 |     | (ResourceTemplates, Resources, Tools, Prompts) defined in 'McpElements.php'.
 11 |     | It runs via the HTTP transport, listening for SSE and POST requests.
 12 |     |
 13 |     | To Use:
 14 |     | 1. Ensure 'McpElements.php' defines classes with MCP attributes.
 15 |     | 2. Run this script from your CLI: `php server.php`
 16 |     |    The server will listen on http://127.0.0.1:8080 by default.
 17 |     | 3. Configure your MCP Client (e.g., Cursor) for this server:
 18 |     |
 19 |     | {
 20 |     |     "mcpServers": {
 21 |     |         "php-http-userprofile": {
 22 |     |             "url": "http://127.0.0.1:8080/mcp/sse" // Use the SSE endpoint
 23 |     |             // Ensure your client can reach this address
 24 |     |         }
 25 |     |     }
 26 |     | }
 27 |     |
 28 |     | The ServerBuilder builds the server, $server->discover() scans for elements,
 29 |     | and then $server->listen() starts the ReactPHP HTTP server.
 30 |     |
 31 |     | If you provided a `CacheInterface` implementation to the ServerBuilder,
 32 |     | the discovery process will be cached, so you can comment out the
 33 |     | discovery call after the first run to speed up subsequent runs.
 34 |     |
 35 | */
 36 | 
 37 | declare(strict_types=1);
 38 | 
 39 | chdir(__DIR__);
 40 | require_once '../../vendor/autoload.php';
 41 | require_once 'McpElements.php';
 42 | require_once 'UserIdCompletionProvider.php';
 43 | 
 44 | use PhpMcp\Schema\ServerCapabilities;
 45 | use PhpMcp\Server\Defaults\BasicContainer;
 46 | use PhpMcp\Server\Server;
 47 | use PhpMcp\Server\Transports\HttpServerTransport;
 48 | use PhpMcp\Server\Transports\StreamableHttpServerTransport;
 49 | use Psr\Log\AbstractLogger;
 50 | use Psr\Log\LoggerInterface;
 51 | 
 52 | class StderrLogger extends AbstractLogger
 53 | {
 54 |     public function log($level, \Stringable|string $message, array $context = []): void
 55 |     {
 56 |         fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
 57 |     }
 58 | }
 59 | 
 60 | try {
 61 |     $logger = new StderrLogger();
 62 |     $logger->info('Starting MCP HTTP User Profile Server...');
 63 | 
 64 |     // --- Setup DI Container for DI in McpElements class ---
 65 |     $container = new BasicContainer();
 66 |     $container->set(LoggerInterface::class, $logger);
 67 | 
 68 |     $server = Server::make()
 69 |         ->withServerInfo('HTTP User Profiles', '1.0.0')
 70 |         ->withCapabilities(ServerCapabilities::make(completions: true, logging: true))
 71 |         ->withLogger($logger)
 72 |         ->withContainer($container)
 73 |         ->withTool(
 74 |             function (float $a, float $b, string $operation = 'add'): array {
 75 |                 $result = match ($operation) {
 76 |                     'add' => $a + $b,
 77 |                     'subtract' => $a - $b,
 78 |                     'multiply' => $a * $b,
 79 |                     'divide' => $b != 0 ? $a / $b : throw new \InvalidArgumentException('Cannot divide by zero'),
 80 |                     default => throw new \InvalidArgumentException("Unknown operation: {$operation}")
 81 |                 };
 82 | 
 83 |                 return [
 84 |                     'operation' => $operation,
 85 |                     'operands' => [$a, $b],
 86 |                     'result' => $result
 87 |                 ];
 88 |             },
 89 |             name: 'calculator',
 90 |             description: 'Perform basic math operations (add, subtract, multiply, divide)'
 91 |         )
 92 |         ->withResource(
 93 |             function (): array {
 94 |                 $memoryUsage = memory_get_usage(true);
 95 |                 $memoryPeak = memory_get_peak_usage(true);
 96 |                 $uptime = time() - $_SERVER['REQUEST_TIME_FLOAT'] ?? time();
 97 |                 $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'CLI';
 98 | 
 99 |                 return [
100 |                     'server_time' => date('Y-m-d H:i:s'),
101 |                     'uptime_seconds' => $uptime,
102 |                     'memory_usage_mb' => round($memoryUsage / 1024 / 1024, 2),
103 |                     'memory_peak_mb' => round($memoryPeak / 1024 / 1024, 2),
104 |                     'php_version' => PHP_VERSION,
105 |                     'server_software' => $serverSoftware,
106 |                     'operating_system' => PHP_OS_FAMILY,
107 |                     'status' => 'healthy'
108 |                 ];
109 |             },
110 |             uri: 'system://status',
111 |             name: 'system_status',
112 |             description: 'Current system status and runtime information',
113 |             mimeType: 'application/json'
114 |         )
115 |         ->build();
116 | 
117 |     $server->discover(__DIR__, ['.']);
118 | 
119 |     // $transport = new HttpServerTransport('127.0.0.1', 8080, 'mcp');
120 |     $transport = new StreamableHttpServerTransport('127.0.0.1', 8080, 'mcp');
121 | 
122 |     $server->listen($transport);
123 | 
124 |     $logger->info('Server listener stopped gracefully.');
125 |     exit(0);
126 | } catch (\Throwable $e) {
127 |     fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
128 |     fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
129 |     fwrite(STDERR, 'File: ' . $e->getFile() . ':' . $e->getLine() . "\n");
130 |     fwrite(STDERR, $e->getTraceAsString() . "\n");
131 |     exit(1);
132 | }
133 | 
```

--------------------------------------------------------------------------------
/src/Session/Session.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\Contracts\SessionInterface;
  9 | 
 10 | class Session implements SessionInterface
 11 | {
 12 |     /**
 13 |      * @var array<string, mixed> Stores all session data.
 14 |      * Keys are snake_case by convention for MCP-specific data.
 15 |      *
 16 |      * Official keys are:
 17 |      * - initialized: bool
 18 |      * - client_info: array|null
 19 |      * - protocol_version: string|null
 20 |      * - subscriptions: array<string, bool>
 21 |      * - message_queue: array<string>
 22 |      * - log_level: string|null
 23 |      */
 24 |     protected array $data = [];
 25 | 
 26 |     public function __construct(
 27 |         protected SessionHandlerInterface $handler,
 28 |         protected string $id = '',
 29 |         ?array $data = null
 30 |     ) {
 31 |         if (empty($this->id)) {
 32 |             $this->id = $this->generateId();
 33 |         }
 34 | 
 35 |         if ($data !== null) {
 36 |             $this->hydrate($data);
 37 |         } elseif ($sessionData = $this->handler->read($this->id)) {
 38 |             $this->data = json_decode($sessionData, true) ?? [];
 39 |         }
 40 |     }
 41 | 
 42 |     /**
 43 |      * Retrieve an existing session instance from handler or return null if session doesn't exist
 44 |      */
 45 |     public static function retrieve(string $id, SessionHandlerInterface $handler): ?SessionInterface
 46 |     {
 47 |         $sessionData = $handler->read($id);
 48 | 
 49 |         if (!$sessionData) {
 50 |             return null;
 51 |         }
 52 | 
 53 |         $data = json_decode($sessionData, true);
 54 |         if ($data === null) {
 55 |             return null;
 56 |         }
 57 | 
 58 |         return new static($handler, $id, $data);
 59 |     }
 60 | 
 61 |     public function getId(): string
 62 |     {
 63 |         return $this->id;
 64 |     }
 65 | 
 66 |     public function getHandler(): SessionHandlerInterface
 67 |     {
 68 |         return $this->handler;
 69 |     }
 70 | 
 71 |     public function generateId(): string
 72 |     {
 73 |         return bin2hex(random_bytes(16));
 74 |     }
 75 | 
 76 |     public function save(): void
 77 |     {
 78 |         $this->handler->write($this->id, json_encode($this->data));
 79 |     }
 80 | 
 81 |     public function get(string $key, mixed $default = null): mixed
 82 |     {
 83 |         $key = explode('.', $key);
 84 |         $data = $this->data;
 85 | 
 86 |         foreach ($key as $segment) {
 87 |             if (is_array($data) && array_key_exists($segment, $data)) {
 88 |                 $data = $data[$segment];
 89 |             } else {
 90 |                 return $default;
 91 |             }
 92 |         }
 93 | 
 94 |         return $data;
 95 |     }
 96 | 
 97 |     public function set(string $key, mixed $value, bool $overwrite = true): void
 98 |     {
 99 |         $segments = explode('.', $key);
100 |         $data = &$this->data;
101 | 
102 |         while (count($segments) > 1) {
103 |             $segment = array_shift($segments);
104 |             if (!isset($data[$segment]) || !is_array($data[$segment])) {
105 |                 $data[$segment] = [];
106 |             }
107 |             $data = &$data[$segment];
108 |         }
109 | 
110 |         $lastKey = array_shift($segments);
111 |         if ($overwrite || !isset($data[$lastKey])) {
112 |             $data[$lastKey] = $value;
113 |         }
114 |     }
115 | 
116 |     public function has(string $key): bool
117 |     {
118 |         $key = explode('.', $key);
119 |         $data = $this->data;
120 | 
121 |         foreach ($key as $segment) {
122 |             if (is_array($data) && array_key_exists($segment, $data)) {
123 |                 $data = $data[$segment];
124 |             } elseif (is_object($data) && isset($data->{$segment})) {
125 |                 $data = $data->{$segment};
126 |             } else {
127 |                 return false;
128 |             }
129 |         }
130 | 
131 |         return true;
132 |     }
133 | 
134 |     public function forget(string $key): void
135 |     {
136 |         $segments = explode('.', $key);
137 |         $data = &$this->data;
138 | 
139 |         while (count($segments) > 1) {
140 |             $segment = array_shift($segments);
141 |             if (!isset($data[$segment]) || !is_array($data[$segment])) {
142 |                 $data[$segment] = [];
143 |             }
144 |             $data = &$data[$segment];
145 |         }
146 | 
147 |         $lastKey = array_shift($segments);
148 |         if (isset($data[$lastKey])) {
149 |             unset($data[$lastKey]);
150 |         }
151 |     }
152 | 
153 |     public function clear(): void
154 |     {
155 |         $this->data = [];
156 |     }
157 | 
158 |     public function pull(string $key, mixed $default = null): mixed
159 |     {
160 |         $value = $this->get($key, $default);
161 |         $this->forget($key);
162 |         return $value;
163 |     }
164 | 
165 |     public function all(): array
166 |     {
167 |         return $this->data;
168 |     }
169 | 
170 |     public function hydrate(array $attributes): void
171 |     {
172 |         $this->data = array_merge(
173 |             [
174 |                 'initialized' => false,
175 |                 'client_info' => null,
176 |                 'protocol_version' => null,
177 |                 'message_queue' => [],
178 |                 'log_level' => null,
179 |             ],
180 |             $attributes
181 |         );
182 |         unset($this->data['id']);
183 |     }
184 | 
185 |     public function queueMessage(string $rawFramedMessage): void
186 |     {
187 |         $this->data['message_queue'][] = $rawFramedMessage;
188 |     }
189 | 
190 |     public function dequeueMessages(): array
191 |     {
192 |         $messages = $this->data['message_queue'] ?? [];
193 |         $this->data['message_queue'] = [];
194 |         return $messages;
195 |     }
196 | 
197 |     public function hasQueuedMessages(): bool
198 |     {
199 |         return !empty($this->data['message_queue']);
200 |     }
201 | 
202 |     public function jsonSerialize(): array
203 |     {
204 |         return $this->all();
205 |     }
206 | }
207 | 
```

--------------------------------------------------------------------------------
/tests/Mocks/Clients/MockJsonHttpClient.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server\Tests\Mocks\Clients;
  6 | 
  7 | use Psr\Http\Message\ResponseInterface;
  8 | use React\Http\Browser;
  9 | use React\Promise\PromiseInterface;
 10 | 
 11 | class MockJsonHttpClient
 12 | {
 13 |     public Browser $browser;
 14 |     public string $baseUrl;
 15 |     public ?string $sessionId = null;
 16 |     public array $lastResponseHeaders = []; // Store last response headers for testing
 17 | 
 18 |     public function __construct(string $host, int $port, string $mcpPath, int $timeout = 2)
 19 |     {
 20 |         $this->browser = (new Browser())->withTimeout($timeout);
 21 |         $this->baseUrl = "http://{$host}:{$port}/{$mcpPath}";
 22 |     }
 23 | 
 24 |     public function sendRequest(string $method, array $params = [], ?string $id = null, array $additionalHeaders = []): PromiseInterface
 25 |     {
 26 |         $payload = ['jsonrpc' => '2.0', 'method' => $method, 'params' => $params];
 27 |         if ($id !== null) {
 28 |             $payload['id'] = $id;
 29 |         }
 30 | 
 31 |         $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json, text/event-stream'];
 32 |         if ($this->sessionId && $method !== 'initialize') {
 33 |             $headers['Mcp-Session-Id'] = $this->sessionId;
 34 |         }
 35 |         $headers += $additionalHeaders;
 36 | 
 37 |         $body = json_encode($payload);
 38 | 
 39 |         return $this->browser->post($this->baseUrl, $headers, $body)
 40 |             ->then(function (ResponseInterface $response) use ($method) {
 41 |                 // Store response headers for testing
 42 |                 $this->lastResponseHeaders = [];
 43 |                 foreach ($response->getHeaders() as $name => $values) {
 44 |                     foreach ($values as $value) {
 45 |                         $this->lastResponseHeaders[] = "{$name}: {$value}";
 46 |                     }
 47 |                 }
 48 | 
 49 |                 $bodyContent = (string) $response->getBody()->getContents();
 50 |                 $statusCode = $response->getStatusCode();
 51 | 
 52 |                 if ($method === 'initialize' && $statusCode === 200) {
 53 |                     $this->sessionId = $response->getHeaderLine('Mcp-Session-Id');
 54 |                 }
 55 | 
 56 |                 if ($statusCode === 202) {
 57 |                     if ($bodyContent !== '') {
 58 |                         throw new \RuntimeException("Expected empty body for 202 response, got: {$bodyContent}");
 59 |                     }
 60 |                     return ['statusCode' => $statusCode, 'body' => null, 'headers' => $response->getHeaders()];
 61 |                 }
 62 | 
 63 |                 try {
 64 |                     $decoded = json_decode($bodyContent, true, 512, JSON_THROW_ON_ERROR);
 65 |                     return ['statusCode' => $statusCode, 'body' => $decoded, 'headers' => $response->getHeaders()];
 66 |                 } catch (\JsonException $e) {
 67 |                     throw new \RuntimeException("Failed to decode JSON response body: {$bodyContent} Error: {$e->getMessage()}", $statusCode, $e);
 68 |                 }
 69 |             });
 70 |     }
 71 | 
 72 |     public function sendBatchRequest(array $batchRequestObjects): PromiseInterface
 73 |     {
 74 |         $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
 75 |         if ($this->sessionId) {
 76 |             $headers['Mcp-Session-Id'] = $this->sessionId;
 77 |         }
 78 |         $body = json_encode($batchRequestObjects);
 79 | 
 80 |         return $this->browser->post($this->baseUrl, $headers, $body)
 81 |             ->then(function (ResponseInterface $response) {
 82 |                 $bodyContent = (string) $response->getBody()->getContents();
 83 |                 $statusCode = $response->getStatusCode();
 84 |                 if ($statusCode === 202) {
 85 |                     if ($bodyContent !== '') {
 86 |                         throw new \RuntimeException("Expected empty body for 202 response, got: {$bodyContent}");
 87 |                     }
 88 |                     return ['statusCode' => $statusCode, 'body' => null, 'headers' => $response->getHeaders()];
 89 |                 }
 90 | 
 91 |                 try {
 92 |                     $decoded = json_decode($bodyContent, true, 512, JSON_THROW_ON_ERROR);
 93 |                     return ['statusCode' => $statusCode, 'body' => $decoded, 'headers' => $response->getHeaders()];
 94 |                 } catch (\JsonException $e) {
 95 |                     throw new \RuntimeException("Failed to decode JSON response body: {$bodyContent} Error: {$e->getMessage()}", $statusCode, $e);
 96 |                 }
 97 |             });
 98 |     }
 99 | 
100 |     public function sendDeleteRequest(): PromiseInterface
101 |     {
102 |         $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
103 |         if ($this->sessionId) {
104 |             $headers['Mcp-Session-Id'] = $this->sessionId;
105 |         }
106 | 
107 |         return $this->browser->delete($this->baseUrl, $headers)
108 |             ->then(function (ResponseInterface $response) {
109 |                 $bodyContent = (string) $response->getBody()->getContents();
110 |                 $statusCode = $response->getStatusCode();
111 |                 return ['statusCode' => $statusCode, 'body' => $bodyContent, 'headers' => $response->getHeaders()];
112 |             });
113 |     }
114 | 
115 |     public function sendNotification(string $method, array $params = []): PromiseInterface
116 |     {
117 |         return $this->sendRequest($method, $params, null);
118 |     }
119 | 
120 |     public function connectSseForNotifications(): PromiseInterface
121 |     {
122 |         return resolve(null);
123 |     }
124 | }
125 | 
```

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

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Fixtures\General;
  4 | 
  5 | use PhpMcp\Schema\Content\PromptMessage;
  6 | use PhpMcp\Schema\Enum\Role;
  7 | use PhpMcp\Schema\Content\TextContent;
  8 | use PhpMcp\Schema\Content\ImageContent;
  9 | use PhpMcp\Schema\Content\AudioContent;
 10 | use PhpMcp\Server\Attributes\CompletionProvider;
 11 | use Psr\Log\LoggerInterface;
 12 | 
 13 | class PromptHandlerFixture
 14 | {
 15 |     public function generateSimpleGreeting(string $name, string $style = "friendly"): array
 16 |     {
 17 |         return [
 18 |             ['role' => 'user', 'content' => "Craft a {$style} greeting for {$name}."]
 19 |         ];
 20 |     }
 21 | 
 22 |     public function returnSinglePromptMessageObject(): PromptMessage
 23 |     {
 24 |         return PromptMessage::make(Role::User, TextContent::make("Single PromptMessage object."));
 25 |     }
 26 | 
 27 |     public function returnArrayOfPromptMessageObjects(): array
 28 |     {
 29 |         return [
 30 |             PromptMessage::make(Role::User, TextContent::make("First message object.")),
 31 |             PromptMessage::make(Role::Assistant, ImageContent::make("img_data", "image/png")),
 32 |         ];
 33 |     }
 34 | 
 35 |     public function returnEmptyArrayForPrompt(): array
 36 |     {
 37 |         return [];
 38 |     }
 39 | 
 40 |     public function returnSimpleUserAssistantMap(): array
 41 |     {
 42 |         return [
 43 |             'user' => "This is the user's turn.",
 44 |             'assistant' => "And this is the assistant's reply."
 45 |         ];
 46 |     }
 47 | 
 48 |     public function returnUserAssistantMapWithContentObjects(): array
 49 |     {
 50 |         return [
 51 |             'user' => TextContent::make("User text content object."),
 52 |             'assistant' => ImageContent::make("asst_img_data", "image/gif"),
 53 |         ];
 54 |     }
 55 | 
 56 |     public function returnUserAssistantMapWithMixedContent(): array
 57 |     {
 58 |         return [
 59 |             'user' => "Plain user string.",
 60 |             'assistant' => AudioContent::make("aud_data", "audio/mp3"),
 61 |         ];
 62 |     }
 63 | 
 64 |     public function returnUserAssistantMapWithArrayContent(): array
 65 |     {
 66 |         return [
 67 |             'user' => ['type' => 'text', 'text' => 'User array content'],
 68 |             'assistant' => ['type' => 'image', 'data' => 'asst_arr_img_data', 'mimeType' => 'image/jpeg'],
 69 |         ];
 70 |     }
 71 | 
 72 |     public function returnListOfRawMessageArrays(): array
 73 |     {
 74 |         return [
 75 |             ['role' => 'user', 'content' => "First raw message string."],
 76 |             ['role' => 'assistant', 'content' => TextContent::make("Second raw message with Content obj.")],
 77 |             ['role' => 'user', 'content' => ['type' => 'image', 'data' => 'raw_img_data', 'mimeType' => 'image/webp']],
 78 |             ['role' => 'assistant', 'content' => ['type' => 'audio', 'data' => 'raw_aud_data', 'mimeType' => 'audio/ogg']],
 79 |             ['role' => 'user', 'content' => ['type' => 'resource', 'resource' => ['uri' => 'file://doc.pdf', 'blob' => base64_encode('pdf-data'), 'mimeType' => 'application/pdf']]],
 80 |             ['role' => 'assistant', 'content' => ['type' => 'resource', 'resource' => ['uri' => 'config://settings.json', 'text' => '{"theme":"dark"}']]],
 81 |         ];
 82 |     }
 83 | 
 84 |     public function returnListOfRawMessageArraysWithScalars(): array
 85 |     {
 86 |         return [
 87 |             ['role' => 'user', 'content' => 123],          // int
 88 |             ['role' => 'assistant', 'content' => true],       // bool
 89 |             ['role' => 'user', 'content' => null],         // null
 90 |             ['role' => 'assistant', 'content' => 3.14],       // float
 91 |             ['role' => 'user', 'content' => ['key' => 'value']], // array that becomes JSON
 92 |         ];
 93 |     }
 94 | 
 95 |     public function returnMixedArrayOfPromptMessagesAndRaw(): array
 96 |     {
 97 |         return [
 98 |             PromptMessage::make(Role::User, TextContent::make("This is a PromptMessage object.")),
 99 |             ['role' => 'assistant', 'content' => "This is a raw message array."],
100 |             PromptMessage::make(Role::User, ImageContent::make("pm_img", "image/bmp")),
101 |             ['role' => 'assistant', 'content' => ['type' => 'text', 'text' => 'Raw message with typed content.']],
102 |         ];
103 |     }
104 | 
105 |     public function promptWithArgumentCompletion(
106 |         #[CompletionProvider(provider: CompletionProviderFixture::class)]
107 |         string $entityName,
108 |         string $action = "describe"
109 |     ): array {
110 |         return [
111 |             ['role' => 'user', 'content' => "Please {$action} the entity: {$entityName}."]
112 |         ];
113 |     }
114 | 
115 |     public function promptReturnsNonArray(): string
116 |     {
117 |         return "This is not a valid prompt return type.";
118 |     }
119 | 
120 |     public function promptReturnsArrayWithInvalidRole(): array
121 |     {
122 |         return [['role' => 'system', 'content' => 'System messages are not directly supported.']];
123 |     }
124 | 
125 |     public function promptReturnsInvalidRole(): array
126 |     {
127 |         return [['role' => 'system', 'content' => 'System messages are not directly supported.']];
128 |     }
129 | 
130 |     public function promptReturnsArrayWithInvalidContentStructure(): array
131 |     {
132 |         return [['role' => 'user', 'content' => ['text_only_no_type' => 'invalid']]];
133 |     }
134 | 
135 |     public function promptReturnsArrayWithInvalidTypedContent(): array
136 |     {
137 |         return [['role' => 'user', 'content' => ['type' => 'image', 'source' => 'url.jpg']]]; // 'image' needs 'data' and 'mimeType'
138 |     }
139 | 
140 |     public function promptReturnsArrayWithInvalidResourceContent(): array
141 |     {
142 |         return [
143 |             [
144 |                 'role' => 'user',
145 |                 'content' => ['type' => 'resource', 'resource' => ['uri' => 'uri://uri']]
146 |             ]
147 |         ];
148 |     }
149 | 
150 |     public function promptHandlerThrows(): void
151 |     {
152 |         throw new \LogicException("Prompt generation failed inside handler.");
153 |     }
154 | }
155 | 
```

--------------------------------------------------------------------------------
/tests/Unit/Utils/HandlerResolverTest.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Unit\Utils;
  4 | 
  5 | use PhpMcp\Server\Utils\HandlerResolver;
  6 | use ReflectionMethod;
  7 | use ReflectionFunction;
  8 | use InvalidArgumentException;
  9 | 
 10 | class ValidHandlerClass
 11 | {
 12 |     public function publicMethod() {}
 13 |     protected function protectedMethod() {}
 14 |     private function privateMethod() {}
 15 |     public static function staticMethod() {}
 16 |     public function __construct() {}
 17 |     public function __destruct() {}
 18 | }
 19 | 
 20 | class ValidInvokableClass
 21 | {
 22 |     public function __invoke() {}
 23 | }
 24 | 
 25 | class NonInvokableClass {}
 26 | 
 27 | abstract class AbstractHandlerClass
 28 | {
 29 |     abstract public function abstractMethod();
 30 | }
 31 | 
 32 | // Test closure support
 33 | it('resolves closures to ReflectionFunction', function () {
 34 |     $closure = function (string $input): string {
 35 |         return "processed: $input";
 36 |     };
 37 | 
 38 |     $resolved = HandlerResolver::resolve($closure);
 39 | 
 40 |     expect($resolved)->toBeInstanceOf(ReflectionFunction::class);
 41 |     expect($resolved->getNumberOfParameters())->toBe(1);
 42 |     expect($resolved->getReturnType()->getName())->toBe('string');
 43 | });
 44 | 
 45 | it('resolves valid array handler', function () {
 46 |     $handler = [ValidHandlerClass::class, 'publicMethod'];
 47 |     $resolved = HandlerResolver::resolve($handler);
 48 | 
 49 |     expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
 50 |     expect($resolved->getName())->toBe('publicMethod');
 51 |     expect($resolved->getDeclaringClass()->getName())->toBe(ValidHandlerClass::class);
 52 | });
 53 | 
 54 | it('resolves valid invokable class string handler', function () {
 55 |     $handler = ValidInvokableClass::class;
 56 |     $resolved = HandlerResolver::resolve($handler);
 57 | 
 58 |     expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
 59 |     expect($resolved->getName())->toBe('__invoke');
 60 |     expect($resolved->getDeclaringClass()->getName())->toBe(ValidInvokableClass::class);
 61 | });
 62 | 
 63 | it('resolves static methods for manual registration', function () {
 64 |     $handler = [ValidHandlerClass::class, 'staticMethod'];
 65 |     $resolved = HandlerResolver::resolve($handler);
 66 | 
 67 |     expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
 68 |     expect($resolved->getName())->toBe('staticMethod');
 69 |     expect($resolved->isStatic())->toBeTrue();
 70 | });
 71 | 
 72 | it('throws for invalid array handler format (count)', function () {
 73 |     HandlerResolver::resolve([ValidHandlerClass::class]);
 74 | })->throws(InvalidArgumentException::class, 'Invalid array handler format. Expected [ClassName::class, \'methodName\'].');
 75 | 
 76 | it('throws for invalid array handler format (types)', function () {
 77 |     HandlerResolver::resolve([ValidHandlerClass::class, 123]);
 78 | })->throws(InvalidArgumentException::class, 'Invalid array handler format. Expected [ClassName::class, \'methodName\'].');
 79 | 
 80 | it('throws for non-existent class in array handler', function () {
 81 |     HandlerResolver::resolve(['NonExistentClass', 'method']);
 82 | })->throws(InvalidArgumentException::class, "Handler class 'NonExistentClass' not found");
 83 | 
 84 | it('throws for non-existent method in array handler', function () {
 85 |     HandlerResolver::resolve([ValidHandlerClass::class, 'nonExistentMethod']);
 86 | })->throws(InvalidArgumentException::class, "Handler method 'nonExistentMethod' not found in class");
 87 | 
 88 | it('throws for non-existent class in string handler', function () {
 89 |     HandlerResolver::resolve('NonExistentInvokableClass');
 90 | })->throws(InvalidArgumentException::class, 'Invalid handler format. Expected Closure, [ClassName::class, \'methodName\'] or InvokableClassName::class string.');
 91 | 
 92 | it('throws for non-invokable class string handler', function () {
 93 |     HandlerResolver::resolve(NonInvokableClass::class);
 94 | })->throws(InvalidArgumentException::class, "Invokable handler class '" . NonInvokableClass::class . "' must have a public '__invoke' method.");
 95 | 
 96 | it('throws for protected method handler', function () {
 97 |     HandlerResolver::resolve([ValidHandlerClass::class, 'protectedMethod']);
 98 | })->throws(InvalidArgumentException::class, 'must be public');
 99 | 
100 | it('throws for private method handler', function () {
101 |     HandlerResolver::resolve([ValidHandlerClass::class, 'privateMethod']);
102 | })->throws(InvalidArgumentException::class, 'must be public');
103 | 
104 | it('throws for constructor as handler', function () {
105 |     HandlerResolver::resolve([ValidHandlerClass::class, '__construct']);
106 | })->throws(InvalidArgumentException::class, 'cannot be a constructor or destructor');
107 | 
108 | it('throws for destructor as handler', function () {
109 |     HandlerResolver::resolve([ValidHandlerClass::class, '__destruct']);
110 | })->throws(InvalidArgumentException::class, 'cannot be a constructor or destructor');
111 | 
112 | it('throws for abstract method handler', function () {
113 |     HandlerResolver::resolve([AbstractHandlerClass::class, 'abstractMethod']);
114 | })->throws(InvalidArgumentException::class, 'cannot be abstract');
115 | 
116 | // Test different closure types
117 | it('resolves closures with different signatures', function () {
118 |     $noParams = function () {
119 |         return 'test';
120 |     };
121 |     $withParams = function (int $a, string $b = 'default') {
122 |         return $a . $b;
123 |     };
124 |     $variadic = function (...$args) {
125 |         return $args;
126 |     };
127 | 
128 |     expect(HandlerResolver::resolve($noParams))->toBeInstanceOf(ReflectionFunction::class);
129 |     expect(HandlerResolver::resolve($withParams))->toBeInstanceOf(ReflectionFunction::class);
130 |     expect(HandlerResolver::resolve($variadic))->toBeInstanceOf(ReflectionFunction::class);
131 | 
132 |     expect(HandlerResolver::resolve($noParams)->getNumberOfParameters())->toBe(0);
133 |     expect(HandlerResolver::resolve($withParams)->getNumberOfParameters())->toBe(2);
134 |     expect(HandlerResolver::resolve($variadic)->isVariadic())->toBeTrue();
135 | });
136 | 
137 | // Test that we can distinguish between closures and callable arrays
138 | it('distinguishes between closures and callable arrays', function () {
139 |     $closure = function () {
140 |         return 'closure';
141 |     };
142 |     $array = [ValidHandlerClass::class, 'publicMethod'];
143 |     $string = ValidInvokableClass::class;
144 | 
145 |     expect(HandlerResolver::resolve($closure))->toBeInstanceOf(ReflectionFunction::class);
146 |     expect(HandlerResolver::resolve($array))->toBeInstanceOf(ReflectionMethod::class);
147 |     expect(HandlerResolver::resolve($string))->toBeInstanceOf(ReflectionMethod::class);
148 | });
149 | 
```

--------------------------------------------------------------------------------
/tests/Unit/Utils/DocBlockParserTest.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Unit\Utils;
  4 | 
  5 | use phpDocumentor\Reflection\DocBlock;
  6 | use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
  7 | use phpDocumentor\Reflection\DocBlock\Tags\Param;
  8 | use phpDocumentor\Reflection\DocBlock\Tags\Return_;
  9 | use phpDocumentor\Reflection\DocBlock\Tags\See;
 10 | use phpDocumentor\Reflection\DocBlock\Tags\Throws;
 11 | use PhpMcp\Server\Utils\DocBlockParser;
 12 | use PhpMcp\Server\Tests\Fixtures\General\DocBlockTestFixture;
 13 | use ReflectionMethod;
 14 | 
 15 | beforeEach(function () {
 16 |     $this->parser = new DocBlockParser();
 17 | });
 18 | 
 19 | test('getSummary returns correct summary', function () {
 20 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
 21 |     $docComment = $method->getDocComment() ?: null;
 22 |     $docBlock = $this->parser->parseDocBlock($docComment);
 23 |     expect($this->parser->getSummary($docBlock))->toBe('Simple summary line.');
 24 | 
 25 |     $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
 26 |     $docComment2 = $method2->getDocComment() ?: null;
 27 |     $docBlock2 = $this->parser->parseDocBlock($docComment2);
 28 |     expect($this->parser->getSummary($docBlock2))->toBe('Summary line here.');
 29 | });
 30 | 
 31 | test('getDescription returns correct description', function () {
 32 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
 33 |     $docComment = $method->getDocComment() ?: null;
 34 |     $docBlock = $this->parser->parseDocBlock($docComment);
 35 |     $expectedDesc = "Summary line here.\n\nThis is a longer description spanning\nmultiple lines.\nIt might contain *markdown* or `code`.";
 36 |     expect($this->parser->getDescription($docBlock))->toBe($expectedDesc);
 37 | 
 38 |     $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
 39 |     $docComment2 = $method2->getDocComment() ?: null;
 40 |     $docBlock2 = $this->parser->parseDocBlock($docComment2);
 41 |     expect($this->parser->getDescription($docBlock2))->toBe('Simple summary line.');
 42 | });
 43 | 
 44 | test('getParamTags returns structured param info', function () {
 45 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithParams');
 46 |     $docComment = $method->getDocComment() ?: null;
 47 |     $docBlock = $this->parser->parseDocBlock($docComment);
 48 |     $params = $this->parser->getParamTags($docBlock);
 49 | 
 50 |     expect($params)->toBeArray()->toHaveCount(6);
 51 |     expect($params)->toHaveKeys(['$param1', '$param2', '$param3', '$param4', '$param5', '$param6']);
 52 | 
 53 |     expect($params['$param1'])->toBeInstanceOf(Param::class);
 54 |     expect($params['$param1']->getVariableName())->toBe('param1');
 55 |     expect($this->parser->getParamTypeString($params['$param1']))->toBe('string');
 56 |     expect($this->parser->getParamDescription($params['$param1']))->toBe('Description for string param.');
 57 | 
 58 |     expect($params['$param2'])->toBeInstanceOf(Param::class);
 59 |     expect($params['$param2']->getVariableName())->toBe('param2');
 60 |     expect($this->parser->getParamTypeString($params['$param2']))->toBe('int|null');
 61 |     expect($this->parser->getParamDescription($params['$param2']))->toBe('Description for nullable int param.');
 62 | 
 63 |     expect($params['$param3'])->toBeInstanceOf(Param::class);
 64 |     expect($params['$param3']->getVariableName())->toBe('param3');
 65 |     expect($this->parser->getParamTypeString($params['$param3']))->toBe('bool');
 66 |     expect($this->parser->getParamDescription($params['$param3']))->toBeNull();
 67 | 
 68 |     expect($params['$param4'])->toBeInstanceOf(Param::class);
 69 |     expect($params['$param4']->getVariableName())->toBe('param4');
 70 |     expect($this->parser->getParamTypeString($params['$param4']))->toBe('mixed');
 71 |     expect($this->parser->getParamDescription($params['$param4']))->toBe('Missing type.');
 72 | 
 73 |     expect($params['$param5'])->toBeInstanceOf(Param::class);
 74 |     expect($params['$param5']->getVariableName())->toBe('param5');
 75 |     expect($this->parser->getParamTypeString($params['$param5']))->toBe('array<string,mixed>');
 76 |     expect($this->parser->getParamDescription($params['$param5']))->toBe('Array description.');
 77 | 
 78 |     expect($params['$param6'])->toBeInstanceOf(Param::class);
 79 |     expect($params['$param6']->getVariableName())->toBe('param6');
 80 |     expect($this->parser->getParamTypeString($params['$param6']))->toBe('stdClass');
 81 |     expect($this->parser->getParamDescription($params['$param6']))->toBe('Object param.');
 82 | });
 83 | 
 84 | test('getReturnTag returns structured return info', function () {
 85 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithReturn');
 86 |     $docComment = $method->getDocComment() ?: null;
 87 |     $docBlock = $this->parser->parseDocBlock($docComment);
 88 |     $returnTag = $this->parser->getReturnTag($docBlock);
 89 | 
 90 |     expect($returnTag)->toBeInstanceOf(Return_::class);
 91 |     expect($this->parser->getReturnTypeString($returnTag))->toBe('string');
 92 |     expect($this->parser->getReturnDescription($returnTag))->toBe('The result of the operation.');
 93 | 
 94 |     $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
 95 |     $docComment2 = $method2->getDocComment() ?: null;
 96 |     $docBlock2 = $this->parser->parseDocBlock($docComment2);
 97 |     expect($this->parser->getReturnTag($docBlock2))->toBeNull();
 98 | });
 99 | 
100 | test('getTagsByName returns specific tags', function () {
101 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithMultipleTags');
102 |     $docComment = $method->getDocComment() ?: null;
103 |     $docBlock = $this->parser->parseDocBlock($docComment);
104 | 
105 |     expect($docBlock)->toBeInstanceOf(DocBlock::class);
106 | 
107 |     $throwsTags = $docBlock->getTagsByName('throws');
108 |     expect($throwsTags)->toBeArray()->toHaveCount(1);
109 |     expect($throwsTags[0])->toBeInstanceOf(Throws::class);
110 |     expect((string) $throwsTags[0]->getType())->toBe('\\RuntimeException');
111 |     expect($throwsTags[0]->getDescription()->render())->toBe('If processing fails.');
112 | 
113 |     $deprecatedTags = $docBlock->getTagsByName('deprecated');
114 |     expect($deprecatedTags)->toBeArray()->toHaveCount(1);
115 |     expect($deprecatedTags[0])->toBeInstanceOf(Deprecated::class);
116 |     expect($deprecatedTags[0]->getDescription()->render())->toBe('Use newMethod() instead.');
117 | 
118 |     $seeTags = $docBlock->getTagsByName('see');
119 |     expect($seeTags)->toBeArray()->toHaveCount(1);
120 |     expect($seeTags[0])->toBeInstanceOf(See::class);
121 |     expect((string) $seeTags[0]->getReference())->toContain('DocBlockTestFixture::newMethod()');
122 | 
123 |     $nonExistentTags = $docBlock->getTagsByName('nosuchtag');
124 |     expect($nonExistentTags)->toBeArray()->toBeEmpty();
125 | });
126 | 
127 | test('handles method with no docblock gracefully', function () {
128 |     $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithNoDocBlock');
129 |     $docComment = $method->getDocComment() ?: null;
130 |     $docBlock = $this->parser->parseDocBlock($docComment);
131 | 
132 |     expect($docBlock)->toBeNull();
133 | 
134 |     expect($this->parser->getSummary($docBlock))->toBeNull();
135 |     expect($this->parser->getDescription($docBlock))->toBeNull();
136 |     expect($this->parser->getParamTags($docBlock))->toBeArray()->toBeEmpty();
137 |     expect($this->parser->getReturnTag($docBlock))->toBeNull();
138 | });
139 | 
```

--------------------------------------------------------------------------------
/src/Attributes/Schema.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server\Attributes;
  6 | 
  7 | use Attribute;
  8 | 
  9 | /**
 10 |  * Defines a JSON Schema for a method's input or an individual parameter.
 11 |  *
 12 |  * When used at the method level, it describes an object schema where properties
 13 |  * correspond to the method's parameters.
 14 |  *
 15 |  * When used at the parameter level, it describes the schema for that specific parameter.
 16 |  * If 'type' is omitted at the parameter level, it will be inferred.
 17 |  */
 18 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PARAMETER)]
 19 | class Schema
 20 | {
 21 |     /**
 22 |      * The complete JSON schema array.
 23 |      * If provided, it takes precedence over individual properties like $type, $properties, etc.
 24 |      */
 25 |     public ?array $definition = null;
 26 | 
 27 |     /**
 28 |      * Alternatively, provide individual top-level schema keywords.
 29 |      * These are used if $definition is null.
 30 |      */
 31 |     public ?string $type = null;
 32 |     public ?string $description = null;
 33 |     public mixed $default = null;
 34 |     public ?array $enum = null; // list of allowed values
 35 |     public ?string $format = null; // e.g., 'email', 'date-time'
 36 | 
 37 |     // Constraints for string
 38 |     public ?int $minLength = null;
 39 |     public ?int $maxLength = null;
 40 |     public ?string $pattern = null;
 41 | 
 42 |     // Constraints for number/integer
 43 |     public int|float|null $minimum = null;
 44 |     public int|float|null $maximum = null;
 45 |     public ?bool $exclusiveMinimum = null;
 46 |     public ?bool $exclusiveMaximum = null;
 47 |     public int|float|null $multipleOf = null;
 48 | 
 49 |     // Constraints for array
 50 |     public ?array $items = null; // JSON schema for array items
 51 |     public ?int $minItems = null;
 52 |     public ?int $maxItems = null;
 53 |     public ?bool $uniqueItems = null;
 54 | 
 55 |     // Constraints for object (primarily used when Schema is on a method or an object-typed parameter)
 56 |     public ?array $properties = null; // [propertyName => [schema array], ...]
 57 |     public ?array $required = null;   // [propertyName, ...]
 58 |     public bool|array|null $additionalProperties = null; // true, false, or a schema array
 59 | 
 60 |     /**
 61 |      * @param array|null $definition A complete JSON schema array. If provided, other parameters are ignored.
 62 |      * @param Type|null $type The JSON schema type.
 63 |      * @param string|null $description Description of the element.
 64 |      * @param array|null $enum Allowed enum values.
 65 |      * @param string|null $format String format (e.g., 'date-time', 'email').
 66 |      * @param int|null $minLength Minimum length for strings.
 67 |      * @param int|null $maxLength Maximum length for strings.
 68 |      * @param string|null $pattern Regex pattern for strings.
 69 |      * @param int|float|null $minimum Minimum value for numbers/integers.
 70 |      * @param int|float|null $maximum Maximum value for numbers/integers.
 71 |      * @param bool|null $exclusiveMinimum Exclusive minimum.
 72 |      * @param bool|null $exclusiveMaximum Exclusive maximum.
 73 |      * @param int|float|null $multipleOf Must be a multiple of this value.
 74 |      * @param array|null $items JSON Schema for items if type is 'array'.
 75 |      * @param int|null $minItems Minimum items for an array.
 76 |      * @param int|null $maxItems Maximum items for an array.
 77 |      * @param bool|null $uniqueItems Whether array items must be unique.
 78 |      * @param array|null $properties Property definitions if type is 'object'. [name => schema_array].
 79 |      * @param array|null $required List of required properties for an object.
 80 |      * @param bool|array|null $additionalProperties Policy for additional properties in an object.
 81 |      */
 82 |     public function __construct(
 83 |         ?array $definition = null,
 84 |         ?string $type = null,
 85 |         ?string $description = null,
 86 |         ?array $enum = null,
 87 |         ?string $format = null,
 88 |         ?int $minLength = null,
 89 |         ?int $maxLength = null,
 90 |         ?string $pattern = null,
 91 |         int|float|null $minimum = null,
 92 |         int|float|null $maximum = null,
 93 |         ?bool $exclusiveMinimum = null,
 94 |         ?bool $exclusiveMaximum = null,
 95 |         int|float|null $multipleOf = null,
 96 |         ?array $items = null,
 97 |         ?int $minItems = null,
 98 |         ?int $maxItems = null,
 99 |         ?bool $uniqueItems = null,
100 |         ?array $properties = null,
101 |         ?array $required = null,
102 |         bool|array|null $additionalProperties = null
103 |     ) {
104 |         if ($definition !== null) {
105 |             $this->definition = $definition;
106 |         } else {
107 |             $this->type = $type;
108 |             $this->description = $description;
109 |             $this->enum = $enum;
110 |             $this->format = $format;
111 |             $this->minLength = $minLength;
112 |             $this->maxLength = $maxLength;
113 |             $this->pattern = $pattern;
114 |             $this->minimum = $minimum;
115 |             $this->maximum = $maximum;
116 |             $this->exclusiveMinimum = $exclusiveMinimum;
117 |             $this->exclusiveMaximum = $exclusiveMaximum;
118 |             $this->multipleOf = $multipleOf;
119 |             $this->items = $items;
120 |             $this->minItems = $minItems;
121 |             $this->maxItems = $maxItems;
122 |             $this->uniqueItems = $uniqueItems;
123 |             $this->properties = $properties;
124 |             $this->required = $required;
125 |             $this->additionalProperties = $additionalProperties;
126 |         }
127 |     }
128 | 
129 |     /**
130 |      * Converts the attribute's definition to a JSON schema array.
131 |      */
132 |     public function toArray(): array
133 |     {
134 |         if ($this->definition !== null) {
135 |             return [
136 |                 'definition' => $this->definition,
137 |             ];
138 |         }
139 | 
140 |         $schema = [];
141 |         if ($this->type !== null) {
142 |             $schema['type'] = $this->type;
143 |         }
144 |         if ($this->description !== null) {
145 |             $schema['description'] = $this->description;
146 |         }
147 |         if ($this->enum !== null) {
148 |             $schema['enum'] = $this->enum;
149 |         }
150 |         if ($this->format !== null) {
151 |             $schema['format'] = $this->format;
152 |         }
153 | 
154 |         // String
155 |         if ($this->minLength !== null) {
156 |             $schema['minLength'] = $this->minLength;
157 |         }
158 |         if ($this->maxLength !== null) {
159 |             $schema['maxLength'] = $this->maxLength;
160 |         }
161 |         if ($this->pattern !== null) {
162 |             $schema['pattern'] = $this->pattern;
163 |         }
164 | 
165 |         // Numeric
166 |         if ($this->minimum !== null) {
167 |             $schema['minimum'] = $this->minimum;
168 |         }
169 |         if ($this->maximum !== null) {
170 |             $schema['maximum'] = $this->maximum;
171 |         }
172 |         if ($this->exclusiveMinimum !== null) {
173 |             $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
174 |         }
175 |         if ($this->exclusiveMaximum !== null) {
176 |             $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
177 |         }
178 |         if ($this->multipleOf !== null) {
179 |             $schema['multipleOf'] = $this->multipleOf;
180 |         }
181 | 
182 |         // Array
183 |         if ($this->items !== null) {
184 |             $schema['items'] = $this->items;
185 |         }
186 |         if ($this->minItems !== null) {
187 |             $schema['minItems'] = $this->minItems;
188 |         }
189 |         if ($this->maxItems !== null) {
190 |             $schema['maxItems'] = $this->maxItems;
191 |         }
192 |         if ($this->uniqueItems !== null) {
193 |             $schema['uniqueItems'] = $this->uniqueItems;
194 |         }
195 | 
196 |         // Object
197 |         if ($this->properties !== null) {
198 |             $schema['properties'] = $this->properties;
199 |         }
200 |         if ($this->required !== null) {
201 |             $schema['required'] = $this->required;
202 |         }
203 |         if ($this->additionalProperties !== null) {
204 |             $schema['additionalProperties'] = $this->additionalProperties;
205 |         }
206 | 
207 |         return $schema;
208 |     }
209 | }
210 | 
```

--------------------------------------------------------------------------------
/src/Server.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace PhpMcp\Server;
  6 | 
  7 | use LogicException;
  8 | use PhpMcp\Server\Contracts\LoggerAwareInterface;
  9 | use PhpMcp\Server\Contracts\LoopAwareInterface;
 10 | use PhpMcp\Server\Contracts\ServerTransportInterface;
 11 | use PhpMcp\Server\Exception\ConfigurationException;
 12 | use PhpMcp\Server\Exception\DiscoveryException;
 13 | use PhpMcp\Server\Session\SessionManager;
 14 | use PhpMcp\Server\Utils\Discoverer;
 15 | use Throwable;
 16 | 
 17 | /**
 18 |  * Core MCP Server instance.
 19 |  *
 20 |  * Holds the configured MCP logic (Configuration, Registry, Protocol)
 21 |  * but is transport-agnostic. It relies on a ServerTransportInterface implementation,
 22 |  * provided via the listen() method, to handle network communication.
 23 |  *
 24 |  * Instances should be created via the ServerBuilder.
 25 |  */
 26 | class Server
 27 | {
 28 |     protected bool $discoveryRan = false;
 29 | 
 30 |     protected bool $isListening = false;
 31 | 
 32 |     /**
 33 |      *  @internal Use ServerBuilder::make()->...->build().
 34 |      *
 35 |      * @param  Configuration  $configuration  Core configuration and dependencies.
 36 |      * @param  Registry  $registry  Holds registered MCP element definitions.
 37 |      * @param  Protocol  $protocol  Handles MCP requests and responses.
 38 |      */
 39 |     public function __construct(
 40 |         protected readonly Configuration $configuration,
 41 |         protected readonly Registry $registry,
 42 |         protected readonly Protocol $protocol,
 43 |         protected readonly SessionManager $sessionManager,
 44 |     ) {
 45 |     }
 46 | 
 47 |     public static function make(): ServerBuilder
 48 |     {
 49 |         return new ServerBuilder();
 50 |     }
 51 | 
 52 |     /**
 53 |      * Runs the attribute discovery process based on the configuration
 54 |      * provided during build time. Caches results if cache is available.
 55 |      * Can be called explicitly, but is also called by ServerBuilder::build()
 56 |      * if discovery paths are configured.
 57 |      *
 58 |      * @param  bool  $force  Re-run discovery even if already run.
 59 |      * @param  bool  $useCache  Attempt to load from/save to cache. Defaults to true if cache is available.
 60 |      *
 61 |      * @throws DiscoveryException If discovery process encounters errors.
 62 |      * @throws ConfigurationException If discovery paths were not configured.
 63 |      */
 64 |     public function discover(
 65 |         string $basePath,
 66 |         array $scanDirs = ['.', 'src'],
 67 |         array $excludeDirs = [],
 68 |         bool $force = false,
 69 |         bool $saveToCache = true,
 70 |         ?Discoverer $discoverer = null
 71 |     ): void {
 72 |         $realBasePath = realpath($basePath);
 73 |         if ($realBasePath === false || ! is_dir($realBasePath)) {
 74 |             throw new \InvalidArgumentException("Invalid discovery base path provided to discover(): {$basePath}");
 75 |         }
 76 | 
 77 |         $excludeDirs = array_merge($excludeDirs, ['vendor', 'tests', 'test', 'storage', 'cache', 'samples', 'docs', 'node_modules', '.git', '.svn']);
 78 | 
 79 |         if ($this->discoveryRan && ! $force) {
 80 |             $this->configuration->logger->debug('Discovery skipped: Already run or loaded from cache.');
 81 | 
 82 |             return;
 83 |         }
 84 | 
 85 |         $cacheAvailable = $this->configuration->cache !== null;
 86 |         $shouldSaveCache = $saveToCache && $cacheAvailable;
 87 | 
 88 |         $this->configuration->logger->info('Starting MCP element discovery...', [
 89 |             'basePath' => $realBasePath,
 90 |             'force' => $force,
 91 |             'saveToCache' => $shouldSaveCache,
 92 |         ]);
 93 | 
 94 |         $this->registry->clear();
 95 | 
 96 |         try {
 97 |             $discoverer ??= new Discoverer($this->registry, $this->configuration->logger);
 98 | 
 99 |             $discoverer->discover($realBasePath, $scanDirs, $excludeDirs);
100 | 
101 |             $this->discoveryRan = true;
102 | 
103 |             if ($shouldSaveCache) {
104 |                 $this->registry->save();
105 |             }
106 |         } catch (Throwable $e) {
107 |             $this->discoveryRan = false;
108 |             $this->configuration->logger->critical('MCP element discovery failed.', ['exception' => $e]);
109 |             throw new DiscoveryException("Element discovery failed: {$e->getMessage()}", $e->getCode(), $e);
110 |         }
111 |     }
112 | 
113 |     /**
114 |      * Binds the server's MCP logic to the provided transport and starts the transport's listener,
115 |      * then runs the event loop, making this a BLOCKING call suitable for standalone servers.
116 |      *
117 |      * For framework integration where the loop is managed externally, use `getProtocol()`
118 |      * and bind it to your framework's transport mechanism manually.
119 |      *
120 |      * @param  ServerTransportInterface  $transport  The transport to listen with.
121 |      *
122 |      * @throws LogicException If called after already listening.
123 |      * @throws Throwable If transport->listen() fails immediately.
124 |      */
125 |     public function listen(ServerTransportInterface $transport, bool $runLoop = true): void
126 |     {
127 |         if ($this->isListening) {
128 |             throw new LogicException('Server is already listening via a transport.');
129 |         }
130 | 
131 |         $this->warnIfNoElements();
132 | 
133 |         if ($transport instanceof LoggerAwareInterface) {
134 |             $transport->setLogger($this->configuration->logger);
135 |         }
136 |         if ($transport instanceof LoopAwareInterface) {
137 |             $transport->setLoop($this->configuration->loop);
138 |         }
139 | 
140 |         $protocol = $this->getProtocol();
141 | 
142 |         $closeHandlerCallback = function (?string $reason = null) use ($protocol) {
143 |             $this->isListening = false;
144 |             $this->configuration->logger->info('Transport closed.', ['reason' => $reason ?? 'N/A']);
145 |             $protocol->unbindTransport();
146 |             $this->configuration->loop->stop();
147 |         };
148 | 
149 |         $transport->once('close', $closeHandlerCallback);
150 | 
151 |         $protocol->bindTransport($transport);
152 | 
153 |         try {
154 |             $transport->listen();
155 | 
156 |             $this->isListening = true;
157 | 
158 |             if ($runLoop) {
159 |                 $this->sessionManager->startGcTimer();
160 | 
161 |                 $this->configuration->loop->run();
162 | 
163 |                 $this->endListen($transport);
164 |             }
165 |         } catch (Throwable $e) {
166 |             $this->configuration->logger->critical('Failed to start listening or event loop crashed.', ['exception' => $e->getMessage()]);
167 |             $this->endListen($transport);
168 |             throw $e;
169 |         }
170 |     }
171 | 
172 |     public function endListen(ServerTransportInterface $transport): void
173 |     {
174 |         $protocol = $this->getProtocol();
175 | 
176 |         $protocol->unbindTransport();
177 | 
178 |         $this->sessionManager->stopGcTimer();
179 | 
180 |         $transport->removeAllListeners('close');
181 |         $transport->close();
182 | 
183 |         $this->isListening = false;
184 |         $this->configuration->logger->info("Server '{$this->configuration->serverInfo->name}' listener shut down.");
185 |     }
186 | 
187 |     /**
188 |      * Warns if no MCP elements are registered and discovery has not been run.
189 |      */
190 |     protected function warnIfNoElements(): void
191 |     {
192 |         if (! $this->registry->hasElements() && ! $this->discoveryRan) {
193 |             $this->configuration->logger->warning(
194 |                 'Starting listener, but no MCP elements are registered and discovery has not been run. ' .
195 |                     'Call $server->discover(...) at least once to find and cache elements before listen().'
196 |             );
197 |         } elseif (! $this->registry->hasElements() && $this->discoveryRan) {
198 |             $this->configuration->logger->warning(
199 |                 'Starting listener, but no MCP elements were found after discovery/cache load.'
200 |             );
201 |         }
202 |     }
203 | 
204 |     /**
205 |      * Gets the Configuration instance associated with this server.
206 |      */
207 |     public function getConfiguration(): Configuration
208 |     {
209 |         return $this->configuration;
210 |     }
211 | 
212 |     /**
213 |      * Gets the Registry instance associated with this server.
214 |      */
215 |     public function getRegistry(): Registry
216 |     {
217 |         return $this->registry;
218 |     }
219 | 
220 |     /**
221 |      * Gets the Protocol instance associated with this server.
222 |      */
223 |     public function getProtocol(): Protocol
224 |     {
225 |         return $this->protocol;
226 |     }
227 | 
228 |     public function getSessionManager(): SessionManager
229 |     {
230 |         return $this->sessionManager;
231 |     }
232 | }
233 | 
```

--------------------------------------------------------------------------------
/tests/Unit/Session/ArraySessionHandlerTest.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | namespace PhpMcp\Server\Tests\Unit\Session;
  4 | 
  5 | use PhpMcp\Server\Session\ArraySessionHandler;
  6 | use PhpMcp\Server\Contracts\SessionHandlerInterface;
  7 | use PhpMcp\Server\Defaults\SystemClock;
  8 | use PhpMcp\Server\Tests\Mocks\Clock\FixedClock;
  9 | 
 10 | const SESSION_ID_ARRAY_1 = 'array-session-id-1';
 11 | const SESSION_ID_ARRAY_2 = 'array-session-id-2';
 12 | const SESSION_ID_ARRAY_3 = 'array-session-id-3';
 13 | const SESSION_DATA_1 = '{"user_id":101,"cart":{"items":[{"id":"prod_A","qty":2},{"id":"prod_B","qty":1}],"total":150.75},"theme":"dark"}';
 14 | const SESSION_DATA_2 = '{"user_id":102,"preferences":{"notifications":true,"language":"en"},"last_login":"2024-07-15T10:00:00Z"}';
 15 | const SESSION_DATA_3 = '{"guest":true,"viewed_products":["prod_C","prod_D"]}';
 16 | const DEFAULT_TTL_ARRAY = 3600;
 17 | 
 18 | beforeEach(function () {
 19 |     $this->fixedClock = new FixedClock();
 20 |     $this->handler = new ArraySessionHandler(DEFAULT_TTL_ARRAY, $this->fixedClock);
 21 | });
 22 | 
 23 | it('implements SessionHandlerInterface', function () {
 24 |     expect($this->handler)->toBeInstanceOf(SessionHandlerInterface::class);
 25 | });
 26 | 
 27 | it('constructs with a default TTL and SystemClock if no clock provided', function () {
 28 |     $handler = new ArraySessionHandler();
 29 |     expect($handler->ttl)->toBe(DEFAULT_TTL_ARRAY);
 30 |     $reflection = new \ReflectionClass($handler);
 31 |     $clockProp = $reflection->getProperty('clock');
 32 |     $clockProp->setAccessible(true);
 33 |     expect($clockProp->getValue($handler))->toBeInstanceOf(SystemClock::class);
 34 | });
 35 | 
 36 | it('constructs with a custom TTL and injected clock', function () {
 37 |     $customTtl = 1800;
 38 |     $clock = new FixedClock();
 39 |     $handler = new ArraySessionHandler($customTtl, $clock);
 40 |     expect($handler->ttl)->toBe($customTtl);
 41 |     $reflection = new \ReflectionClass($handler);
 42 |     $clockProp = $reflection->getProperty('clock');
 43 |     $clockProp->setAccessible(true);
 44 |     expect($clockProp->getValue($handler))->toBe($clock);
 45 | });
 46 | 
 47 | it('writes session data and reads it back correctly', function () {
 48 |     $writeResult = $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
 49 |     expect($writeResult)->toBeTrue();
 50 | 
 51 |     $readData = $this->handler->read(SESSION_ID_ARRAY_1);
 52 |     expect($readData)->toBe(SESSION_DATA_1);
 53 | });
 54 | 
 55 | it('returns false when reading a non-existent session', function () {
 56 |     $readData = $this->handler->read('non-existent-session-id');
 57 |     expect($readData)->toBeFalse();
 58 | });
 59 | 
 60 | it('overwrites existing session data on subsequent write', function () {
 61 |     $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
 62 |     $updatedData = '{"user_id":101,"cart":{"items":[{"id":"prod_A","qty":3}],"total":175.25},"theme":"light"}';
 63 |     $this->handler->write(SESSION_ID_ARRAY_1, $updatedData);
 64 | 
 65 |     $readData = $this->handler->read(SESSION_ID_ARRAY_1);
 66 |     expect($readData)->toBe($updatedData);
 67 | });
 68 | 
 69 | it('returns false and removes data when reading an expired session due to handler TTL', function () {
 70 |     $ttl = 60;
 71 |     $fixedClock = new FixedClock();
 72 |     $handler = new ArraySessionHandler($ttl, $fixedClock);
 73 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
 74 | 
 75 |     $fixedClock->addSeconds($ttl + 1);
 76 | 
 77 |     $readData = $handler->read(SESSION_ID_ARRAY_1);
 78 |     expect($readData)->toBeFalse();
 79 | 
 80 |     $reflection = new \ReflectionClass($handler);
 81 |     $storeProp = $reflection->getProperty('store');
 82 |     $storeProp->setAccessible(true);
 83 |     $internalStore = $storeProp->getValue($handler);
 84 |     expect($internalStore)->not->toHaveKey(SESSION_ID_ARRAY_1);
 85 | });
 86 | 
 87 | it('does not return data if read exactly at TTL expiration time', function () {
 88 |     $shortTtl = 60;
 89 |     $fixedClock = new FixedClock();
 90 |     $handler = new ArraySessionHandler($shortTtl, $fixedClock);
 91 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
 92 | 
 93 |     $fixedClock->addSeconds($shortTtl);
 94 | 
 95 |     $readData = $handler->read(SESSION_ID_ARRAY_1);
 96 |     expect($readData)->toBe(SESSION_DATA_1);
 97 | 
 98 |     $fixedClock->addSecond();
 99 | 
100 |     $readDataExpired = $handler->read(SESSION_ID_ARRAY_1);
101 |     expect($readDataExpired)->toBeFalse();
102 | });
103 | 
104 | 
105 | it('updates timestamp on write, effectively extending session life', function () {
106 |     $veryShortTtl = 5;
107 |     $fixedClock = new FixedClock();
108 |     $handler = new ArraySessionHandler($veryShortTtl, $fixedClock);
109 | 
110 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
111 | 
112 |     $fixedClock->addSeconds(3);
113 | 
114 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_2);
115 | 
116 |     $fixedClock->addSeconds(3);
117 | 
118 |     $readData = $handler->read(SESSION_ID_ARRAY_1);
119 |     expect($readData)->toBe(SESSION_DATA_2);
120 | });
121 | 
122 | it('destroys an existing session and it cannot be read', function () {
123 |     $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
124 |     expect($this->handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);
125 | 
126 |     $destroyResult = $this->handler->destroy(SESSION_ID_ARRAY_1);
127 |     expect($destroyResult)->toBeTrue();
128 |     expect($this->handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
129 | 
130 |     $reflection = new \ReflectionClass($this->handler);
131 |     $storeProp = $reflection->getProperty('store');
132 |     $storeProp->setAccessible(true);
133 |     expect($storeProp->getValue($this->handler))->not->toHaveKey(SESSION_ID_ARRAY_1);
134 | });
135 | 
136 | it('destroy returns true and does nothing for a non-existent session', function () {
137 |     $destroyResult = $this->handler->destroy('non-existent-id');
138 |     expect($destroyResult)->toBeTrue();
139 | });
140 | 
141 | it('garbage collects only sessions older than maxLifetime', function () {
142 |     $gcMaxLifetime = 100;
143 |     $handlerTtl = 300;
144 |     $fixedClock = new FixedClock();
145 |     $handler = new ArraySessionHandler($handlerTtl, $fixedClock);
146 | 
147 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
148 | 
149 |     $fixedClock->addSeconds(50);
150 |     $handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);
151 | 
152 |     $fixedClock->addSeconds(80);
153 | 
154 |     $deletedSessions = $handler->gc($gcMaxLifetime);
155 | 
156 |     expect($deletedSessions)->toBeArray()->toEqual([SESSION_ID_ARRAY_1]);
157 |     expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
158 |     expect($handler->read(SESSION_ID_ARRAY_2))->toBe(SESSION_DATA_2);
159 | });
160 | 
161 | it('garbage collection respects maxLifetime precisely', function () {
162 |     $maxLifetime = 60;
163 |     $fixedClock = new FixedClock();
164 |     $handler = new ArraySessionHandler(300, $fixedClock);
165 | 
166 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
167 | 
168 |     $fixedClock->addSeconds($maxLifetime);
169 |     $deleted = $handler->gc($maxLifetime);
170 |     expect($deleted)->toBeEmpty();
171 |     expect($handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);
172 | 
173 |     $fixedClock->addSecond();
174 |     $deleted2 = $handler->gc($maxLifetime);
175 |     expect($deleted2)->toEqual([SESSION_ID_ARRAY_1]);
176 |     expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
177 | });
178 | 
179 | it('garbage collection returns empty array if no sessions meet criteria', function () {
180 |     $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
181 |     $this->handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);
182 | 
183 |     $this->fixedClock->addSeconds(DEFAULT_TTL_ARRAY / 2);
184 | 
185 |     $deletedSessions = $this->handler->gc(DEFAULT_TTL_ARRAY);
186 |     expect($deletedSessions)->toBeArray()->toBeEmpty();
187 |     expect($this->handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);
188 |     expect($this->handler->read(SESSION_ID_ARRAY_2))->toBe(SESSION_DATA_2);
189 | });
190 | 
191 | it('garbage collection correctly handles an empty store', function () {
192 |     $deletedSessions = $this->handler->gc(DEFAULT_TTL_ARRAY);
193 |     expect($deletedSessions)->toBeArray()->toBeEmpty();
194 | });
195 | 
196 | it('garbage collection removes multiple expired sessions', function () {
197 |     $maxLifetime = 30;
198 |     $fixedClock = new FixedClock();
199 |     $handler = new ArraySessionHandler(300, $fixedClock);
200 | 
201 |     $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
202 | 
203 |     $fixedClock->addSeconds(20);
204 |     $handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);
205 | 
206 |     $fixedClock->addSeconds(20);
207 |     $handler->write(SESSION_ID_ARRAY_3, SESSION_DATA_3);
208 | 
209 |     $fixedClock->addSeconds(20);
210 | 
211 |     $deleted = $handler->gc($maxLifetime);
212 |     expect($deleted)->toHaveCount(2)->toContain(SESSION_ID_ARRAY_1)->toContain(SESSION_ID_ARRAY_2);
213 |     expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
214 |     expect($handler->read(SESSION_ID_ARRAY_2))->toBeFalse();
215 |     expect($handler->read(SESSION_ID_ARRAY_3))->toBe(SESSION_DATA_3);
216 | });
217 | 
```

--------------------------------------------------------------------------------
/src/Defaults/BasicContainer.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1); // Added missing strict_types
  4 | 
  5 | namespace PhpMcp\Server\Defaults;
  6 | 
  7 | use Psr\Container\ContainerExceptionInterface;
  8 | use Psr\Container\ContainerInterface;
  9 | use Psr\Container\NotFoundExceptionInterface;
 10 | use ReflectionClass;
 11 | use ReflectionException;
 12 | use ReflectionNamedType;
 13 | use ReflectionParameter;
 14 | use Throwable; // Changed from \Throwable to Throwable
 15 | 
 16 | /**
 17 |  * A basic PSR-11 container implementation with simple constructor auto-wiring.
 18 |  *
 19 |  * Supports instantiating classes with parameterless constructors or constructors
 20 |  * where all parameters are type-hinted classes/interfaces known to the container,
 21 |  * or have default values. Does NOT support scalar/built-in type injection without defaults.
 22 |  */
 23 | class BasicContainer implements ContainerInterface
 24 | {
 25 |     /** @var array<string, object> Cache for already created instances (shared singletons) */
 26 |     private array $instances = [];
 27 | 
 28 |     /** @var array<string, bool> Track classes currently being resolved to detect circular dependencies */
 29 |     private array $resolving = [];
 30 | 
 31 |     /**
 32 |      * Finds an entry of the container by its identifier and returns it.
 33 |      *
 34 |      * @param  string  $id  Identifier of the entry to look for (usually a FQCN).
 35 |      * @return mixed Entry.
 36 |      *
 37 |      * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
 38 |      * @throws ContainerExceptionInterface Error while retrieving the entry (e.g., dependency resolution failure, circular dependency).
 39 |      */
 40 |     public function get(string $id): mixed
 41 |     {
 42 |         // 1. Check instance cache
 43 |         if (isset($this->instances[$id])) {
 44 |             return $this->instances[$id];
 45 |         }
 46 | 
 47 |         // 2. Check if class exists
 48 |         if (! class_exists($id) && ! interface_exists($id)) { // Also check interface for bindings
 49 |             throw new NotFoundException("Class, interface, or entry '{$id}' not found.");
 50 |         }
 51 | 
 52 |         // 7. Circular Dependency Check
 53 |         if (isset($this->resolving[$id])) {
 54 |             throw new ContainerException("Circular dependency detected while resolving '{$id}'. Resolution path: ".implode(' -> ', array_keys($this->resolving))." -> {$id}");
 55 |         }
 56 | 
 57 |         $this->resolving[$id] = true; // Mark as currently resolving
 58 | 
 59 |         try {
 60 |             // 3. Reflect on the class
 61 |             $reflector = new ReflectionClass($id);
 62 | 
 63 |             // Check if class is instantiable (abstract classes, interfaces cannot be directly instantiated)
 64 |             if (! $reflector->isInstantiable()) {
 65 |                 // We might have an interface bound to a concrete class via set()
 66 |                 // This check is slightly redundant due to class_exists but good practice
 67 |                 throw new ContainerException("Class '{$id}' is not instantiable (e.g., abstract class or interface without explicit binding).");
 68 |             }
 69 | 
 70 |             // 4. Get the constructor
 71 |             $constructor = $reflector->getConstructor();
 72 | 
 73 |             // 5. If no constructor or constructor has no parameters, instantiate directly
 74 |             if ($constructor === null || $constructor->getNumberOfParameters() === 0) {
 75 |                 $instance = $reflector->newInstance();
 76 |             } else {
 77 |                 // 6. Constructor has parameters, attempt to resolve them
 78 |                 $parameters = $constructor->getParameters();
 79 |                 $resolvedArgs = [];
 80 | 
 81 |                 foreach ($parameters as $parameter) {
 82 |                     $resolvedArgs[] = $this->resolveParameter($parameter, $id);
 83 |                 }
 84 | 
 85 |                 // Instantiate with resolved arguments
 86 |                 $instance = $reflector->newInstanceArgs($resolvedArgs);
 87 |             }
 88 | 
 89 |             // Cache the instance
 90 |             $this->instances[$id] = $instance;
 91 | 
 92 |             return $instance;
 93 | 
 94 |         } catch (ReflectionException $e) {
 95 |             throw new ContainerException("Reflection failed for '{$id}'.", 0, $e);
 96 |         } catch (ContainerExceptionInterface $e) { // Re-throw container exceptions directly
 97 |             throw $e;
 98 |         } catch (Throwable $e) { // Catch other instantiation errors
 99 |             throw new ContainerException("Failed to instantiate or resolve dependencies for '{$id}': ".$e->getMessage(), (int) $e->getCode(), $e);
100 |         } finally {
101 |             // 7. Remove from resolving stack once done (success or failure)
102 |             unset($this->resolving[$id]);
103 |         }
104 |     }
105 | 
106 |     /**
107 |      * Attempts to resolve a single constructor parameter.
108 |      *
109 |      * @throws ContainerExceptionInterface If a required dependency cannot be resolved.
110 |      */
111 |     private function resolveParameter(ReflectionParameter $parameter, string $consumerClassId): mixed
112 |     {
113 |         // Check for type hint
114 |         $type = $parameter->getType();
115 | 
116 |         if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
117 |             // Type hint is a class or interface name
118 |             $typeName = $type->getName();
119 |             try {
120 |                 // Recursively get the dependency
121 |                 return $this->get($typeName);
122 |             } catch (NotFoundExceptionInterface $e) {
123 |                 // Dependency class not found, fail ONLY if required
124 |                 if (! $parameter->isOptional() && ! $parameter->allowsNull()) {
125 |                     throw new ContainerException("Unresolvable dependency '{$typeName}' required by '{$consumerClassId}' constructor parameter \${$parameter->getName()}.", 0, $e);
126 |                 }
127 |                 // If optional or nullable, proceed (will check allowsNull/Default below)
128 |             } catch (ContainerExceptionInterface $e) {
129 |                 // Dependency itself failed to resolve (e.g., its own deps, circular)
130 |                 throw new ContainerException("Failed to resolve dependency '{$typeName}' for '{$consumerClassId}' parameter \${$parameter->getName()}: ".$e->getMessage(), 0, $e);
131 |             }
132 |         }
133 | 
134 |         // Check if parameter has a default value
135 |         if ($parameter->isDefaultValueAvailable()) {
136 |             return $parameter->getDefaultValue();
137 |         }
138 | 
139 |         // Check if parameter allows null (and wasn't resolved above)
140 |         if ($parameter->allowsNull()) {
141 |             return null;
142 |         }
143 | 
144 |         // Check if it was a built-in type without a default (unresolvable by this basic container)
145 |         if ($type instanceof ReflectionNamedType && $type->isBuiltin()) {
146 |             throw new ContainerException("Cannot auto-wire built-in type '{$type->getName()}' for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
147 |         }
148 | 
149 |         // Check if it was a union/intersection type without a default (also unresolvable)
150 |         if ($type !== null && ! $type instanceof ReflectionNamedType) {
151 |             throw new ContainerException("Cannot auto-wire complex type (union/intersection) for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
152 |         }
153 | 
154 |         // If we reach here, it's an untyped, required parameter without a default.
155 |         // Or potentially an unresolvable optional class dependency where null is not allowed (edge case).
156 |         throw new ContainerException("Cannot resolve required parameter \${$parameter->getName()} for '{$consumerClassId}' constructor (untyped or unresolvable complex type).");
157 |     }
158 | 
159 |     /**
160 |      * Returns true if the container can return an entry for the given identifier.
161 |      * Checks explicitly set instances and if the class/interface exists.
162 |      * Does not guarantee `get()` will succeed if auto-wiring fails.
163 |      */
164 |     public function has(string $id): bool
165 |     {
166 |         return isset($this->instances[$id]) || class_exists($id) || interface_exists($id);
167 |     }
168 | 
169 |     /**
170 |      * Adds a pre-built instance or a factory/binding to the container.
171 |      * This basic version only supports pre-built instances (singletons).
172 |      */
173 |     public function set(string $id, object $instance): void
174 |     {
175 |         // Could add support for closures/factories later if needed
176 |         $this->instances[$id] = $instance;
177 |     }
178 | }
179 | 
180 | // Keep custom exception classes as they are PSR-11 compliant placeholders
181 | class ContainerException extends \Exception implements ContainerExceptionInterface
182 | {
183 | }
184 | class NotFoundException extends \Exception implements NotFoundExceptionInterface
185 | {
186 | }
187 | 
```
Page 2/7FirstPrevNextLast