Model Context Protocol (MCP) : Révolution de l'Intégration IA avec Symfony

Imaginez pouvoir connecter n'importe quel modèle d'Intelligence Artificielle à n'importe quel outil externe avec un simple "plug-and-play". C'est exactement ce que propose le Model Context Protocol (MCP), introduit par Anthropic en novembre 2024. Ce protocole open-source révolutionne la façon dont les modèles d'IA accèdent aux données externes et interagissent avec les outils.

🤔 Qu'est-ce que le Model Context Protocol ?

Le problème fondamental

Les Modèles de Langage comme ChatGPT, Claude ou Gemini sont incroyablement puissants, mais ils souffrent d'une limitation majeure : ils n'ont pas accès au monde extérieur. Ils ne peuvent pas :

  • 📂 Accéder à vos fichiers locaux
  • 🌐 Naviguer sur Internet en temps réel
  • 💾 Interroger vos bases de données
  • 📧 Envoyer des emails
  • 🔧 Utiliser des APIs externes

La solution MCP

Le Model Context Protocol agit comme un "USB-C universel pour l'IA". Au lieu de créer des intégrations personnalisées pour chaque outil, MCP fournit un protocole standardisé qui permet à n'importe quel modèle d'IA de se connecter à n'importe quel service externe.

[Modèle IA] ←→ [Client MCP] ←→ [Serveur MCP] ←→ [Outil Externe]

🏗️ Architecture du MCP

Les composants clés

Le MCP suit une architecture client-serveur avec trois éléments principaux :

1. MCP Host (Hôte)

L'application qui contient le modèle d'IA (Claude Desktop, VS Code, etc.)

2. MCP Client (Client)

Le pont entre le modèle d'IA et les serveurs MCP. Il :

  • Traduit les demandes du modèle en requêtes MCP
  • Gère l'authentification
  • Maintient les connexions

3. MCP Server (Serveur)

Expose les outils et données externes via l'interface MCP standardisée

Les trois primitives fondamentales

Le MCP expose ses capacités via trois types de primitives :

🔧 Tools (Outils)

Fonctions exécutables que l'IA peut appeler :

{
  "name": "send_email",
  "description": "Envoie un email à un destinataire",
  "inputSchema": {
    "type": "object",
    "properties": {
      "to": { "type": "string", "format": "email" },
      "subject": { "type": "string" },
      "body": { "type": "string" }
    }
  }
}

📊 Resources (Ressources)

Données structurées ou non que l'IA peut consulter :

{
  "uri": "file:///home/user/project/README.md",
  "name": "Documentation du projet",
  "description": "Fichier README principal",
  "mimeType": "text/markdown"
}

💡 Prompts (Invites)

Templates prédéfinis pour guider le comportement de l'IA :

{
  "name": "debug_application",
  "description": "Assistant de débogage d'application",
  "arguments": [
    {
      "name": "error_message",
      "description": "Message d'erreur à analyser"
    }
  ]
}

🚀 Avantages du MCP

✅ Standardisation

  • Un seul protocole pour toutes les intégrations
  • Réduction du problème "M×N" vers "M+N"
  • Interopérabilité entre différents modèles et outils

🔒 Sécurité

  • Contrôle granulaire des accès
  • Isolation des serveurs
  • Authentification standardisée

⚡ Productivité

  • Développement accéléré
  • Réutilisabilité des connecteurs
  • Maintenance simplifiée

🔮 Extensibilité

  • Ajout facile de nouvelles capacités
  • Évolution sans rupture de compatibilité
  • Écosystème ouvert

🐘 MCP avec Symfony : Implementation Pratique

Symfony a embrassé le MCP avec l'initiative Symfony AI et le composant MCP SDK. Voyons comment implémenter un serveur MCP avec Symfony.

Installation du MCP Server Bundle

composer require ecourty/mcp-server-bundle

Configuration du routing

Créez config/routes/mcp.yaml :

mcp_controller:
  path: /mcp
  controller: mcp_server.entrypoint_controller

Création d'un outil simple

1. Schéma d'entrée

<?php

namespace App\Schema;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

class GetWeatherSchema
{
    #[Assert\NotBlank]
    #[OA\Property(type: 'string', description: 'Nom de la ville')]
    public string $city;

    #[Assert\Choice(['metric', 'imperial'])]
    #[OA\Property(type: 'string', enum: ['metric', 'imperial'])]
    public string $unit = 'metric';
}

2. Implémentation de l'outil

<?php

namespace App\Tool;

use App\Schema\GetWeatherSchema;
use Ecourty\McpServerBundle\Attribute\AsTool;
use Ecourty\McpServerBundle\Attribute\ToolAnnotations;
use Ecourty\McpServerBundle\IO\TextToolResult;
use Ecourty\McpServerBundle\IO\ToolResult;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsTool(
    name: 'get_weather',
    description: 'Récupère les informations météo pour une ville donnée',
    annotations: new ToolAnnotations(
        title: 'Service Météo',
        readOnlyHint: true,
        destructiveHint: false,
        idempotentHint: true,
        openWorldHint: true,
    )
)]
class GetWeatherTool
{
    public function __construct(
        private HttpClientInterface $httpClient,
        private string $weatherApiKey
    ) {}

    public function __invoke(GetWeatherSchema $payload): ToolResult
    {
        try {
            $response = $this->httpClient->request('GET', 
                'https://api.openweathermap.org/data/2.5/weather', [
                'query' => [
                    'q' => $payload->city,
                    'appid' => $this->weatherApiKey,
                    'units' => $payload->unit,
                    'lang' => 'fr'
                ]
            ]);

            $data = $response->toArray();
            
            $result = sprintf(
                "🌡️ Météo à %s:\n" .
                "Température: %s°%s\n" .
                "Description: %s\n" .
                "Humidité: %s%%\n" .
                "Vent: %s m/s",
                $data['name'],
                round($data['main']['temp']),
                $payload->unit === 'metric' ? 'C' : 'F',
                $data['weather'][0]['description'],
                $data['main']['humidity'],
                $data['wind']['speed']
            );

            return new ToolResult([
                new TextToolResult($result),
            ]);

        } catch (\Exception $e) {
            return new ToolResult(
                elements: [
                    new TextToolResult('❌ Erreur lors de la récupération de la météo: ' . $e->getMessage())
                ],
                isError: true,
            );
        }
    }
}

Outil plus avancé : Système de notifications intelligent

1. Schéma pour les notifications

<?php

namespace App\Schema;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

class SendNotificationSchema
{
    #[Assert\Choice(['email', 'slack', 'both'])]
    #[OA\Property(type: 'string', enum: ['email', 'slack', 'both'])]
    public string $channel;

    #[Assert\NotBlank]
    #[OA\Property(type: 'string', description: 'Destinataire (email ou @username Slack)')]
    public string $recipient;

    #[Assert\NotBlank]
    #[OA\Property(type: 'string', description: 'Titre de la notification')]
    public string $title;

    #[Assert\NotBlank]
    #[OA\Property(type: 'string', description: 'Contenu du message')]
    public string $message;

    #[Assert\Choice(['low', 'normal', 'high', 'urgent'])]
    #[OA\Property(type: 'string', enum: ['low', 'normal', 'high', 'urgent'])]
    public string $priority = 'normal';

    #[OA\Property(type: 'boolean', description: 'Envoyer immédiatement ou programmer')]
    public bool $immediate = true;
}

2. Implémentation de l'outil de notification

<?php

namespace App\Tool;

use App\Schema\SendNotificationSchema;
use App\Service\SlackService;
use Ecourty\McpServerBundle\Attribute\AsTool;
use Ecourty\McpServerBundle\Attribute\ToolAnnotations;
use Ecourty\McpServerBundle\IO\TextToolResult;
use Ecourty\McpServerBundle\IO\ToolResult;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Psr\Log\LoggerInterface;

#[AsTool(
    name: 'send_notification',
    description: 'Envoie des notifications intelligentes via email et/ou Slack selon le contexte',
    annotations: new ToolAnnotations(
        title: 'Système de Notifications',
        readOnlyHint: false,
        destructiveHint: false,
        idempotentHint: false,
        openWorldHint: true,
    )
)]
class NotificationTool
{
    public function __construct(
        private MailerInterface $mailer,
        private SlackService $slackService,
        private LoggerInterface $logger,
        private string $fromEmail = 'noreply@epickone.com'
    ) {}

