# Building a Production-Ready Laravel MCP Server

**Author:** Mozex | **Published:** 2026-04-08 | **Tags:** Laravel, PHP, AI, MCP | **Package:** laravel/mcp | **URL:** https://mozex.dev/blog/13-building-a-production-ready-laravel-mcp-server

---


Every Laravel developer has seen the MCP hype by now. Laravel's tagline changed to "The clean stack for Artisans and agents." The `laravel/mcp` package landed, tutorials appeared overnight, and suddenly everyone's building weather tools.

But those tutorials stop right where real questions start. How do you authenticate AI clients hitting your server? What happens when an agent sends garbage to a tool that writes to your database? Should you even use MCP, or is your REST API already enough?

I've been digging into this for a project, and these are the decisions and patterns I wish someone had documented before I started.

<!--more-->

## Do You Even Need MCP?

Before you `composer require` anything, ask one question: is an AI model your client?

MCP exists to give AI agents a standardized way to discover and use your application's capabilities. Tool discovery, schema negotiation, session state. If your client is a React frontend or a mobile app, you don't need any of this. Your REST API does the job.

But if you want Claude, Cursor, Copilot, or any MCP-compatible client to interact with your app directly, MCP gives you something REST doesn't: a single protocol that every AI client already speaks.

The mental model that clicked for me: **MCP doesn't replace your REST API. It wraps it.** Your business logic stays in services and repositories. MCP tools call those same services. Think of it as a presentation layer for AI, the same way Blade is a presentation layer for humans.

When MCP makes sense:

- Your users want to query your app through AI assistants
- You're building an AI-powered feature that needs to call your own backend
- You want to expose your app's data to IDE agents like Cursor for project-aware coding

If none of those apply, skip it.

## The Five-Minute Setup

