What to expect in the next generation of Laravel Forge. Read the blog post
Skip to content

Laravel MCP

Introduction

Laravel MCP provides a simple and elegant way for AI clients to interact with your Laravel application through the Model Context Protocol. It offers an expressive, fluent interface for defining servers, tools, resources, and prompts that enable AI-powered interactions with your application.

Installation

To get started, install Laravel MCP into your project using the Composer package manager:

1composer require laravel/mcp

Publishing Routes

After installing Laravel MCP, execute the vendor:publish Artisan command to publish the routes/ai.php file where you will define your MCP servers:

1php artisan vendor:publish --tag=ai-routes

This command creates the routes/ai.php file in your application's routes directory, which you will use to register your MCP servers.

Creating Servers

You can create an MCP server using the make:mcp-server Artisan command. Servers act as the central communication point that exposes MCP capabilities like tools, resources, and prompts to AI clients:

1php artisan make:mcp-server WeatherServer

This command will create a new server class in the app/Mcp/Servers directory. The generated server class extends Laravel MCP's base Laravel\Mcp\Server class and provides properties for registering tools, resources, and prompts:

1<?php
2 
3namespace App\Mcp\Servers;
4 
5use Laravel\Mcp\Server;
6 
7class WeatherServer extends Server
8{
9 /**
10 * The MCP server's name.
11 */
12 protected string $name = 'Weather Server';
13 
14 /**
15 * The MCP server's version.
16 */
17 protected string $version = '1.0.0';
18 
19 /**
20 * The MCP server's instructions for the LLM.
21 */
22 protected string $instructions = 'This server provides weather information and forecasts.';
23 
24 /**
25 * The tools registered with this MCP server.
26 *
27 * @var array<int, class-string<\Laravel\Mcp\Server\Tool>>
28 */
29 protected array $tools = [
30 // GetCurrentWeatherTool::class,
31 ];
32 
33 /**
34 * The resources registered with this MCP server.
35 *
36 * @var array<int, class-string<\Laravel\Mcp\Server\Resource>>
37 */
38 protected array $resources = [
39 // WeatherGuidelinesResource::class,
40 ];
41 
42 /**
43 * The prompts registered with this MCP server.
44 *
45 * @var array<int, class-string<\Laravel\Mcp\Server\Prompt>>
46 */
47 protected array $prompts = [
48 // DescribeWeatherPrompt::class,
49 ];
50}

Server Registration

Once you've created a server, you must register it in your routes/ai.php file to make it accessible. Laravel MCP provides two methods for registering servers: web for HTTP-accessible servers and local for command-line servers.

Web Servers

Web servers are the most common types of servers and are accessible via HTTP POST requests, making them ideal for remote AI clients or web-based integrations. Register a web server using the web method:

1use App\Mcp\Servers\WeatherServer;
2use Laravel\Mcp\Facades\Mcp;
3 
4Mcp::web('/mcp/weather', WeatherServer::class);

Just like normal routes, you may apply middleware to protect your web servers:

1Mcp::web('/mcp/weather', WeatherServer::class)
2 ->middleware(['throttle:mcp']);

Local Servers

Local servers run as Artisan commands, perfect for building local AI assistant integrations like Laravel Boost. Register a local server using the local method:

1use App\Mcp\Servers\WeatherServer;
2use Laravel\Mcp\Facades\Mcp;
3 
4Mcp::local('weather', WeatherServer::class);

Once registered, you should not typically need to manually run the mcp:start Artisan command yourself. Instead, configure your MCP client (AI agent) to start the server or use the MCP Inspector.

Tools

