Vai al contenuto principale

Cos’è un server MCP (guida avanzata)

Il Model Context Protocol (MCP) è una specifica che consente a client AI (Claude, Cursor, GitHub Copilot, ecc.) e applicazioni di comunicare tramite un protocollo standardizzato. MCP ha tre primitive principali.
PrimitivaRuoloCasi d’uso principali
ToolFunzioni che l’AI può invocareOperazioni sui dati, integrazioni API esterne, calcoli
ResourceContesto che l’AI può leggereDocumentazione, informazioni di configurazione, dati dinamici
PromptTemplate riutilizzabiliQuery ricorrenti, guida a un workflow
Il vantaggio di costruire un server MCP in Laravel è poter riutilizzare tutto l’ecosistema Laravel: Eloquent, cache, autenticazione, validazione.
Questa guida avanzata entra nell’implementazione pratica. Per i concetti di base di MCP consulta Intermedio: Laravel MCP.

Installazione e setup iniziale

1

Installare il pacchetto

Installa con Composer.
composer require laravel/mcp
2

Pubblicare il file delle rotte

Con vendor:publish generi routes/ai.php, dove registrerai il server MCP.
php artisan vendor:publish --tag=ai-routes
3

Generare la classe server

Crea la classe server con un comando Artisan.
php artisan make:mcp-server DatabaseServer
Nel file generato app/Mcp/Servers/DatabaseServer.php registri tool, resource e prompt.
4

Registrare il server

In routes/ai.php colleghi il server a una rotta.
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);
Un server web viene raggiunto via HTTP POST. Un server locale gira come comando Artisan e serve per integrare client AI basati su CLI.

Implementazione dei tool

I tool sono funzioni invocabili dai client AI. Puoi usare direttamente service container, validazione ed Eloquent di Laravel.

Creare un tool

php artisan make:mcp-tool SearchUsersTool
Nella classe generata implementi i metodi handle e schema.

Definizione dei parametri (schema)

Nel metodo schema usi il builder Illuminate\Contracts\JsonSchema\JsonSchema per definire i parametri accettati.
<?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('Cerca utenti per nome o email.')]
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' => 'Indica una parola chiave di ricerca. Esempio: "Rossi" o "[email protected]"',
            'limit.max'      => 'Il numero massimo di risultati è 50.',
            'role.in'        => 'Il ruolo deve essere uno tra admin, editor, 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('Nessun utente trovato con i criteri indicati.');
        }

        $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('Parola chiave di ricerca. Cerca per nome o email.')
                ->required(),

            'limit' => $schema->integer()
                ->description('Numero massimo di risultati (default: 10, max: 50).')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('Ruolo per il filtro. Uno tra admin, editor, viewer.')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

Annotazioni dei tool

Le annotazioni del protocollo MCP aiutano il client AI a valutare la sicurezza del tool.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // tool di sola lettura, non modifica l'ambiente
#[IsIdempotent]    // chiamate ripetute con gli stessi argomenti danno lo stesso risultato
class SearchUsersTool extends Tool
{
    // ...
}
AnnotazioneDescrizione
#[IsReadOnly]Non modifica dati
#[IsDestructive]Esegue operazioni distruttive (es. cancellazione)
#[IsIdempotent]Riesecuzione con gli stessi argomenti sicura
#[IsOpenWorld]Comunica con entità esterne

Risposte strutturate

Per restituire una risposta JSON facile da parsare per il client AI usa Response::structured.
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

Risposte in streaming

Per elaborazioni lunghe puoi restituire un Generator per streammare l’avanzamento.
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,
        ]);

        // elaborazione pesante...
        yield Response::text("Elaborazione completata: {$item->name}");
    }
}
Nel server web le risposte in streaming vengono inviate automaticamente come stream SSE (Server-Sent Events).

Registrazione condizionale

Puoi esporre un tool solo a utenti che soddisfano certe condizioni.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

Implementazione delle risorse

Le risorse sono i dati che il client AI legge come contesto: documentazione, configurazioni, dati dinamici.

Risorsa statica

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('Panoramica della documentazione API e delle regole di business dell\'applicazione.')]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? 'Documentazione non trovata.';

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

Risorsa dinamica (URI template)

Con gli URI template puoi fornire risorse dinamiche in base ai parametri dell’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('Recupera le informazioni utente dato l\'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("Utente con ID {$userId} non trovato.");
        }

        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));
    }
}
Il client AI richiede la risorsa con URI del tipo app://users/42/profile; il valore di {userId} è recuperabile con $request->get('userId').

Annotazioni delle risorse

Puoi indicare priorità e audience della risorsa.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // destinata all'assistente AI
#[Priority(0.8)]               // importanza (0.0–1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

Implementazione dei prompt

I prompt sono template riutilizzabili usati dal client AI. Standardizzano query ricorrenti e workflow complessi.

Creare 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('Template di prompt per generare un report di analisi dati.')]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: 'Oggetto dell\'analisi. Esempio: "vendite del mese scorso", "andamento delle iscrizioni"',
                required: true,
            ),
            new Argument(
                name: 'format',
                description: 'Formato di output. "summary" (riassunto) o "detail" (dettagliato)',
                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'
            ? 'Crea un report dettagliato che includa numeri, trend, valori anomali e azioni consigliate.'
            : 'Crea un riassunto conciso con le metriche principali e tre insight chiave.';

        return [
            Response::text(
                "Sei un data analyst. {$instruction}"
            )->asAssistant(),
            Response::text(
                "Analizza i seguenti dati: {$target}"
            ),
        ];
    }
}
Con asAssistant() il messaggio è trattato come output dell’assistente AI. Combinando system prompt e messaggi utente controlli in modo fine il comportamento dell’AI.

Autenticazione e autorizzazione

