> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Créer un serveur MCP avec Laravel

> Construisez un serveur MCP prêt pour la production avec le package laravel/mcp. De l'implémentation d'outils, ressources et prompts jusqu'à l'authentification, les tests et le déploiement, tous les savoirs pratiques y sont réunis.

## 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.

| Primitive                  | Rôle                             | Usages principaux                                            |
| -------------------------- | -------------------------------- | ------------------------------------------------------------ |
| **Outils (Tools)**         | Fonctions que l'IA peut invoquer | Manipulation de données, intégration d'API externes, calculs |
| **Ressources (Resources)** | Contexte que l'IA peut charger   | Documents, informations de configuration, données dynamiques |
| **Prompts (Prompts)**      | Modèles réutilisables            | Requê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.

<Info>
  Ce guide avancé plonge dans l'implémentation concrète. Pour les concepts MCP de base, consultez [Niveau intermédiaire : Laravel MCP](/fr/mcp).
</Info>

## Installation et configuration initiale

<Steps>
  <Step title="Installer le package">
    Installez le package via Composer.

    ```shell theme={null}
    composer require laravel/mcp
    ```
  </Step>

  <Step title="Publier le fichier de routes">
    Avec `vendor:publish`, générez `routes/ai.php`, l'emplacement où enregistrer votre serveur MCP.

    ```shell theme={null}
    php artisan vendor:publish --tag=ai-routes
    ```
  </Step>

  <Step title="Générer la classe serveur">
    Créez la classe serveur via une commande Artisan.

    ```shell theme={null}
    php artisan make:mcp-server DatabaseServer
    ```

    Enregistrez les outils, ressources et prompts dans le fichier généré `app/Mcp/Servers/DatabaseServer.php`.
  </Step>

  <Step title="Enregistrer le serveur">
    Enregistrez le serveur en tant que route dans `routes/ai.php`.

    <CodeGroup>
      ```php Serveur web theme={null}
      use App\Mcp\Servers\DatabaseServer;
      use Laravel\Mcp\Facades\Mcp;

      Mcp::web('/mcp/database', DatabaseServer::class);
      ```

      ```php Serveur local theme={null}
      use App\Mcp\Servers\DatabaseServer;
      use Laravel\Mcp\Facades\Mcp;

      Mcp::local('database', DatabaseServer::class);
      ```
    </CodeGroup>

    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.
  </Step>
</Steps>

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

```shell theme={null}
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 theme={null}
<?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 "dupont@example.com"',
            '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.

```php theme={null}
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
{
    // ...
}
```

| Annotation         | Description                                              |
| ------------------ | -------------------------------------------------------- |
| `#[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`.

```php theme={null}
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.

```php theme={null}
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.

```php theme={null}
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

```shell theme={null}
php artisan make:mcp-resource AppDocumentationResource
```

```php theme={null}
<?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 theme={null}
<?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.

```php theme={null}
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

```shell theme={null}
php artisan make:mcp-prompt DataAnalysisPrompt
```

```php theme={null}
<?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}"
            ),
        ];
    }
}
```

<Tip>
  `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.
</Tip>

## 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>`.

```php theme={null}
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');
```

### Authentification OAuth 2.1

Pour une authentification plus robuste, utilisez Laravel Passport.

```php theme={null}
// 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`.

```shell theme={null}
php artisan vendor:publish --tag=mcp-views
```

```php theme={null}
// 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é.

```php theme={null}
// 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.

```php theme={null}
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é...
}
```

<Warning>
  `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`.
</Warning>

## 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 theme={null}
<?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 theme={null}
<?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 theme={null}
<?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 theme={null}
<?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'),
        ];
    }
}
```

<Warning>
  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 `..`.
</Warning>

## 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()`.

<CodeGroup>
  ```php Pest theme={null}
  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");
  });
  ```

  ```php PHPUnit theme={null}
  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");
  }
  ```
</CodeGroup>

### Tester ressources et prompts

```php theme={null}
// 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éthode                                  | Description                                      |
| ---------------------------------------- | ------------------------------------------------ |
| `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.

```shell theme={null}
# 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.

<CodeGroup>
  ```nginx Nginx theme={null}
  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;
  }
  ```

  ```apache Apache theme={null}
  # 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
  ```
</CodeGroup>

### 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.

<Warning>
  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.
</Warning>

### Limitation du débit

Limitez les requêtes vers le serveur MCP avec le middleware `throttle`.

```php theme={null}
// 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.

```php theme={null}
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.

```php theme={null}
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
```

<Tip>
  En production, surveillez les performances et les exceptions du serveur MCP avec Laravel Telescope ou Sentry.
</Tip>


## Related topics

- [Laravel MCP](/fr/mcp.md)
- [Créer un agent personnalisé pour Boost](/fr/advanced/boost-custom-agent.md)
- [Créer un fournisseur personnalisé pour l'AI SDK](/fr/advanced/ai-sdk-custom-provider.md)
- [Installer PHP, Composer et Node.js avec Homebrew (macOS)](/fr/blog/mac-setup.md)
- [MCP](/fr/packages/laravel-copilot-sdk/mcp.md)
