Saltar al contenido principal

Qué es MCP

Model Context Protocol (MCP) es una especificación que estandariza la comunicación entre clientes de IA (Claude, Cursor, GitHub Copilot, etc.) y las aplicaciones. Al implementar un servidor MCP, los agentes de IA pueden acceder a los datos de tu aplicación Laravel y ejecutar acciones sobre ella.
Laravel MCP es un paquete oficial añadido en Laravel 13. Se distribuye como laravel/ai — más concretamente laravel/mcp — y ofrece todo lo necesario para construir servidores MCP.
Un servidor MCP puede ofrecer tres tipos de capacidades:
CapacidadDescripción
ToolsFunciones invocables por el cliente de IA (búsqueda, actualización, integración con APIs externas, etc.)
ResourcesDatos o información de contexto legible por el cliente
PromptsPlantillas de prompt reutilizables

Instalación

Instala el paquete con Composer.
composer require laravel/mcp
Después ejecuta vendor:publish para generar el archivo routes/ai.php.
php artisan vendor:publish --tag=ai-routes
Este comando crea routes/ai.php, donde registrarás los servidores MCP.

Crear el servidor

Genera una clase de servidor con make:mcp-server.
php artisan make:mcp-server WeatherServer
La clase se ubica en el directorio app/Mcp/Servers.
<?php

namespace App\Mcp\Servers;

use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Weather Server')]
#[Version('1.0.0')]
#[Instructions('This server provides weather information and forecasts.')]
class WeatherServer extends Server
{
    protected array $tools = [
        // GetCurrentWeatherTool::class,
    ];

    protected array $resources = [
        // WeatherGuidelinesResource::class,
    ];

    protected array $prompts = [
        // DescribeWeatherPrompt::class,
    ];
}

Registrar el servidor

Registra el servidor en routes/ai.php. Existen dos modalidades: servidor web y servidor local.

Servidor web

Accesible mediante una petición HTTP POST. Perfecto para clientes de IA remotos o integraciones basadas en web.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class);
Puedes aplicarle middleware como en cualquier ruta.
Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware(['throttle:mcp']);

Servidor local

Se ejecuta como un comando Artisan. Se usa para integraciones locales con clientes como Claude Desktop.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('weather', WeatherServer::class);
El servidor local suele arrancarlo automáticamente el cliente MCP. Normalmente no necesitas ejecutar mcp:start manualmente.

Tools

Un tool es una función invocable por el cliente de IA. Puedes implementar obtención de datos, integraciones con APIs externas, operaciones de base de datos, etc.

Crear un tool

Genera una clase de tool con make:mcp-tool.
php artisan make:mcp-tool CurrentWeatherTool
Registra el tool en la propiedad $tools del servidor.
use App\Mcp\Tools\CurrentWeatherTool;
use Laravel\Mcp\Server;

class WeatherServer extends Server
{
    protected array $tools = [
        CurrentWeatherTool::class,
    ];
}
Ejemplo básico de tool.
<?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('Fetches the current weather forecast for a specified location.')]
class CurrentWeatherTool extends Tool
{
    public function handle(Request $request): Response
    {
        $location = $request->get('location');

        // Obtener los datos meteorológicos...

        return Response::text('The weather is sunny, 22°C.');
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'location' => $schema->string()
                ->description('The location to get the weather for.')
                ->required(),
        ];
    }
}

Nombre y descripción del tool

Por defecto, el nombre y el título se generan a partir del nombre de la clase. Para CurrentWeatherTool, el nombre es current-weather y el título Current Weather Tool. Puedes personalizarlos con los atributos Name y Title.
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;

#[Name('get-optimistic-weather')]
#[Title('Get Optimistic Weather Forecast')]
class CurrentWeatherTool extends Tool
{
    // ...
}
La descripción (Description) no se genera automáticamente. Es imprescindible para que el modelo entienda cómo usar el tool: defínela siempre con un contenido significativo.

