Saltar al contenido principal

Qué es un servidor MCP (visión avanzada)

Model Context Protocol (MCP) es una especificación que permite que los clientes de IA (Claude, Cursor, GitHub Copilot, etc.) y las aplicaciones se comuniquen mediante un protocolo estandarizado. MCP define tres primitivas principales.
PrimitivaRolUsos habituales
Tools (herramientas)Funciones que la IA puede invocar.Manipulación de datos, integración con APIs externas, cálculos.
Resources (recursos)Contexto que la IA puede leer.Documentación, información de configuración, datos dinámicos.
PromptsPlantillas reutilizables.Consultas repetitivas, guiado de flujos de trabajo.
La ventaja de construir un servidor MCP con Laravel es que puedes reutilizar tal cual el ecosistema de Laravel: Eloquent, caché, autenticación, validación, etc.
Esta guía avanzada entra en detalles de implementación práctica. Para los conceptos básicos de MCP, consulta Nivel intermedio: Laravel MCP.

Instalación y configuración inicial

1

Instala el paquete

Instala el paquete con Composer.
composer require laravel/mcp
2

Publica el archivo de rutas

Con vendor:publish genera routes/ai.php, donde se registran los servidores MCP.
php artisan vendor:publish --tag=ai-routes
3

Genera la clase del servidor

Crea la clase del servidor con un comando Artisan.
php artisan make:mcp-server DatabaseServer
Registra las tools, resources y prompts en la clase generada app/Mcp/Servers/DatabaseServer.php.
4

Registra el servidor

Registra el servidor en 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);
El servidor web se accede vía HTTP POST. El servidor local funciona como un comando Artisan y se usa para integrarlo con clientes de IA basados en CLI.

Implementación de tools

Las tools son funciones que la IA puede invocar. Puedes usar sin cambios el contenedor de servicios, la validación y Eloquent de Laravel.

Crear una tool

php artisan make:mcp-tool SearchUsersTool
Implementa handle y schema en la clase generada.

Definir parámetros (schema)

En schema, usa el builder Illuminate\Contracts\JsonSchema\JsonSchema para declarar los parámetros aceptados.
<?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('Busca usuarios por nombre o correo electrónico.')]
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 palabra clave de búsqueda. Ej.: "María" o "[email protected]"',
            'limit.max'      => 'Puedes solicitar como máximo 50 resultados.',
            'role.in'        => 'El rol debe ser admin, editor o 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('No se han encontrado usuarios que coincidan.');
        }

        $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('Palabra clave de búsqueda. Se busca en nombre o email.')
                ->required(),

            'limit' => $schema->integer()
                ->description('Límite de resultados (por defecto 10, máximo 50).')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('Filtro por rol. admin, editor o viewer.')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

Anotaciones de tool

Las anotaciones del protocolo MCP permiten a los clientes de IA valorar la seguridad de la tool.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // Tool de solo lectura, no cambia el entorno
#[IsIdempotent]    // El resultado no cambia al llamar varias veces con los mismos argumentos
class SearchUsersTool extends Tool
{
    // ...
}
AnotaciónDescripción
#[IsReadOnly]No modifica datos.
#[IsDestructive]Realiza operaciones destructivas como borrar.
#[IsIdempotent]Es seguro reejecutarla con los mismos argumentos.
#[IsOpenWorld]Se comunica con entidades externas.

Respuesta estructurada

Para devolver respuestas en JSON fácil de parsear por el cliente de IA, usa Response::structured.
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

Respuesta en streaming

Para procesos largos, devuelve un Generator para emitir progreso.
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,
        ]);

        // Procesamiento pesado...
        yield Response::text("Completado: {$item->name}");
    }
}
En un servidor web, la respuesta en streaming se envía automáticamente como un stream SSE (Server-Sent Events).

Registro condicional

Puedes exponer una tool solo a los usuarios que cumplan cierta condición.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

Implementación de recursos

Los recursos son datos que la IA lee como contexto. Sirven para documentación, información de configuración o datos dinámicos.

Recurso estático

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('Resumen de la documentación de la API y las reglas de negocio de la aplicación.')]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? 'No se encontró la documentación.';

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

Recurso dinámico (plantilla URI)

Con una plantilla URI, puedes ofrecer recursos dinámicos según los parámetros de la 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('Obtiene la información del usuario indicado por su 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("No se encontró el usuario con ID {$userId}.");
        }

        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));
    }
}
El cliente de IA solicita el recurso con una URI como app://users/42/profile, y el valor de {userId} se obtiene con $request->get('userId').

Anotaciones de recurso

Puedes indicar la prioridad y la audiencia del recurso.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // Orientado al asistente de IA
#[Priority(0.8)]               // Importancia (0.0 – 1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

Implementación de prompts

Los prompts son plantillas reutilizables que puede usar el cliente de IA. Estandarizan consultas repetitivas o flujos de trabajo complejos.

Crear 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('Plantilla de prompt para generar un informe de análisis de datos.')]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: 'Objeto a analizar. Ej.: "ventas del mes pasado", "evolución de registros de usuario"',
                required: true,
            ),
            new Argument(
                name: 'format',
                description: 'Formato de salida. "summary" (resumen) o "detail" (detallado)',
                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'
            ? 'Elabora un informe detallado que incluya cifras, tendencias, valores atípicos y acciones recomendadas.'
            : 'Genera un resumen breve con los indicadores principales y tres insights clave.';

        return [
            Response::text(
                "Eres un analista de datos. {$instruction}"
            )->asAssistant(),
            Response::text(
                "Analiza estos datos: {$target}"
            ),
        ];
    }
}
Con asAssistant(), el mensaje se trata como si lo dijera el asistente de IA. Combinando prompts de sistema y mensajes de usuario, puedes controlar con detalle el comportamiento del modelo.