    public function __invoke(SendNotificationSchema $payload): ToolResult
    {
        $results = [];
        $hasError = false;

        // Formatage du message selon la priorité
        $formattedMessage = $this->formatMessage($payload);

        try {
            // Envoi par email
            if (in_array($payload->channel, ['email', 'both'])) {
                $emailResult = $this->sendEmail($payload, $formattedMessage);
                $results[] = $emailResult;
                if (str_contains($emailResult, '❌')) {
                    $hasError = true;
                }
            }

            // Envoi via Slack
            if (in_array($payload->channel, ['slack', 'both'])) {
                $slackResult = $this->sendSlack($payload, $formattedMessage);
                $results[] = $slackResult;
                if (str_contains($slackResult, '❌')) {
                    $hasError = true;
                }
            }

            // Log de l'activité
            $this->logger->info('MCP Notification sent', [
                'channel' => $payload->channel,
                'recipient' => $payload->recipient,
                'priority' => $payload->priority,
                'success' => !$hasError
            ]);

            $finalMessage = implode("\n", $results);
            
            return new ToolResult(
                elements: [new TextToolResult($finalMessage)],
                isError: $hasError
            );

        } catch (\Exception $e) {
            $this->logger->error('MCP Notification failed', [
                'error' => $e->getMessage(),
                'payload' => $payload
            ]);

            return new ToolResult(
                elements: [new TextToolResult('❌ Erreur système: ' . $e->getMessage())],
                isError: true,
            );
        }
    }

    private function formatMessage(SendNotificationSchema $payload): string
    {
        $emoji = match($payload->priority) {
            'urgent' => '🚨',
            'high' => '⚠️',
            'normal' => 'ℹ️',
            'low' => '💬',
        };

        return "$emoji **{$payload->title}**\n\n{$payload->message}";
    }

    private function sendEmail(SendNotificationSchema $payload, string $formattedMessage): string
    {
        try {
            // Validation de l'email
            if (!filter_var($payload->recipient, FILTER_VALIDATE_EMAIL)) {
                return "❌ Email invalide: {$payload->recipient}";
            }

            $email = (new Email())
                ->from($this->fromEmail)
                ->to($payload->recipient)
                ->subject("[{$payload->priority}] {$payload->title}")
                ->html($this->convertToHtml($formattedMessage));

            $this->mailer->send($email);
            
            return "✅ Email envoyé à {$payload->recipient}";

        } catch (\Exception $e) {
            return "❌ Échec email: " . $e->getMessage();
        }
    }

    private function sendSlack(SendNotificationSchema $payload, string $formattedMessage): string
    {
        try {
            // Formatage spécial pour Slack
            $slackMessage = [
                'text' => $payload->title,
                'blocks' => [
                    [
                        'type' => 'section',
                        'text' => [
                            'type' => 'mrkdwn',
                            'text' => $formattedMessage
                        ]
                    ]
                ],
                'channel' => $this->resolveSlackChannel($payload->recipient)
            ];

            $response = $this->slackService->sendMessage($slackMessage);
            
            if ($response['ok']) {
                return "✅ Message Slack envoyé à {$payload->recipient}";
            } else {
                return "❌ Erreur Slack: " . ($response['error'] ?? 'Inconnue');
            }

        } catch (\Exception $e) {
            return "❌ Échec Slack: " . $e->getMessage();
        }
    }

    private function convertToHtml(string $markdown): string
    {
        // Conversion simple Markdown → HTML
        $html = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $markdown);
        $html = nl2br($html);
        
        return "
            <html>
                <body style='font-family: Arial, sans-serif;'>
                    <div style='padding: 20px;'>
                        {$html}
                    </div>
                </body>
            </html>
        ";
    }

    private function resolveSlackChannel(string $recipient): string
    {
        // Si c'est un @username, on le convertit en channel
        if (str_starts_with($recipient, '@')) {
            return $recipient;
        }
        
        // Si c'est un #channel
        if (str_starts_with($recipient, '#')) {
            return $recipient;
        }
        
        // Par défaut, on assume que c'est un username
        return "@{$recipient}";
    }
}