Esquema de entrada

Define los parámetros de entrada en el método schema. Puedes usar el JSON schema builder de Laravel para especificar tipos y restricciones.
public function schema(JsonSchema $schema): array
{
    return [
        'location' => $schema->string()
            ->description('The location to get the weather for.')
            ->required(),

        'units' => $schema->string()
            ->enum(['celsius', 'fahrenheit'])
            ->description('The temperature units to use.')
            ->default('celsius'),
    ];
}

Esquema de salida

Con outputSchema puedes definir la estructura de la respuesta, lo que facilita que el cliente la interprete.
public function outputSchema(JsonSchema $schema): array
{
    return [
        'temperature' => $schema->number()
            ->description('Temperature in Celsius')
            ->required(),

        'conditions' => $schema->string()
            ->description('Weather conditions')
            ->required(),

        'humidity' => $schema->integer()
            ->description('Humidity percentage')
            ->required(),
    ];
}

Validación

Dentro de handle puedes usar la validación estándar de Laravel.
public function handle(Request $request): Response
{
    $validated = $request->validate([
        'location' => 'required|string|max:100',
        'units' => 'in:celsius,fahrenheit',
    ], [
        'location.required' => 'You must specify a location. For example, "New York City" or "Tokyo".',
        'units.in' => 'You must specify either "celsius" or "fahrenheit" for the units.',
    ]);

    // Utiliza los datos validados...
}
Cuando la validación falla, el cliente de IA reintenta apoyándose en el mensaje de error. Proporciona mensajes concretos y accionables.

Inyección de dependencias

El servidor resuelve los tools a través del contenedor de servicios, así que puedes tipar dependencias en el constructor o en handle.
<?php

namespace App\Mcp\Tools;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;

class CurrentWeatherTool extends Tool
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}

    public function handle(Request $request, WeatherRepository $weather): Response
    {
        $location = $request->get('location');
        $forecast = $weather->getForecastFor($location);

        return Response::text("Forecast: {$forecast}");
    }
}

Anotaciones

Añadir anotaciones a un tool proporciona al cliente información adicional sobre su comportamiento.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[IsIdempotent]
#[IsReadOnly]
class CurrentWeatherTool extends Tool
{
    // ...
}
Anotaciones disponibles:
AnotaciónDescripción
#[IsReadOnly]El tool no modifica el entorno
#[IsDestructive]El tool puede realizar actualizaciones destructivas
#[IsIdempotent]Ejecutarlo varias veces con los mismos argumentos no tiene efectos secundarios adicionales
#[IsOpenWorld]Puede interactuar con entidades externas

Registro condicional

Implementa shouldRegister para registrar el tool condicionalmente en tiempo de ejecución.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->subscribed() ?? false;
}
Si devuelve false, el tool no es visible para el cliente.

Respuestas

Los tools deben devolver una instancia de Laravel\Mcp\Response.
return Response::text('Weather Summary: Sunny, 22°C');
return Response::error('Unable to fetch weather data. Please try again.');
return Response::image(file_get_contents(storage_path('weather/radar.png')), 'image/png');

return Response::audio(file_get_contents(storage_path('weather/alert.mp3')), 'audio/mp3');

// Directamente desde el storage (MIME detectado automáticamente)
return Response::fromStorage('weather/radar.png');
public function handle(Request $request): array
{
    return [
        Response::text('Weather Summary: Sunny, 22°C'),
        Response::text("**Detailed Forecast**\n- Morning: 18°C\n- Afternoon: 25°C"),
    ];
}
Devuelve datos estructurados fáciles de interpretar por el cliente.
return Response::structured([
    'temperature' => 22.5,
    'conditions' => 'Partly cloudy',
    'humidity' => 65,
]);
Envía progreso en tiempo real durante procesos largos.
public function handle(Request $request): Generator
{
    $locations = $request->array('locations');

    foreach ($locations as $index => $location) {
        yield Response::notification('processing/progress', [
            'current' => $index + 1,
            'total' => count($locations),
            'location' => $location,
        ]);

        yield Response::text($this->forecastFor($location));
    }
}

