d4v / dev blog
/ Laravel

Créer un serveur MCP avec Laravel : exposer son application à une IA

Créer un serveur MCP avec Laravel : exposer son application à une IA

Créer un serveur MCP avec Laravel

Le Model Context Protocol (MCP) est un protocole ouvert qui standardise la façon dont un client IA (Claude, Cursor, un agent maison…) dialogue avec une application : appeler des actions, lire des données, réutiliser des gabarits de prompt. Plutôt que de bricoler une intégration spécifique par modèle, on expose une surface unique et l'IA s'y branche.

Pour nous, développeurs Laravel, l'intérêt est direct : on peut exposer une application existante — ses recherches, ses créations d'enregistrements, ses ressources — à un assistant, sans réécrire la logique métier. Et bonne nouvelle, depuis fin 2025 il existe un package officiel maintenu par l'équipe Laravel (laravel/mcp). Fini les wrappers tiers à la pérennité incertaine : on reste dans l'écosystème, avec le conteneur, la validation et les middlewares qu'on connaît déjà.

Installation

On commence par installer le package via Composer, puis on publie le fichier de routes dédié :

composer require laravel/mcp
php artisan vendor:publish --tag=ai-routes

Cette dernière commande crée routes/ai.php, le fichier où l'on déclarera nos serveurs. C'est l'équivalent de routes/web.php, mais pour les agents IA.

Créer un serveur

Un serveur est le point central qui expose les capacités (outils, ressources, prompts) au client. On le génère avec une commande Artisan :

php artisan make:mcp-server CrmServer

La classe générée se place dans app/Mcp/Servers. Les métadonnées passent par des attributs PHP 8, et les capacités s'enregistrent dans de simples tableaux :

<?php

namespace App\Mcp\Servers;

use App\Mcp\Tools\SearchClientsTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;

#[Name('CRM Server')]
#[Version('1.0.0')]
#[Instructions('Ce serveur permet de rechercher des clients et de créer des tâches dans le CRM.')]
class CrmServer extends Server
{
    protected array $tools = [
        SearchClientsTool::class,
    ];

    protected array $resources = [
        //
    ];

    protected array $prompts = [
        //
    ];
}

Enregistrer le serveur : web ou local

Deux modes de déclaration dans routes/ai.php. Le mode web expose le serveur en HTTP — c'est le cas le plus courant, idéal pour un client distant :

use App\Mcp\Servers\CrmServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/crm', CrmServer::class)
    ->middleware(['throttle:mcp']);

Comme pour une route classique, on peut y appliquer des middlewares. Le mode local, lui, fait tourner le serveur comme une commande Artisan — pratique pour un assistant qui tourne sur la même machine :

Mcp::local('crm', CrmServer::class);

Le cœur du sujet : les outils

Un outil (tool) est une fonctionnalité que l'IA peut invoquer. C'est là que vit votre logique métier. On le génère ainsi :

php artisan make:mcp-tool SearchClientsTool

Un outil définit trois choses : une description (essentielle, c'est elle qui aide le modèle à savoir quand l'utiliser), un schéma d'entrée et une méthode handle. Voici un exemple complet :

<?php

namespace App\Mcp\Tools;

use App\Models\Client;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Recherche des clients par nom ou ville et renvoie leurs coordonnées.')]
class SearchClientsTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'terme' => ['required', 'string', 'max:100'],
            'limite' => ['integer', 'min:1', 'max:50'],
        ], [
            'terme.required' => 'Vous devez fournir un nom ou une ville à rechercher.',
        ]);

        $clients = Client::query()
            ->where('nom', 'like', "%{$validated['terme']}%")
            ->orWhere('ville', 'like', "%{$validated['terme']}%")
            ->limit($validated['limite'] ?? 10)
            ->get(['id', 'nom', 'ville', 'email']);

        return Response::structured($clients->toArray());
    }

    /**
     * @return array<string, \Illuminate\JsonSchema\Types\Type>
     */
    public function schema(JsonSchema $schema): array
    {
        return [
            'terme' => $schema->string()
                ->description('Le nom ou la ville à rechercher.')
                ->required(),

            'limite' => $schema->integer()
                ->description('Nombre maximum de résultats.')
                ->default(10),
        ];
    }
}

Quelques points qui méritent l'attention.

