Passer au contenu principal

Qu’est-ce qu’un serveur MCP (explication avancée)

Model Context Protocol (MCP) est une spécification qui permet à des clients IA (Claude, Cursor, GitHub Copilot, etc.) et à des applications de communiquer via un protocole standardisé. MCP repose sur trois primitives principales.
PrimitiveRôleUsages principaux
Outils (Tools)Fonctions que l’IA peut invoquerManipulation de données, intégration d’API externes, calculs
Ressources (Resources)Contexte que l’IA peut chargerDocuments, informations de configuration, données dynamiques
Prompts (Prompts)Modèles réutilisablesRequêtes types, guidage de workflow
L’avantage de construire un serveur MCP avec Laravel est de pouvoir mettre à profit l’écosystème Laravel — Eloquent, cache, authentification, validation — tel quel.
Ce guide avancé plonge dans l’implémentation concrète. Pour les concepts MCP de base, consultez Niveau intermédiaire : Laravel MCP.

Installation et configuration initiale

1

Installer le package

Installez le package via Composer.
composer require laravel/mcp
2

Publier le fichier de routes

Avec vendor:publish, générez routes/ai.php, l’emplacement où enregistrer votre serveur MCP.
php artisan vendor:publish --tag=ai-routes
3

Générer la classe serveur

Créez la classe serveur via une commande Artisan.
php artisan make:mcp-server DatabaseServer
Enregistrez les outils, ressources et prompts dans le fichier généré app/Mcp/Servers/DatabaseServer.php.
4

Enregistrer le serveur

Enregistrez le serveur en tant que route dans routes/ai.php.
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/database', DatabaseServer::class);
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('database', DatabaseServer::class);
Le serveur web est accédé via HTTP POST. Le serveur local fonctionne comme une commande Artisan et sert à s’intégrer aux clients IA en ligne de commande.

Implémentation des outils

Un outil est une fonction que le client IA peut invoquer. Vous pouvez utiliser directement le service container, la validation et Eloquent de Laravel.

Créer un outil

php artisan make:mcp-tool SearchUsersTool
Implémentez les méthodes handle et schema dans la classe générée.

Définition des paramètres (schéma)

Dans la méthode schema, utilisez le builder Illuminate\Contracts\JsonSchema\JsonSchema pour définir les paramètres acceptés.
<?php

namespace App\Mcp\Tools;

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 utilisateurs par nom ou adresse e-mail.')]
class SearchUsersTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'query'   => 'required|string|max:100',
            'limit'   => 'integer|min:1|max:50',
            'role'    => 'nullable|string|in:admin,editor,viewer',
        ], [
            'query.required' => 'Veuillez indiquer un mot-clé de recherche. Ex. : "Dupont" ou "[email protected]"',
            'limit.max'      => 'Le nombre maximum de résultats est de 50.',
            'role.in'        => 'Le rôle doit être admin, editor ou viewer.',
        ]);

        $users = \App\Models\User::query()
            ->where(function ($q) use ($validated) {
                $q->where('name', 'like', "%{$validated['query']}%")
                  ->orWhere('email', 'like', "%{$validated['query']}%");
            })
            ->when(isset($validated['role']), fn ($q) => $q->where('role', $validated['role']))
            ->limit($validated['limit'] ?? 10)
            ->get(['id', 'name', 'email', 'role']);

        if ($users->isEmpty()) {
            return Response::text('Aucun utilisateur ne correspond aux critères.');
        }

        $result = $users->map(fn ($u) => "ID:{$u->id} {$u->name} <{$u->email}> [{$u->role}]")
                        ->implode("\n");

        return Response::text($result);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()
                ->description('Mot-clé de recherche. Recherche par nom ou adresse e-mail.')
                ->required(),

            'limit' => $schema->integer()
                ->description('Nombre maximum de résultats (défaut : 10, max : 50).')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('Rôle pour filtrer. admin, editor ou viewer.')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

Annotations d’outils

Les annotations du protocole MCP permettent au client IA de juger de la sécurité de l’outil.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // Outil en lecture seule qui ne modifie pas l'environnement
#[IsIdempotent]    // Le résultat reste le même en cas d'appels multiples avec les mêmes arguments
class SearchUsersTool extends Tool
{
    // ...
}
AnnotationDescription
#[IsReadOnly]Ne modifie pas les données
#[IsDestructive]Effectue une opération destructive comme une suppression
#[IsIdempotent]Réexécution sûre avec les mêmes arguments
#[IsOpenWorld]Communique avec des entités externes

