discoverer = new Discoverer(); } public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void { $discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']); $tools = $discovery->getTools(); $this->assertCount(6, $tools); $this->assertArrayHasKey('greet_user', $tools); $this->assertEquals('greet_user', $tools['greet_user']->tool->name); $this->assertEquals('Greets a user by name.', $tools['greet_user']->tool->description); $this->assertEquals([DiscoverableToolHandler::class, 'greet'], $tools['greet_user']->handler); $this->assertArrayHasKey('name', $tools['greet_user']->tool->inputSchema['properties'] ?? []); $this->assertArrayHasKey('repeatAction', $tools); $this->assertEquals('A tool with more complex parameters and inferred name/description.', $tools['repeatAction']->tool->description); $this->assertTrue($tools['repeatAction']->tool->annotations->readOnlyHint); $this->assertEquals(['count', 'loudly', 'mode'], array_keys($tools['repeatAction']->tool->inputSchema['properties'] ?? [])); $this->assertArrayHasKey('InvokableCalculator', $tools); $this->assertInstanceOf(ToolReference::class, $tools['InvokableCalculator']); $this->assertEquals([InvocableToolFixture::class, '__invoke'], $tools['InvokableCalculator']->handler); $this->assertArrayHasKey('inc_file_name_tool', $tools); $this->assertEquals([AlternativeFileNameToolHandler::class, 'run'], $tools['inc_file_name_tool']->handler); $this->assertArrayNotHasKey('private_tool_should_be_ignored', $tools); $this->assertArrayNotHasKey('protected_tool_should_be_ignored', $tools); $this->assertArrayNotHasKey('static_tool_should_be_ignored', $tools); $resources = $discovery->getResources(); $this->assertCount(4, $resources); $this->assertArrayHasKey('app://info/version', $resources); $this->assertEquals('app_version', $resources['app://info/version']->resource->name); $this->assertEquals('text/plain', $resources['app://info/version']->resource->mimeType); $this->assertArrayHasKey('invokable://config/status', $resources); $this->assertEquals([InvocableResourceFixture::class, '__invoke'], $resources['invokable://config/status']->handler); $this->assertArrayHasKey('ui://widget/clock', $resources); $this->assertEquals(McpApps::MIME_TYPE, $resources['ui://widget/clock']->resource->mimeType); $this->assertJsonStringEqualsJsonString('{"ui":{}}', json_encode($resources['ui://widget/clock']->resource->meta)); $this->assertJsonStringEqualsJsonString( '{"ui":{"resourceUri":"ui://widget/clock","visibility":["app"]}}', json_encode($tools['show_clock']->tool->meta), ); $prompts = $discovery->getPrompts(); $this->assertCount(4, $prompts); $this->assertArrayHasKey('creative_story_prompt', $prompts); $this->assertCount(2, $prompts['creative_story_prompt']->prompt->arguments); $this->assertEquals(CompletionProviderFixture::class, $prompts['creative_story_prompt']->completionProviders['genre']); $this->assertArrayHasKey('simpleQuestionPrompt', $prompts); $this->assertArrayHasKey('InvokableGreeterPrompt', $prompts); $this->assertEquals([InvocablePromptFixture::class, '__invoke'], $prompts['InvokableGreeterPrompt']->handler); $this->assertArrayHasKey('content_creator', $prompts); $this->assertCount(3, $prompts['content_creator']->completionProviders); $templates = $discovery->getResourceTemplates(); $this->assertCount(4, $templates); $this->assertArrayHasKey('product://{region}/details/{productId}', $templates); $this->assertEquals('product_details_template', $templates['product://{region}/details/{productId}']->resourceTemplate->name); $this->assertEquals(CompletionProviderFixture::class, $templates['product://{region}/details/{productId}']->completionProviders['region']); $this->assertEqualsCanonicalizing(['region', 'productId'], $templates['product://{region}/details/{productId}']->getVariableNames()); $this->assertArrayHasKey('invokable://user-profile/{userId}', $templates); $this->assertEquals([InvocableResourceTemplateFixture::class, '__invoke'], $templates['invokable://user-profile/{userId}']->handler); } public function testDoesNotDiscoverElementsFromExcludedDirectories(): void { $discovery = $this->discoverer->discover(__DIR__, ['Fixtures']); $this->assertArrayHasKey('hidden_subdir_tool', $discovery->getTools()); $discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], ['SubDir']); $this->assertArrayNotHasKey('hidden_subdir_tool', $discovery->getTools()); } public function testHandlesEmptyDirectoriesOrDirectoriesWithNoPhpFiles(): void { $discovery = $this->discoverer->discover(__DIR__, ['EmptyDir']); $this->assertTrue($discovery->isEmpty()); } public function testHandlesDefaultAndOverriddenFileNamePatterns(): void { $discovery = $this->discoverer->discover(__DIR__, ['Fixtures']); $this->assertArrayHasKey('greet_user', $discovery->getTools()); $this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools()); $discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], []); $this->assertArrayHasKey('greet_user', $discovery->getTools()); $this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools()); $discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']); $this->assertArrayHasKey('greet_user', $discovery->getTools()); $this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools()); $discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.inc']); $this->assertArrayNotHasKey('greet_user', $discovery->getTools()); $this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools()); } public function testCorrectlyInfersNamesAndDescriptionsFromMethodsOrClassesIfNotSetInAttribute(): void { $discovery = $this->discoverer->discover(__DIR__, ['Fixtures']); $this->assertArrayHasKey('repeatAction', $tools = $discovery->getTools()); $this->assertEquals('repeatAction', $tools['repeatAction']->tool->name); $this->assertEquals('A tool with more complex parameters and inferred name/description.', $tools['repeatAction']->tool->description); $this->assertArrayHasKey('simpleQuestionPrompt', $prompts = $discovery->getPrompts()); $this->assertEquals('simpleQuestionPrompt', $prompts['simpleQuestionPrompt']->prompt->name); $this->assertNull($prompts['simpleQuestionPrompt']->prompt->description); $this->assertArrayHasKey('InvokableCalculator', $tools); $this->assertEquals('InvokableCalculator', $tools['InvokableCalculator']->tool->name); $this->assertEquals('An invokable calculator tool.', $tools['InvokableCalculator']->tool->description); } public function testDiscoversEnhancedCompletionProvidersWithValuesAndEnumAttributes(): void { $discovery = $this->discoverer->discover(__DIR__, ['Fixtures']); $this->assertArrayHasKey('content_creator', $prompts = $discovery->getPrompts()); $this->assertCount(3, $prompts['content_creator']->completionProviders); $typeProvider = $prompts['content_creator']->completionProviders['type']; $this->assertInstanceOf(ListCompletionProvider::class, $typeProvider); $statusProvider = $prompts['content_creator']->completionProviders['status']; $this->assertInstanceOf(EnumCompletionProvider::class, $statusProvider); $priorityProvider = $prompts['content_creator']->completionProviders['priority']; $this->assertInstanceOf(EnumCompletionProvider::class, $priorityProvider); $this->assertArrayHasKey('content://{category}/{slug}', $templates = $discovery->getResourceTemplates()); $this->assertCount(1, $templates['content://{category}/{slug}']->completionProviders); $categoryProvider = $templates['content://{category}/{slug}']->completionProviders['category']; $this->assertInstanceOf(ListCompletionProvider::class, $categoryProvider); } }