Test et débogage

Utilisez la commande de debug pour vérifier vos outils :

bin/console debug:mcp-tools

Résultat attendu :

MCP Tools Debug Information
===========================

Name               Description                                    Input Schema                                           
------------------|--------------------------------------------|-------------------------------------------
get_weather       | Récupère les informations météo            | App\Schema\GetWeatherSchema     
send_notification | Envoie des notifications intelligentes     | App\Schema\SendNotificationSchema 

Test via API

Test de l'outil météo

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_weather",
      "arguments": {
        "city": "Paris",
        "unit": "metric"
      }
    }
  }'

Test de l'outil de notifications

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "send_notification",
      "arguments": {
        "channel": "email",
        "recipient": "admin@epickone.com",
        "title": "Test MCP",
        "message": "Ceci est un test du système MCP",
        "priority": "normal"
      }
    }
  }'

🌟 Cas d'usage concrets

1. Assistant de développement

  • Outils : Analyse de code, génération de tests, déploiement
  • Ressources : Documentation, logs d'erreur, métriques
  • Prompts : Templates de debugging, revue de code

2. Support client automatisé

  • Outils : Recherche commandes, mise à jour statut, envoi notifications
  • Ressources : Base de connaissances, historique client
  • Prompts : Réponses standardisées, escalade

3. Analyse de données

  • Outils : Requêtes SQL, génération graphiques, export rapports
  • Ressources : Datasets, tableaux de bord
  • Prompts : Templates d'analyse, interprétation données

🔮 L'écosystème MCP en expansion

Adoption massive

  • Anthropic : Intégration native dans Claude
  • OpenAI : Support annoncé pour ChatGPT
  • Google : Intégration dans Vertex AI et Gemini
  • Microsoft : Support via .NET SDK

Outils disponibles

Plus de 200 serveurs MCP disponibles sur GitHub :

  • 📊 Databases : PostgreSQL, MySQL, SQLite
  • 🔧 DevOps : Git, Docker, Kubernetes
  • 📱 Services : Slack, GitHub, Jira
  • 💰 E-commerce : Stripe, Shopify

Future du MCP

  • Registry API : Découverte centralisée des serveurs
  • OAuth 2.0 : Authentification standardisée
  • Serveurs distants : Déploiement cloud natif
  • Multi-agents : Orchestration d'agents autonomes

⚡ Meilleures pratiques

Sécurité

// Validation stricte des entrées
#[Assert\Choice(['read', 'write'])]
public string $operation;

// Limitation des permissions
if ($operation === 'write' && !$this->isAuthorized()) {
    throw new UnauthorizedException();
}

Performance

// Cache des résultats fréquents
#[Cache(maxAge: 300)]
public function getExpensiveData(): array
{
    return $this->expensiveOperation();
}

Observabilité

// Logging détaillé
$this->logger->info('MCP tool called', [
    'tool' => 'get_weather',
    'city' => $payload->city,
    'duration' => $stopwatch->stop()->getDuration()
]);

🎯 Conclusion

Le Model Context Protocol représente une évolution majeure dans l'intégration des IA avec le monde réel. En standardisant ces interactions, MCP :

  • Simplifie le développement d'applications IA
  • Accélère l'innovation dans l'écosystème
  • Sécurise les interactions IA-outils
  • Démocratise l'accès aux capacités avancées

Avec Symfony et le MCP Server Bundle, créer des serveurs MCP robustes et sécurisés n'a jamais été aussi simple. L'écosystème PHP peut ainsi participer pleinement à cette révolution de l'IA.

Le futur de l'IA ne sera pas déterminé par le prochain grand modèle, mais par la qualité des intégrations avec nos données et outils métier. MCP nous donne les clés de ce futur.

🚀 Prochaines étapes

  1. Explorez le MCP Server Bundle
  2. Créez votre premier serveur MCP avec Symfony
  3. Contribuez à l'écosystème open-source
  4. Partagez vos créations avec la communauté

L'ère de l'IA contextualisée commence maintenant. Ne ratez pas le train ! 🚂