Réponses structurées

Pour renvoyer une réponse au format JSON facilement analysable par le client IA, utilisez Response::structured.
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

Réponses en streaming

Pour des traitements longs, renvoyer un Generator permet de streamer la progression.
use Generator;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;

public function handle(Request $request): Generator
{
    $items = \App\Models\Product::all();
    $total = $items->count();

    foreach ($items as $index => $item) {
        yield Response::notification('processing/progress', [
            'current' => $index + 1,
            'total'   => $total,
            'name'    => $item->name,
        ]);

        // Traitement lourd...
        yield Response::text("Traitement terminé : {$item->name}");
    }
}
Sur un serveur web, les réponses en streaming sont envoyées automatiquement sous forme de flux SSE (Server-Sent Events).

Enregistrement conditionnel

Vous pouvez ne rendre l’outil disponible qu’aux utilisateurs remplissant certaines conditions.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

Implémentation des ressources

Une ressource est une donnée que le client IA charge comme contexte. Elle fournit des documents, des informations de configuration ou des données dynamiques.

Ressources statiques

php artisan make:mcp-resource AppDocumentationResource
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('app://resources/documentation')]
#[MimeType('text/markdown')]
#[Description("Aperçu de la documentation d'API et des règles métier de l'application.")]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? 'Documentation introuvable.';

        return Response::text($content);
    }
}

Ressources dynamiques (templates d’URI)

Les templates d’URI permettent de fournir des ressources dynamiques en fonction des paramètres de l’URL.
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Support\UriTemplate;

#[MimeType('application/json')]
#[Description("Récupère les informations d'un utilisateur d'après son ID.")]
class UserProfileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('app://users/{userId}/profile');
    }

    public function handle(Request $request): Response
    {
        $userId = $request->get('userId');

        $user = \App\Models\User::find($userId);

        if (! $user) {
            return Response::error("Utilisateur d'ID {$userId} introuvable.");
        }

        return Response::text(json_encode([
            'id'         => $user->id,
            'name'       => $user->name,
            'email'      => $user->email,
            'created_at' => $user->created_at->toIso8601String(),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    }
}
Le client IA demande la ressource via une URI comme app://users/42/profile, et la valeur de {userId} s’obtient avec $request->get('userId').

Annotations de ressources

Vous pouvez expliciter la priorité ou l’audience d’une ressource.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // Destinée à l'assistant IA
#[Priority(0.8)]               // Importance (0.0 à 1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

Implémentation des prompts

Un prompt est un modèle réutilisable que le client IA peut invoquer. Il standardise des requêtes types ou des workflows complexes.

Créer un prompt

php artisan make:mcp-prompt DataAnalysisPrompt
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

#[Description("Modèle de prompt permettant de générer un rapport d'analyse de données.")]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: "Cible de l'analyse. Ex. : \"ventes du mois dernier\", \"évolution du nombre d'inscriptions\"",
                required: true,
            ),
            new Argument(
                name: 'format',
                description: 'Format de sortie. "summary" (résumé) ou "detail" (détaillé)',
                required: false,
            ),
        ];
    }

    public function handle(Request $request): array
    {
        $validated = $request->validate([
            'target' => 'required|string|max:200',
            'format' => 'nullable|in:summary,detail',
        ]);

        $target = $validated['target'];
        $format = $validated['format'] ?? 'summary';
        $instruction = $format === 'detail'
            ? 'Rédigez un rapport détaillé incluant chiffres, tendances, anomalies et actions recommandées.'
            : 'Rédigez un résumé concis incluant les indicateurs clés et trois enseignements majeurs.'; 

        return [
            Response::text(
                "Vous êtes un analyste de données. {$instruction}"
            )->asAssistant(),
            Response::text(
                "Veuillez analyser les données suivantes : {$target}"
            ),
        ];
    }
}
asAssistant() fait considérer le message comme une intervention de l’assistant IA. En combinant système et message utilisateur, vous pouvez contrôler finement le comportement de l’IA.

Authentification et autorisation

Authentification par token via Sanctum

C’est la méthode la plus simple. Le client MCP fournit un en-tête Authorization: Bearer <token>.
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

Authentification OAuth 2.1

