PHP MCP Server: A PHP Developer’s Guide to the Model Context Protocol

MCP for PHP development

A PHP MCP server is a PHP application that exposes tools, resources, and prompts to AI clients through the Model Context Protocol (MCP). In 2026 you build one with the official mcp/sdk package or, in a Laravel app, with laravel/mcp. Since the 2026-07-28 revision made MCP stateless, every call is an ordinary HTTP request, which is how PHP has always worked.

Update, October 2026: I first published this post in August 2025. It aged badly, and fast. About three weeks after it went up, the PHP Foundation, Symfony, and Anthropic’s MCP team announced an official PHP SDK, which made both packages I’d recommended the wrong place to start. Then in July 2026 the protocol itself changed shape. So I’ve thrown out the old version and written this one from scratch. (The URL is the same. Almost nothing else is.)

If you write PHP for a living and somebody has asked you to “make the app work with AI agents,” this is what I’d tell you over coffee.

What changed since the first version

Four things, and I’ll go through them in the order they hurt.

  • There’s an official SDK now. It’s mcp/sdk, it grew out of Kyrian Obikwelu’s PHP-MCP project and the Symfony AI work, and the Symfony team leads maintenance. It covers servers and clients. It’s also still pre-1.0, and the README says “experimental” in plain words, so pin your version.
  • Laravel ships its own package. laravel/mcp is first-party, documented alongside the framework, and comes with Artisan generators, a test API, and an MCP client.
  • The protocol went stateless. The 2026-07-28 revision removed the opening handshake and the protocol-level session. More on this below, because for PHP it’s the big one.
  • Authorization grew up. OAuth 2.1 is the mechanism the spec documents. A bearer token still works with plenty of clients, but “use an API token” (which is all I said last time) isn’t the whole answer anymore.

And one correction I owe you. The JSON example in the old post called a method named tool. There’s no such method. It’s tools/call, and it was tools/call back then too. Sorry about that.

What a PHP MCP server exposes

This part hasn’t changed, so I’ll keep it short. A server offers three kinds of things to an AI client.

Tools are functions the model can call: create a project, send an invoice, look up an order. Resources are read-only data the client can pull in as context, addressed by a URI. Prompts are reusable templates the server hands to the client.

Start with tools. In the servers we’ve built, tools did nearly all the useful work, and resources came a distant second. I’d leave prompts until a real user asks for one.

Why the stateless spec suits PHP

Until this summer, MCP over HTTP was a conversation. The client opened with an initialize handshake, the server handed back a session ID, and every later request had to carry that ID so the server could remember who it was talking to.

That’s fine in Node or Python, where one long-lived process sits in memory holding the session. It’s a nuisance in PHP, because PHP-FPM on an ordinary host gives you a fresh process for each request that remembers nothing, which meant you needed a session store (files on one server, Redis on several), and then you needed the load balancer to cooperate, and then somebody’s shared hosting plan killed the long-running stream the server wanted to keep open, and at that point a perfectly sane developer would ask why a remote procedure call needed all this furniture.

The 2026-07-28 revision removes the furniture. There’s no handshake and no protocol session. Each request carries its own protocol version and client details, any worker on any machine can answer it, and list results come with cache hints. Cloudflare, AWS, and the MCP maintainers all describe it the same way: MCP now scales like any other HTTP API.

PHP got teased for twenty years for forgetting everything between requests. The protocol has now adopted that as its design.

What if your server really does need to remember something, like a cart or a half-finished import? The spec’s guidance is to return a handle from one tool and accept it as an argument on the next. In other words, a primary key. We’ve been doing that since PHP 3.

There’s a cost, and you should hear it from me before you hear it from a client. The new revision isn’t backward compatible. A server that only speaks 2026-07-28 may not work with an older client, and the reverse. So for a while you’ll be supporting both eras, and that’s a good reason to use an SDK instead of hand-rolling the JSON. (We hand-rolled once. I wrote up what happened in building a WordPress MCP server while the spec changed.)

What a tool call looks like on the wire

MCP messages are JSON-RPC 2.0, a small remote procedure call format where every message names a method and carries an ID. Here’s a client asking a server to run a tool.

 

I’ve trimmed the _meta block. The spec requires client info and client capabilities in there too, and your SDK fills them in. On HTTP, the client also sends MCP-Protocol-Version, Mcp-Method, and Mcp-Name headers, so a load balancer or firewall can route the call without opening the body. If you’ve ever fought a web application firewall over a JSON payload (I have, on our own host), you’ll like that.