La description fait tout le travail. Elle n'est pas générée automatiquement et constitue la métadonnée la plus importante : c'est sur elle que le modèle se base pour décider d'appeler l'outil. Soignez-la comme vous soigneriez le nom d'une méthode publique.

La validation est celle de Laravel. Le schéma JSON donne la structure, mais pour les règles fines on retombe sur le validateur habituel dans handle. En cas d'échec, c'est votre message d'erreur que l'IA recevra et interprétera — d'où l'intérêt de messages clairs et actionnables, rédigés pour le modèle.

Le conteneur résout vos dépendances. On peut type-hinter un repository ou un service, que ce soit dans le constructeur ou directement dans handle :

public function handle(Request $request, ClientRepository $clients): Response
{
    // $clients est injecté automatiquement
}

Les réponses

Une méthode handle retourne une instance de Laravel\Mcp\Response. Le cas le plus simple est du texte :

return Response::text('3 clients trouvés à Rouen.');

Pour des données exploitables par le client, on préfère une réponse structurée, qui fournit du JSON parsable tout en gardant une représentation texte de secours :

return Response::structured([
    'total' => 3,
    'clients' => $clients->toArray(),
]);

On peut aussi renvoyer une erreur, plusieurs contenus à la fois, des images, ou même streamer des résultats au fil de l'eau en retournant un générateur (utile pour les traitements longs ; en mode web, un flux SSE s'ouvre alors automatiquement).

Des annotations pour cadrer le comportement

Des attributs permettent d'indiquer au client la nature de l'outil. C'est précieux pour la sécurité : un assistant peut traiter différemment un outil en lecture seule d'un outil destructeur.

use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;

#[IsReadOnly]
#[IsIdempotent]
class SearchClientsTool extends Tool
{
    //
}

Les annotations disponibles couvrent la lecture seule, le caractère destructeur, l'idempotence et l'interaction avec des entités externes.

Ressources et prompts, en deux mots

Au-delà des outils, un serveur peut exposer des ressources (make:mcp-resource) — des contenus que l'IA lit comme contexte : documentation, configuration, données dynamiques via des gabarits d'URI. Et des prompts (make:mcp-prompt) — des gabarits de requête réutilisables, avec arguments, que le client peut instancier. La mécanique (description, validation, injection de dépendances) est rigoureusement la même que pour les outils, ce qui rend l'apprentissage très rapide une fois le premier outil écrit.

Sécuriser l'accès

Un serveur web s'authentifie comme n'importe quelle route, par middleware. Deux approches.

Pour un jeton simple passé dans l'en-tête Authorization, Laravel Sanctum suffit. Pour une protection robuste avec consentement de l'utilisateur, on passe par OAuth 2.1 via Laravel Passport :

use App\Mcp\Servers\CrmServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/crm', CrmServer::class)
    ->middleware('auth:api');

Mcp::oauthRoutes() enregistre les routes de découverte et d'enregistrement client requises par le protocole. L'utilisateur se voit alors présenter un écran d'autorisation pour approuver ou refuser la connexion de l'agent — exactement ce qu'on veut pour exposer des données sensibles.

Tester son serveur

Le package fournit un MCP Inspector, une interface pour explorer et appeler manuellement les capacités du serveur — l'équivalent d'un Postman dédié. Et bien sûr, comme tout résout via le conteneur, vos outils restent testables unitairement avec PHPUnit ou Pest, sans rien simuler de magique.

En pratique

Ce qui frappe en montant un premier serveur, c'est l'absence de friction : on reste dans Laravel de bout en bout. La validation, l'injection de dépendances, les middlewares, les commandes Artisan — tout est là où on l'attend. La vraie réflexion n'est pas technique mais conceptuelle : quelle surface de votre application a du sens à exposer, et avec quelles garanties.

Pour qui auto-héberge ses outils, c'est aussi une perspective intéressante : brancher ses propres applications sur un assistant, sans confier ses données à un service tiers et en gardant la main sur l'authentification. Le protocole standardise l'interface ; à vous de décider ce qui passe la porte.

La documentation officielle (laravel.com/docs/mcp) détaille les cas plus avancés : schémas de sortie, métadonnées _meta, enregistrement conditionnel d'outils, et même le rendu d'applications HTML interactives via les MCP Apps. De quoi aller bien au-delà du simple appel d'outil.

Commentaires

Aucun commentaire pour le moment. Lancez la discussion !