Deploy a Laravel MCP App to Production with Laravel Cloud

Deploy a Laravel MCP App to Production with Laravel Cloud

In a previous post, we covered creating an MCP server so you can expose your Laravel app to AI clients like Claude and ChatGPT. Building that server locally is straightforward. Building and deploying an MCP app that connects to it is slightly more complex.

This post takes a SaaS support workflow from local tools to a deployed Laravel MCP app running on Laravel Cloud. You will build three tools that give Claude live access to subscription data, then test and ship the whole MCP app into production with a single Git push.

For installation and protocol background, see the Laravel MCP documentation.

The Scenario: Building an MCP App for a Support Workflow

Let's say your support team uses Claude daily. A customer emails saying their trial expired before they finished evaluating the product.

The rep needs the customer's current plan, subscription status, invoice history, and confirmation that it is safe to extend the trial without creating a billing conflict.

Without structured tools, that workflow comprises five manual steps:

  • Query the database.
  • Format the result.
  • Paste it into Claude.
  • Interpret Claude's advice.
  • Go back to the database to act on it.

Each handoff introduces error. With a deployed Laravel MCP server, Claude handles the lookup, the invoice check, and the trial extension in a single conversation thread. The tools carry types, annotations, and role gates so the right people can do the right things and nothing else.

But none of that matters if the app only runs on your laptop. The goal is production, so let's build with deployment in mind from the start.

The MCP Tools

IdentifyCustomerTool

Support reps need to identify a customer by email before they can do anything else. This tool returns the plan name, subscription status, and trial end date in a typed response shape that Claude can read directly.

Retrieving Invoice History

The support rep now has the customer's plan and subscription status. The next question is always the same: what have they been billed for, and when?

GetInvoiceHistoryTool pulls the last 12 months of invoice line items for a given user. Like the customer identification tool, it is read-only. Claude can scan the results and flag anomalies (duplicate charges, gaps in billing) without the rep digging through Stripe manually.

The months parameter uses an enum constraint so Claude cannot request an unbounded history dump. The output schema tells Claude exactly what fields to expect, which makes its summaries more reliable.

IssueTrialExtensionTool

Reading data is half the job. A rep who finds a customer with an expired trial needs to act. This tool extends the trial end date and carries #[IsDestructive], which signals to the MCP client that it modifies state. Claude presents destructive tools with an explicit confirmation step before executing them.

The #[IsDestructive] annotation triggers a confirmation step in Claude, but it does not create a record of what changed or who changed it. For a tool that modifies billing data, add an audit entry inside handle() before returning:

Your activity_logs table now records who extended which trial, by how many days, and when. If a rep runs the tool twice on the same account or a billing dispute comes up later, you have a complete trail of every action taken through the MCP server.

Register all three tools on the server and expose the endpoint in routes/ai.php:

Securing the MCP Endpoint

Authenticating the MCP Client

The route file applies auth:sanctum middleware, but that raises a practical question: how does the MCP client get a token in the first place?

The MCP client needs a Sanctum API token scoped to the actions it should perform. You generate this token once per client, store it securely on the client side, and rotate it on a schedule. We've written a guide on Laravel MCP auth and security best practices for more details.

Creating a Scoped Token

Checking Abilities in Tools

Token abilities become useful when you want a single MCP endpoint but different permission levels. A read-only integration might receive a token with only mcp:read. Inside a destructive tool, check the ability before proceeding:

Token Rotation

Tokens should have an expiration date. When a token nears expiry, generate a new one and update the MCP client configuration. For automated rotation, schedule a job that creates a new token and revokes the old one:

Keep token lifetimes short enough that a leaked token does limited damage, but long enough that rotation is not a daily chore. Ninety days is a reasonable starting point.

Rate Limiting the MCP Endpoint

Every MCP tool call hits your database. A misconfigured client or a prompt injection loop can fire hundreds of calls per minute. Rate limiting is the first line of defense.

Laravel's built-in rate limiter works on the MCP route the same way it works on any API endpoint:

Thirty calls per minute is generous for a support workflow. If your tools are read-heavy, you might allow more. If they are write-heavy, tighten the limit.

For destructive tools specifically, consider a stricter per-tool limit inside the handle() method:

This prevents a single operator from extending trials in a loop, whether by accident or through a prompt injection that convinces Claude to keep retrying.

Handling Errors Gracefully

Every handle() method shown so far assumes the happy path. In production, things break. A user ID might not match a subscription. Stripe might be down. The question is not whether errors happen, but what Claude sees when they do.

An unhandled exception returns a raw error to the MCP client. Claude will try to interpret it, but raw stack traces are not useful context for an AI assistant. Structured error responses give Claude something it can act on.

Wrap the body of each handle() method in a try/catch that maps exceptions to typed error responses. Here is IdentifyCustomerTool with explicit error handling:

Three things to notice:

  1. Validation errors return the first human-readable message. Claude can relay this directly to the support rep ("That email address was not found in our system").

  2. Missing models return a clear noun ("User not found") instead of a class path.

  3. Unexpected errors get reported to your error tracker, but Claude only sees a generic message. Never leak internal details through the MCP response.

Apply the same try/catch pattern to every tool that touches the database or an external service.