The server answers like this.

 

Two details matter. structuredContent is the machine-readable result, and the text block is the fallback for clients that only read text. And when your business logic fails, say the due date is in the past, you return a normal result with isError set to true and a message the model can act on. Save real JSON-RPC errors for broken requests and unknown tools.

Build one in plain PHP with the official SDK

This works in any PHP project: plain PHP, CodeIgniter, Slim, a legacy app with no framework at all. The steps below build a local server that talks over STDIO (standard input and output, the way a desktop AI client starts a server as a child process).

Goal. A PHP file exposes one tool named create_project, and MCP Inspector can call it.

Before you start. You need Composer and Node.js installed. Check the PHP version the SDK requires on its Packagist page before you install.

Step 1: Install the SDK. Run this command in an empty project folder. Composer prints the installed version when it finishes.

 

Step 2: Create the server file. Save this code as server.php in the same folder. The tool body is a stub, so replace it with a call to your own service class later.

 

Step 3: Open the server in MCP Inspector. MCP Inspector is the official browser tool for testing a server by hand. Replace the path with the full path to your server.php.

 

Check. The Inspector’s Tools tab lists create_project with two string inputs. You run it, and the result shows project_id 42.

If it fails. The usual cause is stray output. On STDIO, anything your script prints (a warning, a debug echo, a byte order mark) lands in the protocol stream and breaks it. Send logs to a file or to standard error.

Undo. Delete the project folder. Nothing else on your machine changed.

For a remote server you swap the transport for the SDK’s StreamableHttpTransport, which takes a PSR-7 request (the standard PHP interface for an HTTP message) and returns a response. The transports guide has the wiring. Read the protocol versions page while you’re there. As far as I can tell from the repository, support for the stateless revision has been landing release by release, so check what your installed version speaks before you promise a client anything.

Build one in Laravel with laravel/mcp

If the app is Laravel, don’t use the plain SDK. Use the first-party package. You get the container, validation, policies, and middleware you already know, and the tool class ends up looking like a small controller.

Goal. Your Laravel app serves an authenticated MCP endpoint at /mcp/projects with one tool that creates a project.

Before you start. The package needs PHP 8.2 or newer and Laravel 11, 12, or 13. The steps assume Sanctum (Laravel’s token authentication package) is already installed, and that you have a ProjectService class and a Project policy. Swap in your own names.

Step 1: Install the package.

 

Step 2: Publish the routes file. This command creates routes/ai.php, where MCP servers are registered.

 

Step 3: Generate the server class. The file appears in app/Mcp/Servers.

 

Step 4: Generate the tool class. The file appears in app/Mcp/Tools.

 

Step 5: Write the tool. Replace the generated tool with this code. Notice the order: authorize, validate, then call the service.

 

Step 6: Register the tool on the server. Open app/Mcp/Servers/ProjectServer.php and set the $tools property.

 

Step 7: Register the route. Put this code in routes/ai.php. The throttle:mcp middleware expects a rate limiter named mcp, so define one in your AppServiceProvider or remove that entry.

 

Step 8: Open the server in MCP Inspector. Add your Sanctum token as an Authorization: Bearer header when the Inspector asks for connection settings.

 

Check. The Inspector lists create-project (Laravel derives the name from the class). You call it with a past date, and the tool returns your validation message instead of creating anything.

If it fails. A 401 response means the token header is missing or the token is wrong. An empty tool list means Step 6 was skipped.

Undo. Remove the route from routes/ai.php. The endpoint stops existing on the next request.

One thing I’d do before any of this reaches production: write the validation messages for a machine reader. When validation fails, the model reads your error text and tries again, so “The due date must be a future date in YYYY-MM-DD format” gets you a correct retry and “Invalid input” gets you a loop. (Laravel’s type handling has its own traps here. We wrote about one in integers arriving as strings.)

MCP vs REST vs GraphQL

I get asked whether MCP replaces the REST API. No. They have different callers.

Question REST GraphQL MCP
Who calls it? Your frontend or a partner’s code Your frontend An AI model, through a client app
Who decides what to call? A developer, at build time A developer, at build time The model, at run time
How does the caller learn the API? Docs or an OpenAPI file Schema introspection tools/list, with descriptions written for a model
Message format HTTP verbs and URLs A query language over HTTP JSON-RPC 2.0 over STDIO or HTTP
State Stateless Stateless Stateless since 2026-07-28
Typical endpoint count Many One One per server

