# Directory Structure
```
├── .gitignore
├── CHANGELOG.md
├── composer.json
├── examples
│ └── echo.php
├── LICENSE.md
├── README.md
├── src
│ ├── McpServerException.php
│ └── Server.php
└── tests
├── ArchTest.php
└── BasicInteractionTest.php
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Composer Related
composer.lock
/vendor
# Frontend Assets
/node_modules
# Logs
npm-debug.log
yarn-error.log
# Caches
.phpunit.cache
.phpunit.result.cache
/build
# IDE Helper
_ide_helper.php
_ide_helper_models.php
# Editors
/.vscode
# Misc
phpunit.xml
phpstan.neon
testbench.yaml
/docs
/coverage
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# 🐉 The fast, PHP way to build MCP servers
The Model Context Protocol (MCP) is a new, standardized way to provide context and tools to your LLMs, and `pronskiy/mcp` makes building MCP servers simple and intuitive.
Create tools, expose resources, define prompts, and connect components with clean PHP code.
## Installation
With composer:
```bash
composer require pronskiy/mcp
```
## Usage
```php
require 'vendor/autoload.php';
$server = new \Pronskiy\Mcp\Server('simple-mcp-server');
$server
->tool(
'add-numbers',
'Adds two numbers together',
fn(float $num1, float $num2) => "The sum of {$num1} and {$num2} is " . ($num1 + $num2)
)
->tool(
'multiply-numbers',
'Multiplies two numbers',
fn(float $num1, float $num2) => "The product of {$num1} and {$num2} is " . ($num1 * $num2)
)
;
$server->run();
```
## Credits
- https://github.com/logiscape/mcp-sdk-php
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
The MIT License (MIT)
Copyright (c) pronskiy <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
All notable changes to `Mcp` will be documented in this file.
```
--------------------------------------------------------------------------------
/tests/ArchTest.php:
--------------------------------------------------------------------------------
```php
<?php
arch('it will not use debugging functions')
->expect(['dd', 'dump', 'ray', 'var_dump'])
->each->not->toBeUsed();
```
--------------------------------------------------------------------------------
/examples/echo.php:
--------------------------------------------------------------------------------
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
(new \Pronskiy\Mcp\Server('echo-server'))
->tool('echo', 'Echoes text', function(string $text) {
return $text;
})
->run();
```
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
```json
{
"name": "pronskiy/mcp",
"description": "🐉 The fast, PHP way to build MCP servers",
"version": "0.1.1",
"keywords": [
"pronskiy",
"mcp"
],
"homepage": "https://github.com/pronskiy/mcp",
"license": "MIT",
"authors": [
{
"name": "Roman Pronskiy",
"email": "[email protected]"
}
],
"require": {
"php": "^8.3",
"mcp/sdk": "dev-main"
},
"require-dev": {
"phpstan/phpstan": "^2.1.12",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-arch": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"phpstan/extension-installer": "^1.3||^2.0",
"phpstan/phpstan-deprecation-rules": "^1.1||^2.0",
"phpstan/phpstan-phpunit": "^1.3||^2.0",
"symfony/console": "^v7.2.5"
},
"autoload": {
"psr-4": {
"Pronskiy\\Mcp\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Pronskiy\\Mcp\\Tests\\": "tests/"
}
},
"scripts": {
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
}
},
"extra": {
},
"minimum-stability": "dev",
"prefer-stable": true
}
```
--------------------------------------------------------------------------------
/src/McpServerException.php:
--------------------------------------------------------------------------------
```php
<?php
namespace Pronskiy\Mcp;
class McpServerException extends \Exception
{
/**
* Create a new exception for an invalid tool handler result
*/
public static function invalidToolResult(mixed $result): self
{
$type = is_object($result) ? get_class($result) : gettype($result);
return new self("Invalid tool handler result: expected string or CallToolResult, got {$type}");
}
/**
* Create a new exception for an invalid prompt handler result
*/
public static function invalidPromptResult(mixed $result): self
{
$type = is_object($result) ? get_class($result) : gettype($result);
return new self("Invalid prompt handler result: expected string, array, or GetPromptResult, got {$type}");
}
/**
* Create a new exception for an invalid resource handler result
*/
public static function invalidResourceResult(mixed $result): self
{
$type = is_object($result) ? get_class($result) : gettype($result);
return new self("Invalid resource handler result: expected string, SplFileObject, resource, or ReadResourceResult, got {$type}");
}
/**
* Create a new exception for an unknown tool
*/
public static function unknownTool(string $name): self
{
return new self("Unknown tool: {$name}");
}
/**
* Create a new exception for an unknown prompt
*/
public static function unknownPrompt(string $name): self
{
return new self("Unknown prompt: {$name}");
}
/**
* Create a new exception for an unknown resource
*/
public static function unknownResource(string $uri): self
{
return new self("Unknown resource: {$uri}");
}
}
```
--------------------------------------------------------------------------------
/tests/BasicInteractionTest.php:
--------------------------------------------------------------------------------
```php
<?php
use Symfony\Component\Process\InputStream;
use Symfony\Component\Process\Process;
function sendRequest(Process $process, InputStream $input, array $request, $waitForResponse = true)
{
$requestStr = json_encode($request) . "\n";
// Send the request
$input->write($requestStr);
// Return immediately for notifications
if (!$waitForResponse) {
return null;
}
// Read the response
$responseStr = '';
$startTime = microtime(true);
while (empty($responseStr) && (microtime(true) - $startTime) < 5.0) {
$responseStr = $process->getIncrementalOutput();
if (empty($responseStr)) {
usleep(100000); // 100ms
}
}
$responseStr = trim($responseStr);
// Parse and return the response
return json_decode($responseStr, true, 512, JSON_THROW_ON_ERROR);
}
it('can initialize', function () {
$process = new Process([
'php',
'-r',
/** @lang PHP */
'require "vendor/autoload.php";
$server = new \Pronskiy\Mcp\Server("test-mcp-server");
$server->run();'
]);
$input = new InputStream();
$process->setInput($input);
$process->setTty(false);
$process->setTimeout(null);
$process->start();
// @todo test with different protocol versions
$protocolVersion = '2025';
if ($protocolVersion === '2024-11-05') {
$capabilities = [
'supports' => [
'filesystem' => true,
'resources' => true,
'utilities' => true,
'prompt' => true
]
];
} else { // 2025-03-26
$capabilities = [
'tools' => [
'listChanged' => true
],
'resources' => true,
'prompt' => [
'streaming' => true
],
'utilities' => true
];
}
$initializeRequest = [
'jsonrpc' => '2.0',
'id' => 'test-init',
'method' => 'initialize',
'params' => [
'protocolVersion' => $protocolVersion,
'capabilities' => $capabilities,
'clientInfo' => [
'name' => 'TestClient',
'version' => '1.0.0'
]
]
];
$initResponse = sendRequest($process,$input, $initializeRequest);
expect($initResponse)
->toBeArray()
->and(isset($initResponse['result']))->toBeTrue()
->and(isset($initResponse['result']['protocolVersion']))->toBeTrue()
;
});
```
--------------------------------------------------------------------------------
/src/Server.php:
--------------------------------------------------------------------------------
```php
<?php
declare(strict_types=1);
namespace Pronskiy\Mcp;
use Mcp\Server as SdkServer;
use Mcp\Server\ServerBuilder;
use Mcp\Server\Transport\StdioTransport;
use Psr\Log\NullLogger;
/**
* Simple MCP Server implementation (using the official mcp/sdk under the hood)
*
* This class provides a fluent interface for creating MCP servers.
* It stores user-defined tools, prompts, and resources, and registers them
* with the mcp/sdk ServerBuilder when run() is called.
*/
class Server
{
protected static ?self $instance = null;
/**
* Get the singleton instance of the server
*/
public static function getInstance(string $name = 'mcp-server'): self
{
if (self::$instance === null) {
self::$instance = new self($name);
}
return self::$instance;
}
public static function addTool(string $name, string $description, callable $callback): self
{
return self::getInstance()->tool($name, $description, $callback);
}
public static function addPrompt(string $name, string $description, callable $callback): self
{
return self::getInstance()->prompt($name, $description, $callback);
}
public static function addResource(string $uri, string $name, string $description = '', string $mimeType = 'text/plain', ?callable $callback = null): self
{
return self::getInstance()->resource($uri, $name, $description, $mimeType, $callback);
}
public static function start(bool $resourcesChanged = true, bool $toolsChanged = true, bool $promptsChanged = true): void
{
// Flags kept for API compatibility, but no-op with the new SDK here.
self::getInstance()->run($resourcesChanged, $toolsChanged, $promptsChanged);
}
private string $name;
/** @var array<int, array{name:string, description:string, callback:callable}> */
private array $tools = [];
/** @var array<int, array{name:string, description:string, callback:callable}> */
private array $prompts = [];
/** @var array<int, array{uri:string, name:string, description:string, mimeType:string, callback:?callable}> */
private array $resources = [];
public function __construct(string $name)
{
$this->name = $name;
}
/**
* Define a new tool
*/
public function tool(string $name, string $description, callable $callback): self
{
$this->tools[] = compact('name', 'description', 'callback');
return $this;
}
/**
* Define a new prompt
*/
public function prompt(string $name, string $description, callable $callback): self
{
$this->prompts[] = compact('name', 'description', 'callback');
return $this;
}
/**
* Define a new resource
*/
public function resource(string $uri, string $name, string $description = '', string $mimeType = 'text/plain', ?callable $callback = null): self
{
$this->resources[] = compact('uri', 'name', 'description', 'mimeType', 'callback');
return $this;
}
/**
* Run the server using mcp/sdk's ServerBuilder and StdioTransport
*/
public function run(bool $resourcesChanged = true, bool $toolsChanged = true, bool $promptsChanged = true): void
{
// Build with official SDK
/** @var ServerBuilder $builder */
$builder = SdkServer::make()
->withServerInfo($this->name, '0.1.1') // @TODO: keep version in sync with package when possible
->withLogger(new NullLogger());
// Register tools
foreach ($this->tools as $tool) {
$builder->withTool($tool['callback'], $tool['name'], $tool['description']);
}
// Register prompts
foreach ($this->prompts as $prompt) {
$builder->withPrompt($prompt['callback'], $prompt['name'], $prompt['description']);
}
// Register resources
foreach ($this->resources as $res) {
$builder->withResource($res['callback'] ?? fn() => null, $res['uri'], $res['name'], $res['description'], $res['mimeType']);
}
$server = $builder->build();
// Connect to stdio transport (JSON-RPC over STDIN/STDOUT)
$server->connect(new StdioTransport());
}
}
```