Menu

AI

How to Build an MCP Server in PHP (Official PHP SDK, 2026)

Build an MCP server in PHP with the official mcp/sdk package. Install it, add tools with attributes, test with MCP Inspector, then serve it over HTTP.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server in PHP (Official PHP SDK, 2026)

This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.

You build an MCP server in PHP with the official PHP SDK, published on Packagist as mcp/sdk. You write a plain PHP class, mark methods with #[McpTool], and the SDK turns the type hints into the JSON Schema that AI clients see. Three lines of builder code then serve it over stdio or HTTP.

The SDK is a joint project of the PHP Foundation and the Symfony project. It is still labeled experimental and sits at Tier 3 on the official SDK list, so plan for some breaking changes before 1.0.

This guide was checked against mcp/sdk v0.9.0 (released 18 October 2026) and its official README and docs on 2 November 2026. PHP was not available on the machine used for this guide, so the code was not run; it follows the SDK's own examples.

Key Takeaways

  • Install with composer require mcp/sdk; the package needs PHP 8.1 or later.
  • #[McpTool] on a method creates a tool; the docblock becomes the description and the type hints become the input schema.
  • Server::builder()->setDiscovery(...)->build()->run(new StdioTransport()) is the whole stdio wiring.
  • Throw ToolCallException for errors the model should read; other exceptions become generic JSON-RPC errors.
  • The SDK serves both the initialize handshake era and the stateless 2026-07-28 revision.
  • As of November 2026 the PHP SDK is Tier 3 and pre-1.0, so pin versions and read the changelog before upgrading.

Where the PHP SDK stands

The official SDK page lists ten SDKs. PHP is Tier 3, next to Swift and Kotlin. The tiers come from a community policy that sets conformance and maintenance targets; our official MCP SDKs overview explains them.

The repository's own SDK tier document is candid about why. An official audit dated 19 August 2026 measured server conformance at 100% (67 of 67) but client conformance at 20% (10 of 50). Almost all client failures were OAuth scenarios. The other blocker for Tier 2 is that no 1.0.0 release has shipped yet.

For you, that means two things. The server side, which is what this guide covers, passed every conformance check in that audit. And the API can still change between minor versions, since the SDK follows SemVer's pre-1.0 rules and logs each break with a [BC Break] marker in its changelog.

ItemValue (checked 2 November 2026)
Packagemcp/sdk on Packagist
Latest releasev0.9.0, 18 October 2026
PHP requirement^8.1
LicenseApache 2.0 for new contributions, existing code MIT
Official tierTier 3 (target: Tier 2, then Tier 1 after 1.0)
Protocol erasinitialize handshake and stateless 2026-07-28

Prerequisites

  • PHP 8.1 or later, with the fileinfo extension (the Packagist metadata lists ext-fileinfo).
  • Composer.
  • Node.js with npx, only for the MCP Inspector test step.
  • An MCP host such as Claude Desktop if you want to try it in a real app.

If you are new to the moving parts (host, client, server, tools), the MCP course covers them before any code.

Install the SDK

Create a project folder and install the package plus symfony/finder, which attribute discovery needs:

composer require mcp/sdk
composer require symfony/finder

A minimal working server

The SDK's first server guide uses a calculator. Save this as server.php next to your vendor/ folder:

#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;

class Calculator
{
    /**
     * Adds two numbers.
     */
    #[McpTool]
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    #[McpResource(uri: 'config://calculator/settings')]
    public function settings(): array
    {
        return ['precision' => 2];
    }
}

exit(Server::builder()
    ->setServerInfo('Calculator', '1.0.0')
    ->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
    ->build()
    ->run(new StdioTransport()));

What each part does, per the guide:

  • #[McpTool] marks a method the model can call. The name defaults to the method name. The description comes from the docblock.
  • int $a, int $b becomes a JSON Schema with two required integers. You write no schema by hand.
  • #[McpResource] marks read-only data the app can read by URI. Tools are for the model; resources are for the app.
  • setDiscovery() scans the listed folders for attributed classes. Excluding vendor matters, because the scan is recursive.
  • run(new StdioTransport()) speaks JSON-RPC over stdin and stdout and returns an exit code.

Run php server.php. Nothing happens, which is correct: a stdio server waits for a client to speak first.

One rule matters more than any other. With stdio, stdout carries the protocol. The guide warns that a stray echo, print_r() or var_dump() corrupts the stream. Write diagnostics to stderr or use the SDK's logger.

How to add a tool

Add another method with the attribute. The Tools guide shows a named tool with error handling:

use Mcp\Capability\Attribute\McpTool;
use Mcp\Exception\ToolCallException;

class Calculator
{
    /**
     * Performs arithmetic operations with validation.
     */
    #[McpTool(name: 'calculate')]
    public function performCalculation(float $a, float $b, string $operation): float
    {
        return match($operation) {
            'add' => $a + $b,
            'subtract' => $a - $b,
            'multiply' => $a * $b,
            'divide' => $b != 0 ? $a / $b : throw new ToolCallException('Division by zero'),
            default => throw new ToolCallException('Invalid operation')
        };
    }
}

The #[McpTool] attribute accepts these optional parameters: name, title, description, annotations, outputSchema, icons and meta. The annotations value is a ToolAnnotations object with hints such as readOnlyHint and destructiveHint. Our page on MCP tool annotations explains how hosts use them.

Tighter input schemas

Type hints get you far. The schema generation guide adds three more sources:

  • @param descriptions in the docblock become each property's description.
  • A docblock type like string[] becomes an array of strings.
  • A backed enum parameter becomes a string with an enum list, and the SDK passes the matching case to your method.

