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

# Directory Structure

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

# Files

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

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\Schema;

use PhpMcp\Server\Attributes\Schema;
use PhpMcp\Server\Attributes\Schema\Format;
use PhpMcp\Server\Attributes\Schema\ArrayItems;
use PhpMcp\Server\Attributes\Schema\Property;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedIntEnum;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
use PhpMcp\Server\Tests\Fixtures\Enums\UnitEnum;
use stdClass;

class SchemaGenerationTarget
{
    public function noParamsMethod(): void
    {
    }

    /**
     * Method with simple required types.
     * @param string $pString String param
     * @param int $pInt Int param
     * @param bool $pBool Bool param
     * @param float $pFloat Float param
     * @param array $pArray Array param
     * @param stdClass $pObject Object param
     */
    public function simpleRequiredTypes(string $pString, int $pInt, bool $pBool, float $pFloat, array $pArray, stdClass $pObject): void
    {
    }

    /**
     * Method with simple optional types with default values.
     * @param string $pStringOpt String param with default
     * @param int $pIntOpt Int param with default
     * @param bool $pBoolOpt Bool param with default
     * @param ?float $pFloatOptNullable Float param with default, also nullable
     * @param array $pArrayOpt Array param with default
     * @param ?stdClass $pObjectOptNullable Object param with default null
     */
    public function optionalTypesWithDefaults(
        string $pStringOpt = "hello",
        int $pIntOpt = 123,
        bool $pBoolOpt = true,
        ?float $pFloatOptNullable = 1.23,
        array $pArrayOpt = ['a', 'b'],
        ?stdClass $pObjectOptNullable = null
    ): void {
    }

    /**
     * Nullable types without explicit defaults.
     * @param ?string $pNullableString Nullable string
     * @param int|null $pUnionNullableInt Union nullable int
     */
    public function nullableTypes(?string $pNullableString, ?int $pUnionNullableInt, ?BackedStringEnum $pNullableEnum): void
    {
    }

    /**
     * Union types.
     * @param string|int $pStringOrInt String or Int
     * @param bool|float|null $pBoolOrFloatOrNull Bool, Float or Null
     */
    public function unionTypes(string|int $pStringOrInt, $pBoolOrFloatOrNull): void
    {
    } // PHP 7.x style union in docblock usually

    /**
     * Various array type hints.
     * @param string[] $pStringArray Array of strings (docblock style)
     * @param array<int> $pIntArrayGeneric Array of integers (generic style)
     * @param array<string, mixed> $pAssocArray Associative array
     * @param BackedIntEnum[] $pEnumArray Array of enums
     * @param array{name: string, age: int} $pShapeArray Typed array shape
     * @param array<array{id:int, value:string}> $pArrayOfShapes Array of shapes
     */
    public function arrayTypes(
        array $pStringArray,
        array $pIntArrayGeneric,
        array $pAssocArray,
        array $pEnumArray,
        array $pShapeArray,
        array $pArrayOfShapes
    ): void {
    }

    /**
     * Enum types.
     * @param BackedStringEnum $pBackedStringEnum Backed string enum
     * @param BackedIntEnum $pBackedIntEnum Backed int enum
     * @param UnitEnum $pUnitEnum Unit enum
     */
    public function enumTypes(BackedStringEnum $pBackedStringEnum, BackedIntEnum $pBackedIntEnum, UnitEnum $pUnitEnum): void
    {
    }

    /**
     * Variadic parameters.
     * @param string ...$pVariadicStrings Variadic strings
     */
    public function variadicParams(string ...$pVariadicStrings): void
    {
    }

    /**
     * Mixed type.
     * @param mixed $pMixed Mixed type
     */
    public function mixedType(mixed $pMixed): void
    {
    }

    /**
     * With #[Schema] attributes for enhanced validation.
     * @param string $email With email format.
     * @param int $quantity With numeric constraints.
     * @param string[] $tags With array constraints.
     * @param array $userProfile With object property constraints.
     */
    public function withSchemaAttributes(
        #[Schema(format: Format::EMAIL)]
        string $email,
        #[Schema(minimum: 1, maximum: 100, multipleOf: 5)]
        int $quantity,
        #[Schema(minItems: 1, maxItems: 5, uniqueItems: true, items: new ArrayItems(minLength: 3))]
        array $tags,
        #[Schema(
            properties: [
                new Property(name: 'id', minimum: 1),
                new Property(name: 'username', pattern: '^[a-z0-9_]{3,16}$'),
            ],
            required: ['id', 'username'],
            additionalProperties: false
        )]
        array $userProfile
    ): void {
    }
}

```

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

```php
<?php

namespace Mcp\HttpUserProfileExample;

use PhpMcp\Server\Attributes\CompletionProvider;
use PhpMcp\Server\Attributes\McpPrompt;
use PhpMcp\Server\Attributes\McpResource;
use PhpMcp\Server\Attributes\McpResourceTemplate;
use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Exception\McpServerException;
use Psr\Log\LoggerInterface;

class McpElements
{
    // Simulate a simple user database
    private array $users = [
        '101' => ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'],
        '102' => ['name' => 'Bob', 'email' => '[email protected]', 'role' => 'user'],
        '103' => ['name' => 'Charlie', 'email' => '[email protected]', 'role' => 'user'],
    ];

    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->logger->debug('HttpUserProfileExample McpElements instantiated.');
    }

    /**
     * Retrieves the profile data for a specific user.
     *
     * @param  string  $userId  The ID of the user (from URI).
     * @return array User profile data.
     *
     * @throws McpServerException If the user is not found.
     */
    #[McpResourceTemplate(
        uriTemplate: 'user://{userId}/profile',
        name: 'user_profile',
        description: 'Get profile information for a specific user ID.',
        mimeType: 'application/json'
    )]

    public function getUserProfile(
        #[CompletionProvider(values: ['101', '102', '103'])]
        string $userId
    ): array {
        $this->logger->info('Reading resource: user profile', ['userId' => $userId]);
        if (! isset($this->users[$userId])) {
            // Throwing an exception that Processor can turn into an error response
            throw McpServerException::invalidParams("User profile not found for ID: {$userId}");
        }

        return $this->users[$userId];
    }

    /**
     * Retrieves a list of all known user IDs.
     *
     * @return array List of user IDs.
     */
    #[McpResource(
        uri: 'user://list/ids',
        name: 'user_id_list',
        description: 'Provides a list of all available user IDs.',
        mimeType: 'application/json'
    )]
    public function listUserIds(): array
    {
        $this->logger->info('Reading resource: user ID list');

        return array_keys($this->users);
    }

    /**
     * Sends a welcome message to a user.
     * (This is a placeholder - in a real app, it might queue an email)
     *
     * @param  string  $userId  The ID of the user to message.
     * @param  string|null  $customMessage  An optional custom message part.
     * @return array Status of the operation.
     */
    #[McpTool(name: 'send_welcome')]
    public function sendWelcomeMessage(string $userId, ?string $customMessage = null): array
    {
        $this->logger->info('Executing tool: send_welcome', ['userId' => $userId]);
        if (! isset($this->users[$userId])) {
            return ['success' => false, 'error' => "User ID {$userId} not found."];
        }
        $user = $this->users[$userId];
        $message = "Welcome, {$user['name']}!";
        if ($customMessage) {
            $message .= ' ' . $customMessage;
        }
        // Simulate sending
        $this->logger->info("Simulated sending message to {$user['email']}: {$message}");

        return ['success' => true, 'message_sent' => $message];
    }

    #[McpTool(name: 'test_tool_without_params')]
    public function testToolWithoutParams()
    {
        return ['success' => true, 'message' => 'Test tool without params'];
    }

    /**
     * Generates a prompt to write a bio for a user.
     *
     * @param  string  $userId  The user ID to generate the bio for.
     * @param  string  $tone  Desired tone (e.g., 'formal', 'casual').
     * @return array Prompt messages.
     *
     * @throws McpServerException If user not found.
     */
    #[McpPrompt(name: 'generate_bio_prompt')]
    public function generateBio(
        #[CompletionProvider(provider: UserIdCompletionProvider::class)]
        string $userId,
        string $tone = 'professional'
    ): array {
        $this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
        if (! isset($this->users[$userId])) {
            throw McpServerException::invalidParams("User not found for bio prompt: {$userId}");
        }
        $user = $this->users[$userId];

        return [
            ['role' => 'user', 'content' => "Write a short, {$tone} biography for {$user['name']} (Role: {$user['role']}, Email: {$user['email']}). Highlight their role within the system."],
        ];
    }
}

```

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

```php
<?php

namespace Mcp\StdioCalculatorExample;

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

class McpElements
{
    private array $config = [
        'precision' => 2,
        'allow_negative' => true,
    ];

    /**
     * Performs a calculation based on the operation.
     *
     * Supports 'add', 'subtract', 'multiply', 'divide'.
     * Obeys the 'precision' and 'allow_negative' settings from the config resource.
     *
     * @param  float  $a  The first operand.
     * @param  float  $b  The second operand.
     * @param  string  $operation  The operation ('add', 'subtract', 'multiply', 'divide').
     * @return float|string The result of the calculation, or an error message string.
     */
    #[McpTool(name: 'calculate')]
    public function calculate(float $a, float $b, string $operation): float|string
    {
        // Use STDERR for logs
        fwrite(STDERR, "Calculate tool called: a=$a, b=$b, op=$operation\n");

        $op = strtolower($operation);
        $result = null;

        switch ($op) {
            case 'add':
                $result = $a + $b;
                break;
            case 'subtract':
                $result = $a - $b;
                break;
            case 'multiply':
                $result = $a * $b;
                break;
            case 'divide':
                if ($b == 0) {
                    return 'Error: Division by zero.';
                }
                $result = $a / $b;
                break;
            default:
                return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
        }

        if (! $this->config['allow_negative'] && $result < 0) {
            return 'Error: Negative results are disabled.';
        }

        return round($result, $this->config['precision']);
    }

