#
tokens: 4583/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # Composer Related
 2 | composer.lock
 3 | /vendor
 4 | 
 5 | # Frontend Assets
 6 | /node_modules
 7 | 
 8 | # Logs
 9 | npm-debug.log
10 | yarn-error.log
11 | 
12 | # Caches
13 | .phpunit.cache
14 | .phpunit.result.cache
15 | /build
16 | 
17 | # IDE Helper
18 | _ide_helper.php
19 | _ide_helper_models.php
20 | 
21 | # Editors
22 | /.vscode
23 | 
24 | # Misc
25 | phpunit.xml
26 | phpstan.neon
27 | testbench.yaml
28 | /docs
29 | /coverage
30 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # 🐉 The fast, PHP way to build MCP servers
 2 | 
 3 | 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. 
 4 | 
 5 | Create tools, expose resources, define prompts, and connect components with clean PHP code.
 6 | 
 7 | ## Installation
 8 | 
 9 | With composer:
10 | 
11 | ```bash
12 | composer require pronskiy/mcp
13 | ```
14 | 
15 | ## Usage
16 | 
17 | ```php
18 | require 'vendor/autoload.php';
19 | 
20 | $server = new \Pronskiy\Mcp\Server('simple-mcp-server');
21 | 
22 | $server
23 |     ->tool(
24 |         'add-numbers',
25 |         'Adds two numbers together',
26 |         fn(float $num1, float $num2) => "The sum of {$num1} and {$num2} is " . ($num1 + $num2)
27 |     )
28 |     ->tool(
29 |         'multiply-numbers',
30 |         'Multiplies two numbers',
31 |         fn(float $num1, float $num2) => "The product of {$num1} and {$num2} is " . ($num1 * $num2)
32 |     )
33 | ;
34 | 
35 | $server->run();
36 | ```
37 | 
38 | ## Credits
39 | 
40 | - https://github.com/logiscape/mcp-sdk-php
41 | 
42 | ## License
43 | 
44 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
45 | 
```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) pronskiy <[email protected]>
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
1 | # Changelog
2 | 
3 | All notable changes to `Mcp` will be documented in this file.
4 | 
```

--------------------------------------------------------------------------------
/tests/ArchTest.php:
--------------------------------------------------------------------------------

```php
1 | <?php
2 | 
3 | arch('it will not use debugging functions')
4 |     ->expect(['dd', 'dump', 'ray', 'var_dump'])
5 |     ->each->not->toBeUsed();
6 | 
```

--------------------------------------------------------------------------------
/examples/echo.php:
--------------------------------------------------------------------------------

```php
 1 | <?php
 2 | 
 3 | require __DIR__ . '/../vendor/autoload.php';
 4 | 
 5 | (new \Pronskiy\Mcp\Server('echo-server'))
 6 |     ->tool('echo', 'Echoes text', function(string $text) {
 7 |         return $text;
 8 |     })
 9 |     ->run();
10 | 
```

--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "pronskiy/mcp",
 3 |   "description": "🐉 The fast, PHP way to build MCP servers",
 4 |   "version": "0.1.1",
 5 |   "keywords": [
 6 |     "pronskiy",
 7 |     "mcp"
 8 |   ],
 9 |   "homepage": "https://github.com/pronskiy/mcp",