Tools enable your server to expose functionality that AI clients can call. They allow language models to perform actions, run code, or interact with external systems:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Illuminate\JsonSchema\JsonSchema;
6use Laravel\Mcp\Request;
7use Laravel\Mcp\Response;
8use Laravel\Mcp\Server\Tool;
9 
10class CurrentWeatherTool extends Tool
11{
12 /**
13 * The tool's description.
14 */
15 protected string $description = 'Fetches the current weather forecast for a specified location.';
16 
17 /**
18 * Handle the tool request.
19 */
20 public function handle(Request $request): Response
21 {
22 $location = $request->get('location');
23 
24 // Get weather...
25 
26 return Response::text('The weather is...');
27 }
28 
29 /**
30 * Get the tool's input schema.
31 *
32 * @return array<string, \Illuminate\JsonSchema\JsonSchema>
33 */
34 public function schema(JsonSchema $schema): array
35 {
36 return [
37 'location' => $schema->string()
38 ->description('The location to get the weather for.')
39 ->required(),
40 ];
41 }
42}

Creating Tools

To create a tool, run the make:mcp-tool Artisan command:

1php artisan make:mcp-tool CurrentWeatherTool

After creating a tool, register it in your server's $tools property:

1<?php
2 
3namespace App\Mcp\Servers;
4 
5use App\Mcp\Tools\CurrentWeatherTool;
6use Laravel\Mcp\Server;
7 
8class WeatherServer extends Server
9{
10 /**
11 * The tools registered with this MCP server.
12 *
13 * @var array<int, class-string<\Laravel\Mcp\Server\Tool>>
14 */
15 protected array $tools = [
16 CurrentWeatherTool::class,
17 ];
18}

Tool Name, Title, and Description

By default, the tool's name and title are derived from the class name. For example, CurrentWeatherTool will have a name of current-weather and a title of Current Weather Tool. You may customize these values by defining the tool's $name and $title properties:

1class CurrentWeatherTool extends Tool
2{
3 /**
4 * The tool's name.
5 */
6 protected string $name = 'get-optimistic-weather';
7 
8 /**
9 * The tool's title.
10 */
11 protected string $title = 'Get Optimistic Weather Forecast';
12 
13 // ...
14}

Tool descriptions are not automatically generated. You should always provide a meaningful description by defining a $description property on your tool:

1class CurrentWeatherTool extends Tool
2{
3 /**
4 * The tool's description.
5 */
6 protected string $description = 'Fetches the current weather forecast for a specified location.';
7 
8 //
9}

The description is a critical part of the tool's metadata, as it helps AI models understand when and how to use the tool effectively.

Tool Input Schemas

Tools can define input schemas to specify what arguments they accept from AI clients. Use Laravel's Illuminate\JsonSchema\JsonSchema builder to define your tool's input requirements:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Illuminate\JsonSchema\JsonSchema;
6use Laravel\Mcp\Server\Tool;
7 
8class CurrentWeatherTool extends Tool
9{
10 /**
11 * Get the tool's input schema.
12 *
13 * @return array<string, JsonSchema>
14 */
15 public function schema(JsonSchema $schema): array
16 {
17 return [
18 'location' => $schema->string()
19 ->description('The location to get the weather for.')
20 ->required(),
21 
22 'units' => $schema->enum(['celsius', 'fahrenheit'])
23 ->description('The temperature units to use.')
24 ->default('celsius'),
25 ];
26 }
27}

Validating Tool Arguments

JSON Schema definitions provide a basic structure for tool arguments, but you may also want to enforce more complex validation rules.

Laravel MCP integrates seamlessly with Laravel's validation features. You may validate incoming tool arguments within your tool's handle method:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Response;
7use Laravel\Mcp\Server\Tool;
8 
9class CurrentWeatherTool extends Tool
10{
11 /**
12 * Handle the tool request.
13 */
14 public function handle(Request $request): Response
15 {
16 $validated = $request->validate([
17 'location' => 'required|string|max:100',
18 'units' => 'in:celsius,fahrenheit',
19 ]);
20 
21 // Fetch weather data using the validated arguments...
22 }
23}

On validation failure, AI clients will act based on the error messages you provide. As such, is critical to provide clear and actionable error messages:

1$validated = $request->validate([
2 'location' => ['required','string','max:100'],
3 'units' => 'in:celsius,fahrenheit',
4],[
5 'location.required' => 'You must specify a location to get the weather for. For example, "New York City" or "Tokyo".',
6 'units.in' => 'You must specify either "celsius" or "fahrenheit" for the units.',
7]);

Tool Dependency Injection

The Laravel service container is used to resolve all tools. As a result, you are able to type-hint any dependencies your tool may need in its constructor. The declared dependencies will automatically be resolved and injected into the tool instance:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Server\Tool;
7 
8class CurrentWeatherTool extends Tool
9{
10 /**
11 * Create a new tool instance.
12 */
13 public function __construct(
14 protected WeatherRepository $weather,
15 ) {}
16 
17 // ...
18}

In addition to constructor injection, you may also type-hint dependencies in your tool's handle() method. The service container will automatically resolve and inject the dependencies when the method is called:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Request;
7use Laravel\Mcp\Response;
8use Laravel\Mcp\Server\Tool;
9 
10class CurrentWeatherTool extends Tool
11{
12 /**
13 * Handle the tool request.
14 */
15 public function handle(Request $request, WeatherRepository $weather): Response
16 {
17 $location = $request->get('location');
18 
19 $forecast = $weather->getForecastFor($location);
20 
21 // ...
22 }
23}

Tool Annotations

You may enhance your tools with annotations to provide additional metadata to AI clients. These annotations help AI models understand the tool's behavior and capabilities. Annotations are added to tools via attributes:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
6use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
7use Laravel\Mcp\Server\Tool;
8 
9#[IsIdempotent]
10#[IsReadOnly]
11class CurrentWeatherTool extends Tool
12{
13 //
14}

Available annotations include:

Annotation Type Description
#[IsReadOnly] boolean Indicates the tool does not modify its environment.
#[IsDestructive] boolean Indicates the tool may perform destructive updates (only meaningful when not read-only).
#[IsIdempotent] boolean Indicates repeated calls with same arguments have no additional effect (when not read-only).
#[IsOpenWorld] boolean Indicates the tool may interact with external entities.

Conditional Tool Registration

You may conditionally register tools at runtime by implementing the shouldRegister method in your tool class. This method allows you to determine whether a tool should be available based on application state, configuration, or request parameters:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Server\Tool;
7 
8class CurrentWeatherTool extends Tool
9{
10 /**
11 * Determine if the tool should be registered.
12 */
13 public function shouldRegister(Request $request): bool
14 {
15 return $request?->user()?->subscribed() ?? false;
16 }
17}

When a tool's shouldRegister method returns false, it will not appear in the list of available tools and cannot be invoked by AI clients.

Tool Responses

Tools must return an instance of Laravel\Mcp\Response. The Response class provides several convenient methods for creating different types of responses:

For simple text responses, use the text method:

1use Laravel\Mcp\Request;
2use Laravel\Mcp\Response;
3 
4/**
5 * Handle the tool request.
6 */
7public function handle(Request $request): Response
8{
9 // ...
10 
11 return Response::text('Weather Summary: Sunny, 72°F');
12}

To indicate an error occurred during tool execution, use the error method:

1return Response::error('Unable to fetch weather data. Please try again.');

Multiple Content Responses

Tools can return multiple pieces of content by returning an array of Response instances:

1use Laravel\Mcp\Request;
2use Laravel\Mcp\Response;
3 
4/**
5 * Handle the tool request.
6 *
7 * @return array<int, \Laravel\Mcp\Response>
8 */
9public function handle(Request $request): array
10{
11 // ...
12 
13 return [
14 Response::text('Weather Summary: Sunny, 72°F'),
15 Response::text('**Detailed Forecast**\n- Morning: 65°F\n- Afternoon: 78°F\n- Evening: 70°F')
16 ];
17}

Streaming Responses

For long-running operations or real-time data streaming, tools can return a generator from their handle method. This enables sending intermediate updates to the client before the final response:

1<?php
2 
3namespace App\Mcp\Tools;
4 
5use Generator;
6use Laravel\Mcp\Request;
7use Laravel\Mcp\Response;
8use Laravel\Mcp\Server\Tool;
9 
10class CurrentWeatherTool extends Tool
11{
12 /**
13 * Handle the tool request.
14 *
15 * @return \Generator<int, \Laravel\Mcp\Response>
16 */
17 public function handle(Request $request): Generator
18 {
19 $locations = $request->array('locations');
20 
21 foreach ($locations as $index => $location) {
22 yield Response::notification('processing/progress', [
23 'current' => $index + 1,
24 'total' => count($locations),
25 'location' => $location,
26 ]);
27 
28 yield Response::text($this->forecastFor($location));
29 }
30 }
31}

When using web-based servers, streaming responses automatically open an SSE (Server-Sent Events) stream, sending each yielded message as an event to the client.

Prompts

Prompts enable your server to share reusable prompt templates that AI clients can use to interact with language models. They provide a standardized way to structure common queries and interactions.

Creating Prompts

To create a prompt, run the make:mcp-prompt Artisan command:

1php artisan make:mcp-prompt DescribeWeatherPrompt

After creating a prompt, register it in your server's $prompts property:

1<?php
2 
3namespace App\Mcp\Servers;
4 
5use App\Mcp\Prompts\DescribeWeatherPrompt;
6use Laravel\Mcp\Server;
7 
8class WeatherServer extends Server
9{
10 /**
11 * The prompts registered with this MCP server.
12 *
13 * @var array<int, class-string<\Laravel\Mcp\Server\Prompt>>
14 */
15 protected array $prompts = [
16 DescribeWeatherPrompt::class,
17 ];
18}

Prompt Name, Title, and Description

By default, the prompt's name and title are derived from the class name. For example, DescribeWeatherPrompt will have a name of describe-weather and a title of Describe Weather Prompt. You may customize these values by defining $name and $title properties on your prompt:

1class DescribeWeatherPrompt extends Prompt
2{
3 /**
4 * The prompt's name.
5 */
6 protected string $name = 'weather-assistant';
7 
8 /**
9 * The prompt's title.
10 */
11 protected string $title = 'Weather Assistant Prompt';
12 
13 // ...
14}

Prompt descriptions are not automatically generated. You should always provide a meaningful description by defining a $description property on your prompts:

1class DescribeWeatherPrompt extends Prompt
2{
3 /**
4 * The prompt's description.
5 */
6 protected string $description = 'Generates a natural-language explanation of the weather for a given location.';
7 
8 //
9}

The description is a critical part of the prompt's metadata, as it helps AI models understand when and how to get the best use out of the prompt.

Prompt Arguments

Prompts can define arguments that allow AI clients to customize the prompt template with specific values. Use the arguments method to define what arguments your prompt accepts:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use Laravel\Mcp\Server\Prompt;
6use Laravel\Mcp\Server\Prompts\Argument;
7 
8class DescribeWeatherPrompt extends Prompt
9{
10 /**
11 * Get the prompt's arguments.
12 *
13 * @return array<int, \Laravel\Mcp\Server\Prompts\Argument>
14 */
15 public function arguments(): array
16 {
17 return [
18 new Argument(
19 name: 'tone',
20 description: 'The tone to use in the weather description (e.g., formal, casual, humorous).',
21 required: true,
22 ),
23 ];
24 }
25}

Validating Prompt Arguments

Prompt arguments are automatically validated based on their definition, but you may also want to enforce more complex validation rules.

Laravel MCP integrates seamlessly with Laravel's validation features. You may validate incoming prompt arguments within your prompt's handle method:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Response;
7use Laravel\Mcp\Server\Prompt;
8 
9class DescribeWeatherPrompt extends Prompt
10{
11 /**
12 * Handle the prompt request.
13 */
14 public function handle(Request $request): Response
15 {
16 $validated = $request->validate([
17 'tone' => 'required|string|max:50',
18 ]);
19 
20 $tone = $validated['tone'];
21 
22 // Generate the prompt response using the given tone...
23 }
24}