    /**
     * Provides the current calculator configuration.
     * Can be read by clients to understand precision etc.
     *
     * @return array The configuration array.
     */
    #[McpResource(
        uri: 'config://calculator/settings',
        name: 'calculator_config',
        description: 'Current settings for the calculator tool (precision, allow_negative).',
        mimeType: 'application/json' // Return as JSON
    )]
    public function getConfiguration(): array
    {
        fwrite(STDERR, "Resource config://calculator/settings read.\n");

        return $this->config;
    }

    /**
     * Updates a specific configuration setting.
     * Note: This requires more robust validation in a real app.
     *
     * @param  string  $setting  The setting key ('precision' or 'allow_negative').
     * @param  mixed  $value  The new value (int for precision, bool for allow_negative).
     * @return array Success message or error.
     */
    #[McpTool(name: 'update_setting')]
    public function updateSetting(string $setting, mixed $value): array
    {
        fwrite(STDERR, "Update Setting tool called: setting=$setting, value=".var_export($value, true)."\n");
        if (! array_key_exists($setting, $this->config)) {
            return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
        }

        if ($setting === 'precision') {
            if (! is_int($value) || $value < 0 || $value > 10) {
                return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
            }
            $this->config['precision'] = $value;

            // In real app, notify subscribers of config://calculator/settings change
            // $registry->notifyResourceChanged('config://calculator/settings');
            return ['success' => true, 'message' => "Precision updated to {$value}."];
        }

        if ($setting === 'allow_negative') {
            if (! is_bool($value)) {
                // Attempt basic cast for flexibility
                if (in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
                    $value = true;
                } elseif (in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
                    $value = false;
                } else {
                    return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
                }
            }
            $this->config['allow_negative'] = $value;

            // $registry->notifyResourceChanged('config://calculator/settings');
            return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
        }

        return ['success' => false, 'error' => 'Internal error handling setting.']; // Should not happen
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Schema\Content\EmbeddedResource;
use PhpMcp\Schema\Content\TextResourceContents;
use PhpMcp\Schema\Content\BlobResourceContents;
use Psr\Log\LoggerInterface;
use SplFileInfo;

class ResourceHandlerFixture
{
    public static string $staticTextContent = "Default static text content.";
    public array $dynamicContentStore = [];
    public static ?string $unlinkableSplFile = null;

    public function __construct()
    {
        $this->dynamicContentStore['dynamic://data/item1'] = "Content for item 1";
    }

    public function returnStringText(string $uri): string
    {
        return "Plain string content for {$uri}";
    }

    public function returnStringJson(string $uri): string
    {
        return json_encode(['uri_in_json' => $uri, 'data' => 'some json string']);
    }

    public function returnStringHtml(string $uri): string
    {
        return "<html><title>{$uri}</title><body>Content</body></html>";
    }

    public function returnArrayJson(string $uri): array
    {
        return ['uri_in_array' => $uri, 'message' => 'This is JSON data from array', 'timestamp' => time()];
    }

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

    public function returnStream(string $uri) // Returns a stream resource
    {
        $stream = fopen('php://memory', 'r+');
        fwrite($stream, "Streamed content for {$uri}");
        rewind($stream);
        return $stream;
    }

    public function returnSplFileInfo(string $uri): SplFileInfo
    {
        self::$unlinkableSplFile = tempnam(sys_get_temp_dir(), 'res_fixture_spl_');
        file_put_contents(self::$unlinkableSplFile, "Content from SplFileInfo for {$uri}");
        return new SplFileInfo(self::$unlinkableSplFile);
    }

    public function returnEmbeddedResource(string $uri): EmbeddedResource
    {
        return EmbeddedResource::make(
            TextResourceContents::make($uri, 'application/vnd.custom-embedded', 'Direct EmbeddedResource content')
        );
    }

    public function returnTextResourceContents(string $uri): TextResourceContents
    {
        return TextResourceContents::make($uri, 'text/special-contents', 'Direct TextResourceContents');
    }

    public function returnBlobResourceContents(string $uri): BlobResourceContents
    {
        return BlobResourceContents::make($uri, 'application/custom-blob-contents', base64_encode('blobbycontents'));
    }

    public function returnArrayForBlobSchema(string $uri): array
    {
        return ['blob' => base64_encode("Blob for {$uri} via array"), 'mimeType' => 'application/x-custom-blob-array'];
    }

    public function returnArrayForTextSchema(string $uri): array
    {
        return ['text' => "Text from array for {$uri} via array", 'mimeType' => 'text/vnd.custom-array-text'];
    }

    public function returnArrayOfResourceContents(string $uri): array
    {
        return [
            TextResourceContents::make($uri . "_part1", 'text/plain', 'Part 1 of many RC'),
            BlobResourceContents::make($uri . "_part2", 'image/png', base64_encode('pngdata')),
        ];
    }

    public function returnArrayOfEmbeddedResources(string $uri): array
    {
        return [
            EmbeddedResource::make(TextResourceContents::make($uri . "_emb1", 'text/xml', '<doc1/>')),
            EmbeddedResource::make(BlobResourceContents::make($uri . "_emb2", 'font/woff2', base64_encode('fontdata'))),
        ];
    }

    public function returnMixedArrayWithResourceTypes(string $uri): array
    {
        return [
            "A raw string piece", // Will be formatted
            TextResourceContents::make($uri . "_rc1", 'text/markdown', '**Markdown!**'), // Used as is
            ['nested_array_data' => 'value', 'for_uri' => $uri], // Will be formatted (JSON)
            EmbeddedResource::make(TextResourceContents::make($uri . "_emb1", 'text/csv', 'col1,col2')), // Extracted
        ];
    }

    public function handlerThrowsException(string $uri): void
    {
        throw new \DomainException("Cannot read resource {$uri} - handler error.");
    }

    public function returnUnformattableType(string $uri)
    {
        return new \DateTimeImmutable();
    }

    public function resourceHandlerNeedsUri(string $uri): string
    {
        return "Handler received URI: " . $uri;
    }

    public function resourceHandlerDoesNotNeedUri(): string
    {
        return "Handler did not need or receive URI parameter.";
    }

    public function getTemplatedContent(
        string $category,
        string $itemId,
        string $format,
    ): array {
        return [
            'message' => "Content for item {$itemId} in category {$category}, format {$format}.",
            'category_received' => $category,
            'itemId_received' => $itemId,
            'format_received' => $format,
        ];
    }

    public function getStaticText(): string
    {
        return self::$staticTextContent;
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Server\Context;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedIntEnum;
use PhpMcp\Server\Tests\Fixtures\Enums\BackedStringEnum;
use PhpMcp\Server\Tests\Fixtures\Enums\UnitEnum;
use stdClass;

class VariousTypesHandler
{
    public function noArgsMethod(): array
    {
        return compact([]);
    }

    public function simpleRequiredArgs(string $pString, int $pInt, bool $pBool): array
    {
        return compact('pString', 'pInt', 'pBool');
    }

    public function optionalArgsWithDefaults(
        string $pString = 'default_string',
        int $pInt = 100,
        ?bool $pNullableBool = true,
        float $pFloat = 3.14
    ): array {
        return compact('pString', 'pInt', 'pNullableBool', 'pFloat');
    }

    public function nullableArgsWithoutDefaults(?string $pString, ?int $pInt, ?array $pArray): array
    {
        return compact('pString', 'pInt', 'pArray');
    }

    public function mixedTypeArg(mixed $pMixed): array
    {
        return compact('pMixed');
    }

    public function backedEnumArgs(
        BackedStringEnum $pBackedString,
        BackedIntEnum $pBackedInt,
        ?BackedStringEnum $pNullableBackedString = null,
        BackedIntEnum $pOptionalBackedInt = BackedIntEnum::First
    ): array {
        return compact('pBackedString', 'pBackedInt', 'pNullableBackedString', 'pOptionalBackedInt');
    }

    public function unitEnumArg(UnitEnum $pUnitEnum): array
    {
        return compact('pUnitEnum');
    }

    public function arrayArg(array $pArray): array
    {
        return compact('pArray');
    }

    public function objectArg(stdClass $pObject): array
    {
        return compact('pObject');
    }

    public function variadicArgs(string ...$items): array
    {
        return compact('items');
    }

    /**
     * A comprehensive method for testing various argument types and casting.
     * @param string $strParam A string.
     * @param int $intParam An integer.
     * @param bool $boolProp A boolean.
     * @param float $floatParam A float.
     * @param array $arrayParam An array.
     * @param BackedStringEnum $backedStringEnumParam A backed string enum.
     * @param BackedIntEnum $backedIntEnumParam A backed int enum.
     * @param UnitEnum $unitEnumParam A unit enum (passed as instance).
     * @param string|null $nullableStringParam A nullable string.
     * @param int $optionalIntWithDefaultParam An optional int with default.
     * @param mixed $mixedParam A mixed type.
     * @param stdClass $objectParam An object.
     * @param string $stringForIntCast String that should be cast to int.
     * @param string $stringForFloatCast String that should be cast to float.
     * @param string $stringForBoolTrueCast String that should be cast to bool true.
     * @param string $stringForBoolFalseCast String that should be cast to bool false.
     * @param int $intForStringCast Int that should be cast to string.
     * @param int $intForFloatCast Int that should be cast to float.
     * @param bool $boolForStringCast Bool that should be cast to string.
     * @param string $valueForBackedStringEnum String value for backed string enum.
     * @param int $valueForBackedIntEnum Int value for backed int enum.
     */
    public function comprehensiveArgumentTest(
        string $strParam,
        int $intParam,
        bool $boolProp,
        float $floatParam,
        array $arrayParam,
        BackedStringEnum $backedStringEnumParam,
        BackedIntEnum $backedIntEnumParam,
        UnitEnum $unitEnumParam,
        ?string $nullableStringParam,
        mixed $mixedParam,
        stdClass $objectParam,
        string $stringForIntCast,
        string $stringForFloatCast,
        string $stringForBoolTrueCast,
        string $stringForBoolFalseCast,
        int $intForStringCast,
        int $intForFloatCast,
        bool $boolForStringCast,
        string $valueForBackedStringEnum,
        int $valueForBackedIntEnum,
        int $optionalIntWithDefaultParam = 999,
    ): array {
        return compact(
            'strParam',
            'intParam',
            'boolProp',
            'floatParam',
            'arrayParam',
            'backedStringEnumParam',
            'backedIntEnumParam',
            'unitEnumParam',
            'nullableStringParam',
            'optionalIntWithDefaultParam',
            'mixedParam',
            'objectParam',
            'stringForIntCast',
            'stringForFloatCast',
            'stringForBoolTrueCast',
            'stringForBoolFalseCast',
            'intForStringCast',
            'intForFloatCast',
            'boolForStringCast',
            'valueForBackedStringEnum',
            'valueForBackedIntEnum'
        );
    }

    public function methodCausesTypeError(int $mustBeInt): void
    {
    }

    public function contextArg(Context $context): array {
        return [
            'session' => $context->session->get('testKey'),
            'request' => $context->request->getHeaderLine('testHeader'),
        ];
    }
}

```

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

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

/*
    |--------------------------------------------------------------------------
    | MCP HTTP User Profile Server (Attribute Discovery)
    |--------------------------------------------------------------------------
    |
    | This server demonstrates attribute-based discovery for MCP elements
    | (ResourceTemplates, Resources, Tools, Prompts) defined in 'McpElements.php'.
    | It runs via the HTTP transport, listening for SSE and POST requests.
    |
    | To Use:
    | 1. Ensure 'McpElements.php' defines classes with MCP attributes.
    | 2. Run this script from your CLI: `php server.php`
    |    The server will listen on http://127.0.0.1:8080 by default.
    | 3. Configure your MCP Client (e.g., Cursor) for this server:
    |
    | {
    |     "mcpServers": {
    |         "php-http-userprofile": {
    |             "url": "http://127.0.0.1:8080/mcp/sse" // Use the SSE endpoint
    |             // Ensure your client can reach this address
    |         }
    |     }
    | }
    |
    | The ServerBuilder builds the server, $server->discover() scans for elements,
    | and then $server->listen() starts the ReactPHP HTTP server.
    |
    | If you provided a `CacheInterface` implementation to the ServerBuilder,
    | the discovery process will be cached, so you can comment out the
    | discovery call after the first run to speed up subsequent runs.
    |
*/

declare(strict_types=1);

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

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

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

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

    // --- Setup DI Container for DI in McpElements class ---
    $container = new BasicContainer();
    $container->set(LoggerInterface::class, $logger);

    $server = Server::make()
        ->withServerInfo('HTTP User Profiles', '1.0.0')
        ->withCapabilities(ServerCapabilities::make(completions: true, logging: true))
        ->withLogger($logger)
        ->withContainer($container)
        ->withTool(
            function (float $a, float $b, string $operation = 'add'): array {
                $result = match ($operation) {
                    'add' => $a + $b,
                    'subtract' => $a - $b,
                    'multiply' => $a * $b,
                    'divide' => $b != 0 ? $a / $b : throw new \InvalidArgumentException('Cannot divide by zero'),
                    default => throw new \InvalidArgumentException("Unknown operation: {$operation}")
                };

                return [
                    'operation' => $operation,
                    'operands' => [$a, $b],
                    'result' => $result
                ];
            },
            name: 'calculator',
            description: 'Perform basic math operations (add, subtract, multiply, divide)'
        )
        ->withResource(
            function (): array {
                $memoryUsage = memory_get_usage(true);
                $memoryPeak = memory_get_peak_usage(true);
                $uptime = time() - $_SERVER['REQUEST_TIME_FLOAT'] ?? time();
                $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'CLI';

                return [
                    'server_time' => date('Y-m-d H:i:s'),
                    'uptime_seconds' => $uptime,
                    'memory_usage_mb' => round($memoryUsage / 1024 / 1024, 2),
                    'memory_peak_mb' => round($memoryPeak / 1024 / 1024, 2),
                    'php_version' => PHP_VERSION,
                    'server_software' => $serverSoftware,
                    'operating_system' => PHP_OS_FAMILY,
                    'status' => 'healthy'
                ];
            },
            uri: 'system://status',
            name: 'system_status',
            description: 'Current system status and runtime information',
            mimeType: 'application/json'
        )
        ->build();

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

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

    $server->listen($transport);

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

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Session;

use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Contracts\SessionInterface;

class Session implements SessionInterface
{
    /**
     * @var array<string, mixed> Stores all session data.
     * Keys are snake_case by convention for MCP-specific data.
     *
     * Official keys are:
     * - initialized: bool
     * - client_info: array|null
     * - protocol_version: string|null
     * - subscriptions: array<string, bool>
     * - message_queue: array<string>
     * - log_level: string|null
     */
    protected array $data = [];

    public function __construct(
        protected SessionHandlerInterface $handler,
        protected string $id = '',
        ?array $data = null
    ) {
        if (empty($this->id)) {
            $this->id = $this->generateId();
        }

        if ($data !== null) {
            $this->hydrate($data);
        } elseif ($sessionData = $this->handler->read($this->id)) {
            $this->data = json_decode($sessionData, true) ?? [];
        }
    }

    /**
     * Retrieve an existing session instance from handler or return null if session doesn't exist
     */
    public static function retrieve(string $id, SessionHandlerInterface $handler): ?SessionInterface
    {
        $sessionData = $handler->read($id);

        if (!$sessionData) {
            return null;
        }

        $data = json_decode($sessionData, true);
        if ($data === null) {
            return null;
        }

        return new static($handler, $id, $data);
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getHandler(): SessionHandlerInterface
    {
        return $this->handler;
    }

    public function generateId(): string
    {
        return bin2hex(random_bytes(16));
    }

    public function save(): void
    {
        $this->handler->write($this->id, json_encode($this->data));
    }

    public function get(string $key, mixed $default = null): mixed
    {
        $key = explode('.', $key);
        $data = $this->data;

        foreach ($key as $segment) {
            if (is_array($data) && array_key_exists($segment, $data)) {
                $data = $data[$segment];
            } else {
                return $default;
            }
        }

        return $data;
    }

    public function set(string $key, mixed $value, bool $overwrite = true): void
    {
        $segments = explode('.', $key);
        $data = &$this->data;

        while (count($segments) > 1) {
            $segment = array_shift($segments);
            if (!isset($data[$segment]) || !is_array($data[$segment])) {
                $data[$segment] = [];
            }
            $data = &$data[$segment];
        }

        $lastKey = array_shift($segments);
        if ($overwrite || !isset($data[$lastKey])) {
            $data[$lastKey] = $value;
        }
    }

    public function has(string $key): bool
    {
        $key = explode('.', $key);
        $data = $this->data;

        foreach ($key as $segment) {
            if (is_array($data) && array_key_exists($segment, $data)) {
                $data = $data[$segment];
            } elseif (is_object($data) && isset($data->{$segment})) {
                $data = $data->{$segment};
            } else {
                return false;
            }
        }

        return true;
    }

    public function forget(string $key): void
    {
        $segments = explode('.', $key);
        $data = &$this->data;

        while (count($segments) > 1) {
            $segment = array_shift($segments);
            if (!isset($data[$segment]) || !is_array($data[$segment])) {
                $data[$segment] = [];
            }
            $data = &$data[$segment];
        }

        $lastKey = array_shift($segments);
        if (isset($data[$lastKey])) {
            unset($data[$lastKey]);
        }
    }

    public function clear(): void
    {
        $this->data = [];
    }

    public function pull(string $key, mixed $default = null): mixed
    {
        $value = $this->get($key, $default);
        $this->forget($key);
        return $value;
    }

    public function all(): array
    {
        return $this->data;
    }

    public function hydrate(array $attributes): void
    {
        $this->data = array_merge(
            [
                'initialized' => false,
                'client_info' => null,
                'protocol_version' => null,
                'message_queue' => [],
                'log_level' => null,
            ],
            $attributes
        );
        unset($this->data['id']);
    }

    public function queueMessage(string $rawFramedMessage): void
    {
        $this->data['message_queue'][] = $rawFramedMessage;
    }

    public function dequeueMessages(): array
    {
        $messages = $this->data['message_queue'] ?? [];
        $this->data['message_queue'] = [];
        return $messages;
    }

    public function hasQueuedMessages(): bool
    {
        return !empty($this->data['message_queue']);
    }

    public function jsonSerialize(): array
    {
        return $this->all();
    }
}

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Mocks\Clients;

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

class MockJsonHttpClient
{
    public Browser $browser;
    public string $baseUrl;
    public ?string $sessionId = null;
    public array $lastResponseHeaders = []; // Store last response headers for testing

    public function __construct(string $host, int $port, string $mcpPath, int $timeout = 2)
    {
        $this->browser = (new Browser())->withTimeout($timeout);
        $this->baseUrl = "http://{$host}:{$port}/{$mcpPath}";
    }

    public function sendRequest(string $method, array $params = [], ?string $id = null, array $additionalHeaders = []): PromiseInterface
    {
        $payload = ['jsonrpc' => '2.0', 'method' => $method, 'params' => $params];
        if ($id !== null) {
            $payload['id'] = $id;
        }

        $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json, text/event-stream'];
        if ($this->sessionId && $method !== 'initialize') {
            $headers['Mcp-Session-Id'] = $this->sessionId;
        }
        $headers += $additionalHeaders;

        $body = json_encode($payload);

        return $this->browser->post($this->baseUrl, $headers, $body)
            ->then(function (ResponseInterface $response) use ($method) {
                // Store response headers for testing
                $this->lastResponseHeaders = [];
                foreach ($response->getHeaders() as $name => $values) {
                    foreach ($values as $value) {
                        $this->lastResponseHeaders[] = "{$name}: {$value}";
                    }
                }

                $bodyContent = (string) $response->getBody()->getContents();
                $statusCode = $response->getStatusCode();

                if ($method === 'initialize' && $statusCode === 200) {
                    $this->sessionId = $response->getHeaderLine('Mcp-Session-Id');
                }

                if ($statusCode === 202) {
                    if ($bodyContent !== '') {
                        throw new \RuntimeException("Expected empty body for 202 response, got: {$bodyContent}");
                    }
                    return ['statusCode' => $statusCode, 'body' => null, 'headers' => $response->getHeaders()];
                }

                try {
                    $decoded = json_decode($bodyContent, true, 512, JSON_THROW_ON_ERROR);
                    return ['statusCode' => $statusCode, 'body' => $decoded, 'headers' => $response->getHeaders()];
                } catch (\JsonException $e) {
                    throw new \RuntimeException("Failed to decode JSON response body: {$bodyContent} Error: {$e->getMessage()}", $statusCode, $e);
                }
            });
    }

    public function sendBatchRequest(array $batchRequestObjects): PromiseInterface
    {
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
        if ($this->sessionId) {
            $headers['Mcp-Session-Id'] = $this->sessionId;
        }
        $body = json_encode($batchRequestObjects);

        return $this->browser->post($this->baseUrl, $headers, $body)
            ->then(function (ResponseInterface $response) {
                $bodyContent = (string) $response->getBody()->getContents();
                $statusCode = $response->getStatusCode();
                if ($statusCode === 202) {
                    if ($bodyContent !== '') {
                        throw new \RuntimeException("Expected empty body for 202 response, got: {$bodyContent}");
                    }
                    return ['statusCode' => $statusCode, 'body' => null, 'headers' => $response->getHeaders()];
                }

                try {
                    $decoded = json_decode($bodyContent, true, 512, JSON_THROW_ON_ERROR);
                    return ['statusCode' => $statusCode, 'body' => $decoded, 'headers' => $response->getHeaders()];
                } catch (\JsonException $e) {
                    throw new \RuntimeException("Failed to decode JSON response body: {$bodyContent} Error: {$e->getMessage()}", $statusCode, $e);
                }
            });
    }

    public function sendDeleteRequest(): PromiseInterface
    {
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
        if ($this->sessionId) {
            $headers['Mcp-Session-Id'] = $this->sessionId;
        }

        return $this->browser->delete($this->baseUrl, $headers)
            ->then(function (ResponseInterface $response) {
                $bodyContent = (string) $response->getBody()->getContents();
                $statusCode = $response->getStatusCode();
                return ['statusCode' => $statusCode, 'body' => $bodyContent, 'headers' => $response->getHeaders()];
            });
    }

    public function sendNotification(string $method, array $params = []): PromiseInterface
    {
        return $this->sendRequest($method, $params, null);
    }

    public function connectSseForNotifications(): PromiseInterface
    {
        return resolve(null);
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Fixtures\General;

use PhpMcp\Schema\Content\PromptMessage;
use PhpMcp\Schema\Enum\Role;
use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Schema\Content\ImageContent;
use PhpMcp\Schema\Content\AudioContent;
use PhpMcp\Server\Attributes\CompletionProvider;
use Psr\Log\LoggerInterface;

class PromptHandlerFixture
{
    public function generateSimpleGreeting(string $name, string $style = "friendly"): array
    {
        return [
            ['role' => 'user', 'content' => "Craft a {$style} greeting for {$name}."]
        ];
    }

    public function returnSinglePromptMessageObject(): PromptMessage
    {
        return PromptMessage::make(Role::User, TextContent::make("Single PromptMessage object."));
    }

    public function returnArrayOfPromptMessageObjects(): array
    {
        return [
            PromptMessage::make(Role::User, TextContent::make("First message object.")),
            PromptMessage::make(Role::Assistant, ImageContent::make("img_data", "image/png")),
        ];
    }

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

    public function returnSimpleUserAssistantMap(): array
    {
        return [
            'user' => "This is the user's turn.",
            'assistant' => "And this is the assistant's reply."
        ];
    }

    public function returnUserAssistantMapWithContentObjects(): array
    {
        return [
            'user' => TextContent::make("User text content object."),
            'assistant' => ImageContent::make("asst_img_data", "image/gif"),
        ];
    }

    public function returnUserAssistantMapWithMixedContent(): array
    {
        return [
            'user' => "Plain user string.",
            'assistant' => AudioContent::make("aud_data", "audio/mp3"),
        ];
    }

    public function returnUserAssistantMapWithArrayContent(): array
    {
        return [
            'user' => ['type' => 'text', 'text' => 'User array content'],
            'assistant' => ['type' => 'image', 'data' => 'asst_arr_img_data', 'mimeType' => 'image/jpeg'],
        ];
    }

    public function returnListOfRawMessageArrays(): array
    {
        return [
            ['role' => 'user', 'content' => "First raw message string."],
            ['role' => 'assistant', 'content' => TextContent::make("Second raw message with Content obj.")],
            ['role' => 'user', 'content' => ['type' => 'image', 'data' => 'raw_img_data', 'mimeType' => 'image/webp']],
            ['role' => 'assistant', 'content' => ['type' => 'audio', 'data' => 'raw_aud_data', 'mimeType' => 'audio/ogg']],
            ['role' => 'user', 'content' => ['type' => 'resource', 'resource' => ['uri' => 'file://doc.pdf', 'blob' => base64_encode('pdf-data'), 'mimeType' => 'application/pdf']]],
            ['role' => 'assistant', 'content' => ['type' => 'resource', 'resource' => ['uri' => 'config://settings.json', 'text' => '{"theme":"dark"}']]],
        ];
    }

    public function returnListOfRawMessageArraysWithScalars(): array
    {
        return [
            ['role' => 'user', 'content' => 123],          // int
            ['role' => 'assistant', 'content' => true],       // bool
            ['role' => 'user', 'content' => null],         // null
            ['role' => 'assistant', 'content' => 3.14],       // float
            ['role' => 'user', 'content' => ['key' => 'value']], // array that becomes JSON
        ];
    }

    public function returnMixedArrayOfPromptMessagesAndRaw(): array
    {
        return [
            PromptMessage::make(Role::User, TextContent::make("This is a PromptMessage object.")),
            ['role' => 'assistant', 'content' => "This is a raw message array."],
            PromptMessage::make(Role::User, ImageContent::make("pm_img", "image/bmp")),
            ['role' => 'assistant', 'content' => ['type' => 'text', 'text' => 'Raw message with typed content.']],
        ];
    }

    public function promptWithArgumentCompletion(
        #[CompletionProvider(provider: CompletionProviderFixture::class)]
        string $entityName,
        string $action = "describe"
    ): array {
        return [
            ['role' => 'user', 'content' => "Please {$action} the entity: {$entityName}."]
        ];
    }

    public function promptReturnsNonArray(): string
    {
        return "This is not a valid prompt return type.";
    }

    public function promptReturnsArrayWithInvalidRole(): array
    {
        return [['role' => 'system', 'content' => 'System messages are not directly supported.']];
    }

    public function promptReturnsInvalidRole(): array
    {
        return [['role' => 'system', 'content' => 'System messages are not directly supported.']];
    }

    public function promptReturnsArrayWithInvalidContentStructure(): array
    {
        return [['role' => 'user', 'content' => ['text_only_no_type' => 'invalid']]];
    }

    public function promptReturnsArrayWithInvalidTypedContent(): array
    {
        return [['role' => 'user', 'content' => ['type' => 'image', 'source' => 'url.jpg']]]; // 'image' needs 'data' and 'mimeType'
    }

    public function promptReturnsArrayWithInvalidResourceContent(): array
    {
        return [
            [
                'role' => 'user',
                'content' => ['type' => 'resource', 'resource' => ['uri' => 'uri://uri']]
            ]
        ];
    }

    public function promptHandlerThrows(): void
    {
        throw new \LogicException("Prompt generation failed inside handler.");
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Utils;

use PhpMcp\Server\Utils\HandlerResolver;
use ReflectionMethod;
use ReflectionFunction;
use InvalidArgumentException;

class ValidHandlerClass
{
    public function publicMethod() {}
    protected function protectedMethod() {}
    private function privateMethod() {}
    public static function staticMethod() {}
    public function __construct() {}
    public function __destruct() {}
}

class ValidInvokableClass
{
    public function __invoke() {}
}

class NonInvokableClass {}

abstract class AbstractHandlerClass
{
    abstract public function abstractMethod();
}

// Test closure support
it('resolves closures to ReflectionFunction', function () {
    $closure = function (string $input): string {
        return "processed: $input";
    };

    $resolved = HandlerResolver::resolve($closure);

    expect($resolved)->toBeInstanceOf(ReflectionFunction::class);
    expect($resolved->getNumberOfParameters())->toBe(1);
    expect($resolved->getReturnType()->getName())->toBe('string');
});

it('resolves valid array handler', function () {
    $handler = [ValidHandlerClass::class, 'publicMethod'];
    $resolved = HandlerResolver::resolve($handler);

    expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
    expect($resolved->getName())->toBe('publicMethod');
    expect($resolved->getDeclaringClass()->getName())->toBe(ValidHandlerClass::class);
});

it('resolves valid invokable class string handler', function () {
    $handler = ValidInvokableClass::class;
    $resolved = HandlerResolver::resolve($handler);

    expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
    expect($resolved->getName())->toBe('__invoke');
    expect($resolved->getDeclaringClass()->getName())->toBe(ValidInvokableClass::class);
});

it('resolves static methods for manual registration', function () {
    $handler = [ValidHandlerClass::class, 'staticMethod'];
    $resolved = HandlerResolver::resolve($handler);

    expect($resolved)->toBeInstanceOf(ReflectionMethod::class);
    expect($resolved->getName())->toBe('staticMethod');
    expect($resolved->isStatic())->toBeTrue();
});

it('throws for invalid array handler format (count)', function () {
    HandlerResolver::resolve([ValidHandlerClass::class]);
})->throws(InvalidArgumentException::class, 'Invalid array handler format. Expected [ClassName::class, \'methodName\'].');

it('throws for invalid array handler format (types)', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, 123]);
})->throws(InvalidArgumentException::class, 'Invalid array handler format. Expected [ClassName::class, \'methodName\'].');

it('throws for non-existent class in array handler', function () {
    HandlerResolver::resolve(['NonExistentClass', 'method']);
})->throws(InvalidArgumentException::class, "Handler class 'NonExistentClass' not found");

it('throws for non-existent method in array handler', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, 'nonExistentMethod']);
})->throws(InvalidArgumentException::class, "Handler method 'nonExistentMethod' not found in class");

it('throws for non-existent class in string handler', function () {
    HandlerResolver::resolve('NonExistentInvokableClass');
})->throws(InvalidArgumentException::class, 'Invalid handler format. Expected Closure, [ClassName::class, \'methodName\'] or InvokableClassName::class string.');

it('throws for non-invokable class string handler', function () {
    HandlerResolver::resolve(NonInvokableClass::class);
})->throws(InvalidArgumentException::class, "Invokable handler class '" . NonInvokableClass::class . "' must have a public '__invoke' method.");

it('throws for protected method handler', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, 'protectedMethod']);
})->throws(InvalidArgumentException::class, 'must be public');

it('throws for private method handler', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, 'privateMethod']);
})->throws(InvalidArgumentException::class, 'must be public');

it('throws for constructor as handler', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, '__construct']);
})->throws(InvalidArgumentException::class, 'cannot be a constructor or destructor');

it('throws for destructor as handler', function () {
    HandlerResolver::resolve([ValidHandlerClass::class, '__destruct']);
})->throws(InvalidArgumentException::class, 'cannot be a constructor or destructor');

it('throws for abstract method handler', function () {
    HandlerResolver::resolve([AbstractHandlerClass::class, 'abstractMethod']);
})->throws(InvalidArgumentException::class, 'cannot be abstract');

// Test different closure types
it('resolves closures with different signatures', function () {
    $noParams = function () {
        return 'test';
    };
    $withParams = function (int $a, string $b = 'default') {
        return $a . $b;
    };
    $variadic = function (...$args) {
        return $args;
    };

    expect(HandlerResolver::resolve($noParams))->toBeInstanceOf(ReflectionFunction::class);
    expect(HandlerResolver::resolve($withParams))->toBeInstanceOf(ReflectionFunction::class);
    expect(HandlerResolver::resolve($variadic))->toBeInstanceOf(ReflectionFunction::class);

    expect(HandlerResolver::resolve($noParams)->getNumberOfParameters())->toBe(0);
    expect(HandlerResolver::resolve($withParams)->getNumberOfParameters())->toBe(2);
    expect(HandlerResolver::resolve($variadic)->isVariadic())->toBeTrue();
});

// Test that we can distinguish between closures and callable arrays
it('distinguishes between closures and callable arrays', function () {
    $closure = function () {
        return 'closure';
    };
    $array = [ValidHandlerClass::class, 'publicMethod'];
    $string = ValidInvokableClass::class;

    expect(HandlerResolver::resolve($closure))->toBeInstanceOf(ReflectionFunction::class);
    expect(HandlerResolver::resolve($array))->toBeInstanceOf(ReflectionMethod::class);
    expect(HandlerResolver::resolve($string))->toBeInstanceOf(ReflectionMethod::class);
});

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Utils;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\DocBlock\Tags\See;
use phpDocumentor\Reflection\DocBlock\Tags\Throws;
use PhpMcp\Server\Utils\DocBlockParser;
use PhpMcp\Server\Tests\Fixtures\General\DocBlockTestFixture;
use ReflectionMethod;

beforeEach(function () {
    $this->parser = new DocBlockParser();
});

test('getSummary returns correct summary', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);
    expect($this->parser->getSummary($docBlock))->toBe('Simple summary line.');

    $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
    $docComment2 = $method2->getDocComment() ?: null;
    $docBlock2 = $this->parser->parseDocBlock($docComment2);
    expect($this->parser->getSummary($docBlock2))->toBe('Summary line here.');
});

test('getDescription returns correct description', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryAndDescription');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);
    $expectedDesc = "Summary line here.\n\nThis is a longer description spanning\nmultiple lines.\nIt might contain *markdown* or `code`.";
    expect($this->parser->getDescription($docBlock))->toBe($expectedDesc);

    $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
    $docComment2 = $method2->getDocComment() ?: null;
    $docBlock2 = $this->parser->parseDocBlock($docComment2);
    expect($this->parser->getDescription($docBlock2))->toBe('Simple summary line.');
});

test('getParamTags returns structured param info', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithParams');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);
    $params = $this->parser->getParamTags($docBlock);

    expect($params)->toBeArray()->toHaveCount(6);
    expect($params)->toHaveKeys(['$param1', '$param2', '$param3', '$param4', '$param5', '$param6']);

    expect($params['$param1'])->toBeInstanceOf(Param::class);
    expect($params['$param1']->getVariableName())->toBe('param1');
    expect($this->parser->getParamTypeString($params['$param1']))->toBe('string');
    expect($this->parser->getParamDescription($params['$param1']))->toBe('Description for string param.');

    expect($params['$param2'])->toBeInstanceOf(Param::class);
    expect($params['$param2']->getVariableName())->toBe('param2');
    expect($this->parser->getParamTypeString($params['$param2']))->toBe('int|null');
    expect($this->parser->getParamDescription($params['$param2']))->toBe('Description for nullable int param.');

    expect($params['$param3'])->toBeInstanceOf(Param::class);
    expect($params['$param3']->getVariableName())->toBe('param3');
    expect($this->parser->getParamTypeString($params['$param3']))->toBe('bool');
    expect($this->parser->getParamDescription($params['$param3']))->toBeNull();

    expect($params['$param4'])->toBeInstanceOf(Param::class);
    expect($params['$param4']->getVariableName())->toBe('param4');
    expect($this->parser->getParamTypeString($params['$param4']))->toBe('mixed');
    expect($this->parser->getParamDescription($params['$param4']))->toBe('Missing type.');

    expect($params['$param5'])->toBeInstanceOf(Param::class);
    expect($params['$param5']->getVariableName())->toBe('param5');
    expect($this->parser->getParamTypeString($params['$param5']))->toBe('array<string,mixed>');
    expect($this->parser->getParamDescription($params['$param5']))->toBe('Array description.');

    expect($params['$param6'])->toBeInstanceOf(Param::class);
    expect($params['$param6']->getVariableName())->toBe('param6');
    expect($this->parser->getParamTypeString($params['$param6']))->toBe('stdClass');
    expect($this->parser->getParamDescription($params['$param6']))->toBe('Object param.');
});

test('getReturnTag returns structured return info', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithReturn');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);
    $returnTag = $this->parser->getReturnTag($docBlock);

    expect($returnTag)->toBeInstanceOf(Return_::class);
    expect($this->parser->getReturnTypeString($returnTag))->toBe('string');
    expect($this->parser->getReturnDescription($returnTag))->toBe('The result of the operation.');

    $method2 = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithSummaryOnly');
    $docComment2 = $method2->getDocComment() ?: null;
    $docBlock2 = $this->parser->parseDocBlock($docComment2);
    expect($this->parser->getReturnTag($docBlock2))->toBeNull();
});

test('getTagsByName returns specific tags', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithMultipleTags');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);

    expect($docBlock)->toBeInstanceOf(DocBlock::class);

    $throwsTags = $docBlock->getTagsByName('throws');
    expect($throwsTags)->toBeArray()->toHaveCount(1);
    expect($throwsTags[0])->toBeInstanceOf(Throws::class);
    expect((string) $throwsTags[0]->getType())->toBe('\\RuntimeException');
    expect($throwsTags[0]->getDescription()->render())->toBe('If processing fails.');

    $deprecatedTags = $docBlock->getTagsByName('deprecated');
    expect($deprecatedTags)->toBeArray()->toHaveCount(1);
    expect($deprecatedTags[0])->toBeInstanceOf(Deprecated::class);
    expect($deprecatedTags[0]->getDescription()->render())->toBe('Use newMethod() instead.');

    $seeTags = $docBlock->getTagsByName('see');
    expect($seeTags)->toBeArray()->toHaveCount(1);
    expect($seeTags[0])->toBeInstanceOf(See::class);
    expect((string) $seeTags[0]->getReference())->toContain('DocBlockTestFixture::newMethod()');

    $nonExistentTags = $docBlock->getTagsByName('nosuchtag');
    expect($nonExistentTags)->toBeArray()->toBeEmpty();
});

test('handles method with no docblock gracefully', function () {
    $method = new ReflectionMethod(DocBlockTestFixture::class, 'methodWithNoDocBlock');
    $docComment = $method->getDocComment() ?: null;
    $docBlock = $this->parser->parseDocBlock($docComment);

    expect($docBlock)->toBeNull();

    expect($this->parser->getSummary($docBlock))->toBeNull();
    expect($this->parser->getDescription($docBlock))->toBeNull();
    expect($this->parser->getParamTags($docBlock))->toBeArray()->toBeEmpty();
    expect($this->parser->getReturnTag($docBlock))->toBeNull();
});

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Attributes;

use Attribute;

/**
 * Defines a JSON Schema for a method's input or an individual parameter.
 *
 * When used at the method level, it describes an object schema where properties
 * correspond to the method's parameters.
 *
 * When used at the parameter level, it describes the schema for that specific parameter.
 * If 'type' is omitted at the parameter level, it will be inferred.
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PARAMETER)]
class Schema
{
    /**
     * The complete JSON schema array.
     * If provided, it takes precedence over individual properties like $type, $properties, etc.
     */
    public ?array $definition = null;

    /**
     * Alternatively, provide individual top-level schema keywords.
     * These are used if $definition is null.
     */
    public ?string $type = null;
    public ?string $description = null;
    public mixed $default = null;
    public ?array $enum = null; // list of allowed values
    public ?string $format = null; // e.g., 'email', 'date-time'

    // Constraints for string
    public ?int $minLength = null;
    public ?int $maxLength = null;
    public ?string $pattern = null;

    // Constraints for number/integer
    public int|float|null $minimum = null;
    public int|float|null $maximum = null;
    public ?bool $exclusiveMinimum = null;
    public ?bool $exclusiveMaximum = null;
    public int|float|null $multipleOf = null;

    // Constraints for array
    public ?array $items = null; // JSON schema for array items
    public ?int $minItems = null;
    public ?int $maxItems = null;
    public ?bool $uniqueItems = null;

    // Constraints for object (primarily used when Schema is on a method or an object-typed parameter)
    public ?array $properties = null; // [propertyName => [schema array], ...]
    public ?array $required = null;   // [propertyName, ...]
    public bool|array|null $additionalProperties = null; // true, false, or a schema array

    /**
     * @param array|null $definition A complete JSON schema array. If provided, other parameters are ignored.
     * @param Type|null $type The JSON schema type.
     * @param string|null $description Description of the element.
     * @param array|null $enum Allowed enum values.
     * @param string|null $format String format (e.g., 'date-time', 'email').
     * @param int|null $minLength Minimum length for strings.
     * @param int|null $maxLength Maximum length for strings.
     * @param string|null $pattern Regex pattern for strings.
     * @param int|float|null $minimum Minimum value for numbers/integers.
     * @param int|float|null $maximum Maximum value for numbers/integers.
     * @param bool|null $exclusiveMinimum Exclusive minimum.
     * @param bool|null $exclusiveMaximum Exclusive maximum.
     * @param int|float|null $multipleOf Must be a multiple of this value.
     * @param array|null $items JSON Schema for items if type is 'array'.
     * @param int|null $minItems Minimum items for an array.
     * @param int|null $maxItems Maximum items for an array.
     * @param bool|null $uniqueItems Whether array items must be unique.
     * @param array|null $properties Property definitions if type is 'object'. [name => schema_array].
     * @param array|null $required List of required properties for an object.
     * @param bool|array|null $additionalProperties Policy for additional properties in an object.
     */
    public function __construct(
        ?array $definition = null,
        ?string $type = null,
        ?string $description = null,
        ?array $enum = null,
        ?string $format = null,
        ?int $minLength = null,
        ?int $maxLength = null,
        ?string $pattern = null,
        int|float|null $minimum = null,
        int|float|null $maximum = null,
        ?bool $exclusiveMinimum = null,
        ?bool $exclusiveMaximum = null,
        int|float|null $multipleOf = null,
        ?array $items = null,
        ?int $minItems = null,
        ?int $maxItems = null,
        ?bool $uniqueItems = null,
        ?array $properties = null,
        ?array $required = null,
        bool|array|null $additionalProperties = null
    ) {
        if ($definition !== null) {
            $this->definition = $definition;
        } else {
            $this->type = $type;
            $this->description = $description;
            $this->enum = $enum;
            $this->format = $format;
            $this->minLength = $minLength;
            $this->maxLength = $maxLength;
            $this->pattern = $pattern;
            $this->minimum = $minimum;
            $this->maximum = $maximum;
            $this->exclusiveMinimum = $exclusiveMinimum;
            $this->exclusiveMaximum = $exclusiveMaximum;
            $this->multipleOf = $multipleOf;
            $this->items = $items;
            $this->minItems = $minItems;
            $this->maxItems = $maxItems;
            $this->uniqueItems = $uniqueItems;
            $this->properties = $properties;
            $this->required = $required;
            $this->additionalProperties = $additionalProperties;
        }
    }

    /**
     * Converts the attribute's definition to a JSON schema array.
     */
    public function toArray(): array
    {
        if ($this->definition !== null) {
            return [
                'definition' => $this->definition,
            ];
        }

        $schema = [];
        if ($this->type !== null) {
            $schema['type'] = $this->type;
        }
        if ($this->description !== null) {
            $schema['description'] = $this->description;
        }
        if ($this->enum !== null) {
            $schema['enum'] = $this->enum;
        }
        if ($this->format !== null) {
            $schema['format'] = $this->format;
        }

        // String
        if ($this->minLength !== null) {
            $schema['minLength'] = $this->minLength;
        }
        if ($this->maxLength !== null) {
            $schema['maxLength'] = $this->maxLength;
        }
        if ($this->pattern !== null) {
            $schema['pattern'] = $this->pattern;
        }

        // Numeric
        if ($this->minimum !== null) {
            $schema['minimum'] = $this->minimum;
        }
        if ($this->maximum !== null) {
            $schema['maximum'] = $this->maximum;
        }
        if ($this->exclusiveMinimum !== null) {
            $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
        }
        if ($this->exclusiveMaximum !== null) {
            $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
        }
        if ($this->multipleOf !== null) {
            $schema['multipleOf'] = $this->multipleOf;
        }

        // Array
        if ($this->items !== null) {
            $schema['items'] = $this->items;
        }
        if ($this->minItems !== null) {
            $schema['minItems'] = $this->minItems;
        }
        if ($this->maxItems !== null) {
            $schema['maxItems'] = $this->maxItems;
        }
        if ($this->uniqueItems !== null) {
            $schema['uniqueItems'] = $this->uniqueItems;
        }

        // Object
        if ($this->properties !== null) {
            $schema['properties'] = $this->properties;
        }
        if ($this->required !== null) {
            $schema['required'] = $this->required;
        }
        if ($this->additionalProperties !== null) {
            $schema['additionalProperties'] = $this->additionalProperties;
        }

        return $schema;
    }
}

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server;

use LogicException;
use PhpMcp\Server\Contracts\LoggerAwareInterface;
use PhpMcp\Server\Contracts\LoopAwareInterface;
use PhpMcp\Server\Contracts\ServerTransportInterface;
use PhpMcp\Server\Exception\ConfigurationException;
use PhpMcp\Server\Exception\DiscoveryException;
use PhpMcp\Server\Session\SessionManager;
use PhpMcp\Server\Utils\Discoverer;
use Throwable;

/**
 * Core MCP Server instance.
 *
 * Holds the configured MCP logic (Configuration, Registry, Protocol)
 * but is transport-agnostic. It relies on a ServerTransportInterface implementation,
 * provided via the listen() method, to handle network communication.
 *
 * Instances should be created via the ServerBuilder.
 */
class Server
{
    protected bool $discoveryRan = false;

    protected bool $isListening = false;

    /**
     *  @internal Use ServerBuilder::make()->...->build().
     *
     * @param  Configuration  $configuration  Core configuration and dependencies.
     * @param  Registry  $registry  Holds registered MCP element definitions.
     * @param  Protocol  $protocol  Handles MCP requests and responses.
     */
    public function __construct(
        protected readonly Configuration $configuration,
        protected readonly Registry $registry,
        protected readonly Protocol $protocol,
        protected readonly SessionManager $sessionManager,
    ) {
    }

    public static function make(): ServerBuilder
    {
        return new ServerBuilder();
    }

    /**
     * Runs the attribute discovery process based on the configuration
     * provided during build time. Caches results if cache is available.
     * Can be called explicitly, but is also called by ServerBuilder::build()
     * if discovery paths are configured.
     *
     * @param  bool  $force  Re-run discovery even if already run.
     * @param  bool  $useCache  Attempt to load from/save to cache. Defaults to true if cache is available.
     *
     * @throws DiscoveryException If discovery process encounters errors.
     * @throws ConfigurationException If discovery paths were not configured.
     */
    public function discover(
        string $basePath,
        array $scanDirs = ['.', 'src'],
        array $excludeDirs = [],
        bool $force = false,
        bool $saveToCache = true,
        ?Discoverer $discoverer = null
    ): void {
        $realBasePath = realpath($basePath);
        if ($realBasePath === false || ! is_dir($realBasePath)) {
            throw new \InvalidArgumentException("Invalid discovery base path provided to discover(): {$basePath}");
        }

        $excludeDirs = array_merge($excludeDirs, ['vendor', 'tests', 'test', 'storage', 'cache', 'samples', 'docs', 'node_modules', '.git', '.svn']);

        if ($this->discoveryRan && ! $force) {
            $this->configuration->logger->debug('Discovery skipped: Already run or loaded from cache.');

            return;
        }

        $cacheAvailable = $this->configuration->cache !== null;
        $shouldSaveCache = $saveToCache && $cacheAvailable;

        $this->configuration->logger->info('Starting MCP element discovery...', [
            'basePath' => $realBasePath,
            'force' => $force,
            'saveToCache' => $shouldSaveCache,
        ]);

        $this->registry->clear();

        try {
            $discoverer ??= new Discoverer($this->registry, $this->configuration->logger);

            $discoverer->discover($realBasePath, $scanDirs, $excludeDirs);

            $this->discoveryRan = true;

            if ($shouldSaveCache) {
                $this->registry->save();
            }
        } catch (Throwable $e) {
            $this->discoveryRan = false;
            $this->configuration->logger->critical('MCP element discovery failed.', ['exception' => $e]);
            throw new DiscoveryException("Element discovery failed: {$e->getMessage()}", $e->getCode(), $e);
        }
    }

    /**
     * Binds the server's MCP logic to the provided transport and starts the transport's listener,
     * then runs the event loop, making this a BLOCKING call suitable for standalone servers.
     *
     * For framework integration where the loop is managed externally, use `getProtocol()`
     * and bind it to your framework's transport mechanism manually.
     *
     * @param  ServerTransportInterface  $transport  The transport to listen with.
     *
     * @throws LogicException If called after already listening.
     * @throws Throwable If transport->listen() fails immediately.
     */
    public function listen(ServerTransportInterface $transport, bool $runLoop = true): void
    {
        if ($this->isListening) {
            throw new LogicException('Server is already listening via a transport.');
        }

        $this->warnIfNoElements();

        if ($transport instanceof LoggerAwareInterface) {
            $transport->setLogger($this->configuration->logger);
        }
        if ($transport instanceof LoopAwareInterface) {
            $transport->setLoop($this->configuration->loop);
        }

        $protocol = $this->getProtocol();

        $closeHandlerCallback = function (?string $reason = null) use ($protocol) {
            $this->isListening = false;
            $this->configuration->logger->info('Transport closed.', ['reason' => $reason ?? 'N/A']);
            $protocol->unbindTransport();
            $this->configuration->loop->stop();
        };

        $transport->once('close', $closeHandlerCallback);

        $protocol->bindTransport($transport);

        try {
            $transport->listen();

            $this->isListening = true;

            if ($runLoop) {
                $this->sessionManager->startGcTimer();

                $this->configuration->loop->run();

                $this->endListen($transport);
            }
        } catch (Throwable $e) {
            $this->configuration->logger->critical('Failed to start listening or event loop crashed.', ['exception' => $e->getMessage()]);
            $this->endListen($transport);
            throw $e;
        }
    }

    public function endListen(ServerTransportInterface $transport): void
    {
        $protocol = $this->getProtocol();

        $protocol->unbindTransport();

        $this->sessionManager->stopGcTimer();

        $transport->removeAllListeners('close');
        $transport->close();

        $this->isListening = false;
        $this->configuration->logger->info("Server '{$this->configuration->serverInfo->name}' listener shut down.");
    }

    /**
     * Warns if no MCP elements are registered and discovery has not been run.
     */
    protected function warnIfNoElements(): void
    {
        if (! $this->registry->hasElements() && ! $this->discoveryRan) {
            $this->configuration->logger->warning(
                'Starting listener, but no MCP elements are registered and discovery has not been run. ' .
                    'Call $server->discover(...) at least once to find and cache elements before listen().'
            );
        } elseif (! $this->registry->hasElements() && $this->discoveryRan) {
            $this->configuration->logger->warning(
                'Starting listener, but no MCP elements were found after discovery/cache load.'
            );
        }
    }

    /**
     * Gets the Configuration instance associated with this server.
     */
    public function getConfiguration(): Configuration
    {
        return $this->configuration;
    }

    /**
     * Gets the Registry instance associated with this server.
     */
    public function getRegistry(): Registry
    {
        return $this->registry;
    }

    /**
     * Gets the Protocol instance associated with this server.
     */
    public function getProtocol(): Protocol
    {
        return $this->protocol;
    }

    public function getSessionManager(): SessionManager
    {
        return $this->sessionManager;
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Session;

use PhpMcp\Server\Session\ArraySessionHandler;
use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Defaults\SystemClock;
use PhpMcp\Server\Tests\Mocks\Clock\FixedClock;

const SESSION_ID_ARRAY_1 = 'array-session-id-1';
const SESSION_ID_ARRAY_2 = 'array-session-id-2';
const SESSION_ID_ARRAY_3 = 'array-session-id-3';
const SESSION_DATA_1 = '{"user_id":101,"cart":{"items":[{"id":"prod_A","qty":2},{"id":"prod_B","qty":1}],"total":150.75},"theme":"dark"}';
const SESSION_DATA_2 = '{"user_id":102,"preferences":{"notifications":true,"language":"en"},"last_login":"2024-07-15T10:00:00Z"}';
const SESSION_DATA_3 = '{"guest":true,"viewed_products":["prod_C","prod_D"]}';
const DEFAULT_TTL_ARRAY = 3600;

beforeEach(function () {
    $this->fixedClock = new FixedClock();
    $this->handler = new ArraySessionHandler(DEFAULT_TTL_ARRAY, $this->fixedClock);
});

it('implements SessionHandlerInterface', function () {
    expect($this->handler)->toBeInstanceOf(SessionHandlerInterface::class);
});

it('constructs with a default TTL and SystemClock if no clock provided', function () {
    $handler = new ArraySessionHandler();
    expect($handler->ttl)->toBe(DEFAULT_TTL_ARRAY);
    $reflection = new \ReflectionClass($handler);
    $clockProp = $reflection->getProperty('clock');
    $clockProp->setAccessible(true);
    expect($clockProp->getValue($handler))->toBeInstanceOf(SystemClock::class);
});

it('constructs with a custom TTL and injected clock', function () {
    $customTtl = 1800;
    $clock = new FixedClock();
    $handler = new ArraySessionHandler($customTtl, $clock);
    expect($handler->ttl)->toBe($customTtl);
    $reflection = new \ReflectionClass($handler);
    $clockProp = $reflection->getProperty('clock');
    $clockProp->setAccessible(true);
    expect($clockProp->getValue($handler))->toBe($clock);
});

it('writes session data and reads it back correctly', function () {
    $writeResult = $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
    expect($writeResult)->toBeTrue();

    $readData = $this->handler->read(SESSION_ID_ARRAY_1);
    expect($readData)->toBe(SESSION_DATA_1);
});

it('returns false when reading a non-existent session', function () {
    $readData = $this->handler->read('non-existent-session-id');
    expect($readData)->toBeFalse();
});

it('overwrites existing session data on subsequent write', function () {
    $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
    $updatedData = '{"user_id":101,"cart":{"items":[{"id":"prod_A","qty":3}],"total":175.25},"theme":"light"}';
    $this->handler->write(SESSION_ID_ARRAY_1, $updatedData);

    $readData = $this->handler->read(SESSION_ID_ARRAY_1);
    expect($readData)->toBe($updatedData);
});

it('returns false and removes data when reading an expired session due to handler TTL', function () {
    $ttl = 60;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler($ttl, $fixedClock);
    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds($ttl + 1);

    $readData = $handler->read(SESSION_ID_ARRAY_1);
    expect($readData)->toBeFalse();

    $reflection = new \ReflectionClass($handler);
    $storeProp = $reflection->getProperty('store');
    $storeProp->setAccessible(true);
    $internalStore = $storeProp->getValue($handler);
    expect($internalStore)->not->toHaveKey(SESSION_ID_ARRAY_1);
});

it('does not return data if read exactly at TTL expiration time', function () {
    $shortTtl = 60;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler($shortTtl, $fixedClock);
    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds($shortTtl);

    $readData = $handler->read(SESSION_ID_ARRAY_1);
    expect($readData)->toBe(SESSION_DATA_1);

    $fixedClock->addSecond();

    $readDataExpired = $handler->read(SESSION_ID_ARRAY_1);
    expect($readDataExpired)->toBeFalse();
});


it('updates timestamp on write, effectively extending session life', function () {
    $veryShortTtl = 5;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler($veryShortTtl, $fixedClock);

    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds(3);

    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_2);

    $fixedClock->addSeconds(3);

    $readData = $handler->read(SESSION_ID_ARRAY_1);
    expect($readData)->toBe(SESSION_DATA_2);
});

it('destroys an existing session and it cannot be read', function () {
    $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
    expect($this->handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);

    $destroyResult = $this->handler->destroy(SESSION_ID_ARRAY_1);
    expect($destroyResult)->toBeTrue();
    expect($this->handler->read(SESSION_ID_ARRAY_1))->toBeFalse();

    $reflection = new \ReflectionClass($this->handler);
    $storeProp = $reflection->getProperty('store');
    $storeProp->setAccessible(true);
    expect($storeProp->getValue($this->handler))->not->toHaveKey(SESSION_ID_ARRAY_1);
});

it('destroy returns true and does nothing for a non-existent session', function () {
    $destroyResult = $this->handler->destroy('non-existent-id');
    expect($destroyResult)->toBeTrue();
});

it('garbage collects only sessions older than maxLifetime', function () {
    $gcMaxLifetime = 100;
    $handlerTtl = 300;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler($handlerTtl, $fixedClock);

    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds(50);
    $handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);

    $fixedClock->addSeconds(80);

    $deletedSessions = $handler->gc($gcMaxLifetime);

    expect($deletedSessions)->toBeArray()->toEqual([SESSION_ID_ARRAY_1]);
    expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
    expect($handler->read(SESSION_ID_ARRAY_2))->toBe(SESSION_DATA_2);
});

it('garbage collection respects maxLifetime precisely', function () {
    $maxLifetime = 60;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler(300, $fixedClock);

    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds($maxLifetime);
    $deleted = $handler->gc($maxLifetime);
    expect($deleted)->toBeEmpty();
    expect($handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);

    $fixedClock->addSecond();
    $deleted2 = $handler->gc($maxLifetime);
    expect($deleted2)->toEqual([SESSION_ID_ARRAY_1]);
    expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
});

it('garbage collection returns empty array if no sessions meet criteria', function () {
    $this->handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);
    $this->handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);

    $this->fixedClock->addSeconds(DEFAULT_TTL_ARRAY / 2);

    $deletedSessions = $this->handler->gc(DEFAULT_TTL_ARRAY);
    expect($deletedSessions)->toBeArray()->toBeEmpty();
    expect($this->handler->read(SESSION_ID_ARRAY_1))->toBe(SESSION_DATA_1);
    expect($this->handler->read(SESSION_ID_ARRAY_2))->toBe(SESSION_DATA_2);
});

it('garbage collection correctly handles an empty store', function () {
    $deletedSessions = $this->handler->gc(DEFAULT_TTL_ARRAY);
    expect($deletedSessions)->toBeArray()->toBeEmpty();
});

it('garbage collection removes multiple expired sessions', function () {
    $maxLifetime = 30;
    $fixedClock = new FixedClock();
    $handler = new ArraySessionHandler(300, $fixedClock);

    $handler->write(SESSION_ID_ARRAY_1, SESSION_DATA_1);

    $fixedClock->addSeconds(20);
    $handler->write(SESSION_ID_ARRAY_2, SESSION_DATA_2);

    $fixedClock->addSeconds(20);
    $handler->write(SESSION_ID_ARRAY_3, SESSION_DATA_3);

    $fixedClock->addSeconds(20);

    $deleted = $handler->gc($maxLifetime);
    expect($deleted)->toHaveCount(2)->toContain(SESSION_ID_ARRAY_1)->toContain(SESSION_ID_ARRAY_2);
    expect($handler->read(SESSION_ID_ARRAY_1))->toBeFalse();
    expect($handler->read(SESSION_ID_ARRAY_2))->toBeFalse();
    expect($handler->read(SESSION_ID_ARRAY_3))->toBe(SESSION_DATA_3);
});

```

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

```php
<?php

declare(strict_types=1); // Added missing strict_types

namespace PhpMcp\Server\Defaults;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use Throwable; // Changed from \Throwable to Throwable

/**
 * A basic PSR-11 container implementation with simple constructor auto-wiring.
 *
 * Supports instantiating classes with parameterless constructors or constructors
 * where all parameters are type-hinted classes/interfaces known to the container,
 * or have default values. Does NOT support scalar/built-in type injection without defaults.
 */
class BasicContainer implements ContainerInterface
{
    /** @var array<string, object> Cache for already created instances (shared singletons) */
    private array $instances = [];

    /** @var array<string, bool> Track classes currently being resolved to detect circular dependencies */
    private array $resolving = [];

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param  string  $id  Identifier of the entry to look for (usually a FQCN).
     * @return mixed Entry.
     *
     * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry (e.g., dependency resolution failure, circular dependency).
     */
    public function get(string $id): mixed
    {
        // 1. Check instance cache
        if (isset($this->instances[$id])) {
            return $this->instances[$id];
        }

        // 2. Check if class exists
        if (! class_exists($id) && ! interface_exists($id)) { // Also check interface for bindings
            throw new NotFoundException("Class, interface, or entry '{$id}' not found.");
        }

        // 7. Circular Dependency Check
        if (isset($this->resolving[$id])) {
            throw new ContainerException("Circular dependency detected while resolving '{$id}'. Resolution path: ".implode(' -> ', array_keys($this->resolving))." -> {$id}");
        }

        $this->resolving[$id] = true; // Mark as currently resolving

        try {
            // 3. Reflect on the class
            $reflector = new ReflectionClass($id);

            // Check if class is instantiable (abstract classes, interfaces cannot be directly instantiated)
            if (! $reflector->isInstantiable()) {
                // We might have an interface bound to a concrete class via set()
                // This check is slightly redundant due to class_exists but good practice
                throw new ContainerException("Class '{$id}' is not instantiable (e.g., abstract class or interface without explicit binding).");
            }

            // 4. Get the constructor
            $constructor = $reflector->getConstructor();

            // 5. If no constructor or constructor has no parameters, instantiate directly
            if ($constructor === null || $constructor->getNumberOfParameters() === 0) {
                $instance = $reflector->newInstance();
            } else {
                // 6. Constructor has parameters, attempt to resolve them
                $parameters = $constructor->getParameters();
                $resolvedArgs = [];

                foreach ($parameters as $parameter) {
                    $resolvedArgs[] = $this->resolveParameter($parameter, $id);
                }

                // Instantiate with resolved arguments
                $instance = $reflector->newInstanceArgs($resolvedArgs);
            }

            // Cache the instance
            $this->instances[$id] = $instance;

            return $instance;

        } catch (ReflectionException $e) {
            throw new ContainerException("Reflection failed for '{$id}'.", 0, $e);
        } catch (ContainerExceptionInterface $e) { // Re-throw container exceptions directly
            throw $e;
        } catch (Throwable $e) { // Catch other instantiation errors
            throw new ContainerException("Failed to instantiate or resolve dependencies for '{$id}': ".$e->getMessage(), (int) $e->getCode(), $e);
        } finally {
            // 7. Remove from resolving stack once done (success or failure)
            unset($this->resolving[$id]);
        }
    }

    /**
     * Attempts to resolve a single constructor parameter.
     *
     * @throws ContainerExceptionInterface If a required dependency cannot be resolved.
     */
    private function resolveParameter(ReflectionParameter $parameter, string $consumerClassId): mixed
    {
        // Check for type hint
        $type = $parameter->getType();

        if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
            // Type hint is a class or interface name
            $typeName = $type->getName();
            try {
                // Recursively get the dependency
                return $this->get($typeName);
            } catch (NotFoundExceptionInterface $e) {
                // Dependency class not found, fail ONLY if required
                if (! $parameter->isOptional() && ! $parameter->allowsNull()) {
                    throw new ContainerException("Unresolvable dependency '{$typeName}' required by '{$consumerClassId}' constructor parameter \${$parameter->getName()}.", 0, $e);
                }
                // If optional or nullable, proceed (will check allowsNull/Default below)
            } catch (ContainerExceptionInterface $e) {
                // Dependency itself failed to resolve (e.g., its own deps, circular)
                throw new ContainerException("Failed to resolve dependency '{$typeName}' for '{$consumerClassId}' parameter \${$parameter->getName()}: ".$e->getMessage(), 0, $e);
            }
        }

        // Check if parameter has a default value
        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }

        // Check if parameter allows null (and wasn't resolved above)
        if ($parameter->allowsNull()) {
            return null;
        }

        // Check if it was a built-in type without a default (unresolvable by this basic container)
        if ($type instanceof ReflectionNamedType && $type->isBuiltin()) {
            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.");
        }

        // Check if it was a union/intersection type without a default (also unresolvable)
        if ($type !== null && ! $type instanceof ReflectionNamedType) {
            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.");
        }

        // If we reach here, it's an untyped, required parameter without a default.
        // Or potentially an unresolvable optional class dependency where null is not allowed (edge case).
        throw new ContainerException("Cannot resolve required parameter \${$parameter->getName()} for '{$consumerClassId}' constructor (untyped or unresolvable complex type).");
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Checks explicitly set instances and if the class/interface exists.
     * Does not guarantee `get()` will succeed if auto-wiring fails.
     */
    public function has(string $id): bool
    {
        return isset($this->instances[$id]) || class_exists($id) || interface_exists($id);
    }

    /**
     * Adds a pre-built instance or a factory/binding to the container.
     * This basic version only supports pre-built instances (singletons).
     */
    public function set(string $id, object $instance): void
    {
        // Could add support for closures/factories later if needed
        $this->instances[$id] = $instance;
    }
}

// Keep custom exception classes as they are PSR-11 compliant placeholders
class ContainerException extends \Exception implements ContainerExceptionInterface
{
}
class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
}

```

--------------------------------------------------------------------------------
/tests/Unit/Elements/RegisteredToolTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Elements;

use InvalidArgumentException;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PhpMcp\Schema\Tool;
use PhpMcp\Server\Context;
use PhpMcp\Server\Contracts\SessionInterface;
use PhpMcp\Server\Elements\RegisteredTool;
use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Schema\Content\ImageContent;
use PhpMcp\Server\Tests\Fixtures\General\ToolHandlerFixture;
use Psr\Container\ContainerInterface;
use JsonException;
use PhpMcp\Server\Exception\McpServerException;

uses(MockeryPHPUnitIntegration::class);

beforeEach(function () {
    $this->container = Mockery::mock(ContainerInterface::class);
    $this->handlerInstance = new ToolHandlerFixture();
    $this->container->shouldReceive('get')->with(ToolHandlerFixture::class)
        ->andReturn($this->handlerInstance)->byDefault();

    $this->toolSchema = Tool::make(
        name: 'test-tool',
        inputSchema: ['type' => 'object', 'properties' => ['name' => ['type' => 'string']]]
    );

    $this->registeredTool = RegisteredTool::make(
        $this->toolSchema,
        [ToolHandlerFixture::class, 'greet']
    );
    $this->context = new Context(Mockery::mock(SessionInterface::class));
});

it('constructs correctly and exposes schema', function () {
    expect($this->registeredTool->schema)->toBe($this->toolSchema);
    expect($this->registeredTool->handler)->toBe([ToolHandlerFixture::class, 'greet']);
    expect($this->registeredTool->isManual)->toBeFalse();
});

it('can be made as a manual registration', function () {
    $manualTool = RegisteredTool::make($this->toolSchema, [ToolHandlerFixture::class, 'greet'], true);
    expect($manualTool->isManual)->toBeTrue();
});

it('calls the handler with prepared arguments', function () {
    $tool = RegisteredTool::make(
        Tool::make('sum-tool', ['type' => 'object', 'properties' => ['a' => ['type' => 'integer'], 'b' => ['type' => 'integer']]]),
        [ToolHandlerFixture::class, 'sum']
    );
    $mockHandler = Mockery::mock(ToolHandlerFixture::class);
    $mockHandler->shouldReceive('sum')->with(5, 10)->once()->andReturn(15);
    $this->container->shouldReceive('get')->with(ToolHandlerFixture::class)->andReturn($mockHandler);

    $resultContents = $tool->call($this->container, ['a' => 5, 'b' => '10'], $this->context); // '10' will be cast to int by prepareArguments

    expect($resultContents)->toBeArray()->toHaveCount(1);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe('15');
});

it('calls handler with no arguments if tool takes none and none provided', function () {
    $tool = RegisteredTool::make(
        Tool::make('no-args-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'noParamsTool']
    );
    $mockHandler = Mockery::mock(ToolHandlerFixture::class);
    $mockHandler->shouldReceive('noParamsTool')->withNoArgs()->once()->andReturn(['status' => 'done']);
    $this->container->shouldReceive('get')->with(ToolHandlerFixture::class)->andReturn($mockHandler);

    $resultContents = $tool->call($this->container, [], $this->context);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe(json_encode(['status' => 'done'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
});


dataset('tool_handler_return_values', [
    'string'        => ['returnString', "This is a string result."],
    'integer'       => ['returnInteger', "12345"],
    'float'         => ['returnFloat', "67.89"],
    'boolean_true'  => ['returnBooleanTrue', "true"],
    'boolean_false' => ['returnBooleanFalse', "false"],
    'null'          => ['returnNull', "(null)"],
    'array_to_json' => ['returnArray', json_encode(['message' => 'Array result', 'data' => [1, 2, 3]], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)],
    'object_to_json' => ['returnStdClass', json_encode((object)['property' => "value"], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)],
]);

it('formats various scalar and simple object/array handler results into TextContent', function (string $handlerMethod, string $expectedText) {
    $tool = RegisteredTool::make(
        Tool::make('format-test-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, $handlerMethod]
    );

    $resultContents = $tool->call($this->container, [], $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(1);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe($expectedText);
})->with('tool_handler_return_values');

it('returns single Content object from handler as array with one Content object', function () {
    $tool = RegisteredTool::make(
        Tool::make('content-test-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'returnTextContent']
    );
    $resultContents = $tool->call($this->container, [], $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(1);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe("Pre-formatted TextContent.");
});

it('returns array of Content objects from handler as is', function () {
    $tool = RegisteredTool::make(
        Tool::make('content-array-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'returnArrayOfContent']
    );
    $resultContents = $tool->call($this->container, [], $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(2);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe("Part 1");
    expect($resultContents[1])->toBeInstanceOf(ImageContent::class)->data->toBe("imgdata");
});

it('formats mixed array from handler into array of Content objects', function () {
    $tool = RegisteredTool::make(
        Tool::make('mixed-array-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'returnMixedArray']
    );
    $resultContents = $tool->call($this->container, [], $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(8);

    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe("A raw string");
    expect($resultContents[1])->toBeInstanceOf(TextContent::class)->text->toBe("A TextContent object"); // Original TextContent is preserved
    expect($resultContents[2])->toBeInstanceOf(TextContent::class)->text->toBe("123");
    expect($resultContents[3])->toBeInstanceOf(TextContent::class)->text->toBe("true");
    expect($resultContents[4])->toBeInstanceOf(TextContent::class)->text->toBe("(null)");
    expect($resultContents[5])->toBeInstanceOf(TextContent::class)->text->toBe(json_encode(['nested_key' => 'nested_value', 'sub_array' => [4, 5]], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    expect($resultContents[6])->toBeInstanceOf(ImageContent::class)->data->toBe("img_data_mixed"); // Original ImageContent is preserved
    expect($resultContents[7])->toBeInstanceOf(TextContent::class)->text->toBe(json_encode((object)['obj_prop' => 'obj_val'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
});

it('formats empty array from handler into TextContent with "[]"', function () {
    $tool = RegisteredTool::make(
        Tool::make('empty-array-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'returnEmptyArray']
    );
    $resultContents = $tool->call($this->container, [], $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(1);
    expect($resultContents[0])->toBeInstanceOf(TextContent::class)->text->toBe('[]');
});

it('throws JsonException during formatResult if handler returns unencodable value', function () {
    $tool = RegisteredTool::make(
        Tool::make('unencodable-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'toolUnencodableResult']
    );
    $tool->call($this->container, [], $this->context);
})->throws(JsonException::class);

it('re-throws exceptions from handler execution wrapped in McpServerException from handle()', function () {
    $tool = RegisteredTool::make(
        Tool::make('exception-tool', ['type' => 'object', 'properties' => []]),
        [ToolHandlerFixture::class, 'toolThatThrows']
    );

    $this->container->shouldReceive('get')->with(ToolHandlerFixture::class)->once()->andReturn(new ToolHandlerFixture());

    $tool->call($this->container, [], $this->context);
})->throws(InvalidArgumentException::class, "Something went wrong in the tool.");

```

--------------------------------------------------------------------------------
/src/Transports/StdioServerTransport.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Transports;

use Evenement\EventEmitterTrait;
use PhpMcp\Schema\JsonRpc\Parser;
use PhpMcp\Server\Contracts\LoggerAwareInterface;
use PhpMcp\Server\Contracts\LoopAwareInterface;
use PhpMcp\Server\Contracts\ServerTransportInterface;
use PhpMcp\Server\Exception\TransportException;
use PhpMcp\Schema\JsonRpc\Error;
use PhpMcp\Schema\JsonRpc\Message;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use React\ChildProcess\Process;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\ReadableResourceStream;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;
use Throwable;

use function React\Promise\reject;

/**
 * Implementation of the STDIO server transport using ReactPHP Process and Streams.
 * Listens on STDIN, writes to STDOUT, and emits events for the Protocol.
 */
class StdioServerTransport implements ServerTransportInterface, LoggerAwareInterface, LoopAwareInterface
{
    use EventEmitterTrait;

    protected LoggerInterface $logger;

    protected LoopInterface $loop;

    protected ?Process $process = null;

    protected ?ReadableStreamInterface $stdin = null;

    protected ?WritableStreamInterface $stdout = null;

    protected string $buffer = '';

    protected bool $closing = false;

    protected bool $listening = false;

    private const CLIENT_ID = 'stdio';

    /**
     * Constructor takes optional stream resources.
     * Defaults to STDIN and STDOUT if not provided.
     * Dependencies like Logger and Loop are injected via setters.
     *
     * @param  resource|null  $inputStreamResource  The readable resource (e.g., STDIN).
     * @param  resource|null  $outputStreamResource  The writable resource (e.g., STDOUT).
     *
     * @throws TransportException If provided resources are invalid.
     */
    public function __construct(
        protected $inputStreamResource = STDIN,
        protected $outputStreamResource = STDOUT
    ) {
        if (str_contains(PHP_OS, 'WIN') && ($this->inputStreamResource === STDIN && $this->outputStreamResource === STDOUT)) {
            $message = 'STDIN and STDOUT are not supported as input and output stream resources' .
                'on Windows due to PHP\'s limitations with non blocking pipes.' .
                'Please use WSL or HttpServerTransport, or if you are advanced, provide your own stream resources.';

            throw new TransportException($message);
        }

        // if (str_contains(PHP_OS, 'WIN')) {
        //     $this->inputStreamResource = pclose(popen('winpty -c "'.$this->inputStreamResource.'"', 'r'));
        //     $this->outputStreamResource = pclose(popen('winpty -c "'.$this->outputStreamResource.'"', 'w'));
        // }

        if (! is_resource($this->inputStreamResource) || get_resource_type($this->inputStreamResource) !== 'stream') {
            throw new TransportException('Invalid input stream resource provided.');
        }

        if (! is_resource($this->outputStreamResource) || get_resource_type($this->outputStreamResource) !== 'stream') {
            throw new TransportException('Invalid output stream resource provided.');
        }

        $this->logger = new NullLogger();
        $this->loop = Loop::get();
    }

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function setLoop(LoopInterface $loop): void
    {
        $this->loop = $loop;
    }

    /**
     * Starts listening on STDIN.
     *
     * @throws TransportException If already listening or streams cannot be opened.
     */
    public function listen(): void
    {
        if ($this->listening) {
            throw new TransportException('Stdio transport is already listening.');
        }

        if ($this->closing) {
            throw new TransportException('Cannot listen, transport is closing/closed.');
        }

        try {
            $this->stdin = new ReadableResourceStream($this->inputStreamResource, $this->loop);
            $this->stdout = new WritableResourceStream($this->outputStreamResource, $this->loop);
        } catch (Throwable $e) {
            $this->logger->error('Failed to open STDIN/STDOUT streams.', ['exception' => $e]);
            throw new TransportException("Failed to open standard streams: {$e->getMessage()}", 0, $e);
        }

        $this->stdin->on('data', function ($chunk) {
            $this->buffer .= $chunk;
            $this->processBuffer();
        });

        $this->stdin->on('error', function (Throwable $error) {
            $this->logger->error('STDIN stream error.', ['error' => $error->getMessage()]);
            $this->emit('error', [new TransportException("STDIN error: {$error->getMessage()}", 0, $error), self::CLIENT_ID]);
            $this->close();
        });

        $this->stdin->on('close', function () {
            $this->logger->info('STDIN stream closed.');
            $this->emit('client_disconnected', [self::CLIENT_ID, 'STDIN Closed']);
            $this->close();
        });

        $this->stdout->on('error', function (Throwable $error) {
            $this->logger->error('STDOUT stream error.', ['error' => $error->getMessage()]);
            $this->emit('error', [new TransportException("STDOUT error: {$error->getMessage()}", 0, $error), self::CLIENT_ID]);
            $this->close();
        });

        try {
            $signalHandler = function (int $signal) {
                $this->logger->info("Received signal {$signal}, shutting down.");
                $this->close();
            };
            $this->loop->addSignal(SIGTERM, $signalHandler);
            $this->loop->addSignal(SIGINT, $signalHandler);
        } catch (Throwable $e) {
            $this->logger->debug('Signal handling not supported by current event loop.');
        }

        $this->logger->info('Server is up and listening on STDIN 🚀');

        $this->listening = true;
        $this->closing = false;
        $this->emit('ready');
        $this->emit('client_connected', [self::CLIENT_ID]);
    }

    /** Processes the internal buffer to find complete lines/frames. */
    private function processBuffer(): void
    {
        while (str_contains($this->buffer, "\n")) {
            $pos = strpos($this->buffer, "\n");
            $line = substr($this->buffer, 0, $pos);
            $this->buffer = substr($this->buffer, $pos + 1);

            $trimmedLine = trim($line);
            if (empty($trimmedLine)) {
                continue;
            }

            try {
                $message = Parser::parse($trimmedLine);
            } catch (Throwable $e) {
                $this->logger->error('Error parsing message', ['exception' => $e]);
                $error = Error::forParseError("Invalid JSON: " . $e->getMessage());
                $this->sendMessage($error, self::CLIENT_ID);
                continue;
            }

            $this->emit('message', [$message, self::CLIENT_ID]);
        }
    }

    /**
     * Sends a raw, framed message to STDOUT.
     */
    public function sendMessage(Message $message, string $sessionId, array $context = []): PromiseInterface
    {
        if ($this->closing || ! $this->stdout || ! $this->stdout->isWritable()) {
            return reject(new TransportException('Stdio transport is closed or STDOUT is not writable.'));
        }

        $deferred = new Deferred();
        $json = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $written = $this->stdout->write($json . "\n");

        if ($written) {
            $deferred->resolve(null);
        } else {
            $this->logger->debug('STDOUT buffer full, waiting for drain.');
            $this->stdout->once('drain', function () use ($deferred) {
                $this->logger->debug('STDOUT drained.');
                $deferred->resolve(null);
            });
        }

        return $deferred->promise();
    }

    /**
     * Stops listening and cleans up resources.
     */
    public function close(): void
    {
        if ($this->closing) {
            return;
        }
        $this->closing = true;
        $this->listening = false;
        $this->logger->info('Closing Stdio transport...');

        $this->stdin?->close();
        $this->stdout?->close();

        $this->stdin = null;
        $this->stdout = null;

        $this->emit('close', ['StdioTransport closed.']);
        $this->removeAllListeners();
    }
}

```

--------------------------------------------------------------------------------
/src/Elements/RegisteredResource.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Elements;

use PhpMcp\Schema\Content\BlobResourceContents;
use PhpMcp\Schema\Content\EmbeddedResource;
use PhpMcp\Schema\Content\ResourceContents;
use PhpMcp\Schema\Content\TextResourceContents;
use PhpMcp\Schema\Resource;
use PhpMcp\Server\Context;
use Psr\Container\ContainerInterface;
use Throwable;

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

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

    /**
     * Reads the resource content.
     *
     * @return array<TextResourceContents|BlobResourceContents> Array of ResourceContents objects.
     */
    public function read(ContainerInterface $container, string $uri, Context $context): array
    {
        $result = $this->handle($container, ['uri' => $uri], $context);

        return $this->formatResult($result, $uri, $this->schema->mimeType);
    }

    /**
     * Formats the raw result of a resource read operation into MCP ResourceContent items.
     *
     * @param  mixed  $readResult  The raw result from the resource handler method.
     * @param  string  $uri  The URI of the resource that was read.
     * @param  ?string  $mimeType  The MIME type from the ResourceDefinition.
     * @return array<TextResourceContents|BlobResourceContents> Array of ResourceContents objects.
     *
     * @throws \RuntimeException If the result cannot be formatted.
     *
     * Supported result types:
     * - ResourceContent: Used as-is
     * - EmbeddedResource: Resource is extracted from the EmbeddedResource
     * - string: Converted to text content with guessed or provided MIME type
     * - stream resource: Read and converted to blob with provided MIME type
     * - array with 'blob' key: Used as blob content
     * - array with 'text' key: Used as text content
     * - SplFileInfo: Read and converted to blob
     * - array: Converted to JSON if MIME type is application/json or contains 'json'
     *          For other MIME types, will try to convert to JSON with a warning
     */
    protected function formatResult(mixed $readResult, string $uri, ?string $mimeType): array
    {
        if ($readResult instanceof ResourceContents) {
            return [$readResult];
        }

        if ($readResult instanceof EmbeddedResource) {
            return [$readResult->resource];
        }

        if (is_array($readResult)) {
            if (empty($readResult)) {
                return [TextResourceContents::make($uri, 'application/json', '[]')];
            }

            $allAreResourceContents = true;
            $hasResourceContents = false;
            $allAreEmbeddedResource = true;
            $hasEmbeddedResource = false;

            foreach ($readResult as $item) {
                if ($item instanceof ResourceContents) {
                    $hasResourceContents = true;
                    $allAreEmbeddedResource = false;
                } elseif ($item instanceof EmbeddedResource) {
                    $hasEmbeddedResource = true;
                    $allAreResourceContents = false;
                } else {
                    $allAreResourceContents = false;
                    $allAreEmbeddedResource = false;
                }
            }

            if ($allAreResourceContents && $hasResourceContents) {
                return $readResult;
            }

            if ($allAreEmbeddedResource && $hasEmbeddedResource) {
                return array_map(fn($item) => $item->resource, $readResult);
            }

            if ($hasResourceContents || $hasEmbeddedResource) {
                $result = [];
                foreach ($readResult as $item) {
                    if ($item instanceof ResourceContents) {
                        $result[] = $item;
                    } elseif ($item instanceof EmbeddedResource) {
                        $result[] = $item->resource;
                    } else {
                        $result = array_merge($result, $this->formatResult($item, $uri, $mimeType));
                    }
                }
                return $result;
            }
        }

        if (is_string($readResult)) {
            $mimeType = $mimeType ?? $this->guessMimeTypeFromString($readResult);

            return [TextResourceContents::make($uri, $mimeType, $readResult)];
        }

        if (is_resource($readResult) && get_resource_type($readResult) === 'stream') {
            $result = BlobResourceContents::fromStream(
                $uri,
                $readResult,
                $mimeType ?? 'application/octet-stream'
            );

            @fclose($readResult);

            return [$result];
        }

        if (is_array($readResult) && isset($readResult['blob']) && is_string($readResult['blob'])) {
            $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'application/octet-stream';

            return [BlobResourceContents::make($uri, $mimeType, $readResult['blob'])];
        }

        if (is_array($readResult) && isset($readResult['text']) && is_string($readResult['text'])) {
            $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'text/plain';

            return [TextResourceContents::make($uri, $mimeType, $readResult['text'])];
        }

        if ($readResult instanceof \SplFileInfo && $readResult->isFile() && $readResult->isReadable()) {
            if ($mimeType && str_contains(strtolower($mimeType), 'text')) {
                return [TextResourceContents::make($uri, $mimeType, file_get_contents($readResult->getPathname()))];
            }

            return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType)];
        }

        if (is_array($readResult)) {
            if ($mimeType && (str_contains(strtolower($mimeType), 'json') ||
                $mimeType === 'application/json')) {
                try {
                    $jsonString = json_encode($readResult, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);

                    return [TextResourceContents::make($uri, $mimeType, $jsonString)];
                } catch (\JsonException $e) {
                    throw new \RuntimeException("Failed to encode array as JSON for URI '{$uri}': {$e->getMessage()}");
                }
            }

            try {
                $jsonString = json_encode($readResult, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
                $mimeType = $mimeType ?? 'application/json';

                return [TextResourceContents::make($uri, $mimeType, $jsonString)];
            } catch (\JsonException $e) {
                throw new \RuntimeException("Failed to encode array as JSON for URI '{$uri}': {$e->getMessage()}");
            }
        }

        throw new \RuntimeException("Cannot format resource read result for URI '{$uri}'. Handler method returned unhandled type: " . gettype($readResult));
    }

    /** Guesses MIME type from string content (very basic) */
    private function guessMimeTypeFromString(string $content): string
    {
        $trimmed = ltrim($content);

        if (str_starts_with($trimmed, '<') && str_ends_with(rtrim($content), '>')) {
            if (str_contains($trimmed, '<html')) {
                return 'text/html';
            }
            if (str_contains($trimmed, '<?xml')) {
                return 'application/xml';
            }

            return 'text/plain';
        }

        if (str_starts_with($trimmed, '{') && str_ends_with(rtrim($content), '}')) {
            return 'application/json';
        }

        if (str_starts_with($trimmed, '[') && str_ends_with(rtrim($content), ']')) {
            return 'application/json';
        }

        return 'text/plain';
    }

    public function toArray(): array
    {
        return [
            'schema' => $this->schema->toArray(),
            ...parent::toArray(),
        ];
    }

    public static function fromArray(array $data): self|false
    {
        try {
            if (! isset($data['schema']) || ! isset($data['handler'])) {
                return false;
            }

            return new self(
                Resource::fromArray($data['schema']),
                $data['handler'],
                $data['isManual'] ?? false,
            );
        } catch (Throwable $e) {
            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/src/Defaults/FileCache.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Defaults;

use DateInterval;
use DateTimeImmutable;
use InvalidArgumentException;
use Psr\SimpleCache\CacheInterface;
use Throwable;

/**
 * Basic PSR-16 file cache implementation.
 *
 * Stores cache entries serialized in a JSON file.
 * Uses file locking for basic concurrency control during writes.
 * Not recommended for high-concurrency environments.
 */
class FileCache implements CacheInterface
{
    /**
     * @param  string  $cacheFile  Absolute path to the cache file.
     *                             The directory will be created if it doesn't exist.
     * @param  int  $filePermission  Optional file mode (octal) for the cache file (default: 0664).
     * @param  int  $dirPermission  Optional directory mode (octal) for the cache directory (default: 0775).
     */
    public function __construct(
        private readonly string $cacheFile,
        private readonly int $filePermission = 0664,
        private readonly int $dirPermission = 0775
    ) {
        $this->ensureDirectoryExists(dirname($this->cacheFile));
    }

    // ---------------------------------------------------------------------
    // PSR-16 Methods
    // ---------------------------------------------------------------------

    public function get(string $key, mixed $default = null): mixed
    {
        $data = $this->readCacheFile();
        $key = $this->sanitizeKey($key);

        if (! isset($data[$key])) {
            return $default;
        }

        if ($this->isExpired($data[$key]['expiry'])) {
            $this->delete($key); // Clean up expired entry

            return $default;
        }

        return $data[$key]['value'] ?? $default;
    }

    public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
    {
        $data = $this->readCacheFile();
        $key = $this->sanitizeKey($key);

        $data[$key] = [
            'value' => $value,
            'expiry' => $this->calculateExpiry($ttl),
        ];

        return $this->writeCacheFile($data);
    }

    public function delete(string $key): bool
    {
        $data = $this->readCacheFile();
        $key = $this->sanitizeKey($key);

        if (isset($data[$key])) {
            unset($data[$key]);

            return $this->writeCacheFile($data);
        }

        return true; // Key didn't exist, considered successful delete
    }

    public function clear(): bool
    {
        // Write an empty array to the file
        return $this->writeCacheFile([]);
    }

    public function getMultiple(iterable $keys, mixed $default = null): iterable
    {
        $keys = $this->iterableToArray($keys);
        $this->validateKeys($keys);

        $data = $this->readCacheFile();
        $results = [];
        $needsWrite = false;

        foreach ($keys as $key) {
            $sanitizedKey = $this->sanitizeKey($key);
            if (! isset($data[$sanitizedKey])) {
                $results[$key] = $default;

                continue;
            }

            if ($this->isExpired($data[$sanitizedKey]['expiry'])) {
                unset($data[$sanitizedKey]); // Clean up expired entry
                $needsWrite = true;
                $results[$key] = $default;

                continue;
            }

            $results[$key] = $data[$sanitizedKey]['value'] ?? $default;
        }

        if ($needsWrite) {
            $this->writeCacheFile($data);
        }

        return $results;
    }

    public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool
    {
        $values = $this->iterableToArray($values);
        $this->validateKeys(array_keys($values));

        $data = $this->readCacheFile();
        $expiry = $this->calculateExpiry($ttl);

        foreach ($values as $key => $value) {
            $sanitizedKey = $this->sanitizeKey((string) $key);
            $data[$sanitizedKey] = [
                'value' => $value,
                'expiry' => $expiry,
            ];
        }

        return $this->writeCacheFile($data);
    }

    public function deleteMultiple(iterable $keys): bool
    {
        $keys = $this->iterableToArray($keys);
        $this->validateKeys($keys);

        $data = $this->readCacheFile();
        $deleted = false;

        foreach ($keys as $key) {
            $sanitizedKey = $this->sanitizeKey($key);
            if (isset($data[$sanitizedKey])) {
                unset($data[$sanitizedKey]);
                $deleted = true;
            }
        }

        if ($deleted) {
            return $this->writeCacheFile($data);
        }

        return true; // No keys existed or no changes made
    }

    public function has(string $key): bool
    {
        $data = $this->readCacheFile();
        $key = $this->sanitizeKey($key);

        if (! isset($data[$key])) {
            return false;
        }

        if ($this->isExpired($data[$key]['expiry'])) {
            $this->delete($key); // Clean up expired

            return false;
        }

        return true;
    }

    // ---------------------------------------------------------------------
    // Internal Methods
    // ---------------------------------------------------------------------

    private function readCacheFile(): array
    {
        if (! file_exists($this->cacheFile) || filesize($this->cacheFile) === 0) {
            return [];
        }

        $handle = @fopen($this->cacheFile, 'rb');
        if ($handle === false) {
            return [];
        }

        try {
            if (! flock($handle, LOCK_SH)) {
                return [];
            }
            $content = stream_get_contents($handle);
            flock($handle, LOCK_UN);

            if ($content === false || $content === '') {
                return [];
            }

            $data = unserialize($content);
            if ($data === false) {
                return [];
            }

            return $data;
        } finally {
            if (is_resource($handle)) {
                fclose($handle);
            }
        }
    }

    private function writeCacheFile(array $data): bool
    {
        $jsonData = serialize($data);

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

        $handle = @fopen($this->cacheFile, 'cb');
        if ($handle === false) {
            return false;
        }

        try {
            if (! flock($handle, LOCK_EX)) {
                return false;
            }
            if (! ftruncate($handle, 0)) {
                return false;
            }
            if (fwrite($handle, $jsonData) === false) {
                return false;
            }
            fflush($handle);
            flock($handle, LOCK_UN);
            @chmod($this->cacheFile, $this->filePermission);

            return true;
        } catch (Throwable $e) {
            flock($handle, LOCK_UN); // Ensure lock release on error

            return false;
        } finally {
            if (is_resource($handle)) {
                fclose($handle);
            }
        }
    }

    private function ensureDirectoryExists(string $directory): void
    {
        if (! is_dir($directory)) {
            if (! @mkdir($directory, $this->dirPermission, true)) {
                throw new InvalidArgumentException("Cache directory does not exist and could not be created: {$directory}");
            }
            @chmod($directory, $this->dirPermission);
        }
    }

    private function calculateExpiry(DateInterval|int|null $ttl): ?int
    {
        if ($ttl === null) {
            return null;
        }
        $now = time();
        if (is_int($ttl)) {
            return $ttl <= 0 ? $now - 1 : $now + $ttl;
        }
        if ($ttl instanceof DateInterval) {
            try {
                return (new DateTimeImmutable())->add($ttl)->getTimestamp();
            } catch (Throwable $e) {
                return null;
            }
        }
        throw new InvalidArgumentException('Invalid TTL type provided. Must be null, int, or DateInterval.');
    }

    private function isExpired(?int $expiry): bool
    {
        return $expiry !== null && time() >= $expiry;
    }

    private function sanitizeKey(string $key): string
    {
        if ($key === '') {
            throw new InvalidArgumentException('Cache key cannot be empty.');
        }

        // PSR-16 validation (optional stricter check)
        // if (preg_match('/[{}()\/@:]/', $key)) {
        //     throw new InvalidArgumentException("Cache key \"{$key}\" contains reserved characters.");
        // }
        return $key;
    }

    private function validateKeys(array $keys): void
    {
        foreach ($keys as $key) {
            if (! is_string($key)) {
                throw new InvalidArgumentException('Cache key must be a string, got ' . gettype($key));
            }
            $this->sanitizeKey($key);
        }
    }

    private function iterableToArray(iterable $iterable): array
    {
        if (is_array($iterable)) {
            return $iterable;
        }

        return iterator_to_array($iterable);
    }
}

```

--------------------------------------------------------------------------------
/tests/Integration/DiscoveryTest.php:
--------------------------------------------------------------------------------

```php
<?php

use PhpMcp\Server\Defaults\EnumCompletionProvider;
use PhpMcp\Server\Defaults\ListCompletionProvider;
use PhpMcp\Server\Elements\RegisteredPrompt;
use PhpMcp\Server\Elements\RegisteredResource;
use PhpMcp\Server\Elements\RegisteredResourceTemplate;
use PhpMcp\Server\Elements\RegisteredTool;
use PhpMcp\Server\Registry;
use PhpMcp\Server\Tests\Fixtures\Discovery\DiscoverableToolHandler;
use PhpMcp\Server\Tests\Fixtures\Discovery\InvocablePromptFixture;
use PhpMcp\Server\Tests\Fixtures\Discovery\InvocableResourceFixture;
use PhpMcp\Server\Tests\Fixtures\Discovery\InvocableResourceTemplateFixture;
use PhpMcp\Server\Tests\Fixtures\Discovery\InvocableToolFixture;
use PhpMcp\Server\Utils\Discoverer;
use PhpMcp\Server\Utils\DocBlockParser;
use PhpMcp\Server\Utils\SchemaGenerator;
use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
use Psr\Log\NullLogger;

beforeEach(function () {
    $logger = new NullLogger();
    $this->registry = new Registry($logger);

    $docBlockParser = new DocBlockParser($logger);
    $schemaGenerator = new SchemaGenerator($docBlockParser);
    $this->discoverer = new Discoverer($this->registry, $logger, $docBlockParser, $schemaGenerator);

    $this->fixtureBasePath = realpath(__DIR__ . '/../Fixtures');
});

it('discovers all element types correctly from fixture files', function () {
    $scanDir = 'Discovery';

    $this->discoverer->discover($this->fixtureBasePath, [$scanDir]);

    $tools = $this->registry->getTools();
    expect($tools)->toHaveCount(4); // greet_user, repeatAction, InvokableCalculator, hidden_subdir_tool

    $greetUserTool = $this->registry->getTool('greet_user');
    expect($greetUserTool)->toBeInstanceOf(RegisteredTool::class)
        ->and($greetUserTool->isManual)->toBeFalse()
        ->and($greetUserTool->schema->name)->toBe('greet_user')
        ->and($greetUserTool->schema->description)->toBe('Greets a user by name.')
        ->and($greetUserTool->handler)->toBe([DiscoverableToolHandler::class, 'greet']);
    expect($greetUserTool->schema->inputSchema['properties'] ?? [])->toHaveKey('name');

    $repeatActionTool = $this->registry->getTool('repeatAction');
    expect($repeatActionTool)->toBeInstanceOf(RegisteredTool::class)
        ->and($repeatActionTool->isManual)->toBeFalse()
        ->and($repeatActionTool->schema->description)->toBe('A tool with more complex parameters and inferred name/description.')
        ->and($repeatActionTool->schema->annotations->readOnlyHint)->toBeTrue();
    expect(array_keys($repeatActionTool->schema->inputSchema['properties'] ?? []))->toEqual(['count', 'loudly', 'mode']);

    $invokableCalcTool = $this->registry->getTool('InvokableCalculator');
    expect($invokableCalcTool)->toBeInstanceOf(RegisteredTool::class)
        ->and($invokableCalcTool->isManual)->toBeFalse()
        ->and($invokableCalcTool->handler)->toBe([InvocableToolFixture::class, '__invoke']);

    expect($this->registry->getTool('private_tool_should_be_ignored'))->toBeNull();
    expect($this->registry->getTool('protected_tool_should_be_ignored'))->toBeNull();
    expect($this->registry->getTool('static_tool_should_be_ignored'))->toBeNull();


    $resources = $this->registry->getResources();
    expect($resources)->toHaveCount(3); // app_version, ui_settings_discovered, InvocableResourceFixture

    $appVersionRes = $this->registry->getResource('app://info/version');
    expect($appVersionRes)->toBeInstanceOf(RegisteredResource::class)
        ->and($appVersionRes->isManual)->toBeFalse()
        ->and($appVersionRes->schema->name)->toBe('app_version')
        ->and($appVersionRes->schema->mimeType)->toBe('text/plain');

    $invokableStatusRes = $this->registry->getResource('invokable://config/status');
    expect($invokableStatusRes)->toBeInstanceOf(RegisteredResource::class)
        ->and($invokableStatusRes->isManual)->toBeFalse()
        ->and($invokableStatusRes->handler)->toBe([InvocableResourceFixture::class, '__invoke']);


    $prompts = $this->registry->getPrompts();
    expect($prompts)->toHaveCount(4); // creative_story_prompt, simpleQuestionPrompt, InvocablePromptFixture, content_creator

    $storyPrompt = $this->registry->getPrompt('creative_story_prompt');
    expect($storyPrompt)->toBeInstanceOf(RegisteredPrompt::class)
        ->and($storyPrompt->isManual)->toBeFalse()
        ->and($storyPrompt->schema->arguments)->toHaveCount(2) // genre, lengthWords
        ->and($storyPrompt->completionProviders['genre'])->toBe(CompletionProviderFixture::class);

    $simplePrompt = $this->registry->getPrompt('simpleQuestionPrompt'); // Inferred name
    expect($simplePrompt)->toBeInstanceOf(RegisteredPrompt::class)
        ->and($simplePrompt->isManual)->toBeFalse();

    $invokableGreeter = $this->registry->getPrompt('InvokableGreeterPrompt');
    expect($invokableGreeter)->toBeInstanceOf(RegisteredPrompt::class)
        ->and($invokableGreeter->isManual)->toBeFalse()
        ->and($invokableGreeter->handler)->toBe([InvocablePromptFixture::class, '__invoke']);

    $contentCreatorPrompt = $this->registry->getPrompt('content_creator');
    expect($contentCreatorPrompt)->toBeInstanceOf(RegisteredPrompt::class)
        ->and($contentCreatorPrompt->isManual)->toBeFalse()
        ->and($contentCreatorPrompt->completionProviders)->toHaveCount(3);

    $templates = $this->registry->getResourceTemplates();
    expect($templates)->toHaveCount(4); // product_details_template, getFileContent, InvocableResourceTemplateFixture, content_template

    $productTemplate = $this->registry->getResourceTemplate('product://{region}/details/{productId}');
    expect($productTemplate)->toBeInstanceOf(RegisteredResourceTemplate::class)
        ->and($productTemplate->isManual)->toBeFalse()
        ->and($productTemplate->schema->name)->toBe('product_details_template')
        ->and($productTemplate->completionProviders['region'])->toBe(CompletionProviderFixture::class);
    expect($productTemplate->getVariableNames())->toEqualCanonicalizing(['region', 'productId']);

    $invokableUserTemplate = $this->registry->getResourceTemplate('invokable://user-profile/{userId}');
    expect($invokableUserTemplate)->toBeInstanceOf(RegisteredResourceTemplate::class)
        ->and($invokableUserTemplate->isManual)->toBeFalse()
        ->and($invokableUserTemplate->handler)->toBe([InvocableResourceTemplateFixture::class, '__invoke']);
});

it('does not discover elements from excluded directories', function () {
    $this->discoverer->discover($this->fixtureBasePath, ['Discovery']);

    expect($this->registry->getTool('hidden_subdir_tool'))->not->toBeNull();

    $this->registry->clear();

    $this->discoverer->discover($this->fixtureBasePath, ['Discovery'], ['SubDir']);
    expect($this->registry->getTool('hidden_subdir_tool'))->toBeNull();
});

it('handles empty directories or directories with no PHP files', function () {
    $this->discoverer->discover($this->fixtureBasePath, ['EmptyDir']);
    expect($this->registry->getTools())->toBeEmpty();
});

it('correctly infers names and descriptions from methods/classes if not set in attribute', function () {
    $this->discoverer->discover($this->fixtureBasePath, ['Discovery']);

    $repeatActionTool = $this->registry->getTool('repeatAction');
    expect($repeatActionTool->schema->name)->toBe('repeatAction'); // Method name
    expect($repeatActionTool->schema->description)->toBe('A tool with more complex parameters and inferred name/description.'); // Docblock summary

    $simplePrompt = $this->registry->getPrompt('simpleQuestionPrompt');
    expect($simplePrompt->schema->name)->toBe('simpleQuestionPrompt');
    expect($simplePrompt->schema->description)->toBeNull();

    $invokableCalc = $this->registry->getTool('InvokableCalculator'); // Name comes from Attr
    expect($invokableCalc->schema->name)->toBe('InvokableCalculator');
    expect($invokableCalc->schema->description)->toBe('An invokable calculator tool.');
});

it('discovers enhanced completion providers with values and enum attributes', function () {
    $this->discoverer->discover($this->fixtureBasePath, ['Discovery']);

    $contentPrompt = $this->registry->getPrompt('content_creator');
    expect($contentPrompt)->toBeInstanceOf(RegisteredPrompt::class);

    expect($contentPrompt->completionProviders)->toHaveCount(3);

    $typeProvider = $contentPrompt->completionProviders['type'];
    expect($typeProvider)->toBeInstanceOf(ListCompletionProvider::class);

    $statusProvider = $contentPrompt->completionProviders['status'];
    expect($statusProvider)->toBeInstanceOf(EnumCompletionProvider::class);

    $priorityProvider = $contentPrompt->completionProviders['priority'];
    expect($priorityProvider)->toBeInstanceOf(EnumCompletionProvider::class);

    $contentTemplate = $this->registry->getResourceTemplate('content://{category}/{slug}');
    expect($contentTemplate)->toBeInstanceOf(RegisteredResourceTemplate::class);
    expect($contentTemplate->completionProviders)->toHaveCount(1);

    $categoryProvider = $contentTemplate->completionProviders['category'];
    expect($categoryProvider)->toBeInstanceOf(ListCompletionProvider::class);
});

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Session;

use Mockery;
use Mockery\MockInterface;
use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Contracts\SessionInterface;
use PhpMcp\Server\Session\ArraySessionHandler;
use PhpMcp\Server\Session\SessionManager;
use PhpMcp\Server\Tests\Mocks\Clock\FixedClock;
use Psr\Log\LoggerInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;

const SESSION_ID_MGR_1 = 'manager-session-1';
const SESSION_ID_MGR_2 = 'manager-session-2';
const DEFAULT_TTL_MGR = 3600;
const GC_INTERVAL_MGR = 5;

beforeEach(function () {
    /** @var MockInterface&SessionHandlerInterface $sessionHandler */
    $this->sessionHandler = Mockery::mock(SessionHandlerInterface::class);
    /** @var MockInterface&LoggerInterface $logger */
    $this->logger = Mockery::mock(LoggerInterface::class)->shouldIgnoreMissing();
    $this->loop = Loop::get();

    $this->sessionManager = new SessionManager(
        $this->sessionHandler,
        $this->logger,
        $this->loop,
        DEFAULT_TTL_MGR
    );

    $this->sessionHandler->shouldReceive('read')->with(Mockery::any())->andReturn(false)->byDefault();
    $this->sessionHandler->shouldReceive('write')->with(Mockery::any(), Mockery::any())->andReturn(true)->byDefault();
    $this->sessionHandler->shouldReceive('destroy')->with(Mockery::any())->andReturn(true)->byDefault();
    $this->sessionHandler->shouldReceive('gc')->with(Mockery::any())->andReturn([])->byDefault();
});

it('creates a new session with default hydrated values and saves it', function () {
    $this->sessionHandler->shouldReceive('write')
        ->with(SESSION_ID_MGR_1, Mockery::on(function ($dataJson) {
            $data = json_decode($dataJson, true);
            expect($data['initialized'])->toBeFalse();
            expect($data['client_info'])->toBeNull();
            expect($data['protocol_version'])->toBeNull();
            expect($data['subscriptions'])->toEqual([]);
            expect($data['message_queue'])->toEqual([]);
            expect($data['log_level'])->toBeNull();
            return true;
        }))->once()->andReturn(true);

    $sessionCreatedEmitted = false;
    $emittedSessionId = null;
    $emittedSessionObj = null;
    $this->sessionManager->on('session_created', function ($id, $session) use (&$sessionCreatedEmitted, &$emittedSessionId, &$emittedSessionObj) {
        $sessionCreatedEmitted = true;
        $emittedSessionId = $id;
        $emittedSessionObj = $session;
    });

    $session = $this->sessionManager->createSession(SESSION_ID_MGR_1);

    expect($session)->toBeInstanceOf(SessionInterface::class);
    expect($session->getId())->toBe(SESSION_ID_MGR_1);
    expect($session->get('initialized'))->toBeFalse();
    $this->logger->shouldHaveReceived('info')->with('Session created', ['sessionId' => SESSION_ID_MGR_1]);
    expect($sessionCreatedEmitted)->toBeTrue();
    expect($emittedSessionId)->toBe(SESSION_ID_MGR_1);
    expect($emittedSessionObj)->toBe($session);
});

it('gets an existing session if handler read returns data', function () {
    $existingData = ['user_id' => 123, 'initialized' => true];
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_1)->once()->andReturn(json_encode($existingData));

    $session = $this->sessionManager->getSession(SESSION_ID_MGR_1);
    expect($session)->toBeInstanceOf(SessionInterface::class);
    expect($session->getId())->toBe(SESSION_ID_MGR_1);
    expect($session->get('user_id'))->toBe(123);
});

it('returns null from getSession if session does not exist (handler read returns false)', function () {
    $this->sessionHandler->shouldReceive('read')->with('non-existent')->once()->andReturn(false);
    $session = $this->sessionManager->getSession('non-existent');
    expect($session)->toBeNull();
});

it('returns null from getSession if session data is empty after load', function () {
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_1)->once()->andReturn(false);
    $session = $this->sessionManager->getSession(SESSION_ID_MGR_1);
    expect($session)->toBeNull();
});


it('deletes a session successfully and emits event', function () {
    $this->sessionHandler->shouldReceive('destroy')->with(SESSION_ID_MGR_1)->once()->andReturn(true);

    $sessionDeletedEmitted = false;
    $emittedSessionId = null;
    $this->sessionManager->on('session_deleted', function ($id) use (&$sessionDeletedEmitted, &$emittedSessionId) {
        $sessionDeletedEmitted = true;
        $emittedSessionId = $id;
    });

    $success = $this->sessionManager->deleteSession(SESSION_ID_MGR_1);

    expect($success)->toBeTrue();
    $this->logger->shouldHaveReceived('info')->with('Session deleted', ['sessionId' => SESSION_ID_MGR_1]);
    expect($sessionDeletedEmitted)->toBeTrue();
    expect($emittedSessionId)->toBe(SESSION_ID_MGR_1);
});

it('logs warning and does not emit event if deleteSession fails', function () {
    $this->sessionHandler->shouldReceive('destroy')->with(SESSION_ID_MGR_1)->once()->andReturn(false);
    $sessionDeletedEmitted = false;
    $this->sessionManager->on('session_deleted', function () use (&$sessionDeletedEmitted) {
        $sessionDeletedEmitted = true;
    });

    $success = $this->sessionManager->deleteSession(SESSION_ID_MGR_1);

    expect($success)->toBeFalse();
    $this->logger->shouldHaveReceived('warning')->with('Failed to delete session', ['sessionId' => SESSION_ID_MGR_1]);
    expect($sessionDeletedEmitted)->toBeFalse();
});

it('queues message for existing session', function () {
    $sessionData = ['message_queue' => []];
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_1)->andReturn(json_encode($sessionData));
    $message = '{"id":1}';

    $this->sessionHandler->shouldReceive('write')->with(SESSION_ID_MGR_1, Mockery::on(function ($dataJson) use ($message) {
        $data = json_decode($dataJson, true);
        expect($data['message_queue'])->toEqual([$message]);
        return true;
    }))->once()->andReturn(true);

    $this->sessionManager->queueMessage(SESSION_ID_MGR_1, $message);
});

it('does nothing on queueMessage if session does not exist', function () {
    $this->sessionHandler->shouldReceive('read')->with('no-such-session')->andReturn(false);
    $this->sessionHandler->shouldNotReceive('write');
    $this->sessionManager->queueMessage('no-such-session', '{"id":1}');
});

it('dequeues messages from existing session', function () {
    $messages = ['{"id":1}', '{"id":2}'];
    $sessionData = ['message_queue' => $messages];
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_1)->andReturn(json_encode($sessionData));
    $this->sessionHandler->shouldReceive('write')->with(SESSION_ID_MGR_1, Mockery::on(function ($dataJson) {
        $data = json_decode($dataJson, true);
        expect($data['message_queue'])->toEqual([]);
        return true;
    }))->once()->andReturn(true);

    $dequeued = $this->sessionManager->dequeueMessages(SESSION_ID_MGR_1);
    expect($dequeued)->toEqual($messages);
});

it('returns empty array from dequeueMessages if session does not exist', function () {
    $this->sessionHandler->shouldReceive('read')->with('no-such-session')->andReturn(false);
    expect($this->sessionManager->dequeueMessages('no-such-session'))->toBe([]);
});

it('checks hasQueuedMessages for existing session', function () {
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_1)->andReturn(json_encode(['message_queue' => ['msg']]));
    expect($this->sessionManager->hasQueuedMessages(SESSION_ID_MGR_1))->toBeTrue();

    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_MGR_2)->andReturn(json_encode(['message_queue' => []]));
    expect($this->sessionManager->hasQueuedMessages(SESSION_ID_MGR_2))->toBeFalse();
});

it('returns false from hasQueuedMessages if session does not exist', function () {
    $this->sessionHandler->shouldReceive('read')->with('no-such-session')->andReturn(false);
    expect($this->sessionManager->hasQueuedMessages('no-such-session'))->toBeFalse();
});

it('can stop GC timer on stopGcTimer ', function () {
    $loop = Mockery::mock(LoopInterface::class);
    $loop->shouldReceive('addPeriodicTimer')->with(Mockery::any(), Mockery::type('callable'))->once()->andReturn(Mockery::mock(TimerInterface::class));
    $loop->shouldReceive('cancelTimer')->with(Mockery::type(TimerInterface::class))->once();

    $manager = new SessionManager($this->sessionHandler, $this->logger, $loop);
    $manager->startGcTimer();
    $manager->stopGcTimer();
});

it('GC timer callback deletes expired sessions', function () {
    $clock = new FixedClock();

    $sessionHandler = new ArraySessionHandler(60, $clock);
    $sessionHandler->write('sess_expired', 'data');

    // $clock->addSeconds(100);

    $manager = new SessionManager(
        $sessionHandler,
        $this->logger,
        ttl: 30,
        gcInterval: 0.01
    );

    $session = $manager->getSession('sess_expired');
    expect($session)->toBeNull();
});


it('does not start GC timer if already started', function () {
    $this->loop = Mockery::mock(LoopInterface::class);
    $this->loop->shouldReceive('addPeriodicTimer')->once()->andReturn(Mockery::mock(TimerInterface::class));

    $manager = new SessionManager($this->sessionHandler, $this->logger, $this->loop);
    $manager->startGcTimer();
});

```

--------------------------------------------------------------------------------
/src/Elements/RegisteredElement.php:
--------------------------------------------------------------------------------

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Elements;

use InvalidArgumentException;
use JsonSerializable;
use PhpMcp\Server\Context;
use PhpMcp\Server\Exception\McpServerException;
use Psr\Container\ContainerInterface;
use ReflectionException;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use Throwable;
use TypeError;

class RegisteredElement implements JsonSerializable
{
    /** @var callable|array|string */
    public readonly mixed $handler;
    public readonly bool $isManual;

    public function __construct(
        callable|array|string $handler,
        bool $isManual = false,
    ) {
        $this->handler = $handler;
        $this->isManual = $isManual;
    }

    public function handle(ContainerInterface $container, array $arguments, Context $context): mixed
    {
        if (is_string($this->handler)) {
            if (class_exists($this->handler) && method_exists($this->handler, '__invoke')) {
                $reflection = new \ReflectionMethod($this->handler, '__invoke');
                $arguments = $this->prepareArguments($reflection, $arguments, $context);
                $instance = $container->get($this->handler);
                return call_user_func($instance, ...$arguments);
            }

            if (function_exists($this->handler)) {
                $reflection = new \ReflectionFunction($this->handler);
                $arguments = $this->prepareArguments($reflection, $arguments, $context);
                return call_user_func($this->handler, ...$arguments);
            }
        }

        if (is_callable($this->handler)) {
            $reflection = $this->getReflectionForCallable($this->handler);
            $arguments = $this->prepareArguments($reflection, $arguments, $context);
            return call_user_func($this->handler, ...$arguments);
        }

        if (is_array($this->handler)) {
            [$className, $methodName] = $this->handler;
            $reflection = new \ReflectionMethod($className, $methodName);
            $arguments = $this->prepareArguments($reflection, $arguments, $context);

            $instance = $container->get($className);
            return call_user_func([$instance, $methodName], ...$arguments);
        }

        throw new \InvalidArgumentException('Invalid handler type');
    }


    protected function prepareArguments(\ReflectionFunctionAbstract $reflection, array $arguments, Context $context): array
    {
        $finalArgs = [];

        foreach ($reflection->getParameters() as $parameter) {
            // TODO: Handle variadic parameters.
            $paramName = $parameter->getName();
            $paramType = $parameter->getType();
            $paramPosition = $parameter->getPosition();

            if ($paramType instanceof ReflectionNamedType && $paramType->getName() === Context::class) {
                $finalArgs[$paramPosition] = $context;

                continue;
            }

            if (isset($arguments[$paramName])) {
                $argument = $arguments[$paramName];
                try {
                    $finalArgs[$paramPosition] = $this->castArgumentType($argument, $parameter);
                } catch (InvalidArgumentException $e) {
                    throw McpServerException::invalidParams($e->getMessage(), $e);
                } catch (Throwable $e) {
                    throw McpServerException::internalError(
                        "Error processing parameter `{$paramName}`: {$e->getMessage()}",
                        $e
                    );
                }
            } elseif ($parameter->isDefaultValueAvailable()) {
                $finalArgs[$paramPosition] = $parameter->getDefaultValue();
            } elseif ($parameter->allowsNull()) {
                $finalArgs[$paramPosition] = null;
            } elseif ($parameter->isOptional()) {
                continue;
            } else {
                $reflectionName = $reflection instanceof \ReflectionMethod
                    ? $reflection->class . '::' . $reflection->name
                    : 'Closure';
                throw McpServerException::internalError(
                    "Missing required argument `{$paramName}` for {$reflectionName}."
                );
            }
        }

        return array_values($finalArgs);
    }

    /**
     * Gets a ReflectionMethod or ReflectionFunction for a callable.
     */
    private function getReflectionForCallable(callable $handler): \ReflectionMethod|\ReflectionFunction
    {
        if (is_string($handler)) {
            return new \ReflectionFunction($handler);
        }

        if ($handler instanceof \Closure) {
            return new \ReflectionFunction($handler);
        }

        if (is_array($handler) && count($handler) === 2) {
            [$class, $method] = $handler;
            return new \ReflectionMethod($class, $method);
        }

        throw new \InvalidArgumentException('Cannot create reflection for this callable type');
    }

    /**
     * Attempts type casting based on ReflectionParameter type hints.
     *
     * @throws InvalidArgumentException If casting is impossible for the required type.
     * @throws TypeError If internal PHP casting fails unexpectedly.
     */
    private function castArgumentType(mixed $argument, ReflectionParameter $parameter): mixed
    {
        $type = $parameter->getType();

        if ($argument === null) {
            if ($type && $type->allowsNull()) {
                return null;
            }
        }

        if (! $type instanceof ReflectionNamedType) {
            return $argument;
        }

        $typeName = $type->getName();

        if (enum_exists($typeName)) {
            if (is_object($argument) && $argument instanceof $typeName) {
                return $argument;
            }

            if (is_subclass_of($typeName, \BackedEnum::class)) {
                $value = $typeName::tryFrom($argument);
                if ($value === null) {
                    throw new InvalidArgumentException(
                        "Invalid value '{$argument}' for backed enum {$typeName}. Expected one of its backing values.",
                    );
                }
                return $value;
            } else {
                if (is_string($argument)) {
                    foreach ($typeName::cases() as $case) {
                        if ($case->name === $argument) {
                            return $case;
                        }
                    }
                    $validNames = array_map(fn($c) => $c->name, $typeName::cases());
                    throw new InvalidArgumentException(
                        "Invalid value '{$argument}' for unit enum {$typeName}. Expected one of: " . implode(', ', $validNames) . "."
                    );
                } else {
                    throw new InvalidArgumentException(
                        "Invalid value type '{$argument}' for unit enum {$typeName}. Expected a string matching a case name."
                    );
                }
            }
        }

        try {
            return match (strtolower($typeName)) {
                'int', 'integer' => $this->castToInt($argument),
                'string' => (string) $argument,
                'bool', 'boolean' => $this->castToBoolean($argument),
                'float', 'double' => $this->castToFloat($argument),
                'array' => $this->castToArray($argument),
                default => $argument,
            };
        } catch (TypeError $e) {
            throw new InvalidArgumentException(
                "Value cannot be cast to required type `{$typeName}`.",
                0,
                $e
            );
        }
    }

    /** Helper to cast strictly to boolean */
    private function castToBoolean(mixed $argument): bool
    {
        if (is_bool($argument)) {
            return $argument;
        }
        if ($argument === 1 || $argument === '1' || strtolower((string) $argument) === 'true') {
            return true;
        }
        if ($argument === 0 || $argument === '0' || strtolower((string) $argument) === 'false') {
            return false;
        }
        throw new InvalidArgumentException('Cannot cast value to boolean. Use true/false/1/0.');
    }

    /** Helper to cast strictly to integer */
    private function castToInt(mixed $argument): int
    {
        if (is_int($argument)) {
            return $argument;
        }
        if (is_numeric($argument) && floor((float) $argument) == $argument && ! is_string($argument)) {
            return (int) $argument;
        }
        if (is_string($argument) && ctype_digit(ltrim($argument, '-'))) {
            return (int) $argument;
        }
        throw new InvalidArgumentException('Cannot cast value to integer. Expected integer representation.');
    }

    /** Helper to cast strictly to float */
    private function castToFloat(mixed $argument): float
    {
        if (is_float($argument)) {
            return $argument;
        }
        if (is_int($argument)) {
            return (float) $argument;
        }
        if (is_numeric($argument)) {
            return (float) $argument;
        }
        throw new InvalidArgumentException('Cannot cast value to float. Expected numeric representation.');
    }

    /** Helper to cast strictly to array */
    private function castToArray(mixed $argument): array
    {
        if (is_array($argument)) {
            return $argument;
        }
        throw new InvalidArgumentException('Cannot cast value to array. Expected array.');
    }

    public function toArray(): array
    {
        return [
            'handler' => $this->handler,
            'isManual' => $this->isManual,
        ];
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }
}

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Mocks\Clients;

use Psr\Http\Message\ResponseInterface;
use React\EventLoop\Loop;
use React\Http\Browser;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;

use function React\Promise\reject;

class MockSseClient
{
    public Browser $browser;
    private ?ReadableStreamInterface $stream = null;
    private string $buffer = '';
    private array $receivedMessages = []; // Stores decoded JSON-RPC messages
    private array $receivedSseEvents = []; // Stores raw SSE events (type, data, id)
    public ?string $endpointUrl = null; // The /message endpoint URL provided by server
    public ?string $clientId = null; // The clientId from the /message endpoint URL
    public ?ResponseInterface $lastConnectResponse = null; // Last connect response for header testing

    public function __construct(int $timeout = 2)
    {
        $this->browser = (new Browser())->withTimeout($timeout);
    }

    public function connect(string $sseBaseUrl): PromiseInterface
    {
        return $this->browser->requestStreaming('GET', $sseBaseUrl)
            ->then(function (ResponseInterface $response) {
                $this->lastConnectResponse = $response; // Store response for header testing
                if ($response->getStatusCode() !== 200) {
                    $body = (string) $response->getBody();
                    throw new \RuntimeException("SSE connection failed with status {$response->getStatusCode()}: {$body}");
                }
                $stream = $response->getBody();
                assert($stream instanceof ReadableStreamInterface, "SSE response body is not a readable stream");
                $this->stream = $stream;
                $this->stream->on('data', [$this, 'handleSseData']);
                $this->stream->on('close', function () {
                    $this->stream = null;
                });
                return $this;
            });
    }

    public function handleSseData(string $chunk): void
    {
        $this->buffer .= $chunk;

        while (($eventPos = strpos($this->buffer, "\n\n")) !== false) {
            $eventBlock = substr($this->buffer, 0, $eventPos);
            $this->buffer = substr($this->buffer, $eventPos + 2);

            $lines = explode("\n", $eventBlock);
            $event = ['type' => 'message', 'data' => '', 'id' => null];

            foreach ($lines as $line) {
                if (str_starts_with($line, "event:")) {
                    $event['type'] = trim(substr($line, strlen("event:")));
                } elseif (str_starts_with($line, "data:")) {
                    $event['data'] .= (empty($event['data']) ? "" : "\n") . trim(substr($line, strlen("data:")));
                } elseif (str_starts_with($line, "id:")) {
                    $event['id'] = trim(substr($line, strlen("id:")));
                }
            }
            $this->receivedSseEvents[] = $event;

            if ($event['type'] === 'endpoint' && $event['data']) {
                $this->endpointUrl = $event['data'];
                $query = parse_url($this->endpointUrl, PHP_URL_QUERY);
                if ($query) {
                    parse_str($query, $params);
                    $this->clientId = $params['clientId'] ?? null;
                }
            } elseif ($event['type'] === 'message' && $event['data']) {
                try {
                    $decodedJson = json_decode($event['data'], true, 512, JSON_THROW_ON_ERROR);
                    $this->receivedMessages[] = $decodedJson;
                } catch (\JsonException $e) {
                }
            }
        }
    }

    public function getNextMessageResponse(string $expectedRequestId, int $timeoutSecs = 2): PromiseInterface
    {
        $deferred = new Deferred();
        $startTime = microtime(true);

        $checkMessages = null;
        $checkMessages = function () use (&$checkMessages, $deferred, $expectedRequestId, $startTime, $timeoutSecs) {
            foreach ($this->receivedMessages as $i => $msg) {
                if (isset($msg['id']) && $msg['id'] === $expectedRequestId) {
                    unset($this->receivedMessages[$i]); // Consume message
                    $this->receivedMessages = array_values($this->receivedMessages);
                    $deferred->resolve($msg);
                    return;
                }
            }

            if (microtime(true) - $startTime > $timeoutSecs) {
                $deferred->reject(new \RuntimeException("Timeout waiting for SSE message with ID '{$expectedRequestId}'"));
                return;
            }

            if ($this->stream) {
                Loop::addTimer(0.05, $checkMessages);
            } else {
                $deferred->reject(new \RuntimeException("SSE Stream closed while waiting for message ID '{$expectedRequestId}'"));
            }
        };

        $checkMessages(); // Start checking
        return $deferred->promise();
    }

    public function getNextBatchMessageResponse(int $expectedItemCount, int $timeoutSecs = 2): PromiseInterface
    {
        $deferred = new Deferred();
        $startTime = microtime(true);

        $checkMessages = null;
        $checkMessages = function () use (&$checkMessages, $deferred, $expectedItemCount, $startTime, $timeoutSecs) {
            foreach ($this->receivedMessages as $i => $msg) {
                if (is_array($msg) && !isset($msg['jsonrpc']) && count($msg) === $expectedItemCount) {
                    $isLikelyBatchResponse = true;
                    if (empty($msg) && $expectedItemCount === 0) {
                    } elseif (empty($msg) && $expectedItemCount > 0) {
                        $isLikelyBatchResponse = false;
                    } else {
                        foreach ($msg as $item) {
                            if (!is_array($item) || (!isset($item['id']) && !isset($item['method']))) {
                                $isLikelyBatchResponse = false;
                                break;
                            }
                        }
                    }

                    if ($isLikelyBatchResponse) {
                        unset($this->receivedMessages[$i]);
                        $this->receivedMessages = array_values($this->receivedMessages);
                        $deferred->resolve($msg);
                        return;
                    }
                }
            }

            if (microtime(true) - $startTime > $timeoutSecs) {
                $deferred->reject(new \RuntimeException("Timeout waiting for SSE Batch Response with {$expectedItemCount} items."));
                return;
            }

            if ($this->stream) {
                Loop::addTimer(0.05, $checkMessages);
            } else {
                $deferred->reject(new \RuntimeException("SSE Stream closed while waiting for Batch Response."));
            }
        };

        $checkMessages();
        return $deferred->promise();
    }

    public function sendHttpRequest(string $requestId, string $method, array $params = []): PromiseInterface
    {
        if (!$this->endpointUrl || !$this->clientId) {
            return reject(new \LogicException("SSE Client not fully initialized (endpoint or clientId missing)."));
        }
        $payload = [
            'jsonrpc' => '2.0',
            'id' => $requestId,
            'method' => $method,
            'params' => $params,
        ];
        $body = json_encode($payload);

        return $this->browser->post($this->endpointUrl, ['Content-Type' => 'application/json'], $body)
            ->then(function (ResponseInterface $response) use ($requestId) {
                $bodyContent = (string) $response->getBody();
                if ($response->getStatusCode() !== 202) {
                    throw new \RuntimeException("HTTP POST request failed with status {$response->getStatusCode()}: {$bodyContent}");
                }
                return $response;
            });
    }

    public function sendHttpBatchRequest(array $batchRequestObjects): PromiseInterface
    {
        if (!$this->endpointUrl || !$this->clientId) {
            return reject(new \LogicException("SSE Client not fully initialized (endpoint or clientId missing)."));
        }
        $body = json_encode($batchRequestObjects);

        return $this->browser->post($this->endpointUrl, ['Content-Type' => 'application/json'], $body)
            ->then(function (ResponseInterface $response) {
                $bodyContent = (string) $response->getBody();
                if ($response->getStatusCode() !== 202) {
                    throw new \RuntimeException("HTTP BATCH POST request failed with status {$response->getStatusCode()}: {$bodyContent}");
                }
                return $response;
            });
    }

    public function sendHttpNotification(string $method, array $params = []): PromiseInterface
    {
        if (!$this->endpointUrl || !$this->clientId) {
            return reject(new \LogicException("SSE Client not fully initialized (endpoint or clientId missing)."));
        }
        $payload = [
            'jsonrpc' => '2.0',
            'method' => $method,
            'params' => $params,
        ];
        $body = json_encode($payload);
        return $this->browser->post($this->endpointUrl, ['Content-Type' => 'application/json'], $body)
            ->then(function (ResponseInterface $response) {
                $bodyContent = (string) $response->getBody();
                if ($response->getStatusCode() !== 202) {
                    throw new \RuntimeException("HTTP POST notification failed with status {$response->getStatusCode()}: {$bodyContent}");
                }
                return null;
            });
    }

    public function close(): void
    {
        if ($this->stream) {
            $this->stream->close();
            $this->stream = null;
        }
    }
}

```

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

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Session;

use Mockery;
use PhpMcp\Server\Contracts\SessionHandlerInterface;
use PhpMcp\Server\Contracts\SessionInterface;
use PhpMcp\Server\Session\Session;

const SESSION_ID_SESS = 'test-session-obj-id';

beforeEach(function () {
    $this->sessionHandler = Mockery::mock(SessionHandlerInterface::class);
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_SESS)->once()->andReturn(false)->byDefault();
});

it('implements SessionInterface', function () {
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session)->toBeInstanceOf(SessionInterface::class);
});

// --- Constructor and ID Generation ---
it('uses provided ID if given', function () {
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_SESS)->once()->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->getId())->toBe(SESSION_ID_SESS);
});

it('generates an ID if none is provided', function () {
    $this->sessionHandler->shouldReceive('read')->with(Mockery::type('string'))->once()->andReturn(false);
    $session = new Session($this->sessionHandler);
    expect($session->getId())->toBeString()->toHaveLength(32);
});

it('loads data from handler on construction if session exists', function () {
    $initialData = ['foo' => 'bar', 'count' => 5, 'nested' => ['value' => true]];
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_SESS)->once()->andReturn(json_encode($initialData));

    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->all())->toEqual($initialData);
    expect($session->get('foo'))->toBe('bar');
});

it('initializes with empty data if handler read returns false', function () {
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_SESS)->once()->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->all())->toBeEmpty();
});

it('initializes with empty data if handler read returns invalid JSON', function () {
    $this->sessionHandler->shouldReceive('read')->with(SESSION_ID_SESS)->once()->andReturn('this is not json');
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->all())->toBeEmpty();
});

it('saves current data to handler', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->set('name', 'Alice');
    $session->set('level', 10);

    $expectedSavedData = json_encode(['name' => 'Alice', 'level' => 10]);
    $this->sessionHandler->shouldReceive('write')->with(SESSION_ID_SESS, $expectedSavedData)->once()->andReturn(true);

    $session->save();
});

it('sets and gets a top-level attribute', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->set('name', 'Bob');
    expect($session->get('name'))->toBe('Bob');
    expect($session->has('name'))->toBeTrue();
});

it('gets default value if attribute does not exist', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->get('nonexistent', 'default_val'))->toBe('default_val');
    expect($session->has('nonexistent'))->toBeFalse();
});

it('sets and gets nested attributes using dot notation', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->set('user.profile.email', '[email protected]');
    $session->set('user.profile.active', true);
    $session->set('user.roles', ['admin', 'editor']);

    expect($session->get('user.profile'))->toEqual(['email' => '[email protected]', 'active' => true]);
    expect($session->get('user.roles'))->toEqual(['admin', 'editor']);
    expect($session->has('user.profile.email'))->toBeTrue();
    expect($session->has('user.other_profile.settings'))->toBeFalse();
});

it('set does not overwrite if overwrite is false and key exists', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->set('counter', 10);
    $session->set('counter', 20, false);
    expect($session->get('counter'))->toBe(10);

    $session->set('user.id', 1);
    $session->set('user.id', 2, false);
    expect($session->get('user.id'))->toBe(1);
});

it('set overwrites if overwrite is true (default)', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->set('counter', 10);
    $session->set('counter', 20);
    expect($session->get('counter'))->toBe(20);
});


it('forgets a top-level attribute', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode(['name' => 'Alice', 'age' => 30]));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->forget('age');
    expect($session->has('age'))->toBeFalse();
    expect($session->has('name'))->toBeTrue();
    expect($session->all())->toEqual(['name' => 'Alice']);
});