Autenticación y autorización

Autenticación por token con Sanctum

La forma más sencilla. El cliente MCP envía la cabecera Authorization: Bearer <token>.
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

Autenticación con OAuth 2.1

Para una autenticación más robusta, usa Laravel Passport.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
Si usas OAuth, publica las vistas de autorización de MCP y configúralas en 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);
    });
}

Autenticación con middleware personalizado

Si utilizas tokens propios, valida el Authorization con un middleware personalizado.
// 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);
}

Autorización dentro de la tool

Dentro de handle de la tool o el recurso, usa $request->user() para hacer controles de autorización finos.
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error('No tienes permiso para usar esta herramienta. Contacta con un administrador.');
    }

    // Sigue el procesamiento autorizado...
}
shouldRegister solo oculta la tool de la lista. Cuando la tool se invoca, la comprobación de autorización debe hacerse siempre dentro de handle.

Ejemplo práctico: tools de base de datos

Ejemplo completo de tools que buscan y crean datos usando Eloquent.

Clase servidor

<?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('Servidor MCP para buscar y crear posts de blog.')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

Tool de búsqueda (solo lectura)

<?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('Busca posts de blog por palabra clave o estado.')]
#[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('No se han encontrado posts que coincidan.');
        }

        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('Palabra clave contenida en el título o cuerpo.'),

            'status' => $schema->string()
                ->description('Estado del post.')
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('Límite de resultados (por defecto 5, máximo 20).')
                ->default(5),
        ];
    }
}

Tool de creación (escritura)

<?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 nuevo post de blog como borrador.')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error('No tienes permiso para crear posts.');
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => 'Indica el título del post.',
            'body.required'  => 'Indica el cuerpo del post.',
        ]);

        $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'    => 'Se ha creado el post como borrador.',
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description('Título del post (máximo 255 caracteres).')
                ->required(),

            'body' => $schema->string()
                ->description('Cuerpo del post. Se puede usar Markdown.')
                ->required(),

            'tags' => $schema->array()
                ->description('Array de tags que se aplicarán al post. Ej.: ["Laravel", "PHP"]'),
        ];
    }
}

Ejemplo práctico: tool de manipulación de archivos

Ejemplo de tool que manipula archivos con la 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('Lista los archivos del almacenamiento y lee el contenido de archivos de texto.')]
#[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'];

        // Evita ataques de directory traversal
        if (str_contains($path, '..')) {
            return Response::error('Ruta inválida.');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("Archivo no encontrado: {$path}");
        }

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

        // Devuelve el contenido solo si es un archivo de texto
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // Devuelve archivos de imagen como binario
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("Formato no soportado: {$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('Ruta del archivo a leer. Ej.: "reports/2025-01.csv"')
                ->required(),

            'disk' => $schema->string()
                ->description('Disco de almacenamiento a usar.')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
En tools de manipulación de archivos, sanea siempre las rutas y evita accesos fuera de directorios permitidos. Rechaza rutas que contengan ...

Pruebas

Puedes escribir tests unitarios para servidores, tools, recursos y prompts MCP con las funcionalidades de test estándar de Laravel.

Test de una tool

Con Server::tool() invocas la tool directamente para testearla.
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('permite buscar posts', function () {
    Post::factory()->create(['title' => 'Fundamentos de Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avanzado', 'status' => 'draft']);

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

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

test('un usuario sin permisos no puede crear posts', function () {
    $user = User::factory()->create();

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

    $response->assertSee('No tienes permiso para crear posts');
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_permite_buscar_posts(): void
{
    Post::factory()->create(['title' => 'Fundamentos de Laravel', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP avanzado', 'status' => 'draft']);

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

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

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

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

    $response->assertSee('No tienes permiso para crear posts');
}

Test de recursos y prompts

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

// Test de prompt
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => 'ventas del mes pasado',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('analista de datos');

Principales métodos de aserción

MétodoDescripción
assertOk()Verifica que la respuesta no tiene errores.
assertSee($text)Verifica que la respuesta contiene el texto indicado.
assertHasErrors()Verifica que la respuesta contiene errores.
assertHasNoErrors()Verifica que la respuesta no contiene errores.
assertName($name)Comprueba el nombre de la tool.
assertSentNotification($method, $data)Comprueba que se envió una notificación.
assertNotificationCount($count)Comprueba el número de notificaciones enviadas.

Depuración con MCP Inspector

Para depuración interactiva, usa MCP Inspector.
# Inspeccionar un servidor web
php artisan mcp:inspector mcp/database

# Inspeccionar un servidor local
php artisan mcp:inspector database

Consideraciones de despliegue

HTTP streaming y SSE

Si vas a usar respuestas en streaming (Generator) en el servidor web, revisa la configuración del servidor.
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # Desactiva el buffering para SSE
    proxy_cache off;
    proxy_read_timeout 3600s;     # Soporta conexiones largas
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# Si usas 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

Combinación con Laravel Octane

Para servidores MCP de alto tráfico, considera Laravel Octane (FrankenPHP o Swoole). Reduce drásticamente la sobrecarga por petición.
Con Octane, el estado se comparte entre peticiones. No uses propiedades estáticas ni estado global dentro de las tools.

Rate limiting

Limita las peticiones al servidor MCP con el middleware throttle.
// Define un rate limiter dedicado en config/cache.php
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

Caché

Para tools de solo lectura llamadas con frecuencia, usa caché.
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);
}

Logs y monitorización

Registrar las llamadas a las tools MCP te permite entender cómo las usan los clientes de IA.
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 producción, se recomienda monitorizar el rendimiento y las excepciones del servidor MCP con Laravel Telescope o Sentry.
Última modificación el 13 de julio de 2026