Prompts

Los prompts son plantillas reutilizables. Aportan un formato estandarizado para las consultas típicas que el cliente de IA usa al conversar con un modelo de lenguaje.

Crear un prompt

php artisan make:mcp-prompt DescribeWeatherPrompt
Regístralo en la propiedad $prompts del servidor.
use App\Mcp\Prompts\DescribeWeatherPrompt;

class WeatherServer extends Server
{
    protected array $prompts = [
        DescribeWeatherPrompt::class,
    ];
}

Argumentos del prompt

Define los parámetros del prompt en el método arguments.
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

class DescribeWeatherPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'tone',
                description: 'The tone to use in the weather description (e.g., formal, casual, humorous).',
                required: true,
            ),
        ];
    }
}

Validación

Los argumentos se validan automáticamente según su definición, pero puedes aplicar reglas más complejas. Laravel MCP se integra sin fricciones con la validación de Laravel. Puedes validar los argumentos dentro de handle.
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'tone' => 'required|string|max:50',
        ]);

        $tone = $validated['tone'];

        // Generar el prompt utilizando el tono indicado...
    }
}
Cuando la validación falla, el cliente de IA reintenta usando los mensajes. Proporciona mensajes claros y prácticos.
$validated = $request->validate([
    'tone' => ['required', 'string', 'max:50'],
], [
    'tone.*' => 'Debes indicar un tone. Ejemplos: "formal", "casual", "humorous".',
]);

Inyección de dependencias

Los prompts también se resuelven mediante el contenedor de servicios, así que puedes inyectar dependencias en el constructor o en handle.
<?php

namespace App\Mcp\Prompts;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
También puedes tipar el método handle, y el contenedor las resolverá.
public function handle(Request $request, WeatherRepository $weather): Response
{
    $isAvailable = $weather->isServiceAvailable();

    // ...
}

Registro condicional

Implementa shouldRegister para registrar el prompt condicionalmente.
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Prompt;

class CurrentWeatherPrompt extends Prompt
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
Si devuelve false, el prompt no se ve ni se puede invocar.

Respuesta del prompt

handle puede devolver mensajes del usuario y del asistente. Con asAssistant() marcas un mensaje como del asistente.
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): array
    {
        $tone = $request->string('tone');

        $systemMessage = "You are a helpful weather assistant. Please provide a weather description in a {$tone} tone.";
        $userMessage = 'What is the current weather like in Tokyo?';

        return [
            Response::text($systemMessage)->asAssistant(),
            Response::text($userMessage),
        ];
    }
}

Resources

Los resources son datos o información que el cliente de IA puede leer como contexto. Puedes ofrecer documentación, configuraciones o datos dinámicos que mejoren las respuestas de la IA.

Crear un resource

php artisan make:mcp-resource WeatherGuidelinesResource
Regístralo en la propiedad $resources del servidor.
use App\Mcp\Resources\WeatherGuidelinesResource;

class WeatherServer extends Server
{
    protected array $resources = [
        WeatherGuidelinesResource::class,
    ];
}
<?php

namespace App\Mcp\Resources;

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

#[Description('Comprehensive guidelines for using the Weather API.')]
class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        $guidelines = "# Weather API Guidelines\n\n- Always specify a location...";

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

URI y tipo MIME

Por defecto, la URI se genera a partir del nombre de la clase (por ejemplo, weather://resources/weather-guidelines). Puedes personalizarla con los atributos Uri y MimeType.
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('weather://resources/guidelines')]
#[MimeType('application/pdf')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}

Plantillas de resource

Para definir resources dinámicos con variables en la URI, implementa la interfaz HasUriTemplate.
<?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;

