tereta / mcp
Model Context Protocol server for PHP: expose class methods as MCP tools through attributes, transport-agnostic
Requires
- php: >=8.2
- composer-runtime-api: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.13
- wikimedia/composer-merge-plugin: ^2.1
This package is not auto-updated.
Last update: 2026-07-20 11:33:44 UTC
README
🌐 English | Русский | Українська
Introduction
Tereta/Mcp is a PHP implementation of a Model Context Protocol server.
The library turns methods of ordinary PHP classes into tools available to language models: you register an object,
and the tool listing, argument schema and JSON-RPC parsing are produced automatically.
Metadata comes from the #[Method] and #[Method\Argument] attributes, or is declared explicitly through the
Definition builder, so the domain code stays clean and independent of the protocol.
The library is transport-agnostic: Server::handle() takes a decoded JSON-RPC request as an array and returns
the response array. How to deliver it - over HTTP, through stdio or any other way - is up to the calling code.
Installation
PHP 8.2 or newer is required. No extra extensions are needed - the library works only with the configurations you provide and is not bound to a transport.
composer require tereta/mcp
Usage
Declared classes
Tools are described by attributes placed directly on class methods. #[Method] sets the tool name and description,
the repeatable #[Method\Argument] - the JSON schema of a single argument.
use Tereta\Mcp\Attributes\Method;
use Tereta\Mcp\Attributes\Method\Argument;
class Weather
{
#[Method(name: 'weatherCurrent', description: 'Returns the current weather in a city.')]
#[Argument(name: 'city', definition: ['type' => 'string'])]
#[Argument(name: 'units', definition: ['type' => 'string'])]
public function current(string $city, string $units = 'celsius'): string
{
return "It is +20 ({$units}) in {$city} right now.";
}
}
The object is then registered in the Tereta\Mcp\Server class through the register function.
Signature of Tereta\Mcp\Server::register
public function register(object $instance, ?Closure $callable = null): self
The handle function takes a decoded JSON-RPC request as an array and returns the JSON-RPC response array:
public function handle(array $rpc): ?array
Example: rendering an image
A ready-made set of image tools is provided by the tereta/image-mcp package -
its Canvas class is already annotated, so registering it is enough:
composer require tereta/image-mcp
use Tereta\Mcp\Server;
use Tereta\ImageMcp\Canvas;
$server = new Server('image-server', '1.0.0');
$server->register(new Canvas());
The canvas state lives inside the Canvas object, while HTTP requests are independent of each other.
That is why every step — from creating the canvas to rendering - runs within a single imagePipeline tool call:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "imagePipeline",
"arguments": {
"steps": [
{"method": "imageCreate", "arguments": {"width": 120, "height": 60, "background": "white"}},
{"method": "imageDrawCircle", "arguments": {"x": 60, "y": 30, "radius": 25, "color": "red"}},
{"method": "imageFormat", "arguments": {"format": "png"}},
{"method": "imageRender", "arguments": {}}
]
}
}
}
imagePipeline returns the result of the last step. Since imageRender comes last,
the response contains the finished image in base64:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "image",
"data": "iVBORw0KGgoAAAANSUhEUgAAAHgAAAA8CAMAAACa...",
"mimeType": "image/png"
}
],
"isError": false
}
}
If the image has to be written to disk instead of being handed to the model, put imageSave
with the path argument as the last step.
To explore the image package, see tereta/image-mcp
Docker
The dev/docker directory contains a ready-to-use environment: a PHP container with the built-in web server
that exposes the MCP transport over HTTP.
Running
cp dev/docker/.env.example dev/docker/.env
composer docker:transport
The server comes up at the address printed on startup — http://172.16.13.2:80 by default.
The port is deliberately not published to the host: the container gets a static IP in its own mcp bridge network,
so only containers attached to that same network can reach it.
The address and the port are set through the NETWORK_PREFIX and MCP_PORT variables.
Other commands:
composer docker # bash inside the container
composer docker:test # phpcs + phpstan + phpunit
Environment variables
| Variable | Purpose |
|---|---|
MCP_PORT | port of the built-in PHP server |
NETWORK_PREFIX | first three octets of the subnet; the container gets the <PREFIX>.2 address |
PHP_AUTOLOAD | path to autoload.php relative to the package root |
PHP_MCP_CLASSES | classes registered as MCP tools, separated by ; |
PHP_SCRIPT | optional script included after the classes are registered |
PHP_EXTENSIONS_COMMAND | command installing extra PHP extensions at image build time |
Extra extensions
xdebug is always installed — it is declared in the Dockerfile. The PHP_EXTENSIONS_COMMAND variable is
only for additional extensions: it is a shell command executed inside the image during the build.
It is empty by default.
Adding imagick and gd requires both the system libraries and the extensions themselves:
PHP_EXTENSIONS_COMMAND="apt-get install -y --no-install-recommends \
libmagickwand-dev fonts-dejavu-core \
libpng-dev libjpeg-dev libwebp-dev libfreetype6-dev \
&& pecl install imagick \
&& docker-php-ext-enable imagick \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install gd"
After changing it the image has to be rebuilt:
docker compose -f dev/docker/docker-compose.yml up -d --build php
Extensions are installed the same way as in any official php image:
docker-php-ext-install for the bundled ones, pecl install + docker-php-ext-enable for PECL.
gd is the only one that needs a preceding docker-php-ext-configure with the format flags.
Xdebug
The Dockerfile installs xdebug and configures it for step debugging right away: debug mode,
start on every request, client at host.docker.internal:9003.
On the IDE side you need to listen on port 9003, map /packages/tereta-mcp -> the package directory
and set the server name from PHP_IDE_CONFIG (tereta-mcp by default).