You have probably pasted your database schema, your API routes, or a chunk of logs into an AI chat window at some point and watched it go stale the moment you ran the next migration. The Model Context Protocol (MCP) fixes that problem at the root: it is an open standard that lets AI clients talk to your application directly, calling tools and reading resources through a typed interface instead of whatever you happened to copy into the prompt.
Laravel now ships a first-party laravel/mcp package that turns your app into an MCP server with plain Artisan commands and fluent PHP classes. It has recently matured to a 1.0 release, which makes this the right moment to learn it. In this guide you will build a working Laravel MCP server from scratch: a docs server that exposes a search tool, a guidelines resource, and a prompt, then secure it, test it, and connect it to a real AI client.
What you will build (and what you need)
By the end of this post you will have a DocsServer registered at /mcp/docs on a Laravel 13 application. It exposes:
- a tool that searches your documentation,
- a resource with usage guidelines the model can read,
- a prompt that generates a changelog entry from a set of changes,
- Sanctum-based authentication and per-user authorisation,
- unit tests that exercise the server without any AI client at all.
You need a Laravel 13 application with PHP 8.3 or newer, Composer, and somewhere to run Artisan commands. Everything is officially documented under Laravel 13.x, so if you are still on Laravel 12, check the package compatibility before you start. For testing you will use the built-in MCP Inspector, so you do not need an AI subscription to follow along.
1. Install the laravel/mcp package
Install the package with Composer:
composer require laravel/mcp
Then publish the routes/ai.php file, which is where all of your MCP servers get registered:
php artisan vendor:publish --tag=ai-routes
This creates routes/ai.php in your application. Think of it as routes/api.php, but for AI clients instead of HTTP consumers.
2. Create your first MCP server
Generate a server class with Artisan:
php artisan make:mcp-server DocsServer
This creates app/Mcp/Servers/DocsServer.php. The generated class extends Laravel MCP’s base Server and carries three attributes that describe it to clients:
<?php
namespace App\Mcp\Servers;
use App\Mcp\Prompts\WriteChangelogPrompt;
use App\Mcp\Resources\DocsGuidelinesResource;
use App\Mcp\Tools\SearchDocsTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
#[Name('Docs Server')]
#[Version('1.0.0')]
#[Instructions('Answers questions using the application documentation.')]
class DocsServer extends Server
{
/**
* The tools registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Tool>>
*/
protected array $tools = [
SearchDocsTool::class,
];
/**
* The resources registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Resource>>
*/
protected array $resources = [
DocsGuidelinesResource::class,
];
/**
* The prompts registered with this MCP server.
*
* @var array<int, class-string<\Laravel\Mcp\Server\Prompt>>
*/
protected array $prompts = [
WriteChangelogPrompt::class,
];
}
The #[Instructions] attribute matters more than it looks: it is one of the first things a model reads when deciding whether your server can help, so write it like a one-line sales pitch, not a placeholder.
3. Register the server: web vs local
Laravel MCP gives you two ways to expose a server. Open routes/ai.php and register both:
use App\Mcp\Servers\DocsServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp/docs', DocsServer::class);
Mcp::local('docs', DocsServer::class);
Mcp::web exposes the server over HTTP POST, which is what remote clients and web-based integrations use. Mcp::local registers it as a local Artisan-driven server, which is the right choice for tools running on your own machine, like Claude Code or Cursor. You do not run the mcp:start command yourself for local servers; your MCP client starts it for you.
4. Add a tool
Tools are the heart of an MCP server. They are functions your AI client can call: search something, create a record, trigger a workflow. Generate one:
php artisan make:mcp-tool SearchDocsTool
Then give it a description, an input schema, and a handler:
<?php
namespace App\Mcp\Tools;
use App\Services\DocsSearch;
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('Searches the application documentation and returns matching articles.')]
class SearchDocsTool extends Tool
{
/**
* Handle the tool request.
*/
public function handle(Request $request, DocsSearch $search): Response
{
$results = $search->query(
$request->get('query'),
$request->get('locale', 'en'),
);
if ($results->isEmpty()) {
return Response::error('No documentation matched that query.');
}
return Response::text($results->toText());
}
/**
* Get the tool's input schema.
*
* @return array<string, \Illuminate\JsonSchema\Types\Type>
*/
public function schema(JsonSchema $schema): array
{
return [
'query' => $schema->string()
->description('The search terms, e.g. "refund policy".')
->required(),
'locale' => $schema->string()
->description('The documentation language to search.')
->default('en'),
];
}
}
A few things worth noticing. The schema is built with Laravel’s JSON schema builder, so ->required(), ->description(), ->enum() and ->default() all work the way you would expect. Dependencies are injected by the service container, in the constructor or directly into handle(), exactly like a controller. And Response::error() is how you report a failure back to the model, distinct from returning text.
The tool’s name and title are derived from the class name automatically: SearchDocsTool becomes search-docs / “Search Docs Tool”. If you want something more deliberate, customise them with #[Name] and #[Title] attributes. Always write a #[Description] — descriptions are never generated for you, and the model relies on them to decide when to call your tool.
If a server ends up with dozens of tools, you do not have to advertise every one of them up front. Laravel MCP supports searchable tool catalogues: some tools stay hidden behind a search_tools / execute_tools pair so the client discovers them on demand instead of loading everything into context.
5. Add a resource
While tools do things, resources expose data: guidelines, configuration, read-only reference material. Generate one:
php artisan make:mcp-resource DocsGuidelinesResource
<?php
namespace App\Mcp\Resources;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;
#[Description('Guidelines for interpreting and citing the documentation.')]
#[Uri('docs://resources/guidelines')]
#[MimeType('text/markdown')]
class DocsGuidelinesResource extends Resource
{
/**
* Handle the resource request.
*/
public function handle(Request $request): Response
{
return Response::text(view('mcp.docs-guidelines')->render());
}
/**
* Determine if the resource should be registered.
*/
public function shouldRegister(Request $request): bool
{
return $request?->user()?->can('view-docs') ?? false;
}
}
Unlike tools, resources have no input schema, but they do have a URI and a MIME type that tell the client how to interpret the content. You can also expose dynamic resources with URI templates (implementing HasUriTemplate, e.g. docs://articles/{slug}), and you can gate a resource per-request with shouldRegister(), as shown above.
6. Add a prompt
Prompts are reusable, templated message flows. Instead of every user hand-writing “write a changelog entry for these changes, keep it under 80 characters, use present tense”, you define that workflow once and the client invokes it with arguments:
php artisan make:mcp-prompt WriteChangelogPrompt
Register it in the server’s $prompts array (already done in step 2), define the arguments it accepts, and validate them the same way you validate tool input. Prompts are the least-used of the three primitives, but they are the difference between an MCP server that only answers questions and one that standardises how your team talks to the model.
7. Secure the server
An unauthenticated MCP server that can read your data is a liability, so protect it before you connect anything. If your app already uses Sanctum, the simplest path is one line of middleware in routes/ai.php:
Mcp::web('/mcp/docs', DocsServer::class)
->middleware('auth:sanctum');
Clients then pass an Authorization: Bearer <token> header. Inside tools and resources you have the authenticated user via $request->user(), so you can apply the same authorisation rules you use everywhere else:
public function handle(Request $request): Response
{
if (! $request->user()->can('view-docs')) {
return Response::error('Permission denied.');
}
// ...
}
Two things to know before you pick an auth strategy. First, OAuth 2.1 is the documented authentication mechanism in the MCP specification and is the most widely supported among MCP clients, so the package recommends Laravel Passport when it is an option, including Mcp::oauthRoutes to wire up the authorisation flow with a single mcp:use scope. Second, if Sanctum is already your auth layer, adding Passport just for MCP is usually not worth it — protect the routes with auth:sanctum until an OAuth-only client forces your hand.
8. Test it before any client touches it
You can debug any registered server with the built-in MCP Inspector, which launches an interactive client for trying out tools, resources, and prompts:
# Web server...
php artisan mcp:inspector mcp/docs
# Local server named "docs"...
php artisan mcp:inspector docs
The command prints the client settings you can copy into your MCP client configuration, which removes most of the “why won’t it connect” guesswork. If your web server sits behind auth middleware, include the Authorization: Bearer header when connecting.
For regression tests, the package lets you invoke server primitives directly in PHPUnit or Pest, no client required:
test('docs search tool returns results', function () {
$response = DocsServer::tool(SearchDocsTool::class, [
'query' => 'refund policy',
]);
$response->assertOk()->assertSee('Refund');
});
test('docs search requires permission', function () {
$user = User::factory()->create();
$response = DocsServer::actingAs($user)
->tool(SearchDocsTool::class, ['query' => 'refund policy']);
$response->assertHasNoErrors();
});
Prompts and resources get the same treatment via DocsServer::prompt(...) and DocsServer::resource(...). Assertions cover status (assertOk), content (assertSee), errors (assertHasErrors / assertHasNoErrors), and metadata (assertName, assertTitle, assertDescription).
9. Connect it to a real AI client
With the server registered locally as docs, point your MCP client at the stdio command php artisan mcp:start docs. In Claude Code or Cursor that means adding a stdio server entry to your MCP configuration whose command is php and whose arguments are artisan mcp:start docs run from your project directory. Restart the client, ask it “what tools do you have?”, and your search-docs tool should show up alongside its built-ins.
For the web server, register the URL (https://your-app.test/mcp/docs) as a remote/streamable-HTTP MCP server in your client and supply the Sanctum bearer token in the headers. One practical tip from the trenches: get the local server working first, because auth headers on remote servers are where most first-time setups stall.
Mistakes almost everyone makes
- Skipping descriptions. Tool, resource, and prompt descriptions are never auto-generated. A vague or missing description is the number one reason a model ignores a perfectly good tool.
- Exposing everything as tools. If the model only needs to read something, make it a resource, not a tool. Tools imply actions; resources imply data.
- Forgetting local servers are stdio. A local server runs as an Artisan command your client spawns. Do not try to hit it with HTTP, and do not run
mcp:startyourself. - No auth on the web server.
Mcp::webwithout middleware is a public endpoint. Sanctum middleware is one line; use it. - Advertising 40 tools at once. Every advertised tool eats context. Use searchable tool catalogues once your server grows past a handful of primitives.
Where to go from here
Once your server is live, the package also works in the other direction: Laravel MCP includes an MCP client, so your application can connect to other people’s MCP servers. Register a named client once and call external tools from your own agents:
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;
Mcp::registerClient('github', fn () => Client::web('https://api.githubcopilot.com/mcp/'));
$result = Mcp::client('github')->callTool('list_issues', [
'owner' => 'laravel',
'repo' => 'framework',
]);
echo $result->text();
That pairs naturally with the Laravel AI SDK, where tools from an MCP client can be handed straight to an agent. And if you want to go beyond text, look into MCP Apps: the package supports tools that render interactive HTML dashboards inside supported hosts via the #[RendersApp] attribute and Blade views.
You built this server in under an hour, and every part of it — the tool schemas, the Sanctum guard, the unit tests — is ordinary Laravel. That is the whole point: your AI tooling should live in your application, versioned, tested, and secured like everything else.
Further Reading & References
- Laravel MCP — official documentation (Laravel 13.x) — the complete reference for servers, tools, resources, prompts, authentication, and the MCP client.
- Laravel News: MCP 1.0, Laravel 14 & MariaDB Vector Search — the roundup covering the
laravel/mcp1.0 milestone. - Laravel Nightwatch MCP Server docs — a real-world example of an MCP server shipped by the Laravel team, with OAuth setup for Claude Code, Cursor, and others.
- MCP + Laravel integration walkthrough (Guava) — an early community tutorial exposing blog articles and app internals over MCP.
- sourcecodeguy1/laravel-mcp-server — community package giving AI agents read-only access to routes, schema, models, and migrations.
- sirikkoster/laravel-loop — community MCP server with a worked example of protecting endpoints with Sanctum middleware.
- Truss MCP server guide — how Truss registers a read-only schema server on top of
laravel/mcpfor coding agents. - laravel-ai-guardrails MCP guide — a good example of hardening an MCP surface: input caps, withheld metadata, and default-off registration.
- Laravel Sanctum Setup: Complete Auth Guide (HowToShipIt) — our own walkthrough of Sanctum tokens and SPA auth, useful background for the auth section above.
Verified against the official Laravel 13.x MCP documentation and community sources on 11 October 2026. The laravel/mcp API described here reflects the documented 1.0-era interface.