#[Description('Access user files by ID')]
#[MimeType('text/plain')]
class UserFileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('file://users/{userId}/files/{fileId}');
    }

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

        // Obtener y devolver el contenido del archivo...

        return Response::text("File {$fileId} for user {$userId}");
    }
}
Las variables de la URI se incorporan automáticamente a la request y se recuperan con get.

Request en un resource

A diferencia de tools y prompts, los resources no definen un esquema de entrada ni argumentos. Aun así, dentro de handle puedes acceder a la información de la petición mediante el objeto request.
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        // Consultar información de la request...
    }
}

Inyección de dependencias en resources

Los resources también se resuelven por el contenedor. Puedes inyectar dependencias en el constructor o en handle.
<?php

namespace App\Mcp\Resources;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
handle también puede tipar dependencias.
public function handle(WeatherRepository $weather): Response
{
    return Response::text($weather->guidelines());
}

Anotaciones de un resource

A los resources se les pueden añadir anotaciones como audiencia, prioridad o fecha de última modificación.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\LastModified;
use Laravel\Mcp\Server\Annotations\Priority;
use Laravel\Mcp\Server\Resource;

#[Audience(Role::User)]
#[LastModified('2025-01-12T15:00:58Z')]
#[Priority(0.9)]
class UserDashboardResource extends Resource
{
    // ...
}
AnotaciónTipoDescripción
#[Audience]Role o arrayAudiencia (Role::User, Role::Assistant o ambas)
#[Priority]floatPrioridad (0.0 a 1.0)
#[LastModified]stringFecha ISO 8601 de última modificación

Registro condicional del resource

Implementa shouldRegister para registrar el resource de forma condicional.
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
Si devuelve false, el resource no se ve ni es accesible.

Respuestas del resource

Un resource debe devolver una instancia de Laravel\Mcp\Response. Para contenido textual utiliza text.
return Response::text($weatherData);

Respuesta de tipo enlace

El método resourceLink devuelve un enlace a un resource. En vez de incrustar el contenido, se envía un puntero URI que el cliente descarga por su cuenta.
return Response::resourceLink(
    uri: 'file:///data/report.json',
    name: 'monthly-report',
    mimeType: 'application/json',
);
También puedes pasar una clase o instancia de resource ya registrado; la URI, nombre, título, descripción y MIME se heredan automáticamente.
return Response::resourceLink(new WeatherForecastResource);

Respuesta binaria (blob)

Para contenidos binarios utiliza blob. El MIME se toma del atributo #[MimeType] del resource.
return Response::blob(file_get_contents(storage_path('weather/radar.png')));
#[MimeType('image/png')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}

Respuesta de error

Para indicar un error utiliza error.
return Response::error('No se han podido obtener los datos meteorológicos del lugar indicado.');

Apps

Laravel MCP soporta las MCP Apps, una extensión del Model Context Protocol que permite que los tools rendericen aplicaciones HTML interactivas dentro de un iframe seguro alojado por el host compatible. Así puedes ofrecer dashboards, formularios, visualizaciones y experiencias enriquecidas más allá de las respuestas de texto plano. Las MCP Apps se apoyan en dos piezas que trabajan juntas:
  • App resource: devuelve el HTML autocontenido de la aplicación.
  • Tool: se enlaza al app resource mediante el atributo #[RendersApp]. Cuando se invoca el tool, el host obtiene el resource enlazado y lo renderiza.

Crear un app resource

Genera un app resource con make:mcp-app-resource.
php artisan make:mcp-app-resource WeatherDashboardApp
El comando crea dos archivos: una clase PHP en app/Mcp/Resources y una vista Blade en resources/views/mcp. El nombre de la vista se deduce de la clase (por ejemplo, WeatherDashboardAppmcp.weather-dashboard-app).
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\AppResource;

#[Description('An interactive weather dashboard.')]
#[AppMeta]
class WeatherDashboardApp extends AppResource
{
    /**
     * Handle the app resource request.
     */
    public function handle(Request $request): Response
    {
        return Response::view('mcp.weather-dashboard-app', [
            'title' => $this->title(),
        ]);
    }
}
AppResource extiende Resource y configura automáticamente el esquema de URI ui:// y el MIME text/html;profile=mcp-app que exige la especificación de MCP Apps. Como cualquier otro resource, debes registrarlo en la propiedad $resources del servidor. La vista Blade generada utiliza el componente <x-mcp::app>, que renderiza un documento HTML completo con el SDK cliente de MCP empaquetado.
<x-mcp::app :title="$title">
    <x-slot:head>
        <script type="module">
        createMcpApp(async (app) => {
            document.getElementById('run-btn').addEventListener('click', async () => {
                const result = await app.callServerTool('get-weather-data', {});
                document.getElementById('output').textContent = result.content[0]?.text ?? '';
            });
        });
        </script>
    </x-slot:head>

    <div id="app">
        <button id="run-btn">Refresh</button>
        <p id="output"></p>
    </div>
</x-mcp::app>
La función global createMcpApp la aporta el SDK empaquetado. Gestiona la conexión del iframe con el servidor, aplica el tema del host y expone helpers y callbacks como callServerTool, sendMessage, openLink, etc. Consulta la API cliente completa en la especificación de MCP Apps.

Renderizar la app desde un tool

Para mostrar un app resource, enlázalo desde un tool con el atributo #[RendersApp]. Al invocarse el tool, Laravel MCP incluye la URI del resource en la metadata para que el host pueda renderizar la app en un iframe seguro.
<?php

namespace App\Mcp\Tools;

use App\Mcp\Resources\WeatherDashboardApp;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Tool;

#[RendersApp(resource: WeatherDashboardApp::class)]
class ShowWeatherDashboard extends Tool
{
    /**
     * Handle the tool request.
     */
    public function handle(Request $request): Response
    {
        return Response::text('Weather dashboard loaded.');
    }
}
Cuando se registra un AppResource, Laravel MCP anuncia automáticamente la capacidad io.modelcontextprotocol/ui. No requiere configuración adicional del servidor.

Visibilidad de los tools de la app

Cada tool #[RendersApp] puede restringir sus invocadores mediante el argumento visibility. Es útil para exponer tools privados destinados solo a la app (por ejemplo, para cargar o refrescar datos) que no debe ver el modelo.
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Ui\Enums\Visibility;

#[RendersApp(resource: WeatherDashboardApp::class, visibility: [Visibility::App])]
class GetWeatherData extends Tool
{
    // ...
}
El enum Visibility tiene los valores Model y App; por defecto se aplican ambos. Usa [Visibility::App] para acciones backend que solo debe llamar la UI y [Visibility::Model] para deshabilitar el tool desde la UI.

Configuración de la app

El atributo #[AppMeta] del app resource configura la Content Security Policy del iframe, los permisos del navegador y las librerías que se incluyen en el <head> de la vista.
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Ui\Enums\Library;
use Laravel\Mcp\Server\Ui\Enums\Permission;

#[AppMeta(
    connectDomains: ['https://api.weather.com'],
    permissions: [Permission::Geolocation],
    libraries: [Library::Tailwind, Library::Alpine],
)]
class WeatherDashboardApp extends AppResource
{
    // ...
}
El enum Library incluye scripts CDN preconfigurados para librerías populares (Tailwind, Alpine, etc.) y sus orígenes se combinan automáticamente en el CSP. Permission cubre permisos habituales del navegador (Camera, Microphone, Geolocation, ClipboardWrite…).
Si necesitas configuración dinámica, sobrescribe el método appMeta del resource utilizando los builders fluidos AppMeta, Csp y Permissions del namespace Laravel\Mcp\Server\Ui.

Desarrollo de apps con Boost

Laravel MCP incluye una skill de referencia específica de Boost para construir MCP Apps. Si tienes Laravel Boost instalado, tu agente puede invocar la skill mcp-development y generar automáticamente el app resource, la vista Blade y los tools enlazados. Para la referencia completa (API cliente, esquemas, etc.), consulta la documentación oficial de MCP Apps.

Metadatos

Puedes añadir el campo _meta (definido por la especificación MCP) a las respuestas de tools, resources y prompts.
// Metadatos del contenido de la respuesta
return Response::text('The weather is sunny.')
    ->withMeta(['source' => 'weather-api', 'cached' => true]);
Para añadir metadatos al envelope completo de la respuesta, utiliza Response::make.
return Response::make(
    Response::text('The weather is sunny.')
)->withMeta(['request_id' => '12345']);
Para añadir metadatos a la propia clase de tool, resource o prompt, define la propiedad $meta.
class CurrentWeatherTool extends Tool
{
    protected ?array $meta = [
        'version' => '2.0',
        'author' => 'Weather Team',
    ];
}

Iconos

Los clientes MCP pueden mostrar iconos del servidor y de sus primitivas. El atributo Icon permite declarar iconos en el servidor, tools, resources y prompts.
use Laravel\Mcp\Enums\IconTheme;
use Laravel\Mcp\Server\Attributes\Icon;

#[Icon('mcp/server.png', mimeType: 'image/png', sizes: ['48x48'])]
#[Icon('mcp/server-dark.svg', theme: IconTheme::Dark)]
class WeatherServer extends Server
{
    // ...
}
El atributo es repetible, por lo que puedes declarar varias variantes para tamaños o temas (claro/oscuro). También puedes definir los iconos programáticamente sobrescribiendo el método icons. Es útil cuando los iconos dependen de condiciones en tiempo de ejecución.
use Laravel\Mcp\Schema\Icon;

class CurrentWeatherTool extends Tool
{
    /**
     * Obtiene los iconos del tool.
     *
     * @return array<int, Icon>
     */
    public function icons(): array
    {
        return [
            Icon::from('mcp/tool.png', mimeType: 'image/png'),
        ];
    }
}
Los iconos declarados con atributos y con el método icons se combinan automáticamente. Las rutas se resuelven así:
  • Rutas con esquema (https:, data:, etc.) se usan tal cual.
  • Rutas relativas se resuelven con el helper asset de Laravel.

Autenticación

Los servidores web se autentican con el middleware estándar de Laravel.

Sanctum

Autenticación por token con Laravel Sanctum. El cliente MCP envía la cabecera Authorization: Bearer <token>.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:sanctum');

OAuth 2.1

Autenticación OAuth con Laravel Passport, idónea cuando necesitas seguridad más robusta.
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:api');
Para usar OAuth publica las vistas de autorización de Passport y configúralas en el service provider.
php artisan vendor:publish --tag=mcp-views
// AppServiceProvider::boot()
use Laravel\Passport\Passport;

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

Autorización

Con $request->user() obtienes el usuario autenticado y puedes comprobar permisos dentro de tools y resources.
public function handle(Request $request): Response
{
    if (! $request->user()->can('read-weather')) {
        return Response::error('Permission denied.');
    }

    // Continuar el procesamiento...
}

Cliente MCP

Laravel MCP no solo permite construir servidores: también incluye un cliente para conectar con otros servidores MCP. Con él puedes descubrir e invocar los tools expuestos por servidores externos. Es especialmente útil para exponer capacidades de servidores MCP externos a agentes de IA.

Conectar con un servidor

Para servidores accesibles por HTTP utiliza Client::web, pasando la URL del servidor.
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com');
Para servidores locales que arrancan como comando, usa Client::local con el comando y sus argumentos.
use Laravel\Mcp\Client;

$client = Client::local('php', ['artisan', 'mcp:start']);
El cliente conecta de forma perezosa: la conexión se establece automáticamente cuando obtienes el listado de tools o los invocas por primera vez. Si prefieres gestionarla manualmente, dispones de connect, connected, ping y disconnect.
$client->connect();

