Add a Model Context Protocol (MCP) server to your Laravel application so AI coding agents can interact with your data, run tools, and consume reusable resources and prompts.
Model Context Protocol (MCP) is a standardised protocol for communication between AI clients (Claude, Cursor, GitHub Copilot, etc.) and your application. By implementing an MCP server you let AI agents read your application’s data and execute actions on your behalf.
Laravel MCP is an official package added in Laravel 13, available as laravel/mcp. It provides everything you need to build MCP servers.
An MCP server can expose three types of capabilities:
Capability
Description
Tools
Functions an AI client can call — search, update, or integrate with external APIs
<?phpnamespace 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'); // Fetch weather data... 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(), ]; }}
The tool’s name and title are automatically derived from the class name. CurrentWeatherTool becomes name current-weather and title Current Weather Tool. Override with Name and Title attributes:
The Description attribute is not generated automatically. Always provide a meaningful description — it is how the AI model understands what the tool does and when to use it.
Define input parameters in the schema method using Laravel’s JSON schema builder:
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'), ];}
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, e.g. "New York City" or "Tokyo".', 'units.in' => 'Units must be "celsius" or "fahrenheit".', ]); // Use $validated...}
When validation fails the AI client receives the error message and can retry. Write clear, actionable error messages to help the agent self-correct.
return Response::error('Unable to fetch weather data. Please try again.');
Image and audio responses
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');// Load directly from storage (MIME type is auto-detected)return Response::fromStorage('weather/radar.png');
Multiple content items
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"), ];}
<?phpnamespace 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 for the weather description (e.g., formal, casual, humorous).', required: true, ), ]; }}
Prompt arguments are automatically validated based on their definition, but you may enforce more complex validation rules by calling validate inside the handle method:
<?phpnamespace 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']; // Generate the prompt response using the given tone... }}
On validation failure, AI clients will act based on the error messages you provide. Provide clear and actionable messages:
$validated = $request->validate([ 'tone' => ['required', 'string', 'max:50'],], [ 'tone.*' => 'You must specify a tone. Examples: "formal", "casual", "humorous".',]);
Return user and assistant messages from the handle method. Use asAssistant() to mark a message as coming from the assistant:
<?phpnamespace 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'); return [ Response::text("You are a helpful weather assistant. Describe the weather in a {$tone} tone.")->asAssistant(), Response::text('What is the current weather like in Tokyo?'), ]; }}
Resources are data or information an AI client can load as context — documentation, configuration, or dynamic application data that improves the quality of AI responses.
<?phpnamespace 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); }}
Unlike tools and prompts, resources cannot define input schemas or arguments. However, you can still interact with the request object within your resource’s handle method:
<?phpnamespace 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 { // Access request information... }}
Laravel MCP supports MCP Apps, an extension of the Model Context Protocol that allows tools to render interactive HTML applications within sandboxed iframes in supported hosts. This allows you to build dashboards, forms, visualizations, and other rich experiences that go beyond plain text responses.An MCP app consists of two parts working together:
An app resource that returns the self-contained HTML for your application.
A tool that is linked to the app resource using the #[RendersApp] attribute. When the tool is called, the host fetches and renders the linked resource.
This command creates two files: a PHP class in app/Mcp/Resources and a Blade view in resources/views/mcp. The view name is automatically inferred from the class name. For example, WeatherDashboardApp maps to mcp.weather-dashboard-app:
AppResource extends the base Resource class and automatically configures the ui:// URI scheme and the text/html;profile=mcp-app MIME type required by the MCP Apps specification. Like any other resource, you must register it in your server’s $resources array.The generated Blade view uses the <x-mcp::app> component, which renders a complete HTML document with the client-side MCP SDK bundled and ready to use:
The createMcpApp global is provided by the bundled SDK and handles connecting the iframe to the server, applying host theming, and exposing helpers such as callServerTool, sendMessage, openLink, and event callbacks. For the full client-side API, refer to the MCP Apps specification.
To display an app resource, link a tool to it using the #[RendersApp] attribute. When the tool is called, Laravel MCP includes the resource’s URI in the tool metadata so the host can render the app in a sandboxed iframe:
<?phpnamespace 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.'); }}
Laravel MCP automatically advertises the io.modelcontextprotocol/ui capability whenever any AppResource is registered, so no additional server configuration is required.
Each #[RendersApp] tool can limit who may invoke it via the visibility argument. This is useful for exposing private, app-only tools that the UI calls to load or refresh data without making those tools visible to the model:
The Visibility enum has two cases, Model and App, and defaults to both. Use [Visibility::App] for backend actions the UI calls directly, or [Visibility::Model] to make a tool unavailable to the UI.
The #[AppMeta] attribute on your app resource configures the iframe’s Content Security Policy, browser permissions, and any library scripts that should be included in the view’s <head>:
The Library enum includes pre-configured CDN scripts for common front-end libraries such as Library::Tailwind and Library::Alpine, and their CDN origins are automatically merged into the CSP. The Permission enum covers browser permissions such as Camera, Microphone, Geolocation, and ClipboardWrite.
For computed or dynamic configuration, override the appMeta method on your resource using the fluent AppMeta, Csp, and Permissions builders from the Laravel\Mcp\Server\Ui namespace.
Laravel MCP includes a dedicated Boost skill reference for building MCP Apps. If you have Laravel Boost installed, your AI coding agent can invoke the mcp-development skill and ask it to scaffold an app resource, Blade view, and linked tool for you.For the complete protocol reference, including the full client-side API and schema details, see the official MCP Apps documentation.
The Icon attribute is repeatable, so you may declare multiple icons to provide different sizes or light and dark theme variants.Alternatively, define icons programmatically by overriding the icons method. This is useful when an icon depends on runtime conditions:
use Laravel\Mcp\Schema\Icon;class CurrentWeatherTool extends Tool{ /** * Get the tool's icons. * * @return array<int, Icon> */ public function icons(): array { return [ Icon::from('mcp/tool.png', mimeType: 'image/png'), ]; }}
Icons defined via the attribute and the icons method are combined automatically. Icon paths are resolved as follows:
Paths with a URI scheme such as https: or data: are used as-is.
Relative paths are resolved to a URL using Laravel’s asset helper.
use App\Mcp\Servers\WeatherServer;use Laravel\Mcp\Facades\Mcp;Mcp::oauthRoutes();Mcp::web('/mcp/weather', WeatherServer::class) ->middleware('auth:api');
When using OAuth, publish the MCP authorization views and register them with Passport:
Access the authenticated user inside a tool or resource via $request->user():
public function handle(Request $request): Response{ if (! $request->user()->can('read-weather')) { return Response::error('Permission denied.'); } // Continue...}
In addition to building servers, Laravel MCP includes a client for connecting to other MCP servers, whether first-party or third-party. The client lets your application discover and call the tools exposed by an MCP server, which is especially useful for giving your AI agents access to capabilities provided by external MCP servers.
Connect to an HTTP-accessible MCP server using the Client::web method, passing the server’s URL:
use Laravel\Mcp\Client;$client = Client::web('https://mcp.example.com');
To connect to a local MCP server that runs as a command, use the Client::local method, providing the command and any arguments needed to start the server:
use Laravel\Mcp\Client;$client = Client::local('php', ['artisan', 'mcp:start']);
The client connects lazily, automatically establishing the connection the first time you list or call tools. If you need to manage the connection manually, you may use the connect, connected, ping, and disconnect methods:
To connect to a web MCP server protected by a bearer token, use the withToken method. You may pass a token string or a closure that lazily resolves the token:
The clientId and clientSecret arguments may be omitted when the MCP server supports dynamic client registration, in which case the client registers itself automatically.
Register the OAuth routes for the named client in routes/ai.php using the oAuthRoutesFor method:
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');});
This registers two named routes: a connect route (mcp.oauth.{client}.connect) that redirects the user to the authorization server, and a callback route (mcp.oauth.{client}.callback) that exchanges the authorization code and invokes your handler. To begin the authorization flow, redirect the user to the connect route:
Retrieve the tools exposed by an MCP server using the tools method, which returns a collection keyed by tool name:
use Laravel\Mcp\Facades\Mcp;$tools = Mcp::client('github')->tools();foreach ($tools as $tool) { $tool->name; $tool->title; $tool->description; $tool->inputSchema;}
The client automatically paginates through all available tools. Limit the number returned using the limit argument:
$tools = Mcp::client('github')->tools(limit: 10);
Invoke a tool with the callTool method, passing the tool name and an array of arguments:
use Laravel\Mcp\Facades\Mcp;$result = Mcp::client('github')->callTool('current-weather', [ 'location' => 'New York',]);$result->text(); // The text content of the response(string) $result; // Equivalent to calling text()$result->isError; // Whether the tool reported an error$result->structuredContent; // Structured content, if any
You may also call a tool directly from a listed tool instance:
If you are building agents with the Laravel AI SDK, you may provide tools from an MCP client directly to an agent. See the MCP Tools section of the AI SDK documentation for more information.
To retrieve a prompt, use the getPrompt method, passing the prompt name and an array of arguments. The returned PromptResult instance exposes the generated messages:
use Laravel\Mcp\Facades\Mcp;$result = Mcp::client('github')->getPrompt('describe-weather', [ 'location' => 'New York',]);$result->text(); // The text content of the messages(string) $result; // Equivalent to calling text()$result->messages; // The raw messages returned by the prompt$result->description; // The prompt description, if any
To read a resource, use the readResource method, passing the resource URI. The returned ResourceReadResult instance exposes the resource content:
use Laravel\Mcp\Facades\Mcp;$result = Mcp::client('github')->readResource('weather://guidelines');$result->content(); // The content of the resource, decoding base64 blobs as needed(string) $result; // Equivalent to calling content()$result->mimeType(); // The MIME type of the resource, if any$result->contents; // The raw contents returned by the resource
Use the interactive mcp:inspector command to debug your server:
# Web serverphp artisan mcp:inspector mcp/weather# Local server (named 'weather')php artisan mcp:inspector weather
The command launches the MCP Inspector and displays a client configuration you can copy. If your server uses authentication middleware, include the Authorization header when connecting.
Write unit tests directly against your tools, resources, and 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.');});
$response->assertOk(); // No error in the response$response->assertSee('...'); // Response contains the given text
Assert that an error is present:
$response->assertHasErrors();$response->assertHasErrors([ 'Something went wrong.',]);
Assert that no errors are present:
$response->assertHasNoErrors();
Assert the name, title, or description of the tool, resource, or prompt:
$response->assertName('current-weather');$response->assertTitle('Current Weather Tool');$response->assertDescription('Fetches the current weather forecast for a specified location.');
Assert streaming notifications using assertSentNotification and assertNotificationCount: