Construye un servidor MCP listo para producción con el paquete laravel/mcp. Cubre desde la implementación de herramientas, recursos y prompts hasta la autenticación, las pruebas y el despliegue.
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.
Primitiva
Rol
Usos 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.
Prompts
Plantillas 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.
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.
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 argumentosclass SearchUsersTool extends Tool{ // ...}
<?phpnamespace 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); }}
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{ // ...}
<?phpnamespace 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.
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.
<?phpnamespace 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"]'), ]; }}
// 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');