Gating Tools by Role with shouldRegister

IssueTrialExtensionTool writes to the database. It should not be available to every authenticated user. The shouldRegister method lets each tool inspect the current request and decide whether to register itself at all. The gate lives inside the tool, not scattered across middleware or controller logic.

A billing administrator sees all three tools. A read-only support agent sees two. When shouldRegister returns false, the tool is invisible to the MCP client. Claude never learns that it exists and cannot be prompted into attempting the call. This is a more reliable guard than a runtime permission check inside handle because the attack surface disappears entirely.

The same pattern works for subscription-based access. If certain tools should only appear for users on paid plans:

Testing Your MCP App

Laravel MCP ships with testing helpers that let you call tools directly on your server class and assert on the response. You call the tool the same way Claude does, then chain fluent assertions on the result.

Testing the Read-Only Tool

The SupportServer::tool() method calls IdentifyCustomerTool with the given arguments and returns a test response. assertStructuredContent validates the exact JSON shape Claude receives. assertHasErrors confirms that validation failures return structured errors instead of raw exceptions.

Testing the Destructive Tool

The trial extension tool changes data. Tests verify both the response and the database state, plus the audit trail.

actingAs($admin) sets the authenticated user for the request, the same way Sanctum authenticates a real MCP client. The tool sees $request->user() as the admin, so audit logs record the correct actor.

Testing shouldRegister Gating

The shouldRegister method controls which tools Claude can see. Test that non-admins never see the destructive tool:

When shouldRegister returns false, the server responds as if the tool does not exist. The assertHasErrors call confirms the tool is invisible, not just forbidden.

These tests call each tool the same way the MCP client does: through the server, with arguments and an optional authenticated user. There are no manual request objects to build and no response arrays to destructure. The fluent assertions read like a spec for what Claude should see.

Deploying to Laravel Cloud in Minutes

Everything up to this point runs on your local machine. The tools work, the gates are in place, and Claude can query live data through the MCP endpoint. Now all you need to do is ship the MCP app.

Laravel Cloud is the production destination for this server. It handles provisioning, scaling, zero-downtime deployments, and SSL automatically. Plus, it's purpose-built for Laravel apps so you ship faster without managing infrastructure. You can sign up and start for free.

Connect your repository, configure environment variables in the dashboard, and every git push to your production branch triggers an automatic deployment. There is no server to provision, no Nginx to configure, and no SSL certificate to renew.

  1. Connect your repository. Sign in to Laravel Cloud, create a new project, and connect your GitHub or GitLab repository. Cloud builds a Docker image tuned for Laravel on every push, using the PHP version you specify.
  2. Set your environment variables. Add everything your app needs for production in the Cloud dashboard, including your APP_KEY, database credentials, and any Laravel Sanctum configuration required to authenticate MCP clients.
  3. Add a deploy command. In the deployment settings, configure:
  1. Push to ship.

Laravel Cloud picks up the push, builds the Docker image, runs your deploy commands against the live database, then brings the new container online while gracefully terminating the old one. Your support team's Claude integration stays available throughout. The endpoint is live at https://your-app.laravel.cloud/mcp/support within minutes of the push, with zero downtime.

This speed matters because MCP tool development is iterative. You write a tool, test it with Claude, realize the response needs one more field, push the change, and it is live before you finish writing the ticket. There is no stage-test-promote cycle unless you build one.

For more on what Cloud handles beyond MCP deployments, see the documentation.

Watching Your App in Production

A deployed MCP server is an API surface that an AI model calls autonomously. You cannot review every invocation in real time. Logging and monitoring fill that gap.

Logging Tool Invocations

Add a middleware or listener that logs every tool call with the parameters, the authenticated user, and the response time:

Log to a dedicated mcp channel so tool invocations do not get buried in your application log. In config/logging.php, add a channel that writes to a separate file or ships to your log aggregator.

What to Monitor

Track these metrics from your MCP logs:

  • Invocation count per tool: A spike in a destructive tool might mean a prompt injection loop or a misconfigured client.
  • Error rate per tool: A sudden increase in not_found errors on the lookup tool could mean a data sync issue upstream.
  • p95 latency per tool: MCP tools block the conversation thread. If a tool takes more than a few seconds, Claude's response feels sluggish to the rep.
  • Unique users per hour: Helps you right-size rate limits based on actual usage patterns.

Your MCP App, Live in a Single Git Push

The gap between "tool I built for myself" and "tool my whole support team uses" used to mean weeks of infrastructure work. With laravel/mcp and Laravel Cloud, it is a single git push.

You write the tools in the Eloquent patterns you already know, define the schemas that tell Claude exactly what to expect, gate access with shouldRegister, and deploy to a managed Cloud environment that handles the rest.

The three tools in this post cover the core of a support workflow: identify the customer with IdentifyCustomerTool, audit their billing history with GetInvoiceHistoryTool, and act with IssueTrialExtensionTool. The #[IsReadOnly] and #[IsDestructive] annotations keep Claude informed. The shouldRegister gate keeps your data safe.

When you're ready to go further, read the Laravel MCP documentation for resources, prompts, OAuth 2.1. Then try Laravel Cloud for free to see how fast the full cycle actually is.

Keep reading