Building MCP Servers: Extend AI Applications with PHP
Chris Tankersley · Vonage
$ pip install mcp
$ npx create-mcp-server
If you're a PHP dev, the implicit message has been: "rebuild your stack to participate."
That's a false choice. And it's been false for almost a year.
AI is a Python/Node thing. We weren't invited. The legacy stays legacy.
PHP runs WordPress, Magento, Drupal, every Laravel SaaS, every internal admin nobody talks about. MCP changes the equation in both directions.
An open standard for connecting AI applications to your data and tools. Released by Anthropic in November 2024, now governed under LF Projects.
One socket. Many devices: Claude, Cursor, Codex, Cline, agent frameworks.
The protocol is symmetric — same JSON, both directions. You'll build both sides today.
Actions the model can invoke. "Refund this order." Model-controlled.
Read-only data. Files, configs, query results. URI-addressed. App-controlled.
Reusable templates the user can pick from the host UI.
This is what makes MCP feel agentic instead of REST-with-extra-steps.
Server runs as a subprocess. Client speaks over stdin/stdout. Local CLIs, dev loops, single-user tools.
One endpoint, JSON-RPC over POST, optional SSE upgrade. Production. Multi-user. Remote.
Legacy SSE-only is deprecated. The 2026 roadmap deliberately keeps this surface small.
Each AI assistant that wants to talk to your app needs a bespoke integration. A Claude plugin. Then a Cursor command. Then a Copilot extension. Then…
After MCP: write one server. Every current and future MCP client speaks to it.
Same shift OpenAPI brought to HTTP APIs — but for AI clients.
Business logic, validations, RBAC, audit trails — all of that already lives in your PHP app. MCP lets you expose it without rewriting it.
Your Eloquent models, your Symfony services, your WordPress hooks become AI-callable as-is.
And the part that drives the LLM? That can also be PHP.
MCP officially blessed PHP as a first-class language. SDK maintained by Anthropic's MCP team, the PHP Foundation, and Symfony.
Started from the community php-mcp project by Kyrian Obikwelu — now an official maintainer.
10th SDK Python · TypeScript · Java · Kotlin · C# · Swift · Ruby · Rust · Go · PHP.
Most of these support both server and client. The protocol is the same; switching libraries later is a port, not a rewrite.
Code patterns are similar across the field — attribute-driven, builder-pattern setup.
mcp/sdkcomposer require mcp/sdk
Use when: you don't have a strong framework opinion, or you want to be closest to upstream.
laravel/mcpcomposer require laravel/mcp
Mcp::local('server')->tool(MyTool::class).Use when: you're in Laravel. No reason to fight your framework.
symfony/mcp-bundlecomposer require symfony/mcp-bundle
symfony/ai for the client / agent side.Use when: you're in Symfony.
logiscape/mcp-sdk-phpUse when: you're not on a VPS — you're on shared hosting.
| Library | Notes |
|---|---|
pronskiy/mcp | Thin developer-experience layer on top of mcp/sdk. |
php-mcp/server + php-mcp/client | Original community projects. Official SDK is the successor for new work. |
swisnl/mcp-client | Client-only. Modern PHP 8.2+. Lightweight. |
josbeir/cakephp-synapse | CakePHP plugin. |
dtyq/php-mcp | Full server + client, multiple transports. |
The protocol is the same. Switching libraries later is a port, not a rewrite.
mkdir my-mcp-server && cd my-mcp-server
composer init --no-interaction
composer require mcp/sdk
<?php
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\McpResource;
class CalculatorCapabilities
{
#[McpTool]
public function add(int $a, int $b): int
{
return $a + $b;
}
#[McpResource(uri: 'config://calculator/settings')]
public function getSettings(): array
{
return ['precision' => 2];
}
}
Method signature is the JSON Schema. Doc-comments and param names are model-readable. Treat them like product copy.
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
$server = Server::builder()
->setServerInfo('Calculator Server', '1.0.0')
->setDiscovery(__DIR__, ['.'])
->build();
$server->run(new StdioTransport());
setDiscovery scans the path for attribute-tagged methods. No manual registration.
That's the whole server. ~25 lines including imports.
{
"mcpServers": {
"calculator": {
"command": "php",
"args": ["/full/path/to/server.php"]
}
}
}
Drop into claude_desktop_config.json. Restart Claude.
Same shape for Cursor, Cline, and most other clients.
Claude calls your PHP function. Returns 166. Done.
It's the dumbest possible demo on purpose. Everything else in this talk is variations on this pattern.
→ {"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25", "...":"..."}}
← {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}, "...":"..."}}}
→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add","inputSchema":{...}}]}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"add","arguments":{"a":47,"b":119}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"166"}]}}
Plain text. Log it during development — it's the best debugger you have.
Expose a query(string $sql) tool that runs against a read-only DB user.
Why it works: database privileges enforce safety. The model can only do what the DB user can.
Non-technical staff can ask "how many signups did we get last week from California?" — the AI writes and runs the SQL.
Wrap repository methods or Eloquent queries: findCustomer($email), recentOrders($id).
Why it works: existing policies still gate access. Sanctum / Symfony firewall tokens identify the acting user.
Support staff get an AI assistant that knows their data without bypassing authorization.
Read tools (inventory, order lookup) + write tools that require elicitation before executing.
Why it works: elicitation is the human-in-the-loop primitive. The server asks the user before refunding.
Real agentic workflows without trusting the model on financial transactions.
Wrap its repository methods as MCP tools. Don't touch the UI.
Why it works: you expose the intent of operations, not the raw data layer. The model talks to your domain language.
Years of business logic become AI-callable without a frontend rewrite.
#[McpTool]
function rawQuery(string $sql): array
{
return DB::select($sql);
}
Now your model is a SQL injection vector with a co-pilot.
#[McpTool]
function cancelSubscription(
int $id,
string $reason,
): array {
return $this->subs->cancel($id, $reason);
}
Tools match business operations.
The shape of your tools is product design, not API design.
Up to here, PHP was the back-end for someone else's AI. Now PHP becomes the agent.
Same protocol, mirrored. Your PHP app consumes MCP servers built by other people — or other teams inside your company.
This is what makes PHP a viable platform for building AI agents.
Anywhere you'd previously have called a REST API or shelled out to Node — call an MCP server from PHP instead.
<?php
use Mcp\Client;
use Mcp\Client\Transport\StdioTransport;
$client = Client::builder()
->setClientInfo('My Laravel Agent', '1.0.0')
->setRequestTimeout(120)
->build();
// Connect to a local MCP server running as a subprocess
$transport = new StdioTransport(
command: 'php',
args: ['/path/to/server.php'],
);
$client->connect($transport);
// Discover what's available
$tools = $client->listTools();
// Use a tool
$result = $client->callTool('add', ['a' => 47, 'b' => 119]);
// Read a resource
$content = $client->readResource('config://calculator/settings');
$client->disconnect();
use Mcp\Client\Transport\HttpTransport;
$transport = new HttpTransport('https://your-server.example.com/mcp');
$client->connect($transport);
$tools = $client->listTools();
$result = $client->callTool('search_repos', ['q' => 'mcp']);
One line changes between local stdio and remote HTTP. The rest of your agent code is identical.
symfony/ai) wraps this loop — declare an Agent, hand it MCP clients as toolboxes.| Library | When to reach for it |
|---|---|
mcp/sdk | Official. Server & client. Sync API. Start here. |
php-mcp/client | ReactPHP-based. Both sync (blocking) and async (Promise) APIs. Use when you need concurrent fan-out. |
swisnl/mcp-client | Modern PHP 8.2+. Client-only. Clean transport set: SSE, stdio, Process, Streamable HTTP. |
logiscape/mcp-sdk-php | Includes a client. The included web-client is a great teaching demo. |
Most PHP apps are sync request/response. Async is overkill unless you're fanning out across many servers.
As server: you worry about your tool inputs.
As client: you worry about the servers you trust.
Local dev. Single-user tools. IDE plugins. Server lives as long as the host.
Everything else. Multi-user. Remote. Behind a load balancer.
Same SDK; one line of code difference. Start stdio. Graduate to HTTP when you have users.
// In-memory — fine for stdio, single instance
$server = Server::builder()->setSession(ttl: 7200)->build();
// Redis-backed — production, scaled HTTP
$server = Server::builder()
->setSession(new Psr16SessionStore(
cache: new Psr16Cache($redisAdapter),
prefix: 'mcp-',
ttl: 3600,
))
->build();
For HTTP transport at scale: PSR-16 backend (Redis, Memcached). File-based works for single-server setups.
The official SDK gives you the primitives; framework bundles give you the ergonomics.
The one-year anniversary release.
| Feature | What it does |
|---|---|
| Elicitation | Formal human-in-the-loop. Form mode (JSON Schema) for structured data. URL mode for sensitive info that must bypass the client. |
| Structured output | Tools declare a JSON Schema outputSchema. Clients validate; downstream tooling parses reliably. |
| Async tasks | Any request can be tagged as a task. Client polls for status. Native answer to "tool takes 8 minutes." |
| Better OAuth + extensions | Tighter OAuth 2.1 (Resource Indicators, DCR). First-class extension mechanism — vendors add capabilities without forking the spec. |
composer require mcp/sdk — or your framework's equivalent.You already know
how to build this.
The hard part was a Python problem. PHP is officially supported on both sides of MCP — server, client, agent. All your existing knowledge applies.
github.com/modelcontextprotocol/php-sdkphp.sdk.modelcontextprotocol.iomodelcontextprotocol.iogithub.com/modelcontextprotocol/serverslaravel.com/docs/mcpsymfony.com/doc/current/ai/bundles/mcp-bundle.htmlai.symfony.comgithub.com/logiscape/mcp-sdk-phpgithub.com/php-mcp/clientgithub.com/swisnl/mcp-clientgithub.com/your-handle/php-mcp-phptekI'll be around, happy to answer any questions
Chris Tankersley · Vonage
chris.tankersley@vonage.com
chris@ctankersley.com
https://phpc.social/@dragonmantank
https://bsky.app/profile/dragonmantank.bsky.social