10 |   "license": "MIT",
11 |   "authors": [
12 |     {
13 |       "name": "Roman Pronskiy",
14 |       "email": "[email protected]"
15 |     }
16 |   ],
17 |   "require": {
18 |     "php": "^8.3",
19 |     "mcp/sdk": "dev-main"
20 |   },
21 |   "require-dev": {
22 |     "phpstan/phpstan": "^2.1.12",
23 |     "pestphp/pest": "^3.0",
24 |     "pestphp/pest-plugin-arch": "^3.0",
25 |     "pestphp/pest-plugin-laravel": "^3.0",
26 |     "phpstan/extension-installer": "^1.3||^2.0",
27 |     "phpstan/phpstan-deprecation-rules": "^1.1||^2.0",
28 |     "phpstan/phpstan-phpunit": "^1.3||^2.0",
29 |     "symfony/console": "^v7.2.5" 
30 |   },
31 |   "autoload": {
32 |     "psr-4": {
33 |       "Pronskiy\\Mcp\\": "src/"
34 |     }
35 |   },
36 |   "autoload-dev": {
37 |     "psr-4": {
38 |       "Pronskiy\\Mcp\\Tests\\": "tests/"
39 |     }
40 |   },
41 |   "scripts": {
42 |     "analyse": "vendor/bin/phpstan analyse",
43 |     "test": "vendor/bin/pest",
44 |     "test-coverage": "vendor/bin/pest --coverage"
45 |   },
46 |   "config": {
47 |     "sort-packages": true,
48 |     "allow-plugins": {
49 |       "pestphp/pest-plugin": true,
50 |       "phpstan/extension-installer": true
51 |     }
52 |   },
53 |   "extra": {
54 |   },
55 |   "minimum-stability": "dev",
56 |   "prefer-stable": true
57 | }
58 | 
```

--------------------------------------------------------------------------------
/src/McpServerException.php:
--------------------------------------------------------------------------------

```php
 1 | <?php
 2 | 
 3 | namespace Pronskiy\Mcp;
 4 | 
 5 | class McpServerException extends \Exception
 6 | {
 7 |     /**
 8 |      * Create a new exception for an invalid tool handler result
 9 |      */
10 |     public static function invalidToolResult(mixed $result): self
11 |     {
12 |         $type = is_object($result) ? get_class($result) : gettype($result);
13 |         return new self("Invalid tool handler result: expected string or CallToolResult, got {$type}");
14 |     }
15 | 
16 |     /**
17 |      * Create a new exception for an invalid prompt handler result
18 |      */
19 |     public static function invalidPromptResult(mixed $result): self
20 |     {
21 |         $type = is_object($result) ? get_class($result) : gettype($result);
22 |         return new self("Invalid prompt handler result: expected string, array, or GetPromptResult, got {$type}");
23 |     }
24 | 
25 |     /**
26 |      * Create a new exception for an invalid resource handler result
27 |      */
28 |     public static function invalidResourceResult(mixed $result): self
29 |     {
30 |         $type = is_object($result) ? get_class($result) : gettype($result);
31 |         return new self("Invalid resource handler result: expected string, SplFileObject, resource, or ReadResourceResult, got {$type}");
32 |     }
33 | 
34 |     /**
35 |      * Create a new exception for an unknown tool
36 |      */
37 |     public static function unknownTool(string $name): self
38 |     {
39 |         return new self("Unknown tool: {$name}");
40 |     }
41 | 
42 |     /**
43 |      * Create a new exception for an unknown prompt
44 |      */
45 |     public static function unknownPrompt(string $name): self
46 |     {
47 |         return new self("Unknown prompt: {$name}");
48 |     }
49 | 
50 |     /**
51 |      * Create a new exception for an unknown resource
52 |      */
53 |     public static function unknownResource(string $uri): self
54 |     {
55 |         return new self("Unknown resource: {$uri}");
56 |     }
57 | }
58 | 
```

--------------------------------------------------------------------------------
/tests/BasicInteractionTest.php:
--------------------------------------------------------------------------------

```php
 1 | <?php
 2 | 
 3 | use Symfony\Component\Process\InputStream;
 4 | use Symfony\Component\Process\Process;
 5 | 
 6 | function sendRequest(Process $process, InputStream $input, array $request, $waitForResponse = true)
 7 | {
 8 |     $requestStr = json_encode($request) . "\n";
 9 | 
10 |     // Send the request
11 |     $input->write($requestStr);
12 | 
13 |     // Return immediately for notifications
14 |     if (!$waitForResponse) {
15 |         return null;
16 |     }
17 | 
18 |     // Read the response
19 |     $responseStr = '';
20 |     $startTime = microtime(true);
21 | 
22 |     while (empty($responseStr) && (microtime(true) - $startTime) < 5.0) {
23 |         $responseStr = $process->getIncrementalOutput();
24 |         if (empty($responseStr)) {
25 |             usleep(100000); // 100ms
26 |         }
27 |     }
28 | 
29 |     $responseStr = trim($responseStr);
30 | 
31 |     // Parse and return the response
32 |     return json_decode($responseStr, true, 512, JSON_THROW_ON_ERROR);
33 | }
34 | 
35 | it('can initialize', function () {
36 |     $process = new Process([
37 |         'php',
38 |         '-r',
39 |         /** @lang PHP */ 
40 |         'require "vendor/autoload.php"; 
41 |          $server = new \Pronskiy\Mcp\Server("test-mcp-server"); 
42 |          $server->run();'
43 |     ]);
44 |     $input = new InputStream();
45 |     $process->setInput($input);
46 |     $process->setTty(false);
47 |     $process->setTimeout(null);
48 |     $process->start();
49 | 
50 |     // @todo test with different protocol versions
51 |     $protocolVersion = '2025';
52 |     if ($protocolVersion === '2024-11-05') {
53 |         $capabilities = [
54 |             'supports' => [
55 |                 'filesystem' => true,
56 |                 'resources' => true,
57 |                 'utilities' => true,
58 |                 'prompt' => true
59 |             ]
60 |         ];
61 |     } else {  // 2025-03-26
62 |         $capabilities = [
63 |             'tools' => [
64 |                 'listChanged' => true
65 |             ],
66 |             'resources' => true,
67 |             'prompt' => [
68 |                 'streaming' => true
69 |             ],
70 |             'utilities' => true
71 |         ];
72 |     }
73 | 
74 |     $initializeRequest = [
75 |         'jsonrpc' => '2.0',
76 |         'id' => 'test-init',
77 |         'method' => 'initialize',
78 |         'params' => [
79 |             'protocolVersion' => $protocolVersion,
80 |             'capabilities' => $capabilities,
81 |             'clientInfo' => [
82 |                 'name' => 'TestClient',
83 |                 'version' => '1.0.0'
84 |             ]
85 |         ]
86 |     ];
87 | 
88 |     $initResponse = sendRequest($process,$input, $initializeRequest);
89 | 
90 |     expect($initResponse)
91 |         ->toBeArray()
92 |         ->and(isset($initResponse['result']))->toBeTrue()
93 |         ->and(isset($initResponse['result']['protocolVersion']))->toBeTrue()
94 |     ;
95 | });
96 | 
```

--------------------------------------------------------------------------------
/src/Server.php:
--------------------------------------------------------------------------------

```php
  1 | <?php
  2 | 
  3 | declare(strict_types=1);
  4 | 
  5 | namespace Pronskiy\Mcp;
  6 | 
  7 | use Mcp\Server as SdkServer;
  8 | use Mcp\Server\ServerBuilder;
  9 | use Mcp\Server\Transport\StdioTransport;
 10 | use Psr\Log\NullLogger;
 11 | 
 12 | /**
 13 |  * Simple MCP Server implementation (using the official mcp/sdk under the hood)
 14 |  *
 15 |  * This class provides a fluent interface for creating MCP servers.
 16 |  * It stores user-defined tools, prompts, and resources, and registers them
 17 |  * with the mcp/sdk ServerBuilder when run() is called.
 18 |  */
 19 | class Server
 20 | {
 21 |     protected static ?self $instance = null;
 22 | 
 23 |     /**
 24 |      * Get the singleton instance of the server
 25 |      */
 26 |     public static function getInstance(string $name = 'mcp-server'): self
 27 |     {
 28 |         if (self::$instance === null) {
 29 |             self::$instance = new self($name);
 30 |         }
 31 |         return self::$instance;
 32 |     }
 33 | 
 34 |     public static function addTool(string $name, string $description, callable $callback): self
 35 |     {
 36 |         return self::getInstance()->tool($name, $description, $callback);
 37 |     }
 38 | 
 39 |     public static function addPrompt(string $name, string $description, callable $callback): self
 40 |     {
 41 |         return self::getInstance()->prompt($name, $description, $callback);
 42 |     }
 43 | 
 44 |     public static function addResource(string $uri, string $name, string $description = '', string $mimeType = 'text/plain', ?callable $callback = null): self
 45 |     {
 46 |         return self::getInstance()->resource($uri, $name, $description, $mimeType, $callback);
 47 |     }
 48 | 
 49 |     public static function start(bool $resourcesChanged = true, bool $toolsChanged = true, bool $promptsChanged = true): void
 50 |     {
 51 |         // Flags kept for API compatibility, but no-op with the new SDK here.
 52 |         self::getInstance()->run($resourcesChanged, $toolsChanged, $promptsChanged);
 53 |     }
 54 | 
 55 |     private string $name;
 56 | 
 57 |     /** @var array<int, array{name:string, description:string, callback:callable}> */
 58 |     private array $tools = [];
 59 | 
 60 |     /** @var array<int, array{name:string, description:string, callback:callable}> */
 61 |     private array $prompts = [];
 62 | 
 63 |     /** @var array<int, array{uri:string, name:string, description:string, mimeType:string, callback:?callable}> */
 64 |     private array $resources = [];
 65 | 
 66 |     public function __construct(string $name)
 67 |     {
 68 |         $this->name = $name;
 69 |     }
 70 | 
 71 |     /**
 72 |      * Define a new tool
 73 |      */
 74 |     public function tool(string $name, string $description, callable $callback): self
 75 |     {
 76 |         $this->tools[] = compact('name', 'description', 'callback');
 77 |         return $this;
 78 |     }
 79 | 
 80 |     /**
 81 |      * Define a new prompt
 82 |      */
 83 |     public function prompt(string $name, string $description, callable $callback): self
 84 |     {
 85 |         $this->prompts[] = compact('name', 'description', 'callback');
 86 |         return $this;
 87 |     }
 88 | 
 89 |     /**
 90 |      * Define a new resource
 91 |      */
 92 |     public function resource(string $uri, string $name, string $description = '', string $mimeType = 'text/plain', ?callable $callback = null): self
 93 |     {
 94 |         $this->resources[] = compact('uri', 'name', 'description', 'mimeType', 'callback');
 95 |         return $this;
 96 |     }
 97 | 
 98 |     /**
 99 |      * Run the server using mcp/sdk's ServerBuilder and StdioTransport
100 |      */
101 |     public function run(bool $resourcesChanged = true, bool $toolsChanged = true, bool $promptsChanged = true): void
102 |     {
103 |         // Build with official SDK
104 |         /** @var ServerBuilder $builder */
105 |         $builder = SdkServer::make()
106 |             ->withServerInfo($this->name, '0.1.1') // @TODO: keep version in sync with package when possible
107 |             ->withLogger(new NullLogger());
108 | 
109 |         // Register tools
110 |         foreach ($this->tools as $tool) {
111 |             $builder->withTool($tool['callback'], $tool['name'], $tool['description']);
112 |         }
113 | 
114 |         // Register prompts
115 |         foreach ($this->prompts as $prompt) {
116 |             $builder->withPrompt($prompt['callback'], $prompt['name'], $prompt['description']);
117 |         }
118 | 
119 |         // Register resources
120 |         foreach ($this->resources as $res) {
121 |             $builder->withResource($res['callback'] ?? fn() => null, $res['uri'], $res['name'], $res['description'], $res['mimeType']);
122 |         }
123 | 
124 |         $server = $builder->build();
125 | 
126 |         // Connect to stdio transport (JSON-RPC over STDIN/STDOUT)
127 |         $server->connect(new StdioTransport());
128 |     }
129 | }
130 | 
```