Autenticazione token con Sanctum

Il metodo più semplice. Il client MCP invia l’header Authorization: Bearer <token>.
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

Autenticazione con OAuth 2.1

Per un’autenticazione più solida, usa Laravel Passport.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
Per OAuth pubblichi le view di autorizzazione fornite da MCP e le configuri in 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);
    });
}

Autenticazione con middleware custom

Se usi API token proprietari, validali con un middleware custom sull’header Authorization.
// 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);
}

Autorizzazione all’interno dei tool

Nel metodo handle di un tool o risorsa puoi usare $request->user() per controlli d’autorizzazione fine.
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error('Non hai i permessi per usare questo tool. Contatta un amministratore.');
    }

    // logica autorizzata...
}
shouldRegister si limita a nascondere il tool dalla lista. Il controllo di autorizzazione all’invocazione va sempre eseguito nel metodo handle.

Esempio pratico: tool per operazioni sul database

Esempio completo di tool che cercano e creano dati usando Eloquent.

Classe server

<?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('Server MCP per cercare e creare articoli del blog.')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

Tool di ricerca (sola lettura)

<?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('Cerca articoli del blog per parola chiave o stato.')]
#[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('Nessun articolo corrisponde ai criteri indicati.');
        }

        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('Parola chiave presente nel titolo o nel corpo.'),

            'status' => $schema->string()
                ->description('Stato dell\'articolo.')
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('Numero massimo di risultati (default: 5, max: 20).')
                ->default(5),
        ];
    }
}

Tool di creazione (scrittura)

<?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('Crea un nuovo articolo del blog come bozza.')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error('Non hai il permesso di creare articoli.');
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => 'Indica il titolo dell\'articolo.',
            'body.required'  => 'Indica il corpo dell\'articolo.',
        ]);

        $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'    => 'Articolo creato come bozza.',
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description('Titolo dell\'articolo (max 255 caratteri).')
                ->required(),

            'body' => $schema->string()
                ->description('Corpo dell\'articolo. È possibile usare Markdown.')
                ->required(),

            'tags' => $schema->array()
                ->description('Array di tag da assegnare. Esempio: ["Laravel", "PHP"]'),
        ];
    }
}

Esempio pratico: tool per il filesystem

Esempio di tool che opera sui file tramite il facade 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('Elenca i file nello storage e legge il contenuto dei file di testo.')]
#[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'];

        // previene la directory traversal
        if (str_contains($path, '..')) {
            return Response::error('È stato indicato un path non valido.');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("File non trovato: {$path}");
        }

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

        // restituisci il contenuto solo per i file di testo
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // restituisci i file immagine come binari
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("Formato file non supportato: {$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('Path del file da leggere. Esempio: "reports/2025-01.csv"')
                ->required(),

            'disk' => $schema->string()
                ->description('Disco storage da usare.')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
Nei tool che operano sui file esegui sempre la sanitizzazione del path e impedisci l’accesso al di fuori delle directory autorizzate. Rifiuta i path che contengono ...

Test

Server MCP, tool, risorse e prompt sono testabili con gli strumenti di test standard di Laravel.

Testare un tool

Chiami il tool direttamente con il metodo Server::tool().
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('è possibile cercare articoli', function () {
    Post::factory()->create(['title' => 'Basi di Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avanzato', 'status' => 'draft']);

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

    $response
        ->assertOk()
        ->assertSee('Basi di Laravel');
});

test('gli utenti senza permessi non possono creare articoli', function () {
    $user = User::factory()->create();

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

    $response->assertSee('Non hai il permesso di creare articoli');
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_can_search_posts(): void
{
    Post::factory()->create(['title' => 'Basi di Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avanzato', 'status' => 'draft']);

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

    $response
        ->assertOk()
        ->assertSee('Basi di Laravel');
}

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

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

    $response->assertSee('Non hai il permesso di creare articoli');
}

Testare risorse e prompt

// test di una risorsa
$response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class);
$response->assertOk()->assertSee('API');

// test di un prompt
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => 'vendite del mese scorso',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('data analyst');

Metodi di assertion principali

MetodoDescrizione
assertOk()La response non contiene errori
assertSee($text)La response contiene il testo indicato
assertHasErrors()La response contiene errori
assertHasNoErrors()La response non contiene errori
assertName($name)Verifica il nome del tool
assertSentNotification($method, $data)Verifica l’invio di una notifica
assertNotificationCount($count)Verifica il numero di notifiche inviate

Debug con MCP Inspector

Per debug interattivo usa MCP Inspector.
# ispezione del server web
php artisan mcp:inspector mcp/database

# ispezione del server locale
php artisan mcp:inspector database

Considerazioni per il deploy

HTTP streaming e SSE

Se usi risposte in streaming (Generator) sul server web, verifica la configurazione del server.
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # disabilita il buffering per SSE
    proxy_cache off;
    proxy_read_timeout 3600s;     # supporto per connessioni lunghe
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# usando 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

Combinazione con Laravel Octane

Per server MCP ad alto traffico valuta Laravel Octane (FrankenPHP o Swoole). L’overhead per richiesta si riduce significativamente.
Con Octane lo stato viene condiviso tra le richieste. Fai attenzione a non usare proprietà statiche o stato globale nei tool.

Rate limit

Limita le richieste al server MCP con il middleware throttle.
// definisci un rate limiter dedicato in config/cache.php
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

Cache

Per tool di sola lettura invocati di frequente sfrutta la cache.
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);
}

Log e monitoraggio

Loggando le chiamate ai tool MCP tieni sotto controllo l’utilizzo dei client AI.
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
In produzione ti consigliamo di monitorare performance ed eccezioni del server MCP con Laravel Telescope o Sentry.
Ultima modifica il 13 luglio 2026