On validation failure, AI clients will act based on the error messages you provide. As such, is critical to provide clear and actionable error messages:

1$validated = $request->validate([
2 'tone' => ['required','string','max:50'],
3],[
4 'tone.*' => 'You must specify a tone for the weather description. Examples include "formal", "casual", or "humorous".',
5]);

Prompt Dependency Injection

The Laravel service container is used to resolve all prompts. As a result, you are able to type-hint any dependencies your prompt may need in its constructor. The declared dependencies will automatically be resolved and injected into the prompt instance:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Server\Prompt;
7 
8class DescribeWeatherPrompt extends Prompt
9{
10 /**
11 * Create a new prompt instance.
12 */
13 public function __construct(
14 protected WeatherRepository $weather,
15 ) {}
16 
17 //
18}

In addition to constructor injection, you may also type-hint dependencies in your prompt's handle method. The service container will automatically resolve and inject the dependencies when the method is called:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Request;
7use Laravel\Mcp\Response;
8use Laravel\Mcp\Server\Prompt;
9 
10class DescribeWeatherPrompt extends Prompt
11{
12 /**
13 * Handle the prompt request.
14 */
15 public function handle(Request $request, WeatherRepository $weather): Response
16 {
17 $isAvailable = $weather->isServiceAvailable();
18 
19 // ...
20 }
21}

Conditional Prompt Registration

You may conditionally register prompts at runtime by implementing the shouldRegister method in your prompt class. This method allows you to determine whether a prompt should be available based on application state, configuration, or request parameters:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Server\Prompt;
7 
8class CurrentWeatherPrompt extends Prompt
9{
10 /**
11 * Determine if the prompt should be registered.
12 */
13 public function shouldRegister(Request $request): bool
14 {
15 return $request?->user()?->subscribed() ?? false;
16 }
17}

When a prompt's shouldRegister method returns false, it will not appear in the list of available prompts and cannot be invoked by AI clients.

Prompt Responses

Prompts may return a single Laravel\Mcp\Response or an iterable of Laravel\Mcp\Response instances. These responses encapsulate the content that will be sent to the AI client:

1<?php
2 
3namespace App\Mcp\Prompts;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Response;
7use Laravel\Mcp\Server\Prompt;
8 
9class DescribeWeatherPrompt extends Prompt
10{
11 /**
12 * Handle the prompt request.
13 *
14 * @return array<int, \Laravel\Mcp\Response>
15 */
16 public function handle(Request $request): array
17 {
18 $tone = $request->string('tone');
19 
20 $systemMessage = "You are a helpful weather assistant. Please provide a weather description in a {$tone} tone.";
21 
22 $userMessage = "What is the current weather like in New York City?";
23 
24 return [
25 Response::text($systemMessage)->asAssistant(),
26 Response::text($userMessage),
27 ];
28 }
29}

You can use the asAssistant() method to indicate that a response message should be treated as coming from the AI assistant, while regular messages are treated as user input.

Resources

Resources enable your server to expose data and content that AI clients can read and use as context when interacting with language models. They provide a way to share static or dynamic information like documentation, configuration, or any data that helps inform AI responses.

Creating Resources

To create a resource, run the make:mcp-resource Artisan command:

1php artisan make:mcp-resource WeatherGuidelinesResource

After creating a resource, register it in your server's $resources property:

1<?php
2 
3namespace App\Mcp\Servers;
4 
5use App\Mcp\Resources\WeatherGuidelinesResource;
6use Laravel\Mcp\Server;
7 
8class WeatherServer extends Server
9{
10 /**
11 * The resources registered with this MCP server.
12 *
13 * @var array<int, class-string<\Laravel\Mcp\Server\Resource>>
14 */
15 protected array $resources = [
16 WeatherGuidelinesResource::class,
17 ];
18}