The row that matters is the second one. With REST, a programmer read your docs and decided which endpoint to hit. With MCP, a model reads your tool descriptions and decides on the spot. So the description is part of the interface, and a vague one is a bug.

Don’t write the logic twice, though. Your MCP tool and your REST controller should call the same service class. If your controllers are fat and the logic lives in them, fix that first. It’s the same advice I’d give for splitting a PHP app into services, and it pays off here for the same reason.

Security: what I’d lock down first

An MCP tool is a function on your production app that a language model can run. You are giving a caller that doesn’t understand consequences a button. You are trusting its client to ask a human first. You are assuming nobody slipped instructions into the text it just read. Treat every argument the way you’d treat a form field from a stranger.

The spec lists four things a server must do: validate inputs, enforce access control, rate limit, and sanitize outputs. Here’s how that turns into a pre-launch list.

  • Every web MCP route sits behind authentication middleware. No public endpoints, not even for a demo.
  • Each tool checks what this user may do, with a policy or gate, inside the tool. Being logged in isn’t permission.
  • Each tool validates its arguments with real rules, not just the JSON schema.
  • The route has a rate limiter. Models retry, and they retry quickly.
  • Read-only tools are marked read-only, and destructive ones are marked destructive, so clients can ask for confirmation.
  • Nothing destructive ships in version one. No delete tools, no bulk updates.
  • Every tool call is logged with the user, the tool name, and the arguments.
  • Tool output is trimmed to what the model needs. No password hashes, no internal IDs you wouldn’t put in an API response.

On authentication: Laravel’s own docs recommend OAuth through Passport, since that’s what the spec documents and what the widest range of clients support. They also say that if you’re already on Sanctum, stay there until a client forces the issue. I agree with that. Don’t bolt on an OAuth server for an internal tool three people use.

And keep a person in the loop for anything that writes. We run an MCP server on one of our own sites that lets an AI assistant edit pages. Every edit lands as a draft, and a human publishes it. That one rule has saved us more than once. It’s the same thinking behind controlled AI coding, and behind my standing worry that the code an AI wants to delete is load-bearing.

Where I’d wait

I’d build an internal MCP server today. I’d be slower about a public, customer-facing one, for two reasons.

Both PHP packages are pre-1.0. APIs will move. The examples in this post matched the documentation on the day I checked them (October 9, 2026), and I’d bet at least one is out of date within a year. Pin versions, and read the changelog before you run composer update.

The protocol also just had its biggest revision since launch. The maintainers added a formal deprecation policy alongside it, which is a good sign, but I’d let clients and SDKs settle for a few months before signing a contract that names a spec version.

If you’re not on Laravel: Symfony has symfony/mcp-bundle, Drupal has an mcp_server module, and API Platform and CakePHP have integrations built on the same official SDK. For WordPress, read the post I linked earlier. That’s about all I know that’s worth passing on. If you’ve shipped a PHP MCP server and something here is wrong, tell me in the comments and I’ll fix it with credit.

FAQ

Do I need Laravel to build a PHP MCP server?

No. The official mcp/sdk package is framework-agnostic and runs in plain PHP, CodeIgniter, Symfony, or a legacy codebase. Laravel developers should use laravel/mcp because it plugs into validation, policies, and middleware they already have.

Can a PHP MCP server run on shared hosting?

A server that follows the stateless 2026-07-28 revision handles each call as a normal HTTP request, so it fits standard PHP hosting far better than the older session-based design did. Check that your host allows the PHP version the package needs, and that your installed SDK version supports the stateless revision.

Does MCP replace my REST API?

No. REST serves code written by developers. MCP serves AI models that choose tools at run time. Most apps will keep both, with MCP tools and REST controllers calling the same service classes.

Which MCP spec version should a new PHP server target?

Target 2026-07-28 for new work, and keep support for the previous revision (2025-11-25) while your clients catch up, because the two are not backward compatible. An SDK that speaks both is the easiest way to do that.

How do I test a PHP MCP server?

Use MCP Inspector to call tools by hand. In Laravel, run php artisan mcp:inspector and write unit tests with the package’s test API. For the plain SDK, start the Inspector with npx @modelcontextprotocol/inspector and point it at your server file.

Need a PHP or Laravel App Made Ready for AI Agents?

We’ve been writing PHP since 2002, and we build MCP servers for our own sites. If you want tools exposed safely on an existing app, we can scope it with you.

Talk to Our PHP Team

Visited 159 times, 1 visit(s) today

Related Posts

Search

 

Popular Posts

@macronimous Copyright © 2026.
Visit Main Site