it('forgets a nested attribute using dot notation', function () {
    $initialData = ['user' => ['profile' => ['email' => '[email protected]', 'status' => 'active'], 'id' => 1]];
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode($initialData));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);

    $session->forget('user.profile.status');
    expect($session->has('user.profile.status'))->toBeFalse();
    expect($session->has('user.profile.email'))->toBeTrue();
    expect($session->get('user.profile'))->toEqual(['email' => '[email protected]']);

    $session->forget('user.profile');
    expect($session->has('user.profile'))->toBeFalse();
    expect($session->get('user'))->toEqual(['id' => 1]);
});

it('forget does nothing if key does not exist', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode(['name' => 'Test']));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->forget('nonexistent');
    $session->forget('another_nonexistent');
    expect($session->all())->toEqual(['name' => 'Test']);
});

it('pulls an attribute (gets and forgets)', function () {
    $initialData = ['item' => 'important', 'user' => ['token' => 'abc123xyz']];
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode($initialData));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);

    $pulledItem = $session->pull('item', 'default');
    expect($pulledItem)->toBe('important');
    expect($session->has('item'))->toBeFalse();

    $pulledToken = $session->pull('user.token');
    expect($pulledToken)->toBe('abc123xyz');
    expect($session->has('user.token'))->toBeFalse();
    expect($session->has('user'))->toBeTrue();

    $pulledNonExistent = $session->pull('nonexistent', 'fallback');
    expect($pulledNonExistent)->toBe('fallback');
});

