May 17, 2026

How to Build an AI Agent with Laravel and MCP (2026)

Photo of Marco Orta Marco Orta | 8 min read
Compartir
MCP server built with Laravel connecting AI agents to a PHP application

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: Instructions are 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:

  1. 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.
  2. $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.
  3. Return structured data with Response::json() when the content is tabular. Reserve Response::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:

ModeWhen 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_run flag or requiring confirmation.
  • For the Mcp::web() endpoint, enable authentication with Mcp::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:

What tool are you going to expose first in your MCP server?

Compartir

Search

Tags