Resource Name, Title, and Description

By default, the resource's name and title are derived from the class name. For example, WeatherGuidelinesResource will have a name of weather-guidelines and a title of Weather Guidelines Resource. You may customize these values by defining the $name and $title properties on your resource:

1class WeatherGuidelinesResource extends Resource
2{
3 /**
4 * The resource's name.
5 */
6 protected string $name = 'weather-api-docs';
7 
8 /**
9 * The resource's title.
10 */
11 protected string $title = 'Weather API Documentation';
12 
13 // ...
14}

Resource descriptions are not automatically generated. You should always provide a meaningful description by defining the $description property on your resource:

1class WeatherGuidelinesResource extends Resource
2{
3 /**
4 * The resource's description.
5 */
6 protected string $description = 'Comprehensive guidelines for using the Weather API.';
7 
8 //
9}

The description is a critical part of the resource's metadata, as it helps AI models understand when and how to use the resource effectively.

Resource URI and MIME Type

Each resource is identified by a unique URI and has an associated MIME type that helps AI clients understand the resource's format.

By default, the resource's URI is generated based on the resource's name, so WeatherGuidelinesResource will have a URI of weather://resources/weather-guidelines. The default MIME type is text/plain.

You may customize these values by defining the $uri and $mimeType properties on your resource:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use Laravel\Mcp\Server\Resource;
6 
7class WeatherGuidelinesResource extends Resource
8{
9 /**
10 * The resource's URI.
11 */
12 protected string $uri = 'weather://resources/guidelines';
13 
14 /**
15 * The resource's MIME type.
16 */
17 protected string $mimeType = 'application/pdf';
18}

The URI and MIME type help AI clients determine how to process and interpret the resource content appropriately.

Resource Request

Unlike tools and prompts, resources can not define input schemas or arguments. However, you can still interact with request object within your resource's handle method:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Response;
7use Laravel\Mcp\Server\Resource;
8 
9class WeatherGuidelinesResource extends Resource
10{
11 /**
12 * Handle the resource request.
13 */
14 public function handle(Request $request): Response
15 {
16 // ...
17 }
18}

Resource Dependency Injection

The Laravel service container is used to resolve all resources. As a result, you are able to type-hint any dependencies your resource may need in its constructor. The declared dependencies will automatically be resolved and injected into the resource instance:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Server\Resource;
7 
8class WeatherGuidelinesResource extends Resource
9{
10 /**
11 * Create a new resource instance.
12 */
13 public function __construct(
14 protected WeatherRepository $weather,
15 ) {}
16 
17 // ...
18}

In addition to constructor injection, you may also type-hint dependencies in your resource's handle method. The service container will automatically resolve and inject the dependencies when the method is called:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use App\Repositories\WeatherRepository;
6use Laravel\Mcp\Request;
7use Laravel\Mcp\Response;
8use Laravel\Mcp\Server\Resource;
9 
10class WeatherGuidelinesResource extends Resource
11{
12 /**
13 * Handle the resource request.
14 */
15 public function handle(WeatherRepository $weather): Response
16 {
17 $guidelines = $weather->guidelines();
18 
19 return Response::text($guidelines);
20 }
21}

Conditional Resource Registration

You may conditionally register resources at runtime by implementing the shouldRegister method in your resource class. This method allows you to determine whether a resource should be available based on application state, configuration, or request parameters:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use Laravel\Mcp\Request;
6use Laravel\Mcp\Server\Resource;
7 
8class WeatherGuidelinesResource extends Resource
9{
10 /**
11 * Determine if the resource should be registered.
12 */
13 public function shouldRegister(Request $request): bool
14 {
15 return $request?->user()?->subscribed() ?? false;
16 }
17}

When a resource's shouldRegister method returns false, it will not appear in the list of available resources and cannot be accessed by AI clients.

Resource Responses