it('clears all session data', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode(['a' => 1, 'b' => 2]));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $session->clear();
    expect($session->all())->toBeEmpty();
});

it('returns all data with all()', function () {
    $data = ['a' => 1, 'b' => ['c' => 3]];
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode($data));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->all())->toEqual($data);
});

it('hydrates session data, merging with defaults and removing id', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $newAttributes = [
        'client_info' => ['name' => 'TestClient', 'version' => '1.1'],
        'protocol_version' => '2024-custom',
        'user_custom_key' => 'my_value',
        'id' => 'should_be_ignored_on_hydrate'
    ];
    $session->hydrate($newAttributes);

    $allData = $session->all();
    expect($allData['initialized'])->toBeFalse();
    expect($allData['client_info'])->toEqual(['name' => 'TestClient', 'version' => '1.1']);
    expect($allData['protocol_version'])->toBe('2024-custom');
    expect($allData['message_queue'])->toEqual([]);
    expect($allData['log_level'])->toBeNull();
    expect($allData['user_custom_key'])->toBe('my_value');
    expect($allData)->not->toHaveKey('id');
});

it('queues messages correctly', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect($session->hasQueuedMessages())->toBeFalse();

    $msg1 = '{"jsonrpc":"2.0","method":"n1"}';
    $msg2 = '{"jsonrpc":"2.0","method":"n2"}';
    $session->queueMessage($msg1);
    $session->queueMessage($msg2);

    expect($session->hasQueuedMessages())->toBeTrue();
    expect($session->get('message_queue'))->toEqual([$msg1, $msg2]);
});