Pour une authentification plus robuste, utilisez Laravel Passport.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
Avec l’authentification OAuth, publiez les vues d’autorisation fournies par MCP et configurez-les dans AppServiceProvider.
php artisan vendor:publish --tag=mcp-views
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::authorizationView(function ($parameters) {
        return view('mcp.authorize', $parameters);
    });
}

Authentification via un middleware personnalisé

Si vous utilisez des tokens API maison, validez l’en-tête Authorization dans un middleware personnalisé.
// app/Http/Middleware/McpTokenMiddleware.php
public function handle($request, Closure $next)
{
    $token = $request->bearerToken();

    if (! $token || ! \App\Models\ApiToken::where('token', hash('sha256', $token))->exists()) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $next($request);
}

Autorisation à l’intérieur de l’outil

Dans la méthode handle d’un outil ou d’une ressource, utilisez $request->user() pour effectuer des contrôles d’autorisation fins.
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error("Vous n'avez pas la permission d'utiliser cet outil. Contactez un administrateur.");
    }

    // Suite du traitement autorisé...
}
shouldRegister ne fait que masquer l’outil de la liste. Effectuez systématiquement le contrôle d’autorisation au moment de l’appel, dans la méthode handle.

Exemple pratique : outils d’accès à la base de données

Voici une implémentation complète d’outils recherchant et créant des données via Eloquent.

Classe serveur

<?php

namespace App\Mcp\Servers;

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

#[Name('Blog Management Server')]
#[Version('1.0.0')]
#[Instructions('Serveur MCP qui permet de rechercher et de créer des articles de blog.')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

Outil de recherche (lecture seule)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('Recherche des articles de blog par mot-clé et statut.')]
#[IsReadOnly]
#[IsIdempotent]
class SearchPostsTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'keyword' => 'nullable|string|max:100',
            'status'  => 'nullable|in:draft,published,archived',
            'limit'   => 'integer|min:1|max:20',
        ]);

        $posts = Post::query()
            ->when($validated['keyword'] ?? null, fn ($q, $kw) =>
                $q->where('title', 'like', "%{$kw}%")
                  ->orWhere('body', 'like', "%{$kw}%")
            )
            ->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
            ->latest()
            ->limit($validated['limit'] ?? 5)
            ->get(['id', 'title', 'status', 'published_at']);

        if ($posts->isEmpty()) {
            return Response::text('Aucun article ne correspond aux critères.');
        }

        return Response::structured([
            'total' => $posts->count(),
            'posts' => $posts->map(fn ($p) => [
                'id'           => $p->id,
                'title'        => $p->title,
                'status'       => $p->status,
                'published_at' => $p->published_at?->toIso8601String(),
            ])->toArray(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'keyword' => $schema->string()
                ->description('Mot-clé contenu dans le titre ou le corps.'),

            'status' => $schema->string()
                ->description("Statut de l'article.")
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('Nombre maximum de résultats (défaut : 5, max : 20).')
                ->default(5),
        ];
    }
}

Outil de création (écriture)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
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('Crée un nouvel article de blog en tant que brouillon.')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error("Vous n'avez pas la permission de créer un article.");
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => "Veuillez indiquer le titre de l'article.",
            'body.required'  => "Veuillez indiquer le corps de l'article.",
        ]);

        $post = Post::create([
            'title'   => $validated['title'],
            'body'    => $validated['body'],
            'status'  => 'draft',
            'user_id' => $user->id,
        ]);

        if (! empty($validated['tags'])) {
            $post->syncTags($validated['tags']);
        }

        return Response::structured([
            'id'         => $post->id,
            'title'      => $post->title,
            'status'     => $post->status,
            'created_at' => $post->created_at->toIso8601String(),
            'message'    => "Article créé en tant que brouillon.",
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description("Titre de l'article (255 caractères max).")
                ->required(),

            'body' => $schema->string()
                ->description("Corps de l'article. Markdown accepté.")
                ->required(),

            'tags' => $schema->array()
                ->description('Tableau de tags à attribuer à l\'article. Ex. : ["Laravel", "PHP"]'),
        ];
    }
}

Exemple pratique : outil d’opérations sur le système de fichiers