Resources must return an instance of Laravel\Mcp\Response. The Response class provides several convenient methods for creating different types of responses:

For simple text content, use the text method:

1use Laravel\Mcp\Request;
2use Laravel\Mcp\Response;
3 
4/**
5 * Handle the resource request.
6 */
7public function handle(Request $request): Response
8{
9 // ...
10 
11 return Response::text($weatherData);
12}

Blob Responses

To return blob content, use the blob method, providing the blob content:

1return Response::blob(file_get_contents(storage_path('weather/radar.png')));

When returning blob content, the MIME type will be determined by the value of the $mimeType property on the resource class:

1<?php
2 
3namespace App\Mcp\Resources;
4 
5use Laravel\Mcp\Server\Resource;
6 
7class WeatherGuidelinesResource extends Resource
8{
9 /**
10 * The resource's MIME type.
11 */
12 protected string $mimeType = 'image/png';
13 
14 //
15}

Error Responses

To indicate an error occurred during resource retrieval, use the error() method:

1return Response::error('Unable to fetch weather data for the specified location.');

Authentication

You can authenticate web MCP servers with middleware just like you would for routes. This will require a user to authenticate before using any capability of the server.

There are two ways to authenticate access to your MCP server: simple, token based authentication via Laravel Sanctum, or any other arbitrary API tokens which are passed via the Authorization HTTP header. Or, you may authenticate via OAuth using Laravel Passport.

OAuth 2.1

The most robust way to protect your web-based MCP servers is with OAuth through Laravel Passport.

When authenticating your MCP server via OAuth, you will invoke the Mcp::oauthRoutes method in your routes/ai.php file to register the required OAuth2 discovery and client registration routes. Then, apply Passport's auth:api middleware to your Mcp::web route in your routes/ai.php file:

1use App\Mcp\Servers\WeatherExample;
2use Laravel\Mcp\Facades\Mcp;
3 
4Mcp::oauthRoutes();
5 
6Mcp::web('/mcp/weather', WeatherExample::class)
7 ->middleware('auth:api');

New Passport Installation

If your application is not already using Laravel Passport, start by following Passport's installation and deployment steps. You should have an OAuthenticatable model, new authentication guard, and passport keys before moving on.

Next, you should publish Laravel MCP's provided Passport authorization view:

1php artisan vendor:publish --tag=mcp-views

Then, instruct Passport to use this view using the Passport::authorizationView method. Typically, this method should be invoked in the boot method of your application's AppServiceProvider:

1use Laravel\Passport\Passport;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Passport::authorizationView(function ($parameters) {
9 return view('mcp.authorize', $parameters);
10 });
11}

This view will be displayed to the end-user during authentication to reject or approve the AI agent's authentication attempt.

Authorization screen example

In this scenario, we're simply using OAuth as a translation layer to the underlying authenticatable model. We are ignoring many aspects of OAuth, such as scopes.

Using an Existing Passport Installation

If your application is already using Laravel Passport, Laravel MCP should work seamlessly within your existing Passport installation, but custom scopes aren't currently supported as OAuth is primarily used as a translation layer to the underlying authenticatable model.

Laravel MCP, via the Mcp::oauthRoutes() method discussed above, adds, advertises, and uses a single mcp:use scope.

Passport vs. Sanctum

OAuth2.1 is the documented authentication mechanism in the Model Context Protocol specification, and is the most widely supported among MCP clients. For that reason, we recommend using Passport when possible.

If your application is already using Sanctum then adding Passport may be cumbersome. In this instance, we recommend using Sanctum without Passport until you have a clear, necessary requirement to use an MCP client that only supports OAuth.

Sanctum

If you would like to protect your MCP server using Sanctum, simply add Sanctum's authentication middleware to your server in your routes/ai.php file. Then, ensure your MCP clients provide a Authorization: Bearer <token> header to ensure successful authentication:

1use App\Mcp\Servers\WeatherExample;
2use Laravel\Mcp\Facades\Mcp;
3 
4Mcp::web('/mcp/demo', WeatherExample::class)
5 ->middleware('auth:sanctum');