it('dequeues messages and clears queue', function () {
    $this->sessionHandler->shouldReceive('read')->andReturn(false);
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    $msg1 = '{"id":1}';
    $msg2 = '{"id":2}';
    $session->queueMessage($msg1);
    $session->queueMessage($msg2);

    $dequeued = $session->dequeueMessages();
    expect($dequeued)->toEqual([$msg1, $msg2]);
    expect($session->hasQueuedMessages())->toBeFalse();
    expect($session->get('message_queue', 'not_found'))->toEqual([]);

    expect($session->dequeueMessages())->toEqual([]);
});

it('jsonSerializes to all session data', function () {
    $data = ['serialize' => 'me', 'nested' => ['ok' => true]];
    $this->sessionHandler->shouldReceive('read')->andReturn(json_encode($data));
    $session = new Session($this->sessionHandler, SESSION_ID_SESS);
    expect(json_encode($session))->toBe(json_encode($data));
});

```

--------------------------------------------------------------------------------
/tests/Unit/Elements/RegisteredResourceTemplateTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit\Elements;

use Mockery;
use PhpMcp\Schema\ResourceTemplate;
use PhpMcp\Server\Context;
use PhpMcp\Server\Contracts\SessionInterface;
use PhpMcp\Server\Elements\RegisteredResourceTemplate;
use PhpMcp\Schema\Content\TextResourceContents;
use PhpMcp\Server\Tests\Fixtures\General\ResourceHandlerFixture;
use PhpMcp\Server\Tests\Fixtures\General\CompletionProviderFixture;
use Psr\Container\ContainerInterface;
use PhpMcp\Schema\Annotations;

beforeEach(function () {
    $this->container = Mockery::mock(ContainerInterface::class);
    $this->handlerInstance = new ResourceHandlerFixture();
    $this->container->shouldReceive('get')
        ->with(ResourceHandlerFixture::class)
        ->andReturn($this->handlerInstance)
        ->byDefault();

    $this->templateUri = 'item://{category}/{itemId}/details';
    $this->resourceTemplateSchema = ResourceTemplate::make(
        $this->templateUri,
        'item-details-template',
        mimeType: 'application/json'
    );

    $this->defaultHandlerMethod = 'getUserDocument';
    $this->matchingTemplateSchema = ResourceTemplate::make(
        'user://{userId}/doc/{documentId}',
        'user-doc-template',
        mimeType: 'application/json'
    );

    $this->context = new Context(Mockery::mock(SessionInterface::class));
});

it('constructs correctly with schema, handler, and completion providers', function () {
    $completionProviders = [
        'userId' => CompletionProviderFixture::class,
        'documentId' => 'Another\ProviderClass'
    ];

    $schema = ResourceTemplate::make(
        'user://{userId}/doc/{documentId}',
        'user-doc-template',
        mimeType: 'application/json'
    );

    $template = RegisteredResourceTemplate::make(
        schema: $schema,
        handler: [ResourceHandlerFixture::class, 'getUserDocument'],
        completionProviders: $completionProviders
    );

    expect($template->schema)->toBe($schema);
    expect($template->handler)->toBe([ResourceHandlerFixture::class, 'getUserDocument']);
    expect($template->isManual)->toBeFalse();
    expect($template->completionProviders)->toEqual($completionProviders);
    expect($template->completionProviders['userId'])->toBe(CompletionProviderFixture::class);
    expect($template->completionProviders['documentId'])->toBe('Another\ProviderClass');
    expect($template->completionProviders)->not->toHaveKey('nonExistentVar');
});

it('can be made as a manual registration', function () {
    $schema = ResourceTemplate::make(
        'user://{userId}/doc/{documentId}',
        'user-doc-template',
        mimeType: 'application/json'
    );

    $manualTemplate = RegisteredResourceTemplate::make(
        schema: $schema,
        handler: [ResourceHandlerFixture::class, 'getUserDocument'],
        isManual: true
    );

    expect($manualTemplate->isManual)->toBeTrue();
});

dataset('uri_template_matching_cases', [
    'simple_var'        => ['user://{userId}', 'user://12345', ['userId' => '12345']],
    'simple_var_alpha'  => ['user://{userId}', 'user://abc-def', ['userId' => 'abc-def']],
    'no_match_missing_var_part' => ['user://{userId}', 'user://', null],
    'no_match_prefix'   => ['user://{userId}', 'users://12345', null],
    'multi_var'         => ['item://{category}/{itemId}/details', 'item://books/978-abc/details', ['category' => 'books', 'itemId' => '978-abc']],
    'multi_var_empty_segment_fail' => ['item://{category}/{itemId}/details', 'item://books//details', null], // [^/]+ fails on empty segment
    'multi_var_wrong_literal_end' => ['item://{category}/{itemId}/details', 'item://books/978-abc/summary', null],
    'multi_var_no_suffix_literal' => ['item://{category}/{itemId}', 'item://tools/hammer', ['category' => 'tools', 'itemId' => 'hammer']],
    'multi_var_extra_segment_fail' => ['item://{category}/{itemId}', 'item://tools/hammer/extra', null],
    'mixed_literals_vars' => ['user://{userId}/profile/pic_{picId}.jpg', 'user://kp/profile/pic_main.jpg', ['userId' => 'kp', 'picId' => 'main']],
    'mixed_wrong_extension' => ['user://{userId}/profile/pic_{picId}.jpg', 'user://kp/profile/pic_main.png', null],
    'mixed_wrong_literal_prefix' => ['user://{userId}/profile/img_{picId}.jpg', 'user://kp/profile/pic_main.jpg', null],
    'escapable_chars_in_literal' => ['search://{query}/results.json?page={pageNo}', 'search://term.with.dots/results.json?page=2', ['query' => 'term.with.dots', 'pageNo' => '2']],
]);

it('matches URIs against template and extracts variables correctly', function (string $templateString, string $uriToTest, ?array $expectedVariables) {
    $schema = ResourceTemplate::make($templateString, 'test-match');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'getUserDocument']);

    if ($expectedVariables !== null) {
        expect($template->matches($uriToTest))->toBeTrue();
        $reflection = new \ReflectionClass($template);
        $prop = $reflection->getProperty('uriVariables');
        $prop->setAccessible(true);
        expect($prop->getValue($template))->toEqual($expectedVariables);
    } else {
        expect($template->matches($uriToTest))->toBeFalse();
    }
})->with('uri_template_matching_cases');

it('gets variable names from compiled template', function () {
    $schema = ResourceTemplate::make('foo://{varA}/bar/{varB_ext}.{format}', 'vars-test');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'getUserDocument']);
    expect($template->getVariableNames())->toEqualCanonicalizing(['varA', 'varB_ext', 'format']);
});

it('reads resource using handler with extracted URI variables', function () {
    $uriTemplate = 'item://{category}/{itemId}?format={format}';
    $uri = 'item://electronics/tv-123?format=json_pretty';
    $schema = ResourceTemplate::make($uriTemplate, 'item-details-template');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'getTemplatedContent']);

    expect($template->matches($uri))->toBeTrue();

    $resultContents = $template->read($this->container, $uri, $this->context);

    expect($resultContents)->toBeArray()->toHaveCount(1);

    $content = $resultContents[0];
    expect($content)->toBeInstanceOf(TextResourceContents::class);
    expect($content->uri)->toBe($uri);
    expect($content->mimeType)->toBe('application/json');

    $decodedText = json_decode($content->text, true);
    expect($decodedText['message'])->toBe("Content for item tv-123 in category electronics, format json_pretty.");
    expect($decodedText['category_received'])->toBe('electronics');
    expect($decodedText['itemId_received'])->toBe('tv-123');
    expect($decodedText['format_received'])->toBe('json_pretty');
});

it('uses mimeType from schema if handler result does not specify one', function () {
    $uriTemplate = 'item://{category}/{itemId}?format={format}';
    $uri = 'item://books/bestseller?format=json_pretty';
    $schema = ResourceTemplate::make($uriTemplate, 'test-mime', mimeType: 'application/vnd.custom-template-xml');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'getTemplatedContent']);
    expect($template->matches($uri))->toBeTrue();

    $resultContents = $template->read($this->container, $uri, $this->context);
    expect($resultContents[0]->mimeType)->toBe('application/vnd.custom-template-xml');
});

it('formats a simple string result from handler correctly for template', function () {
    $uri = 'item://tools/hammer';
    $schema = ResourceTemplate::make('item://{type}/{name}', 'test-simple-string', mimeType: 'text/x-custom');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'returnStringText']);
    expect($template->matches($uri))->toBeTrue();

    $mockHandler = Mockery::mock(ResourceHandlerFixture::class);
    $mockHandler->shouldReceive('returnStringText')->with($uri)->once()->andReturn('Simple content from template handler');
    $this->container->shouldReceive('get')->with(ResourceHandlerFixture::class)->andReturn($mockHandler);

    $resultContents = $template->read($this->container, $uri, $this->context);
    expect($resultContents[0])->toBeInstanceOf(TextResourceContents::class)
        ->and($resultContents[0]->text)->toBe('Simple content from template handler')
        ->and($resultContents[0]->mimeType)->toBe('text/x-custom'); // From schema
});

it('propagates exceptions from handler during read', function () {
    $uri = 'item://tools/hammer';
    $schema = ResourceTemplate::make('item://{type}/{name}', 'test-simple-string', mimeType: 'text/x-custom');
    $template = RegisteredResourceTemplate::make($schema, [ResourceHandlerFixture::class, 'handlerThrowsException']);
    expect($template->matches($uri))->toBeTrue();
    $template->read($this->container, $uri, $this->context);
})->throws(\DomainException::class, "Cannot read resource");

it('can be serialized to array and deserialized', function () {
    $schema = ResourceTemplate::make(
        'obj://{type}/{id}',
        'my-template',
        mimeType: 'application/template+json',
        annotations: Annotations::make(priority: 0.7)
    );

    $providers = ['type' => CompletionProviderFixture::class];
    $serializedProviders = ['type' => serialize(CompletionProviderFixture::class)];

    $original = RegisteredResourceTemplate::make(
        $schema,
        [ResourceHandlerFixture::class, 'getUserDocument'],
        true,
        $providers
    );

    $array = $original->toArray();

    expect($array['schema']['uriTemplate'])->toBe('obj://{type}/{id}');
    expect($array['schema']['name'])->toBe('my-template');
    expect($array['schema']['mimeType'])->toBe('application/template+json');
    expect($array['schema']['annotations']['priority'])->toBe(0.7);
    expect($array['handler'])->toBe([ResourceHandlerFixture::class, 'getUserDocument']);
    expect($array['isManual'])->toBeTrue();
    expect($array['completionProviders'])->toEqual($serializedProviders);

    $rehydrated = RegisteredResourceTemplate::fromArray($array);
    expect($rehydrated)->toBeInstanceOf(RegisteredResourceTemplate::class);
    expect($rehydrated->schema->uriTemplate)->toEqual($original->schema->uriTemplate);
    expect($rehydrated->schema->name)->toEqual($original->schema->name);
    expect($rehydrated->isManual)->toBeTrue();
    expect($rehydrated->completionProviders)->toEqual($providers);
});

it('fromArray returns false on failure', function () {
    $badData = ['schema' => ['uriTemplate' => 'fail']];
    expect(RegisteredResourceTemplate::fromArray($badData))->toBeFalse();
});

```

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