The [official docs](https://laravel.com/docs/13.x/mcp) cover this well, so I'll keep it brief.

```bash
composer require laravel/mcp
php artisan vendor:publish --tag=ai-routes
```

This gives you `routes/ai.php`. Register a server:

```php
use App\Mcp\Servers\ProjectServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/projects', ProjectServer::class)
    ->middleware(['auth:sanctum']);
```

Generate the server and a tool:

```bash
php artisan make:mcp-server ProjectServer
php artisan make:mcp-tool ListProjectsTool
```

Wire the tool into your server's `$tools` array, define `schema()` and `handle()`, and you're running. Test it with the MCP Inspector before connecting any real client.

That's the getting-started part. Now for the stuff they skip.

## Authentication: Sanctum or Passport?

This is the first real decision, and it comes down to who's connecting.

**Sanctum** works when you control both sides. Internal tools, your own AI features, development environments. Generate a token, pass it as a bearer header, done.

```php
Mcp::web('/mcp/projects', ProjectServer::class)
    ->middleware(['auth:sanctum']);
```

Token generation is standard Sanctum:

```php
$token = $user->createToken('mcp-access')->plainTextToken;
```

**Passport with OAuth 2.1** is what you need when external clients connect. The MCP specification mandates OAuth 2.1 for HTTP transport, and for good reason. Clients register dynamically via [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591), discover your endpoints automatically, and go through a proper authorization flow with PKCE.

```php
Mcp::oauthRoutes();

Mcp::web('/mcp/projects', ProjectServer::class)
    ->middleware(['auth:api']);
```

The flow is fully automated. An unauthenticated client hits your MCP endpoint, gets a 401, discovers your OAuth endpoints through `/.well-known/oauth-protected-resource`, registers itself, opens a browser for user consent, and exchanges the auth code for a token. Your users see a consent screen, approve the connection, and the AI client is authenticated. All without you writing a single line of OAuth logic.

My take: **start with Sanctum for development, switch to Passport before anyone outside your team connects.** OAuth 2.1 is more ceremony, but it's the only option that gives users real control over which AI clients access their data.

## Tool Design Patterns That Matter

A tool is just a class with `schema()` and `handle()`. Straightforward syntax. The design decisions are where things get interesting.

### Write Descriptions for AI, Not Humans

The `#[Description]` attribute isn't documentation. It's the only context an AI model has when deciding whether to call your tool. Vague descriptions cause wrong tools to fire at wrong times.

```php
// The model has no idea when to pick this over a search tool
#[Description('Gets projects')]

// Now the model knows scope, ownership, and sort behavior
#[Description('Returns all active projects owned by the authenticated user, sorted by last activity. Does not include archived projects.')]
```

Two extra sentences in the description saves you from debugging bizarre tool selection behavior down the line.

### Scope Every Query to the Authenticated User

The most important security pattern in your MCP server. Easy to forget. Devastating when you do.

```php
public function handle(Request $request): Response
{
    // WRONG: any user's project, accessible by guessing IDs
    $project = Project::findOrFail($request->get('project_id'));

    // RIGHT: scoped to the authenticated user
    $project = $request->user()
        ->projects()
        ->findOrFail($request->get('project_id'));

    return Response::json($project->toArray());
}
```

Without scoping, an AI agent can request any record in your database by iterating IDs. Your auth middleware verified the user's identity, but the tool itself decides what data that user can reach.

One gotcha worth mentioning: import `Laravel\Mcp\Request`, not `Illuminate\Http\Request`. They look similar, but the MCP request class is what gives you access to tool arguments and the authenticated user through `$request->user()`.

### Hide Tools With shouldRegister()

Don't just check permissions inside `handle()`. If a user shouldn't know a tool exists, hide it during discovery:

```php
public function shouldRegister(Request $request): bool
{
    $user = $request->user();

    return $user && $user->hasRole('admin');
}
```

This keeps the tool list clean. The AI model only sees tools the current user can actually use. An admin-only delete tool won't appear for regular users, which means the model can't even attempt to call it.

### Validate Like You Would in a Controller

MCP tools accept input from AI models. Models hallucinate. Treat every input as untrusted, because it is.

```php
public function handle(Request $request): Response
{
    $validated = $request->validate([
        'project_id' => 'required|integer|exists:projects,id',
        'status' => 'sometimes|in:active,archived,completed',
    ]);

    // Safe to use $validated
}
```

Laravel's validation works identically inside MCP tools. No new concepts to learn.

## Security Gaps Worth Knowing About

The MCP ecosystem is young, and its security surface is still being mapped. Some of these problems live in deployment, not in your application code.

### NeighborJack

Backslash Security [audited over 7,000 MCP servers](https://virtualizationreview.com/articles/2025/06/25/mcp-servers-hit-by-neighborjack-vulnerability-and-more.aspx) and found hundreds bound to `0.0.0.0` with no authentication. The attack: anyone on the same local network (coffee shop WiFi, coworking space, corporate LAN) connects and invokes your tools. They called it NeighborJack.

This doesn't apply if you're using `Mcp::web()` behind Nginx with proper middleware, since your server runs through Laravel's HTTP stack. But if you have a local MCP server for development bound to all interfaces, you're exposed.

**Rule:** Local servers bind to `127.0.0.1` only. Web servers always have authentication middleware. No exceptions.

### Token Passthrough

If your tool calls a third-party API, don't forward the bearer token that the MCP client sent you.

```php
// WRONG: grabbing the HTTP bearer token to call Stripe
Http::withToken(request()->bearerToken())
    ->get('https://api.stripe.com/v1/charges');

// RIGHT: use your app's stored credentials
Http::withToken(config('services.stripe.secret'))
    ->get('https://api.stripe.com/v1/charges');
```

The client's token authenticates the AI agent to *your* server, not your server to Stripe. Passing it through breaks audit trails, bypasses rate limits, and can expose user tokens to services they never authorized.

### Prompt Injection in Tool Inputs

AI agents relay user input to your tools. If a user tells Claude "delete all my projects" and your server exposes a delete tool, the model might call it. This isn't a bug in MCP or in the model. It's an architecture decision.

Defense in depth: validate inputs, check permissions in every tool, use `shouldRegister()` to hide dangerous capabilities from most users, and consider returning a confirmation prompt instead of executing destructive operations immediately.

### The OWASP MCP Top 10

[It exists already.](https://owasp.org/www-project-mcp-top-10/) The MCP ecosystem is new enough that security best practices are still being formalized, but OWASP has identified the ten most common vulnerability patterns. If you're exposing any MCP endpoint to the internet, read it before you ship.

## Lessons From the Process

**Start with one tool.** I initially planned a suite covering every model in the app. Most would never get called. AI clients work best with a focused, well-described set of capabilities. Get one tool working perfectly, prove it's useful, then expand.

**Test with MCP Inspector first.** The Inspector shows exactly what an AI client sees: your tool list, descriptions, schemas, and raw responses. If a description looks confusing there, it'll confuse a model even more.

**Don't return raw Eloquent models.** Tempting to `return Response::json($project->toArray())` and call it done. But `toArray()` dumps every column: internal flags, soft-delete timestamps, pivot data. Use API resources or explicit arrays to control what gets returned. Same discipline you'd apply to a REST endpoint.

**Read the [security best practices post](https://laravel.com/blog/laravel-mcp-server-auth-security-best-practices) on the Laravel blog.** It covers OAuth setup, token management, and authorization patterns in more depth than I can here. One of the better security-focused posts they've published.

## Where This Is Heading

MCP is the first real standard for AI-to-application communication. Every major AI client supports it. Laravel has first-party support. The tooling is young but improving fast.

If you've built APIs in Laravel before, you already know most of what matters here. MCP adds a new type of client to think about, but the fundamentals haven't changed: authenticate, authorize, validate, and be deliberate about what data you expose. Treat your MCP server with the same rigor you'd give any public-facing endpoint, and you'll be ahead of most.