$client->ping();

if ($client->connected()) {
    // ...
}

$client->disconnect();
Con withTimeout puedes personalizar el timeout de la petición.
$client = Client::web('https://mcp.example.com')->withTimeout(30);

Clientes con nombre

En lugar de crear el cliente cada vez, puedes registrar clientes con nombre reutilizables. Normalmente se hace con el facade Mcp en el método boot de un service provider.
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com'));
Después puedes resolverlo por nombre.
use Laravel\Mcp\Facades\Mcp;

$client = Mcp::client('github');
Un cliente con nombre se resuelve una sola vez por petición y se desconecta automáticamente al terminar el ciclo.

Autenticación del cliente

Para conectar con servidores web MCP protegidos por Bearer token, utiliza withToken. Puedes pasar la cadena o una closure de resolución perezosa.
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com')->withToken($token);

$client = Client::web('https://mcp.example.com')->withToken(
    fn () => Auth::user()->mcpToken(),
);
Para servidores protegidos con OAuth 2.1, utiliza withOAuth.
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com')->withOAuth(
    clientId: config('services.github_mcp.client_id'),
    clientSecret: config('services.github_mcp.client_secret'),
));
Si el servidor MCP admite registro dinámico de clientes, puedes omitir clientId y clientSecret: el cliente se registra automáticamente.
A continuación, en routes/ai.php registra las rutas OAuth del cliente con nombre mediante oAuthRoutesFor. La closure recibe el nombre del cliente y un TokenSet una vez intercambiado el código por un access token.
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client\OAuth\TokenSet;
use Laravel\Mcp\Facades\Mcp;

Mcp::oAuthRoutesFor('github', function (string $client, TokenSet $token) {
    Auth::user()->update([
        'github_mcp_token' => $token->accessToken,
    ]);

    return redirect('/dashboard');
});
Se registran dos rutas con nombre: la ruta connect (mcp.oauth.{client}.connect), que redirige al usuario al servidor de autorización, y la ruta callback (mcp.oauth.{client}.callback), que intercambia el código y ejecuta el handler. Ambas usan el grupo de middleware web (personalizable con el argumento middleware). Para iniciar el flujo, redirige al usuario a la ruta connect.
return redirect()->route('mcp.oauth.github.connect');

Tools

Con tools obtienes los tools expuestos por un servidor MCP. Devuelve una colección con el nombre como clave.
use Laravel\Mcp\Facades\Mcp;

$tools = Mcp::client('github')->tools();

foreach ($tools as $tool) {
    $tool->name;
    $tool->title;
    $tool->description;
    $tool->inputSchema;
}
El cliente maneja automáticamente la paginación para traerlos todos. Con el argumento limit puedes acotar el resultado.
$tools = Mcp::client('github')->tools(limit: 10);
Para invocar un tool utiliza callTool con su nombre y los argumentos. Devuelve un ToolResult.
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->callTool('current-weather', [
    'location' => 'New York',
]);

$result->text();             // Contenido textual
(string) $result;            // Equivalente a text()
$result->isError;            // Indica si el tool reportó un error
$result->structuredContent;  // Contenido estructurado (si existe)
También puedes invocarlo directamente sobre una instancia obtenida en el listado.
$tools = Mcp::client('github')->tools();

$result = $tools['current-weather']->call([
    'location' => 'New York',
]);
Si construyes agentes con el Laravel AI SDK, puedes pasar los tools del cliente MCP directamente al agente para que el modelo los invoque durante sus respuestas. Consulta la sección MCP tools del AI SDK.

Prompts

Con prompts obtienes los prompts del servidor MCP en forma de colección indexada por nombre.
use Laravel\Mcp\Facades\Mcp;

$prompts = Mcp::client('github')->prompts();