Custom MCP Authentication

If your application issues its own custom API tokens, you may authenticate your MCP server by assigning any middleware you wish to your Mcp::web routes. Your custom middleware can inspect the Authorization header manually to authenticate the incoming MCP request.

Authorization

You may access the currently authenticated user via the $request->user() method, allowing you to perform authorization checks within your MCP tools and resources:

1use Laravel\Mcp\Request;
2use Laravel\Mcp\Response;
3 
4/**
5 * Handle the tool request.
6 */
7public function handle(Request $request): Response
8{
9 if (! $request->user()->can('read-weather')) {
10 return Response::error('Permission denied.');
11 }
12 
13 // ...
14}

Testing Servers

You may test your MCP servers using the built-in MCP Inspector or by writing unit tests.

MCP Inspector

The MCP Inspector is an interactive tool for testing and debugging your MCP servers. Use it to connect to your server, verify authentication, and try out tools, resources, and prompts.

You may run the inspector for any registered server:

1# Web server...
2php artisan mcp:inspector /mcp/weather
3 
4# Local server named "weather"...
5php artisan mcp:inspector weather

This command launches the MCP Inspector and provides the client settings that you may copy into your MCP client to ensure everything is configured correctly. If your web server is protected by an authentication middleware, make sure to include the required headers, such as an Authorization bearer token, when connecting.

Unit Tests

You may write unit tests for your MCP servers, tools, resources, and prompts.

To get started, create a new test case and invoke the desired primitive on the server that registers it. For example, to test a tool on the WeatherServer:

1test('tool', function () {
2 $response = WeatherServer::tool(CurrentWeatherTool::class, [
3 'location' => 'New York City',
4 'units' => 'fahrenheit',
5 ]);
6 
7 $response
8 ->assertOk()
9 ->assertSee('The current weather in New York City is 72°F and sunny.');
10});
1/**
2 * Test a tool.
3 */
4public function test_tool(): void
5{
6 $response = WeatherServer::tool(CurrentWeatherTool::class, [
7 'location' => 'New York City',
8 'units' => 'fahrenheit',
9 ]);
10 
11 $response
12 ->assertOk()
13 ->assertSee('The current weather in New York City is 72°F and sunny.');
14}

Similarly, you may test prompts and resources:

1$response = WeatherServer::prompt(...);
2$response = WeatherServer::resource(...);

You may also act as an authenticated user by chaining the actingAs method before invoking the primitive:

1$response = WeatherServer::actingAs($user)->tool(...);

Once you receive the response, you may use various assertion methods to verify the content and status of the response.

You may assert that a response is successful using the assertOk method. This checks that the response does not have any errors:

1$response->assertOk();

You may assert that a response contains specific text using the assertSee method:

1$response->assertSee('The current weather in New York City is 72°F and sunny.');

You may assert that a response contains an error using the assertHasErrors method:

1$response->assertHasErrors();
2 
3$response->assertHasErrors([
4 'Something went wrong.',
5]);

You may assert that a response does not contain an error using the assertHasNoErrors method:

1$response->assertHasNoErrors();

You may assert that a response contains specific metadata using the assertName(), assertTitle(), and assertDescription() methods:

1$response->assertName('current-weather');
2$response->assertTitle('Current Weather Tool');
3$response->assertDescription('Fetches the current weather forecast for a specified location.');

You may assert that notifications were sent using the assertSentNotification and assertNotificationCount methods:

1$response->assertSentNotification('processing/progress', [
2 'step' => 1,
3 'total' => 5,
4]);
5 
6$response->assertSentNotification('processing/progress', [
7 'step' => 2,
8 'total' => 5,
9]);
10 
11$response->assertNotificationCount(5);

Finally, if you wish to inspect the raw response content, you may use the dd or dump methods to output the response for debugging purposes:

1$response->dd();
2$response->dump();