[Mate] Add AI Mate MCP integration with custom Symfony AI features capability

This commit is contained in:
Johannes Wachter
2025-12-21 22:40:43 +01:00
committed by Christopher Hertel
parent efac2cac26
commit e246bdd1d0
11 changed files with 646 additions and 1 deletions

1
demo/.mcp.json Symbolic link
View File

@@ -0,0 +1 @@
mcp.json

View File

@@ -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

View File

@@ -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
```

View File

@@ -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": {

View File

@@ -1,5 +1,7 @@
<?php
// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
/**
@@ -369,6 +371,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
* }>,
* pinecone?: array<string, array{ // Default: []
* client?: string, // Default: "Probots\\Pinecone\\Client"
* index_name: string,
* namespace?: string,
* filter?: list<scalar|null>,
* top_k?: int,

0
demo/mate/.env Normal file
View File

1
demo/mate/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env.local

22
demo/mate/config.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
// User's service configuration file
// This file is loaded into the Symfony DI container
use App\Mate\SymfonyAiFeaturesTool;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
return static function (ContainerConfigurator $container): void {
$container->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');
};

10
demo/mate/extensions.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
// This file is managed by 'mate discover'
// You can manually edit to enable/disable extensions
return [
'symfony/ai-mate' => ['enabled' => true],
'symfony/ai-monolog-mate-extension' => ['enabled' => true],
'symfony/ai-symfony-mate-extension' => ['enabled' => true],
];

View File

@@ -0,0 +1,426 @@
<?php
namespace App\Mate;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Component\Yaml\Yaml;
/**
* @author Johannes Wachter <johannes@sulu.io>
*/
class SymfonyAiFeaturesTool
{
public function __construct(
private string $projectDir,
) {
}
/**
* @return array{
* success: bool,
* summary: array<string, int>,
* platforms?: array<int, array<string, mixed>>,
* agents?: array<int, array<string, mixed>>,
* stores?: array<int, array<string, mixed>>,
* tools?: array<int, array<string, mixed>>,
* multi_agent_setups?: array<int, array<string, mixed>>,
* indexers?: array<int, array<string, mixed>>,
* retrievers?: array<int, array<string, mixed>>,
* vectorizers?: array<int, array<string, mixed>>,
* installed_packages?: array<int, array<string, string>>,
* 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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, string>>
*/
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<int, array<string, mixed>>
*/
private function parseTools(array $tools): array
{
return array_map(fn($tool) => $this->parseTool($tool), $tools);
}
/**
* @return array<string, mixed>
*/
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<string, int>
*/
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']),
];
}
}

10
demo/mcp.json Normal file
View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"symfony-ai-mate": {
"command": "./vendor/bin/mate",
"args": [
"serve"
]
}
}
}