Voici un outil manipulant des fichiers via la façade Storage.
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('Récupère la liste des fichiers du stockage et lit le contenu des fichiers texte.')]
#[IsReadOnly]
class ReadFileTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'path'    => 'required|string|max:500',
            'disk'    => 'nullable|string|in:local,public,s3',
        ]);

        $disk = $validated['disk'] ?? 'local';
        $path = $validated['path'];

        // Empêche les attaques par traversée de répertoire
        if (str_contains($path, '..')) {
            return Response::error('Chemin invalide.');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("Fichier introuvable : {$path}");
        }

        $mimeType = Storage::disk($disk)->mimeType($path);

        // Retourne le contenu uniquement pour les fichiers texte
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // Renvoie les images en binaire
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("Format de fichier non pris en charge : {$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('Chemin du fichier à lire. Ex. : "reports/2025-01.csv"')
                ->required(),

            'disk' => $schema->string()
                ->description('Disque de stockage à utiliser.')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
Pour les outils de fichiers, assainissez systématiquement les chemins pour empêcher l’accès à des dossiers non autorisés. Il est essentiel de refuser les chemins contenant ...

Tests

Serveur MCP, outils, ressources et prompts se testent unitairement avec les fonctionnalités de test standard de Laravel.

Tester un outil

Appelez directement l’outil via Server::tool().
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('les articles peuvent être recherchés', function () {
    Post::factory()->create(['title' => 'Les bases de Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avancé', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Les bases de Laravel');
});

test("un utilisateur sans permission ne peut pas créer d'article", function () {
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => 'Article de test',
            'body'  => 'Corps',
        ]);

    $response->assertSee("Vous n'avez pas la permission de créer un article");
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_les_articles_peuvent_etre_recherches(): void
{
    Post::factory()->create(['title' => 'Les bases de Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avancé', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Les bases de Laravel');
}

public function test_un_utilisateur_sans_permission_ne_peut_pas_creer_darticle(): void
{
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => 'Article de test',
            'body'  => 'Corps',
        ]);

    $response->assertSee("Vous n'avez pas la permission de créer un article");
}

Tester ressources et prompts

// Test d'une ressource
$response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class);
$response->assertOk()->assertSee('API');

// Test d'un prompt
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => 'ventes du mois dernier',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('analyste de données');

Principales méthodes d’assertion

MéthodeDescription
assertOk()Vérifie que la réponse ne contient pas d’erreur
assertSee($text)Vérifie que la réponse contient le texte donné
assertHasErrors()Vérifie que la réponse comporte des erreurs
assertHasNoErrors()Vérifie que la réponse ne comporte pas d’erreurs
assertName($name)Vérifie le nom de l’outil
assertSentNotification($method, $data)Vérifie qu’une notification a été envoyée
assertNotificationCount($count)Vérifie le nombre de notifications envoyées

Débogage avec MCP Inspector

Pour un débogage interactif, utilisez MCP Inspector.
# Inspection d'un serveur web
php artisan mcp:inspector mcp/database

# Inspection d'un serveur local
php artisan mcp:inspector database

Points à considérer pour le déploiement

HTTP streaming et SSE

Si vous utilisez des réponses en streaming (Generator) sur un serveur web, vérifiez la configuration du serveur.
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # Désactiver la mise en tampon pour SSE
    proxy_cache off;
    proxy_read_timeout 3600s;     # Autoriser les connexions longues
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# Avec mod_proxy_http
ProxyPass /mcp http://127.0.0.1:8000/mcp
ProxyPassReverse /mcp http://127.0.0.1:8000/mcp
SetEnv proxy-sendchunks 1

Combinaison avec Laravel Octane

Pour les serveurs MCP à fort trafic, envisagez Laravel Octane (FrankenPHP ou Swoole). L’overhead par requête est fortement réduit.
Avec Octane, l’état est partagé entre les requêtes. Prenez garde à ne pas utiliser de propriétés statiques ni d’état global à l’intérieur de vos outils.

Limitation du débit

Limitez les requêtes vers le serveur MCP avec le middleware throttle.
// Définir un rate limiter dédié dans config/cache.php
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

Cache

Tirez parti du cache pour les outils en lecture seule fréquemment invoqués.
public function handle(Request $request): Response
{
    $data = \Illuminate\Support\Facades\Cache::remember(
        "mcp:search:{$request->get('keyword')}",
        now()->addMinutes(5),
        fn () => $this->fetchFromDatabase($request)
    );

    return Response::structured($data);
}

Journalisation et supervision

Journaliser les appels aux outils MCP permet de comprendre comment le client IA les utilise.
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
En production, surveillez les performances et les exceptions du serveur MCP avec Laravel Telescope ou Sentry.
Dernière modification le 13 juillet 2026