For constraints, put #[Schema] on a parameter, for example #[Schema(minimum: 18, maximum: 120)] int $age. A method-level #[Schema] with definition replaces the whole input schema when you need full control.

Return values and structured output

The SDK wraps return values for you. A string, number, boolean or array becomes text content; arrays are sent as JSON. For machine-readable results, declare outputSchema on the attribute and return an associative array. The SDK then sends it as structuredContent and also JSON-encodes it into content for clients that ignore structured output.

The guide notes one era difference. Before 2026-07-28, structuredContent had to be a JSON object, so a plain PHP list is omitted there. From 2026-07-28 on, lists are kept. See MCP structured tool output for the spec side.

Errors the model can fix

The Tools guide splits exceptions in two:

  • ToolCallException becomes a tool result with isError: true, so the model reads the message and can retry.
  • Any other exception becomes a JSON-RPC error with a generic message.

Use ToolCallException with a helpful message whenever the model could correct the call. The tools spec calls these tool execution errors and reports them with isError: true.

Register without attributes

If you prefer explicit wiring, the registration guide shows ->addTool([Calculator::class, 'add'], 'add_numbers') on the builder. Handlers can be closures, class and method pairs, instances or invokable classes. Attributes and manual registration can be mixed.

Test with MCP Inspector

The SDK's Inspector guide launches the server for you:

npx @modelcontextprotocol/inspector php server.php

Open the URL it prints. Under Tools, call add with a=1 and b=2 and you should get 3. The form fields come from your type hints, which is the same schema every other host sees. Our MCP Inspector lesson shows the CLI mode too.

To use the server in Claude Desktop, add an entry under mcpServers with "command": "php" and the absolute path to server.php in args, then restart the app. The config generator can write that block for you.

Serve it over HTTP

For a remote server, swap the transport. The HTTP transport is a PSR-7 request handler, not a web server. The Inspector guide's HTTP variant replaces the last lines of server.php:

use Http\Discovery\Psr17Factory;
use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;

$request = (new Psr17Factory())->createServerRequestFromGlobals();

$response = Server::builder()
    ->setServerInfo('Calculator', '1.0.0')
    ->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
    ->setSession(new FileSessionStore(__DIR__.'/sessions'))
    ->build()
    ->run(new StreamableHttpTransport($request));

(new SapiEmitter())->emit($response);

Install the extras with composer require nyholm/psr7 laminas/laminas-httphandlerrunner, run php -S localhost:8000 server.php, and point the Inspector at http://localhost:8000.

The framework integration guide shows controller examples for Symfony, Laravel and Slim. Each converts the framework's request to PSR-7, hands it to StreamableHttpTransport, and returns the PSR-7 response. The README also lists packages built on the SDK, including symfony/mcp-bundle, api-platform/mcp and a Drupal mcp_server module.

Deploy notes

  • Sessions. The HTTP examples use FileSessionStore, and the integration guide comments that HTTP needs persistent sessions for handshake-era clients. If you run several PHP workers or servers, use a session store they all share.
  • Discovery cost. Discovery is lazy and runs on the first request that needs the registry. Under PHP-FPM, every request may start fresh, so consider manual registration or the SDK's caching options for busy endpoints.
  • Auth. A public HTTP endpoint needs authorization. The SDK has an authorization guide; the spec side is in the authorization spec.
  • Pinning. Pre-1.0, pin an exact minor version in composer.json and read CHANGELOG.md for [BC Break] lines before you bump.

Common pitfalls

  • Output on stdout. One var_dump() breaks a stdio server. Log to stderr.
  • Scanning vendor. Forgetting excludeDirs: ['vendor'] makes discovery read every dependency.
  • Missing symfony/finder. Discovery needs it, and it is a separate install.
  • Relative paths in host configs. Hosts do not start in your project folder. Use absolute paths.
  • Old community packages. The official SDK grew out of the earlier php-mcp/server project, per the README credits. Tutorials written for that package use different class names. Check imports start with Mcp\.
  • Generic errors. Throwing a plain Exception hides your message from the model. Use ToolCallException.

Next step

Once one tool works, read how to build your first MCP server for design advice that applies in any language. If you would rather have a PHP or WordPress MCP server built and secured for you, our MCP development team can scope it.

More in the MCP Guide

Free tools: server.json Validator · MCP Error Code Lookup

Latest research: State of MCP: October 2026

Frequently asked questions

Is there an official MCP SDK for PHP?

Yes. The official PHP SDK lives at github.com/modelcontextprotocol/php-sdk and installs with composer require mcp/sdk. As of November 2026 it is listed as Tier 3 on the official SDK page and is still pre-1.0.

What PHP version does the MCP SDK need?

The Packagist metadata for v0.9.0 requires PHP ^8.1 and the fileinfo extension. Attribute discovery also needs symfony/finder, installed separately.

Can I use the PHP MCP SDK with Laravel or Symfony?

Yes. The SDK's framework integration guide shows controller examples for Symfony, Laravel and Slim, all using the PSR-7 HTTP transport. The SDK README also lists symfony/mcp-bundle, a Symfony integration bundle built on the SDK.

Does the PHP SDK support the 2026-07-28 spec?

The README says it supports both protocol eras: the initialize handshake and the stateless 2026-07-28 revision. The repository publishes weekly conformance scores for both revisions.

Why does my PHP MCP server work in the terminal but not in Claude Desktop?

Usually a relative path or output on stdout. Use the absolute path to server.php in the host config, and make sure nothing prints to stdout.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.