```php
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Tests\Mocks\Clients;

use Psr\Http\Message\ResponseInterface;
use React\EventLoop\Loop;
use React\Http\Browser;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;

use function React\Promise\reject;

class MockStreamHttpClient
{
    public Browser $browser;
    public string $baseMcpUrl;
    public ?string $sessionId = null;

    private ?ReadableStreamInterface $mainSseGetStream = null;
    private string $mainSseGetBuffer = '';
    private array $mainSseReceivedNotifications = [];

    public function __construct(string $host, int $port, string $mcpPath, int $timeout = 2)
    {
        $this->browser = (new Browser())->withTimeout($timeout);
        $this->baseMcpUrl = "http://{$host}:{$port}/{$mcpPath}";
    }

    public function connectMainSseStream(): PromiseInterface
    {
        if (!$this->sessionId) {
            return reject(new \LogicException("Cannot connect main SSE stream without a session ID. Initialize first."));
        }

        return $this->browser->requestStreaming('GET', $this->baseMcpUrl, [
            'Accept' => 'text/event-stream',
            'Mcp-Session-Id' => $this->sessionId
        ])
            ->then(function (ResponseInterface $response) {
                if ($response->getStatusCode() !== 200) {
                    $body = (string) $response->getBody();
                    throw new \RuntimeException("Main SSE GET connection failed with status {$response->getStatusCode()}: {$body}");
                }
                $stream = $response->getBody();
                assert($stream instanceof ReadableStreamInterface);
                $this->mainSseGetStream = $stream;

                $this->mainSseGetStream->on('data', function ($chunk) {
                    $this->mainSseGetBuffer .= $chunk;
                    $this->processBufferForNotifications($this->mainSseGetBuffer, $this->mainSseReceivedNotifications);
                });
                return $this;
            });
    }

    private function processBufferForNotifications(string &$buffer, array &$targetArray): void
    {
        while (($eventPos = strpos($buffer, "\n\n")) !== false) {
            $eventBlock = substr($buffer, 0, $eventPos);
            $buffer = substr($buffer, $eventPos + 2);
            $lines = explode("\n", $eventBlock);
            $eventData = '';
            foreach ($lines as $line) {
                if (str_starts_with($line, "data:")) {
                    $eventData .= (empty($eventData) ? "" : "\n") . trim(substr($line, strlen("data:")));
                }
            }
            if (!empty($eventData)) {
                try {
                    $decodedJson = json_decode($eventData, true, 512, JSON_THROW_ON_ERROR);
                    if (isset($decodedJson['method']) && str_starts_with($decodedJson['method'], 'notifications/')) {
                        $targetArray[] = $decodedJson;
                    }
                } catch (\JsonException $e) { /* ignore non-json data lines or log */
                }
            }
        }
    }


    public function sendInitializeRequest(array $params, string $id = 'init-stream-1'): PromiseInterface
    {
        $payload = ['jsonrpc' => '2.0', 'method' => 'initialize', 'params' => $params, 'id' => $id];
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'text/event-stream'];
        $body = json_encode($payload);

        return $this->browser->requestStreaming('POST', $this->baseMcpUrl, $headers, $body)
            ->then(function (ResponseInterface $response) use ($id) {
                $statusCode = $response->getStatusCode();

                if ($statusCode !== 200 || !str_contains($response->getHeaderLine('Content-Type'), 'text/event-stream')) {
                    throw new \RuntimeException("Initialize POST failed or did not return SSE stream. Status: {$statusCode}");
                }

                $this->sessionId = $response->getHeaderLine('Mcp-Session-Id');

                $stream = $response->getBody();
                assert($stream instanceof ReadableStreamInterface);
                return $this->collectSingleSseResponse($stream, $id, "Initialize");
            });
    }

    public function sendRequest(string $method, array $params, string $id, array $additionalHeaders = []): PromiseInterface
    {
        $payload = ['jsonrpc' => '2.0', 'method' => $method, 'params' => $params, 'id' => $id];
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'text/event-stream'];
        if ($this->sessionId) {
            $headers['Mcp-Session-Id'] = $this->sessionId;
        }
        $headers += $additionalHeaders;

        $body = json_encode($payload);

        return $this->browser->requestStreaming('POST', $this->baseMcpUrl, $headers, $body)
            ->then(function (ResponseInterface $response) use ($id, $method) {
                $statusCode = $response->getStatusCode();

                if ($statusCode !== 200 || !str_contains($response->getHeaderLine('Content-Type'), 'text/event-stream')) {
                    $bodyContent = (string) $response->getBody();
                    throw new \RuntimeException("Request '{$method}' (ID: {$id}) POST failed or did not return SSE stream. Status: {$statusCode}, Body: {$bodyContent}");
                }

                $stream = $response->getBody();
                assert($stream instanceof ReadableStreamInterface);
                return $this->collectSingleSseResponse($stream, $id, $method);
            });
    }

    public function sendBatchRequest(array $batchPayload): PromiseInterface
    {
        if (!$this->sessionId) {
            return reject(new \LogicException("Session ID not set. Initialize first for batch request."));
        }

        $headers = ['Content-Type' => 'application/json', 'Accept' => 'text/event-stream', 'Mcp-Session-Id' => $this->sessionId];
        $body = json_encode($batchPayload);

        return $this->browser->requestStreaming('POST', $this->baseMcpUrl, $headers, $body)
            ->then(function (ResponseInterface $response) {
                $statusCode = $response->getStatusCode();

                if ($statusCode !== 200 || !str_contains($response->getHeaderLine('Content-Type'), 'text/event-stream')) {
                    throw new \RuntimeException("Batch POST failed or did not return SSE stream. Status: {$statusCode}");
                }

                $stream = $response->getBody();
                assert($stream instanceof ReadableStreamInterface);
                return $this->collectSingleSseResponse($stream, null, "Batch", true);
            });
    }

    private function collectSingleSseResponse(ReadableStreamInterface $stream, ?string $expectedRequestId, string $contextHint, bool $expectBatchArray = false): PromiseInterface
    {
        $deferred = new Deferred();
        $buffer = '';
        $streamClosed = false;

        $dataListener = function ($chunk) use (&$buffer, $deferred, $expectedRequestId, $expectBatchArray, $contextHint, &$streamClosed, &$dataListener, $stream) {
            if ($streamClosed) return;
            $buffer .= $chunk;

            if (str_contains($buffer, "event: message\n")) {
                if (preg_match('/data: (.*)\n\n/s', $buffer, $matches)) {
                    $jsonData = trim($matches[1]);

                    try {
                        $decoded = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR);
                        $isValid = false;
                        if ($expectBatchArray) {
                            $isValid = is_array($decoded) && !isset($decoded['jsonrpc']);
                        } else {
                            $isValid = isset($decoded['id']) && $decoded['id'] === $expectedRequestId;
                        }

                        if ($isValid) {
                            $deferred->resolve($decoded);
                            $stream->removeListener('data', $dataListener);
                            $stream->close();
                            return;
                        }
                    } catch (\JsonException $e) {
                        $deferred->reject(new \RuntimeException("SSE JSON decode failed for {$contextHint}: {$jsonData}", 0, $e));
                        $stream->removeListener('data', $dataListener);
                        $stream->close();
                        return;
                    }
                }
            }
        };

        $stream->on('data', $dataListener);
        $stream->on('close', function () use ($deferred, $contextHint, &$streamClosed) {
            $streamClosed = true;
            $deferred->reject(new \RuntimeException("SSE stream for {$contextHint} closed before expected response was received."));
        });
        $stream->on('error', function ($err) use ($deferred, $contextHint, &$streamClosed) {
            $streamClosed = true;
            $deferred->reject(new \RuntimeException("SSE stream error for {$contextHint}.", 0, $err instanceof \Throwable ? $err : null));
        });

        return timeout($deferred->promise(), 2, Loop::get())
            ->finally(function () use ($stream, $dataListener) {
                if ($stream->isReadable()) {
                    $stream->removeListener('data', $dataListener);
                }
            });
    }

    public function sendHttpNotification(string $method, array $params = []): PromiseInterface
    {
        if (!$this->sessionId) {
            return reject(new \LogicException("Session ID not set for notification. Initialize first."));
        }
        $payload = ['jsonrpc' => '2.0', 'method' => $method, 'params' => $params];
        $headers = ['Content-Type' => 'application/json', 'Accept' => 'application/json', 'Mcp-Session-Id' => $this->sessionId];
        $body = json_encode($payload);

        return $this->browser->post($this->baseMcpUrl, $headers, $body)
            ->then(function (ResponseInterface $response) {
                $statusCode = $response->getStatusCode();

                if ($statusCode !== 202) {
                    throw new \RuntimeException("POST Notification failed with status {$statusCode}: " . (string)$response->getBody());
                }

                return ['statusCode' => $statusCode, 'body' => null];
            });
    }

    public function sendDeleteRequest(): PromiseInterface
    {
        if (!$this->sessionId) {
            return reject(new \LogicException("Session ID not set for DELETE request. Initialize first."));
        }

        $headers = ['Mcp-Session-Id' => $this->sessionId];

        return $this->browser->request('DELETE', $this->baseMcpUrl, $headers)
            ->then(function (ResponseInterface $response) {
                $statusCode = $response->getStatusCode();
                return ['statusCode' => $statusCode, 'body' => (string)$response->getBody()];
            });
    }

    public function closeMainSseStream(): void
    {
        if ($this->mainSseGetStream) {
            $this->mainSseGetStream->close();
            $this->mainSseGetStream = null;
        }
    }
}

```

