How to Build an AI Agent with Laravel and MCP (2026)
Just a year ago, “integrating AI” into a Laravel application meant calling an OpenAI REST API and returning the text to the frontend. In 2026 the game has changed: agents no longer just generate text — they take real actions on your systems — creating users, querying orders, generating reports — and they do it through an open standard called Model Context Protocol (MCP).
In this tutorial you’ll build an MCP server in Laravel that exposes your business logic as tools invokable by Claude Desktop, Claude Code, Cursor, or any compatible client. Best of all: we’ll use the official laravel/mcp package, maintained by the Laravel team.
What is MCP and why does it matter in 2026?
Model Context Protocol is an open protocol created by Anthropic in 2024 that standardizes how AI models communicate with external systems. Think of MCP as the “USB-C of AI”: instead of writing a different wrapper for each client (one for Claude, another for Cursor, another for your own agent), you expose your tools once following the protocol and all compatible clients can use them.
An MCP server can expose three things:
- Tools — actions the agent executes (create a user, send an email, query the DB).
- Resources — read-only data the agent can read (a config file, the contents of a post).
- Prompts — reusable templates the client can invoke.
If you’ve already integrated OpenAI following the guide to integrating the OpenAI API with Laravel, MCP is the natural next step: instead of your backend calling the model, the model calls your backend when it needs to.
Prerequisites
- PHP 8.2 or higher. If you don’t have it yet, check out how to install PHP on Windows.
- A working Laravel 11 or 12 project. If you’re coming from an older version, see how to upgrade Laravel 9 to Laravel 10.
- Claude Desktop or Claude Code installed to test your server.
- Composer and a basic understanding of routes and controllers in Laravel.
Step 1: Install the official package
From your project root:
composer require laravel/mcp
The package automatically registers a set of Artisan commands with the make:mcp-* and mcp:* prefixes. Verify they are available:
php artisan list mcp
You should see at least: make:mcp-server, make:mcp-tool, make:mcp-resource, make:mcp-prompt, mcp:start, and mcp:inspector.
Step 2: Create the server
Let’s create a server called AppServer that will act as the agent’s “entry point” to your application:
php artisan make:mcp-server AppServer
This generates app/Mcp/Servers/AppServer.php. Open it and configure it with clear metadata — models use this information to decide when to invoke your tools:
<?php
namespace App\Mcp\Servers;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
#[Name('Ortamarco Blog Server')]
#[Version('1.0.0')]
#[Instructions(
'MCP server for the Ortamarco blog. Allows agents to list ' .
'published posts, create drafts, and query basic statistics.'
)]
class AppServer extends Server
{
protected array $tools = [
\App\Mcp\Tools\ListPostsTool::class,
\App\Mcp\Tools\CreateDraftTool::class,
];
protected array $resources = [];
protected array $prompts = [];
}
Tip:
Instructionsare critical. A server with vague instructions will be ignored by the model — be specific about what it does and when it should be used.
Step 3: Create your first tool
Let’s build a tool that lists the most recent blog posts. Generate the file:
php artisan make:mcp-tool ListPostsTool
Edit app/Mcp/Tools/ListPostsTool.php:
<?php
namespace App\Mcp\Tools;
use App\Models\BlogPost;
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(
'Lists published blog posts ordered by date. ' .
'Useful when the user asks about recent content or a specific topic.'
)]
class ListPostsTool extends Tool
{
public function handle(Request $request): Response
{
$validated = $request->validate([
'limit' => 'sometimes|integer|min:1|max:50',
'search' => 'sometimes|string|max:100',
]);
$posts = BlogPost::query()
->where('status', 'published')
->when(
$validated['search'] ?? null,
fn ($q, $term) => $q->where('title', 'like', "%{$term}%")
)
->latest('published_at')
->limit($validated['limit'] ?? 10)
->get(['id', 'title', 'slug', 'published_at']);
return Response::json([
'count' => $posts->count(),
'posts' => $posts,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'limit' => $schema->integer()
->description('Maximum number of posts to return (1-50). Default: 10'),
'search' => $schema->string()
->description('Optional term to filter by title'),
];
}
}
Three key points about this tool:
- The schema is a contract: the model reads it and generates valid calls. Describe each parameter as if you were explaining it to a new developer.
$request->validate()works just like in a controller: if the model passes invalid data, it receives a clear error it can correct on the next call.- Return structured data with
Response::json()when the content is tabular. ReserveResponse::text()for natural-language messages.
A second tool: creating drafts
Generate and fill in CreateDraftTool:
php artisan make:mcp-tool CreateDraftTool
<?php
namespace App\Mcp\Tools;
use App\Models\BlogPost;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Str;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;
#[Description('Creates a blog post draft. Returns the ID and the edit URL.')]
class CreateDraftTool extends Tool
{
public function handle(Request $request): Response
{
$data = $request->validate([
'title' => 'required|string|max:120',
'content' => 'required|string|min:50',
'tags' => 'sometimes|array',
'tags.*' => 'string|max:30',
]);
$post = BlogPost::create([
'title' => $data['title'],
'slug' => Str::slug($data['title']),
'content' => $data['content'],
'status' => 'draft',
'author_id' => config('mcp.default_author_id'),
]);
if (!empty($data['tags'])) {
$post->tags()->sync($data['tags']);
}
return Response::text(
"Draft #{$post->id} created: {$post->title}. " .
"Edit it at /admin/posts/{$post->id}/edit"
);
}
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()
->description('Post title (max 120 characters)')
->required(),
'content' => $schema->string()
->description('Content in Markdown, minimum 50 characters')
->required(),
'tags' => $schema->array()
->description('Optional list of tags to associate'),
];
}
}
Step 4: Register the server routes
Open routes/ai.php (create it if it doesn’t exist — Laravel 11+ loads it automatically when present) or use routes/web.php:
<?php
use App\Mcp\Servers\AppServer;
use Laravel\Mcp\Facades\Mcp;
// HTTP/SSE endpoint for remote clients (Claude.ai, Cursor cloud, etc.)
Mcp::web('/mcp/blog', AppServer::class);
// Local stdio endpoint for CLIs (Claude Code, Claude Desktop local)
Mcp::local('blog-server', AppServer::class);
The difference between the two modes:
| Mode | When to use it |
|---|---|
Mcp::web() | When the client lives on another machine (claude.ai, cloud deploys) |
Mcp::local() | When the client runs on your machine and starts the server as a child process |
Step 5: Test with the MCP Inspector
Before connecting Claude, validate that the server responds correctly. Laravel ships with a built-in inspector:
php artisan mcp:inspector
Open the URL it prints to the console (usually http://127.0.0.1:6274), select your server, and test each tool manually. If the inspector doesn’t see your tools, it’s almost always because you forgot to register them in the server’s $tools array.
Step 6: Connect Claude Desktop
Open the Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server under the mcpServers section:
{
"mcpServers": {
"ortamarco-blog": {
"command": "php",
"args": [
"/absolute/path/to/your/project/artisan",
"mcp:start",
"blog-server"
]
}
}
}
Restart Claude Desktop. A tools icon will appear in the bottom bar of the chat: clicking it should show list_posts_tool and create_draft_tool. Try it with a prompt like:
“List the 5 most recent blog posts and suggest one that might be outdated.”
Claude will automatically invoke ListPostsTool and reason about the response.
Important considerations
1. Security
An MCP tool is code a model can execute at will. Apply the same principles as for a public API:
- Always validate input with
$request->validate()— never assume the model respects the schema. - For destructive tools (delete, send emails, charge payments) consider adding a
dry_runflag or requiring confirmation. - For the
Mcp::web()endpoint, enable authentication withMcp::oauthRoutes()and Laravel Passport — without auth, anyone with the URL can invoke your tools. - If you handle user data, also review passwordless authentication in Laravel for modern auth flows.
2. Performance
Each tool invocation is an additional HTTP/stdio request. If your tool queries the database, apply the same techniques as for any public endpoint:
- Cache expensive results with
Cache::remember(). - Paginate large responses instead of returning thousands of rows.
- Move heavy work (sending emails, generating PDFs) to Queues.
If this sounds unfamiliar, I recommend the Laravel performance optimization guide.
3. Testing
The package ships with fluent testing helpers. A typical test:
it('lists published posts', function () {
BlogPost::factory()->count(3)->published()->create();
AppServer::tool(ListPostsTool::class, ['limit' => 5])
->assertOk()
->assertSee('count');
});
This is invaluable: it lets you iterate on your tools without having to reopen Claude each time.
4. Observability
Log every invocation with the tool name, parameters, and execution time. When an agent behaves unexpectedly, the tool call log is the first place to look — it’s the equivalent of a stack trace for agents.
Frequently asked questions
Do I absolutely need Laravel 12? The package supports Laravel 11+. It won’t work on older versions due to the new routing structure and PHP 8.2+ attributes.
Does MCP replace traditional REST APIs? No. MCP is complementary: your REST API keeps serving your frontend and machine-to-machine integrations. MCP is the layer for AI agents to consume your logic in a structured way. If you need to brush up on REST concepts, see how to build a REST API with Laravel.
Does it work with models other than Claude? Yes. MCP is an open standard. Cursor, Cline, Continue.dev, and a growing number of clients support it. Any GPT-4 or Gemini wrapper that implements MCP can also invoke your server.
Can I deploy it to production?
Yes, via Mcp::web(). Remember to enable OAuth, rate limiting, and deploy it behind the same proxy as your API. For high traffic, consider running the server on Octane or FrankenPHP.
Conclusion
MCP turns your Laravel application into a platform that AI agents can reason about and act on, without you having to maintain a separate wrapper for each client. The laravel/mcp package reduces all the boilerplate to three familiar concepts: a server, a tool, and a route.
If you want to keep building:
- How to integrate the OpenAI API with Laravel — to combine MCP with text generation in the same backend.
- How to build a REST API with Laravel — fundamentals that apply equally to your MCP tools.
- Laravel performance optimization — for when your MCP server starts receiving real traffic.
What tool are you going to expose first in your MCP server?