foreach ($prompts as $prompt) {
    $prompt->name;
    $prompt->title;
    $prompt->description;
    $prompt->arguments;
}
El cliente pagina automáticamente. Con limit acotas el listado.
$prompts = Mcp::client('github')->prompts(limit: 10);
Para obtener un prompt utiliza getPrompt con su nombre y los argumentos. Devuelve un PromptResult.
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->getPrompt('describe-weather', [
    'location' => 'New York',
]);

$result->text();        // Texto del mensaje
(string) $result;       // Equivalente a text()
$result->messages;      // Mensajes devueltos por el prompt (raw)
$result->description;   // Descripción del prompt (si existe)

Resources

Con resources obtienes los resources del servidor MCP en una colección indexada por URI.
use Laravel\Mcp\Facades\Mcp;

$resources = Mcp::client('github')->resources();

foreach ($resources as $resource) {
    $resource->uri;
    $resource->name;
    $resource->title;
    $resource->description;
    $resource->mimeType;
    $resource->size;
}
El cliente pagina automáticamente. Con limit acotas el listado.
$resources = Mcp::client('github')->resources(limit: 10);
Para leer un resource utiliza readResource con la URI. Devuelve un ResourceReadResult.
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->readResource('weather://guidelines');

$result->content();   // Contenido (los blobs base64 se decodifican automáticamente)
(string) $result;     // Equivalente a content()
$result->mimeType();  // Tipo MIME (si está presente)
$result->contents;    // Contenido devuelto (raw)

Pruebas

MCP Inspector

Para probar servidores MCP de forma interactiva, utiliza «MCP Inspector».
# Servidor web
php artisan mcp:inspector mcp/weather

# Servidor local (registrado con el nombre "weather")
php artisan mcp:inspector weather
El comando abre el inspector y te permite copiar la configuración del cliente. Si has configurado middleware de autenticación, incluye la cabecera Authorization en la conexión.

Tests unitarios

Puedes escribir tests unitarios de tools, resources y prompts.
test('tool', function () {
    $response = WeatherServer::tool(CurrentWeatherTool::class, [
        'location' => 'Tokyo',
        'units' => 'celsius',
    ]);

    $response
        ->assertOk()
        ->assertSee('The current weather in Tokyo is 22°C and sunny.');
});
public function test_tool(): void
{
    $response = WeatherServer::tool(CurrentWeatherTool::class, [
        'location' => 'Tokyo',
        'units' => 'celsius',
    ]);

    $response
        ->assertOk()
        ->assertSee('The current weather in Tokyo is 22°C and sunny.');
}
Los prompts y resources se prueban del mismo modo.
$response = WeatherServer::prompt(DescribeWeatherPrompt::class, ['tone' => 'casual']);
$response = WeatherServer::resource(WeatherGuidelinesResource::class);
Para ejecutar como usuario autenticado, utiliza actingAs.
$response = WeatherServer::actingAs($user)->tool(CurrentWeatherTool::class, [...]);
Aserciones principales:
$response->assertOk();           // Sin errores
$response->assertSee('...');     // Contiene el texto indicado
Verifica presencia o ausencia de errores con assertHasErrors / assertHasNoErrors.
$response->assertHasErrors();

$response->assertHasErrors([
    'Something went wrong.',
]);

$response->assertHasNoErrors();
Comprueba el nombre, el título o la descripción del tool, resource o prompt.
$response->assertName('current-weather');
$response->assertTitle('Current Weather Tool');
$response->assertDescription('Fetches the current weather forecast for a specified location.');
Para respuestas en streaming, valida las notificaciones con assertSentNotification y assertNotificationCount.
$response->assertSentNotification('processing/progress', [
    'step' => 1,
    'total' => 5,
]);

$response->assertSentNotification('processing/progress', [
    'step' => 2,
    'total' => 5,
]);

$response->assertNotificationCount(5);
Para depurar la respuesta, utiliza dd o dump.
$response->dd();
$response->dump();
Última modificación el 13 de julio de 2026