--------------------------------------------------------------------------------
/tests/Unit/ServerTest.php:
--------------------------------------------------------------------------------

```php
<?php

namespace PhpMcp\Server\Tests\Unit;

use LogicException;
use Mockery;
use Mockery\MockInterface;
use PhpMcp\Server\Configuration;
use PhpMcp\Server\Contracts\LoggerAwareInterface;
use PhpMcp\Server\Contracts\LoopAwareInterface;
use PhpMcp\Server\Contracts\ServerTransportInterface;
use PhpMcp\Server\Exception\DiscoveryException;
use PhpMcp\Schema\Implementation;
use PhpMcp\Schema\ServerCapabilities;
use PhpMcp\Server\Protocol;
use PhpMcp\Server\Registry;
use PhpMcp\Server\Server;
use PhpMcp\Server\Session\ArraySessionHandler;
use PhpMcp\Server\Session\SessionManager;
use PhpMcp\Server\Utils\Discoverer;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;

beforeEach(function () {
    /** @var MockInterface&LoggerInterface $logger */
    $this->logger = Mockery::mock(LoggerInterface::class)->shouldIgnoreMissing();
    /** @var MockInterface&LoopInterface $loop */
    $this->loop = Mockery::mock(LoopInterface::class)->shouldIgnoreMissing();
    /** @var MockInterface&CacheInterface $cache */
    $this->cache = Mockery::mock(CacheInterface::class);
    /** @var MockInterface&ContainerInterface $container */
    $this->container = Mockery::mock(ContainerInterface::class);

    $this->configuration = new Configuration(
        serverInfo: Implementation::make('TestServerInstance', '1.0'),
        capabilities: ServerCapabilities::make(),
        logger: $this->logger,
        loop: $this->loop,
        cache: $this->cache,
        container: $this->container
    );

    /** @var MockInterface&Registry $registry */
    $this->registry = Mockery::mock(Registry::class);
    /** @var MockInterface&Protocol $protocol */
    $this->protocol = Mockery::mock(Protocol::class);
    /** @var MockInterface&Discoverer $discoverer */
    $this->discoverer = Mockery::mock(Discoverer::class);

    $this->sessionManager = new SessionManager(new ArraySessionHandler(), $this->logger, $this->loop);

    $this->server = new Server(
        $this->configuration,
        $this->registry,
        $this->protocol,
        $this->sessionManager
    );

    $this->registry->allows('hasElements')->withNoArgs()->andReturn(false)->byDefault();
    $this->registry->allows('clear')->withAnyArgs()->byDefault();
    $this->registry->allows('save')->withAnyArgs()->andReturn(true)->byDefault();
});

afterEach(function () {
    $this->sessionManager->stopGcTimer();
});

it('provides getters for core components', function () {
    expect($this->server->getConfiguration())->toBe($this->configuration);
    expect($this->server->getRegistry())->toBe($this->registry);
    expect($this->server->getProtocol())->toBe($this->protocol);
    expect($this->server->getSessionManager())->toBe($this->sessionManager);
});

it('provides a static make method returning ServerBuilder', function () {
    expect(Server::make())->toBeInstanceOf(\PhpMcp\Server\ServerBuilder::class);
});

it('skips discovery if already run and not forced', function () {
    $reflector = new \ReflectionClass($this->server);
    $prop = $reflector->getProperty('discoveryRan');
    $prop->setAccessible(true);
    $prop->setValue($this->server, true);

    $this->registry->shouldNotReceive('clear');
    $this->discoverer->shouldNotReceive('discover');
    $this->registry->shouldNotReceive('save');

    $this->server->discover(sys_get_temp_dir(), discoverer: $this->discoverer);
    $this->logger->shouldHaveReceived('debug')->with('Discovery skipped: Already run or loaded from cache.');
});

it('forces discovery even if already run, calling injected discoverer', function () {
    $reflector = new \ReflectionClass($this->server);
    $prop = $reflector->getProperty('discoveryRan');
    $prop->setAccessible(true);
    $prop->setValue($this->server, true);

    $basePath = realpath(sys_get_temp_dir());
    $scanDirs = ['.', 'src'];


    $this->registry->shouldReceive('clear')->once();
    $this->discoverer->shouldReceive('discover')
        ->with($basePath, $scanDirs, Mockery::type('array'))
        ->once();
    $this->registry->shouldReceive('save')->once()->andReturn(true);

    $this->server->discover($basePath, $scanDirs, [], force: true, discoverer: $this->discoverer);

    expect($prop->getValue($this->server))->toBeTrue();
});

it('calls registry clear and discoverer, then saves to cache by default', function () {
    $basePath = realpath(sys_get_temp_dir());
    $scanDirs = ['app', 'lib'];
    $userExcludeDirs = ['specific_exclude'];
    $finalExcludeDirs = array_unique(array_merge(
        ['vendor', 'tests', 'test', 'storage', 'cache', 'samples', 'docs', 'node_modules', '.git', '.svn'],
        $userExcludeDirs
    ));


    $this->registry->shouldReceive('clear')->once();
    $this->discoverer->shouldReceive('discover')
        ->with($basePath, $scanDirs, Mockery::on(function ($arg) use ($finalExcludeDirs) {
            expect($arg)->toBeArray();
            expect($arg)->toEqualCanonicalizing($finalExcludeDirs);
            return true;
        }))
        ->once();
    $this->registry->shouldReceive('save')->once()->andReturn(true);

    $this->server->discover($basePath, $scanDirs, $userExcludeDirs, discoverer: $this->discoverer);

    $reflector = new \ReflectionClass($this->server);
    $prop = $reflector->getProperty('discoveryRan');
    $prop->setAccessible(true);
    expect($prop->getValue($this->server))->toBeTrue();
});

it('does not save to cache if saveToCache is false', function () {
    $basePath = realpath(sys_get_temp_dir());

    $this->registry->shouldReceive('clear')->once();
    $this->discoverer->shouldReceive('discover')->once();
    $this->registry->shouldNotReceive('save');

    $this->server->discover($basePath, saveToCache: false, discoverer: $this->discoverer);
});

it('throws InvalidArgumentException for bad base path in discover', function () {
    $this->discoverer->shouldNotReceive('discover');
    $this->server->discover('/non/existent/path/for/sure/I/hope', discoverer: $this->discoverer);
})->throws(\InvalidArgumentException::class, 'Invalid discovery base path');

it('throws DiscoveryException if Discoverer fails during discovery', function () {
    $basePath = realpath(sys_get_temp_dir());

    $this->registry->shouldReceive('clear')->once();
    $this->discoverer->shouldReceive('discover')->once()->andThrow(new \RuntimeException('Filesystem error'));
    $this->registry->shouldNotReceive('save');

    $this->server->discover($basePath, discoverer: $this->discoverer);
})->throws(DiscoveryException::class, 'Element discovery failed: Filesystem error');

it('resets discoveryRan flag on Discoverer failure', function () {
    $basePath = realpath(sys_get_temp_dir());
    $this->registry->shouldReceive('clear')->once();
    $this->discoverer->shouldReceive('discover')->once()->andThrow(new \RuntimeException('Failure'));

    try {
        $this->server->discover($basePath, discoverer: $this->discoverer);
    } catch (DiscoveryException $e) {
        // Expected
    }

    $reflector = new \ReflectionClass($this->server);
    $prop = $reflector->getProperty('discoveryRan');
    $prop->setAccessible(true);
    expect($prop->getValue($this->server))->toBeFalse();
});


// --- Listening Tests ---
it('throws LogicException if listen is called when already listening', function () {
    $transport = Mockery::mock(ServerTransportInterface::class)->shouldIgnoreMissing();
    $this->protocol->shouldReceive('bindTransport')->with($transport)->once();

    $this->server->listen($transport, false);
    $this->server->listen($transport, false);
})->throws(LogicException::class, 'Server is already listening');

it('warns if no elements and discovery not run when listen is called', function () {
    $transport = Mockery::mock(ServerTransportInterface::class)->shouldIgnoreMissing();
    $this->protocol->shouldReceive('bindTransport')->with($transport)->once();

    $this->registry->shouldReceive('hasElements')->andReturn(false);

    $this->logger->shouldReceive('warning')
        ->once()
        ->with(Mockery::pattern('/Starting listener, but no MCP elements are registered and discovery has not been run/'));

    $this->server->listen($transport, false);
});

it('injects logger and loop into aware transports during listen', function () {
    $transport = Mockery::mock(ServerTransportInterface::class, LoggerAwareInterface::class, LoopAwareInterface::class);
    $transport->shouldReceive('setLogger')->with($this->logger)->once();
    $transport->shouldReceive('setLoop')->with($this->loop)->once();
    $transport->shouldReceive('on', 'once', 'listen', 'emit', 'close', 'removeAllListeners')->withAnyArgs();
    $this->protocol->shouldReceive('bindTransport', 'unbindTransport')->withAnyArgs();

    $this->server->listen($transport);
});

it('binds protocol, starts transport listener, and runs loop by default', function () {
    $transport = Mockery::mock(ServerTransportInterface::class)->shouldIgnoreMissing();
    $transport->shouldReceive('listen')->once();
    $this->protocol->shouldReceive('bindTransport')->with($transport)->once();
    $this->loop->shouldReceive('run')->once();
    $this->protocol->shouldReceive('unbindTransport')->once();

    $this->server->listen($transport);
    expect(getPrivateProperty($this->server, 'isListening'))->toBeFalse();
});

it('does not run loop if runLoop is false in listen', function () {
    $transport = Mockery::mock(ServerTransportInterface::class)->shouldIgnoreMissing();
    $this->protocol->shouldReceive('bindTransport')->with($transport)->once();

    $this->loop->shouldNotReceive('run');

    $this->server->listen($transport, runLoop: false);
    expect(getPrivateProperty($this->server, 'isListening'))->toBeTrue();

    $this->protocol->shouldReceive('unbindTransport');
    $transport->shouldReceive('removeAllListeners');
    $transport->shouldReceive('close');
    $this->server->endListen($transport);
});

it('calls endListen if transport listen throws immediately', function () {
    $transport = Mockery::mock(ServerTransportInterface::class)->shouldIgnoreMissing();
    $transport->shouldReceive('listen')->once()->andThrow(new \RuntimeException("Port in use"));
    $this->protocol->shouldReceive('bindTransport')->once();
    $this->protocol->shouldReceive('unbindTransport')->once();

    $this->loop->shouldNotReceive('run');

    try {
        $this->server->listen($transport);
    } catch (\RuntimeException $e) {
        expect($e->getMessage())->toBe("Port in use");
    }
    expect(getPrivateProperty($this->server, 'isListening'))->toBeFalse();
});

it('endListen unbinds protocol and closes transport if listening', function () {
    $transport = Mockery::mock(ServerTransportInterface::class);
    $reflector = new \ReflectionClass($this->server);
    $prop = $reflector->getProperty('isListening');
    $prop->setAccessible(true);
    $prop->setValue($this->server, true);

    $this->protocol->shouldReceive('unbindTransport')->once();
    $transport->shouldReceive('removeAllListeners')->with('close')->once();
    $transport->shouldReceive('close')->once();

    $this->server->endListen($transport);
    expect($prop->getValue($this->server))->toBeFalse();
});

```
Page 2/5FirstPrevNextLast