From e246bdd1d0daef35c0d0288134645bb0c4a17994 Mon Sep 17 00:00:00 2001 From: Johannes Wachter Date: Sun, 21 Dec 2025 22:40:43 +0100 Subject: [PATCH] [Mate] Add AI Mate MCP integration with custom Symfony AI features capability --- demo/.mcp.json | 1 + demo/CLAUDE.md | 91 +++++ demo/README.md | 69 ++++ demo/composer.json | 14 +- demo/config/reference.php | 3 + demo/mate/.env | 0 demo/mate/.gitignore | 1 + demo/mate/config.php | 22 ++ demo/mate/extensions.php | 10 + demo/mate/src/SymfonyAiFeaturesTool.php | 426 ++++++++++++++++++++++++ demo/mcp.json | 10 + 11 files changed, 646 insertions(+), 1 deletion(-) create mode 120000 demo/.mcp.json create mode 100644 demo/mate/.env create mode 100644 demo/mate/.gitignore create mode 100644 demo/mate/config.php create mode 100644 demo/mate/extensions.php create mode 100644 demo/mate/src/SymfonyAiFeaturesTool.php create mode 100644 demo/mcp.json diff --git a/demo/.mcp.json b/demo/.mcp.json new file mode 120000 index 00000000..90c3eb66 --- /dev/null +++ b/demo/.mcp.json @@ -0,0 +1 @@ +mcp.json \ No newline at end of file diff --git a/demo/CLAUDE.md b/demo/CLAUDE.md index 82893752..3d2c7ca5 100644 --- a/demo/CLAUDE.md +++ b/demo/CLAUDE.md @@ -92,6 +92,97 @@ Each chat type follows the pattern: ### Session Management Chat history stored in Symfony sessions with component-specific keys (e.g., 'blog-chat', 'stream-chat'). +## Available MCP Tools + +**IMPORTANT**: This project includes the Symfony AI Mate MCP server with powerful debugging and inspection tools. **USE THESE TOOLS PROACTIVELY** whenever working with Symfony AI features, logs, or system information. + +### Symfony AI Inspection Tools + +**`symfony-ai-features`** - **USE THIS FIRST** when analyzing or modifying AI configuration +- Detects and lists all AI platforms, agents, tools, stores, vectorizers, indexers, retrievers, and multi-agent setups +- Provides summary counts and detailed configuration information +- Analyzes `config/packages/ai.yaml` and `composer.json` +- Use when: Starting any AI-related task, debugging agent configuration, understanding project AI capabilities + +``` +Example: Before modifying agents, call symfony-ai-features to understand current setup +``` + +**`symfony-services`** - Get complete list of Symfony services +- Lists all registered services in the DI container +- Use when: Debugging dependency injection, finding service IDs, understanding available services + +### Monolog/Logging Tools + +**`monolog-search`** - Search logs by text term +- Parameters: `term` (required), `level`, `channel`, `environment`, `from`, `to`, `limit` (default: 100) +- Use when: Debugging errors, finding specific log messages, tracking events + +**`monolog-search-regex`** - Search logs using regex patterns +- Parameters: `pattern` (required), `level`, `channel`, `environment`, `limit` +- Use when: Complex log searching, pattern matching, finding formatted data + +**`monolog-context-search`** - Search logs by context field +- Parameters: `key` (required), `value` (required), `level`, `environment`, `limit` +- Use when: Finding logs with specific context data (user_id, request_id, etc.) + +**`monolog-tail`** - Get last N log entries (like `tail -f`) +- Parameters: `lines` (default: 50), `level`, `environment` +- Use when: Checking recent activity, monitoring real-time logs, debugging current issues + +**`monolog-list-files`** - List available log files +- Parameters: `environment` (optional) +- Use when: Finding log locations, checking available environments + +**`monolog-list-channels`** - List all log channels +- Use when: Understanding logging structure, finding channel names for filtering + +**`monolog-by-level`** - Filter logs by severity level +- Parameters: `level` (required: DEBUG, INFO, WARNING, ERROR, CRITICAL, etc.), `environment`, `limit` +- Use when: Finding errors, warnings, or specific severity issues + +### System Information Tools + +**`php-version`** - Get PHP version +- Use when: Checking compatibility, debugging version-specific issues + +**`php-extensions`** - List installed PHP extensions +- Use when: Verifying required extensions, debugging extension-related issues + +**`operating-system`** - Get current OS +- Use when: Platform-specific debugging, environment verification + +**`operating-system-family`** - Get OS family (Windows, Linux, Darwin) +- Use when: Cross-platform compatibility checks + +### Tool Usage Guidelines + +**Proactive Usage Patterns**: +1. **Before modifying AI config**: Call `symfony-ai-features` to understand current setup +2. **When debugging errors**: Use `monolog-tail` and `monolog-search` to find relevant logs +3. **When analyzing agents**: Use `symfony-ai-features` to see all agents, tools, and configurations +4. **When troubleshooting**: Combine log tools with system info tools for complete context +5. **When adding new features**: Check existing services with `symfony-services` + +**Example Workflows**: + +```bash +# Investigating AI agent issues +1. symfony-ai-features (get agent configuration) +2. monolog-search term:"agent" (find agent-related logs) +3. monolog-by-level level:"ERROR" (check for errors) + +# Debugging application errors +1. monolog-tail lines:100 (recent activity) +2. monolog-by-level level:"ERROR" (find errors) +3. monolog-context-search key:"exception" (error details) + +# Understanding project structure +1. symfony-ai-features (AI capabilities) +2. symfony-services (available services) +3. php-extensions (installed extensions) +``` + ## Development Notes - Uses PHP 8.4+ with strict typing and modern PHP features diff --git a/demo/README.md b/demo/README.md index 0efcf8a1..92976364 100644 --- a/demo/README.md +++ b/demo/README.md @@ -142,3 +142,72 @@ npx @modelcontextprotocol/inspector php bin/console mcp:server ``` Which opens a web UI to interactively test the MCP server. + +## AI Mate - MCP Development Assistant + +[Symfony AI Mate](https://github.com/symfony/ai-mate) is an MCP (Model Context Protocol) server that provides AI +assistants with Symfony-specific development capabilities. + +### Installation & Setup + +**This demo is already configured!** For new projects you can set up AI Mate as follows: + +```shell +# Install AI Mate +composer require --dev symfony/ai-mate + +# Initialize configuration +vendor/bin/mate init + +# Discover available tools +vendor/bin/mate discover +``` + +### MCP Client Configuration + +The `mcp.json` file in the project root enables automatic MCP client detection: + +```json +{ + "mcpServers": { + "symfony-ai-mate": { + "command": "./vendor/bin/mate", + "args": ["serve"] + } + } +} +``` + +For other projects, add AI Mate to your MCP client settings (e.g., `~/.claude/mcp.json`, IDE settings, etc.). + +### Custom Capability Example + +This demo includes a **`symfony-ai-features`** tool (see `mate/SymfonyAiFeaturesTool.php`) that analyzes the project's +AI configuration and reports all available platforms, agents, tools, stores, and packages. + +**Try it in your MCP-enabled chat:** + +> "Which Symfony AI features are available in this demo?" +> +> "What AI agents are configured in this project?" +> +> "Show me all the Symfony AI tools and their configuration" +> +> "What is the current PHP version used in this project?" +> +> "Is the php extension intl installed?" + +The AI assistant will use the `symfony-ai-features` and other MCP tool to provide detailed information about project +internals. + +### Creating Custom Tools + +Create tools in `mate/src/` and register them in `mate/config.php`. See the +[AI Mate documentation](https://symfony.com/doc/current/ai/components/mate.html) for detailed guides. + +### Testing + +```shell +# Test with MCP Inspector +npx @modelcontextprotocol/inspector ./vendor/bin/mate serve +``` diff --git a/demo/composer.json b/demo/composer.json index 5e0f1701..bb1e2db3 100644 --- a/demo/composer.json +++ b/demo/composer.json @@ -49,6 +49,9 @@ "phpstan/phpstan": "^2.1.32", "phpstan/phpstan-strict-rules": "^2.0.7", "phpunit/phpunit": "^12.1", + "symfony/ai-mate": "@dev", + "symfony/ai-monolog-mate-extension": "@dev", + "symfony/ai-symfony-mate-extension": "@dev", "symfony/browser-kit": "^8.0", "symfony/css-selector": "^8.0", "symfony/debug-bundle": "^8.0", @@ -80,7 +83,8 @@ }, "autoload-dev": { "psr-4": { - "App\\Tests\\": "tests/" + "App\\Tests\\": "tests/", + "App\\Mate\\": "mate/src/" } }, "config": { @@ -95,6 +99,14 @@ "symfony": { "allow-contrib": false, "require": "8.0.*" + }, + "ai-mate": { + "scan-dirs": [ + "mate/src" + ], + "includes": [ + "config.php" + ] } }, "scripts": { diff --git a/demo/config/reference.php b/demo/config/reference.php index df1d953c..89f557d3 100644 --- a/demo/config/reference.php +++ b/demo/config/reference.php @@ -1,5 +1,7 @@ , * pinecone?: array, * top_k?: int, diff --git a/demo/mate/.env b/demo/mate/.env new file mode 100644 index 00000000..e69de29b diff --git a/demo/mate/.gitignore b/demo/mate/.gitignore new file mode 100644 index 00000000..11ee7581 --- /dev/null +++ b/demo/mate/.gitignore @@ -0,0 +1 @@ +.env.local diff --git a/demo/mate/config.php b/demo/mate/config.php new file mode 100644 index 00000000..e4b3d206 --- /dev/null +++ b/demo/mate/config.php @@ -0,0 +1,22 @@ +parameters() + // Override default parameters here + // ->set('mate.cache_dir', sys_get_temp_dir().'/mate') + // ->set('mate.env_file', ['.env']) // This will load mate/.env and mate/.env.local + ; + + $container->services() + ->set(SymfonyAiFeaturesTool::class) + ->arg('$projectDir', param('mate.root_dir')) + ->tag('mcp.capability'); +}; diff --git a/demo/mate/extensions.php b/demo/mate/extensions.php new file mode 100644 index 00000000..7ba984f6 --- /dev/null +++ b/demo/mate/extensions.php @@ -0,0 +1,10 @@ + ['enabled' => true], + 'symfony/ai-monolog-mate-extension' => ['enabled' => true], + 'symfony/ai-symfony-mate-extension' => ['enabled' => true], +]; diff --git a/demo/mate/src/SymfonyAiFeaturesTool.php b/demo/mate/src/SymfonyAiFeaturesTool.php new file mode 100644 index 00000000..28c51760 --- /dev/null +++ b/demo/mate/src/SymfonyAiFeaturesTool.php @@ -0,0 +1,426 @@ + + */ +class SymfonyAiFeaturesTool +{ + public function __construct( + private string $projectDir, + ) { + } + + /** + * @return array{ + * success: bool, + * summary: array, + * platforms?: array>, + * agents?: array>, + * stores?: array>, + * tools?: array>, + * multi_agent_setups?: array>, + * indexers?: array>, + * retrievers?: array>, + * vectorizers?: array>, + * installed_packages?: array>, + * error?: string, + * message?: string + * } + */ + #[McpTool('symfony-ai-features', 'Detects and lists all available Symfony AI features, platforms, agents, tools, and configurations in this project')] + public function getFeatures(bool $includeDetails = true): array + { + $configPath = $this->projectDir . '/config/packages/ai.yaml'; + $composerPath = $this->projectDir . '/composer.json'; + + if (!file_exists($configPath)) { + return [ + 'success' => false, + 'error' => 'AI configuration file not found', + 'summary' => [], + ]; + } + + try { + $config = Yaml::parseFile($configPath); + $aiConfig = $config['ai'] ?? []; + + $composer = json_decode(file_get_contents($composerPath), true); + + $features = [ + 'platforms' => $this->detectPlatforms($aiConfig, $includeDetails), + 'agents' => $this->detectAgents($aiConfig, $includeDetails), + 'stores' => $this->detectStores($aiConfig, $includeDetails), + 'tools' => $this->detectTools($aiConfig, $includeDetails), + 'multi_agent_setups' => $this->detectMultiAgentSetups($aiConfig, $includeDetails), + 'indexers' => $this->detectIndexers($aiConfig, $includeDetails), + 'retrievers' => $this->detectRetrievers($aiConfig, $includeDetails), + 'vectorizers' => $this->detectVectorizers($aiConfig, $includeDetails), + 'installed_packages' => $this->detectInstalledPackages($composer), + ]; + + return [ + 'success' => true, + 'summary' => $this->generateSummary($features), + ...$features, + ]; + } catch (\Exception $e) { + return [ + 'success' => false, + 'error' => 'Failed to parse configuration', + 'message' => $e->getMessage(), + 'summary' => [], + ]; + } + } + + /** + * @return array> + */ + private function detectPlatforms(array $config, bool $includeDetails): array + { + $platforms = []; + $platformConfig = $config['platform'] ?? []; + + foreach ($platformConfig as $name => $settings) { + $platform = [ + 'name' => $name, + 'configured' => true, + ]; + + if ($includeDetails) { + $platform['has_api_key'] = isset($settings['api_key']); + if (isset($settings['api_key'])) { + $platform['api_key_env_var'] = $this->extractEnvVar($settings['api_key']); + } + } + + $platforms[] = $platform; + } + + return $platforms; + } + + /** + * @return array> + */ + private function detectAgents(array $config, bool $includeDetails): array + { + $agents = []; + $agentConfig = $config['agent'] ?? []; + + foreach ($agentConfig as $name => $settings) { + $agent = [ + 'name' => $name, + 'platform' => $settings['platform'] ?? 'unknown', + 'model' => is_array($settings['model'] ?? null) + ? ($settings['model']['name'] ?? 'unknown') + : ($settings['model'] ?? 'unknown'), + ]; + + if ($includeDetails) { + $agent['has_custom_prompt'] = isset($settings['prompt']); + $agent['tools_enabled'] = ($settings['tools'] ?? null) !== false; + + if ($agent['tools_enabled'] && is_array($settings['tools'] ?? null)) { + $agent['tools'] = $this->parseTools($settings['tools']); + } + + if (isset($settings['prompt'])) { + if (is_array($settings['prompt'])) { + $agent['prompt_type'] = isset($settings['prompt']['file']) ? 'file' : 'config'; + if (isset($settings['prompt']['file'])) { + $agent['prompt_source'] = basename($settings['prompt']['file']); + } + } else { + $agent['prompt_type'] = 'inline'; + $agent['prompt_length'] = strlen($settings['prompt']); + } + } + + if (isset($settings['model']['options'])) { + $agent['model_options'] = $settings['model']['options']; + } + + $agent['include_sources'] = $settings['include_sources'] ?? false; + } + + $agents[] = $agent; + } + + return $agents; + } + + /** + * @return array> + */ + private function detectStores(array $config, bool $includeDetails): array + { + $stores = []; + $storeConfig = $config['store'] ?? []; + + foreach ($storeConfig as $type => $instances) { + foreach ($instances as $name => $settings) { + $store = [ + 'name' => $name, + 'type' => $type, + ]; + + if ($includeDetails && isset($settings['collection'])) { + $store['collection'] = $settings['collection']; + } + + $stores[] = $store; + } + } + + return $stores; + } + + /** + * @return array> + */ + private function detectTools(array $config, bool $includeDetails): array + { + $tools = []; + $agentConfig = $config['agent'] ?? []; + $toolsIndex = []; + + foreach ($agentConfig as $agentName => $settings) { + if (isset($settings['tools']) && is_array($settings['tools'])) { + foreach ($settings['tools'] as $tool) { + $toolInfo = $this->parseTool($tool); + $toolKey = $toolInfo['class'] ?? $toolInfo['agent'] ?? $toolInfo['name'] ?? 'unknown'; + + if (!isset($toolsIndex[$toolKey])) { + $toolInfo['used_by_agents'] = [$agentName]; + $toolsIndex[$toolKey] = $toolInfo; + } else { + $toolsIndex[$toolKey]['used_by_agents'][] = $agentName; + } + } + } + } + + return array_values($toolsIndex); + } + + /** + * @return array> + */ + private function detectMultiAgentSetups(array $config, bool $includeDetails): array + { + $setups = []; + $multiAgentConfig = $config['multi_agent'] ?? []; + + foreach ($multiAgentConfig as $name => $settings) { + $setup = [ + 'name' => $name, + 'orchestrator' => $settings['orchestrator'] ?? null, + 'fallback' => $settings['fallback'] ?? null, + ]; + + if ($includeDetails && isset($settings['handoffs'])) { + $setup['handoffs'] = $settings['handoffs']; + $setup['handoff_count'] = count($settings['handoffs']); + } + + $setups[] = $setup; + } + + return $setups; + } + + /** + * @return array> + */ + private function detectIndexers(array $config, bool $includeDetails): array + { + $indexers = []; + $indexerConfig = $config['indexer'] ?? []; + + foreach ($indexerConfig as $name => $settings) { + $indexer = [ + 'name' => $name, + 'loader' => $this->extractClassName($settings['loader'] ?? 'unknown'), + 'source' => $settings['source'] ?? null, + ]; + + if ($includeDetails) { + $indexer['has_filters'] = !empty($settings['filters']); + $indexer['has_transformers'] = !empty($settings['transformers']); + $indexer['vectorizer'] = $settings['vectorizer'] ?? null; + $indexer['store'] = $settings['store'] ?? null; + + if (!empty($settings['transformers'])) { + $indexer['transformers'] = array_map( + fn($t) => $this->extractClassName($t), + $settings['transformers'] + ); + } + } + + $indexers[] = $indexer; + } + + return $indexers; + } + + /** + * @return array> + */ + private function detectRetrievers(array $config, bool $includeDetails): array + { + $retrievers = []; + $retrieverConfig = $config['retriever'] ?? []; + + foreach ($retrieverConfig as $name => $settings) { + $retriever = [ + 'name' => $name, + 'vectorizer' => $settings['vectorizer'] ?? null, + 'store' => $settings['store'] ?? null, + ]; + + $retrievers[] = $retriever; + } + + return $retrievers; + } + + /** + * @return array> + */ + private function detectVectorizers(array $config, bool $includeDetails): array + { + $vectorizers = []; + $vectorizerConfig = $config['vectorizer'] ?? []; + + foreach ($vectorizerConfig as $name => $settings) { + $vectorizer = [ + 'name' => $name, + 'platform' => $settings['platform'] ?? 'unknown', + 'model' => $settings['model'] ?? 'unknown', + ]; + + $vectorizers[] = $vectorizer; + } + + return $vectorizers; + } + + /** + * @return array> + */ + private function detectInstalledPackages(array $composer): array + { + $packages = []; + $allDeps = array_merge( + $composer['require'] ?? [], + $composer['require-dev'] ?? [] + ); + + foreach ($allDeps as $package => $version) { + if (str_starts_with($package, 'symfony/ai-')) { + $packages[] = [ + 'name' => $package, + 'version' => $version, + 'type' => $this->categorizePackage($package), + ]; + } + } + + return $packages; + } + + /** + * @return array> + */ + private function parseTools(array $tools): array + { + return array_map(fn($tool) => $this->parseTool($tool), $tools); + } + + /** + * @return array + */ + private function parseTool(mixed $tool): array + { + if (is_string($tool)) { + return [ + 'type' => 'service', + 'class' => $this->extractClassName($tool), + 'full_class' => $tool, + ]; + } + + if (is_array($tool)) { + if (isset($tool['agent'])) { + return [ + 'type' => 'sub_agent', + 'agent' => $tool['agent'], + 'name' => $tool['name'] ?? null, + 'description' => $tool['description'] ?? null, + ]; + } + + return [ + 'type' => 'service', + 'service' => $tool['service'] ?? null, + 'name' => $tool['name'] ?? null, + 'description' => $tool['description'] ?? null, + 'method' => $tool['method'] ?? null, + ]; + } + + return ['type' => 'unknown']; + } + + private function extractClassName(string $classOrService): string + { + $parts = explode('\\', $classOrService); + return end($parts); + } + + private function extractEnvVar(string $value): ?string + { + if (preg_match('/%env\(([^)]+)\)%/', $value, $matches)) { + return $matches[1]; + } + return null; + } + + private function categorizePackage(string $package): string + { + return match (true) { + str_contains($package, 'platform') => 'platform', + str_contains($package, 'store') => 'store', + str_contains($package, 'tool') => 'tool', + str_contains($package, 'bundle') => 'bundle', + str_contains($package, 'mate') => 'development', + default => 'other', + }; + } + + /** + * @return array + */ + private function generateSummary(array $features): array + { + return [ + 'total_platforms' => count($features['platforms']), + 'total_agents' => count($features['agents']), + 'total_stores' => count($features['stores']), + 'total_tools' => count($features['tools']), + 'total_multi_agent_setups' => count($features['multi_agent_setups']), + 'total_indexers' => count($features['indexers']), + 'total_retrievers' => count($features['retrievers']), + 'total_vectorizers' => count($features['vectorizers']), + 'total_packages' => count($features['installed_packages']), + ]; + } +} diff --git a/demo/mcp.json b/demo/mcp.json new file mode 100644 index 00000000..d4136463 --- /dev/null +++ b/demo/mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "symfony-ai-mate": { + "command": "./vendor/bin/mate", + "args": [ + "serve" + ] + } + } +}