├── .php-cs-fixer.dist.php
├── src
├── MiddlewareRegistryInterface.php
├── Attribute
│ ├── Tool.php
│ ├── IsReadonly.php
│ ├── IsDestructive.php
│ ├── IsIdempotent.php
│ └── IsOpenWorld.php
├── MiddlewareRepositoryInterface.php
├── SchemaMapperInterface.php
├── MiddlewareManager.php
├── Valinor
│ ├── MapperBuilder.php
│ └── SchemaMapper.php
├── Entrypoint
│ ├── McpCommand.php
│ └── McpDispatcher.php
├── Discovery
│ ├── ClassHandler.php
│ ├── ToolsLocator.php
│ ├── AttributesParser.php
│ └── ToolFactory.php
└── Bootloader
│ ├── ValinorMapperBootloader.php
│ └── McpServerBootloader.php
├── context.yaml
├── phpunit.xml
├── rector.php
├── LICENSE
├── composer.json
└── README.md
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | include(__DIR__ . '/src')
9 | ->include(__DIR__ . '/tests')
10 | ->include(__FILE__)
11 | ->build();
12 |
--------------------------------------------------------------------------------
/src/MiddlewareRegistryInterface.php:
--------------------------------------------------------------------------------
1 | |null $class
17 | * @return T
18 | */
19 | public function toObject(string $json, ?string $class = null): object;
20 | }
21 |
--------------------------------------------------------------------------------
/context.yaml:
--------------------------------------------------------------------------------
1 | $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json'
2 |
3 | documents:
4 | - description: 'Project structure overview'
5 | outputPath: project-structure.md
6 | sources:
7 | - type: tree
8 | sourcePaths:
9 | - src
10 | showCharCount: true
11 |
12 | - description: Source code
13 | outputPath: source.md
14 | sources:
15 | - type: file
16 | sourcePaths:
17 | - src
18 |
19 |
--------------------------------------------------------------------------------
/src/Attribute/IsIdempotent.php:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | tests/src
9 |
10 |
11 |
12 |
13 | src
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/MiddlewareManager.php:
--------------------------------------------------------------------------------
1 | middlewares[] = $middleware;
22 | }
23 |
24 | public function all(): array
25 | {
26 | return $this->middlewares;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Valinor/MapperBuilder.php:
--------------------------------------------------------------------------------
1 | enableFlexibleCasting()
21 | ->allowPermissiveTypes();
22 |
23 | if ($this->cache) {
24 | $builder = $builder->withCache($this->cache);
25 | }
26 |
27 | return $builder->mapper();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
12 | __DIR__ . '/src',
13 | __DIR__ . '/tests',
14 | ]);
15 |
16 | // Register rules for PHP 8.4 migration
17 | $rectorConfig->sets([
18 | SetList::PHP_83,
19 | LevelSetList::UP_TO_PHP_83,
20 | ]);
21 |
22 | // Skip vendor directories
23 | $rectorConfig->skip([
24 | __DIR__ . '/vendor',
25 | AddOverrideAttributeToOverriddenMethodsRector::class,
26 | ]);
27 | };
28 |
--------------------------------------------------------------------------------
/src/Entrypoint/McpCommand.php:
--------------------------------------------------------------------------------
1 | listen($transport);
26 | } catch (\Throwable $e) {
27 | $errorHandler->report($e);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Discovery/ClassHandler.php:
--------------------------------------------------------------------------------
1 | factory->make($this->class->getName());
27 |
28 | if ($this->schemaClass === null) {
29 | return $tool();
30 | }
31 |
32 | $object = $this->schemaMapper->toObject(
33 | json: \json_encode($arguments),
34 | class: $this->schemaClass,
35 | );
36 |
37 | return $tool($object);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) spiral
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 |
--------------------------------------------------------------------------------
/src/Entrypoint/McpDispatcher.php:
--------------------------------------------------------------------------------
1 | get('SAPI', 'cli') === 'mcp';
27 | }
28 |
29 | public function serve(): void
30 | {
31 | /** @var Server $server */
32 | $server = $this->container->get(Server::class);
33 | $transport = $this->container->get(ServerTransportInterface::class);
34 |
35 | try {
36 | $server->listen($transport);
37 | } catch (\Throwable $e) {
38 | $this->errorHandler->report($e);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Valinor/SchemaMapper.php:
--------------------------------------------------------------------------------
1 | generator->generate($class)->jsonSerialize();
26 | }
27 |
28 | throw new \InvalidArgumentException(\sprintf('Invalid class or JSON schema provided: %s', $class));
29 | }
30 |
31 | public function toObject(string $json, ?string $class = null): object
32 | {
33 | if ($class === null) {
34 | return \json_decode($json, associative: false);
35 | }
36 |
37 | return $this->mapper->map($class, \json_decode($json, associative: true));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Discovery/ToolsLocator.php:
--------------------------------------------------------------------------------
1 | isInstantiable()) {
28 | return;
29 | }
30 |
31 | $toolAttribute = $this->attributesParser->parseToolAttribute($class);
32 | if ($toolAttribute === null) {
33 | return;
34 | }
35 |
36 | $annotations = $this->attributesParser->parseAnnotationAttributes($class);
37 | $tool = $this->toolFactory->createTool($class, $toolAttribute, $annotations);
38 | $handler = $this->toolFactory->createHandler($class);
39 |
40 | $this->registry->registerTool($tool, $handler);
41 | }
42 |
43 | public function finalize(): void {}
44 | }
45 |
--------------------------------------------------------------------------------
/src/Bootloader/ValinorMapperBootloader.php:
--------------------------------------------------------------------------------
1 | static function (
22 | AppEnvironment $env,
23 | DirectoriesInterface $dirs,
24 | JsonSchemaGenerator $generator,
25 | ): SchemaMapper {
26 | $mapper = new MapperBuilder(
27 | cache: match ($env) {
28 | AppEnvironment::Production => new FileSystemCache(
29 | cacheDir: $dirs->get('runtime') . 'cache/valinor',
30 | ),
31 | default => null,
32 | },
33 | );
34 |
35 | $treeMapper = $mapper->build();
36 |
37 | return new SchemaMapper($generator, $treeMapper);
38 | },
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spiral/mcp-server",
3 | "description": "Spiral bridge for MCP server",
4 | "keywords": [
5 | "spiral",
6 | "spiral",
7 | "mcp-server"
8 | ],
9 | "homepage": "https://github.com/spiral/mcp-server",
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Pavel Buchnev",
14 | "email": "butschster@gmail.com",
15 | "role": "Developer"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.3",
20 | "cuyz/valinor": "^1.7",
21 | "spiral/json-schema-generator": "^2.0",
22 | "llm/mcp-server": "^1.0",
23 | "spiral/boot": "^3.15",
24 | "spiral/core": "^3.15",
25 | "spiral/tokenizer": "^3.15",
26 | "spiral/attributes": "^3.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^10.1",
30 | "rector/rector": "^2.0",
31 | "spiral/code-style": "^2.2",
32 | "spiral/testing": "^2.3",
33 | "vimeo/psalm": "^6.10"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Spiral\\McpServer\\": "src"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "Spiral\\McpServer\\Tests\\App\\": "tests/app",
43 | "Spiral\\McpServer\\Tests\\": "tests/src"
44 | }
45 | },
46 | "scripts": {
47 | "cs:fix": "php-cs-fixer fix -v",
48 | "psalm": "psalm",
49 | "psalm:baseline": "psalm --set-baseline=psalm-baseline.xml",
50 | "refactor": "rector process --config=rector.php",
51 | "refactor:ci": "rector process --config=rector.php --dry-run --ansi",
52 | "test": "phpunit",
53 | "test-coverage": "phpunit --coverage"
54 | },
55 | "config": {
56 | "sort-packages": true
57 | },
58 | "extra": {
59 | "spiral": {
60 | "bootloaders": [
61 | "Spiral\\McpServer\\Bootloader\\McpServerBootloader"
62 | ]
63 | }
64 | },
65 | "minimum-stability": "dev",
66 | "prefer-stable": true
67 | }
68 |
--------------------------------------------------------------------------------
/src/Discovery/AttributesParser.php:
--------------------------------------------------------------------------------
1 | reader->firstClassMetadata($class, Attribute\Tool::class);
23 | }
24 |
25 | public function parseAnnotationAttributes(\ReflectionClass $class): ?ToolAnnotations
26 | {
27 | $readOnlyHint = $this->parseReadOnlyAttribute($class);
28 | $destructiveHint = $this->parseDestructiveAttribute($class);
29 | $idempotentHint = $this->parseIdempotentAttribute($class);
30 | $openWorldHint = $this->parseOpenWorldAttribute($class);
31 |
32 | // Only create ToolAnnotations if at least one attribute was found
33 | if ($readOnlyHint === null && $destructiveHint === null &&
34 | $idempotentHint === null && $openWorldHint === null) {
35 | return null;
36 | }
37 |
38 | return ToolAnnotations::make(
39 | readOnlyHint: $readOnlyHint,
40 | destructiveHint: $destructiveHint,
41 | idempotentHint: $idempotentHint,
42 | openWorldHint: $openWorldHint,
43 | );
44 | }
45 |
46 | private function parseReadOnlyAttribute(\ReflectionClass $class): ?bool
47 | {
48 | $attribute = $this->reader->firstClassMetadata($class, Attribute\IsReadonly::class);
49 | return $attribute?->readOnlyHint;
50 | }
51 |
52 | private function parseDestructiveAttribute(\ReflectionClass $class): ?bool
53 | {
54 | $attribute = $this->reader->firstClassMetadata($class, Attribute\IsDestructive::class);
55 | return $attribute?->destructive;
56 | }
57 |
58 | private function parseIdempotentAttribute(\ReflectionClass $class): ?bool
59 | {
60 | $attribute = $this->reader->firstClassMetadata($class, Attribute\IsIdempotent::class);
61 | return $attribute?->idempotent;
62 | }
63 |
64 | private function parseOpenWorldAttribute(\ReflectionClass $class): ?bool
65 | {
66 | $attribute = $this->reader->firstClassMetadata($class, Attribute\IsOpenWorld::class);
67 | return $attribute?->openWorld;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Discovery/ToolFactory.php:
--------------------------------------------------------------------------------
1 | validateToolClass($class);
30 |
31 | $method = $class->getMethod('__invoke');
32 | $this->assertHandlerMethodIsPublic($method);
33 |
34 | $name = $toolAttribute->name;
35 | $description = $toolAttribute->description;
36 | [$_, $inputSchema] = $this->findSchema($method);
37 |
38 | return Tool::make($name, $inputSchema, $description, $annotations);
39 | }
40 |
41 | public function createHandler(\ReflectionClass $class): HandlerInterface
42 | {
43 | $method = $class->getMethod('__invoke');
44 | [$schemaClass, $_] = $this->findSchema($method);
45 |
46 | return new ClassHandler(
47 | factory: $this->factory,
48 | schemaMapper: $this->schemaMapper,
49 | class: $class,
50 | schemaClass: $schemaClass,
51 | );
52 | }
53 |
54 | private function validateToolClass(\ReflectionClass $class): void
55 | {
56 | if (!$class->isInstantiable()) {
57 | throw new \InvalidArgumentException(
58 | \sprintf('Class %s must be instantiable.', $class->getName()),
59 | );
60 | }
61 |
62 | if (!$class->hasMethod('__invoke')) {
63 | throw new \InvalidArgumentException(
64 | \sprintf('Class %s must have __invoke method.', $class->getName()),
65 | );
66 | }
67 | }
68 |
69 | private function assertHandlerMethodIsPublic(\ReflectionMethod $method): void
70 | {
71 | if (!$method->isPublic()) {
72 | throw new \InvalidArgumentException(
73 | \sprintf(
74 | 'Handler method %s:%s should be public.',
75 | $method->getDeclaringClass()->getName(),
76 | $method->getName(),
77 | ),
78 | );
79 | }
80 | }
81 |
82 | private function findSchema(\ReflectionMethod $method): array
83 | {
84 | $properties = $method->getParameters();
85 | if (\count($properties) === 0) {
86 | return [null, ['type' => 'object']];
87 | }
88 |
89 | if (\count($properties) > 1) {
90 | throw new \InvalidArgumentException(
91 | \sprintf(
92 | 'Handler method %s should have exactly one parameter or no parameters at all.',
93 | $method->getName(),
94 | ),
95 | );
96 | }
97 |
98 | $schema = $properties[0];
99 | if ($schema->getType() === null) {
100 | throw new \InvalidArgumentException(
101 | \sprintf(
102 | 'Handler method %s should have exactly one parameter.',
103 | $method->getName(),
104 | ),
105 | );
106 | }
107 |
108 | $schemaClass = $schema->getType()->getName();
109 | if (!\class_exists($schemaClass)) {
110 | throw new \InvalidArgumentException(
111 | \sprintf(
112 | 'Handler method %s parameter should be a class, %s given.',
113 | $method->getName(),
114 | $schemaClass,
115 | ),
116 | );
117 | }
118 |
119 | return [
120 | $schemaClass,
121 | $this->schemaMapper->toJsonSchema($schemaClass),
122 | ];
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spiral MCP Server
2 |
3 | A powerful and flexible **Model Context Protocol (MCP) Server** implementation for the Spiral Framework. This package
4 | provides a complete MCP server solution with automatic tool discovery, attribute-based configuration, and seamless
5 | integration with Spiral's dependency injection container.
6 |
7 | ## Table of Contents
8 |
9 | - [Features](#features)
10 | - [Requirements](#requirements)
11 | - [Installation](#installation)
12 | - [Quick Start](#quick-start)
13 | - [Configuration](#configuration)
14 | - [Creating Tools](#creating-tools)
15 | - [Tool Attributes](#tool-attributes)
16 | - [Middleware](#middleware)
17 | - [Contributing](#contributing)
18 | - [License](#license)
19 |
20 | ## Features
21 |
22 | ✨ **Automatic Tool Discovery** - Automatically discovers and registers MCP tools using PHP attributes
23 | 🔧 **Multiple Transport Options** - Supports HTTP, streaming HTTP, and STDIO transports
24 | 🎯 **Attribute-Based Configuration** - Define tool behavior with simple PHP attributes
25 | 🛡️ **Schema Validation** - Automatic JSON schema generation using spiral/json-schema-generator
26 | ⚡ **Middleware Support** - Extensible middleware system for request/response processing
27 | 🔌 **Spiral Integration** - First-class integration with Spiral Framework's IoC container
28 | 📊 **Session Management** - Built-in session handling with configurable TTL
29 | 🚀 **Production Ready** - Designed for high-performance production environments
30 |
31 | ## Requirements
32 |
33 | - PHP 8.3 or higher
34 | - Spiral Framework 3.15+
35 | - Composer
36 |
37 | ## Installation
38 |
39 | Install the package via Composer:
40 |
41 | ```bash
42 | composer require spiral/mcp-server
43 | ```
44 |
45 | Add the bootloader to your Spiral application:
46 |
47 | ```php
48 | // app/src/Application/Kernel.php
49 | use Spiral\McpServer\Bootloader\McpServerBootloader;
50 |
51 | protected const LOAD = [
52 | // ... other bootloaders
53 | McpServerBootloader::class,
54 | ];
55 | ```
56 |
57 | ## Quick Start
58 |
59 | ### 1. Create Your First Tool
60 |
61 | Create a simple calculator tool:
62 |
63 | ```php
64 | $request->a + $request->b,
82 | 'operation' => 'addition'
83 | ];
84 | }
85 | }
86 | ```
87 |
88 | ### 2. Define the Request Schema
89 |
90 | ```php
91 | **Note**: For more information about DTO schema definition
116 | > visit [spiral/json-schema-generator](https://github.com/spiral/json-schema-generator).
117 |
118 | ### 3. Set Environment Variables
119 |
120 | ```bash
121 | # .env
122 | MCP_TRANSPORT=http
123 | MCP_HOST=127.0.0.1
124 | MCP_PORT=8090
125 | MCP_SERVER_NAME="My MCP Server"
126 | MCP_SERVER_VERSION="1.0.0"
127 | ```
128 |
129 | ### 4. Start the Server
130 |
131 | You can start the server in two ways:
132 |
133 | **Option 1: Using the MCP Dispatcher (Recommended for production)**
134 |
135 | ```bash
136 | SAPI=mcp php app.php
137 | ```
138 |
139 | **Option 2: Using the Console Command**
140 |
141 | ```bash
142 | php app.php mcp
143 | ```
144 |
145 | Your MCP server is now running and ready to accept requests!
146 |
147 | ## Configuration
148 |
149 | ### Environment Variables
150 |
151 | | Variable | Default | Description |
152 | |----------------------|--------------|----------------------------------------------|
153 | | `MCP_TRANSPORT` | `http` | Transport type: `http`, `stream`, or `stdio` |
154 | | `MCP_HOST` | `127.0.0.1` | Server host address |
155 | | `MCP_PORT` | `8090` | Server port number |
156 | | `MCP_SERVER_NAME` | `MCP Server` | Server identification name |
157 | | `MCP_SERVER_VERSION` | `1.0.0` | Server version |
158 | | `MCP_SESSION_TTL` | `3600` | Session TTL in seconds |
159 | | `SAPI` | `cli` | Set to `mcp` to enable MCP dispatcher |
160 |
161 | ### Custom Configuration
162 |
163 | You can override the default configuration by binding your own `Configuration` instance:
164 |
165 | ```php
166 | use PhpMcp\Server\Configuration;
167 | use PhpMcp\Schema\Implementation;
168 | use PhpMcp\Schema\ServerCapabilities;
169 |
170 | // In your bootloader
171 | $this->container->bindSingleton(Configuration::class, function() {
172 | return new Configuration(
173 | serverInfo: Implementation::make('Custom Server', '2.0.0'),
174 | capabilities: ServerCapabilities::make(),
175 | // ... other options
176 | );
177 | });
178 | ```
179 |
180 | ## Creating Tools
181 |
182 | ### Basic Tool Structure
183 |
184 | Tools are simple PHP classes that implement the `__invoke()` method. They support Dependency Injection (DI) and can be
185 | injected with any dependencies, like in the following example:
186 |
187 | ```php
188 | files->exists($request->path)) {
210 | throw new \InvalidArgumentException('File not found');
211 | }
212 |
213 | return [
214 | 'content' => $this->files->read($request->path),
215 | 'size' => $this->files->size($request->path),
216 | ];
217 | }
218 | }
219 |
220 | final readonly class FileRequest
221 | {
222 | public function __construct(
223 | /**
224 | * @param not-empty-string $path
225 | */
226 | #[Field(
227 | title: 'File Path',
228 | description: 'Absolute path to the file to read'
229 | )]
230 | public string $path,
231 |
232 | #[Field(
233 | title: 'Encoding',
234 | description: 'Expected file encoding',
235 | default: 'utf-8'
236 | )]
237 | public string $encoding = 'utf-8',
238 |
239 | /**
240 | * @param positive-int $maxSize
241 | */
242 | #[Field(
243 | title: 'Maximum Size',
244 | description: 'Maximum file size in bytes',
245 | default: 1048576
246 | )]
247 | public int $maxSize = 1048576, // 1MB
248 | ) {}
249 | }
250 | ```
251 |
252 | > **Note**: For more information about DTO schema definition
253 | > visit [spiral/json-schema-generator](https://github.com/spiral/json-schema-generator).
254 |
255 | ### Tools Without Parameters
256 |
257 | For tools that don't require input parameters:
258 |
259 | ```php
260 | #[Tool(
261 | name: 'system_status',
262 | description: 'Gets current system status'
263 | )]
264 | class SystemStatusTool
265 | {
266 | public function __invoke(): array
267 | {
268 | return [
269 | 'memory_usage' => memory_get_usage(true),
270 | 'peak_memory' => memory_get_peak_usage(true),
271 | 'uptime' => $this->getUptime(),
272 | ];
273 | }
274 |
275 | private function getUptime(): int
276 | {
277 | // Implementation details...
278 | return 0;
279 | }
280 | }
281 | ```
282 |
283 | ## Tool Attributes
284 |
285 | ### Core Tool Attribute
286 |
287 | The `#[Tool]` attribute is required for all MCP tools:
288 |
289 | ```php
290 | #[Tool(
291 | name: 'unique_tool_name',
292 | description: 'Clear description of what this tool does'
293 | )]
294 | ```
295 |
296 | ### Behavioral Attributes
297 |
298 | #### Read-Only Tools
299 |
300 | ```php
301 | #[IsReadonly(readOnlyHint: true)] // Tool doesn't modify environment
302 | ```
303 |
304 | #### Destructive Operations
305 |
306 | ```php
307 | #[IsDestructive(destructive: true)] // Tool may perform destructive updates
308 | ```
309 |
310 | #### Idempotent Operations
311 |
312 | ```php
313 | #[IsIdempotent(idempotent: true)] // Repeated calls have no additional effect
314 | ```
315 |
316 | #### Open World Tools
317 |
318 | ```php
319 | #[IsOpenWorld(openWorld: true)] // Tool interacts with external entities
320 | ```
321 |
322 | ### Complete Example with All Attributes
323 |
324 | ```php
325 | #[Tool(
326 | name: 'file_manager',
327 | description: 'Manages file operations with external storage'
328 | )]
329 | #[IsDestructive(destructive: true)]
330 | #[IsIdempotent(idempotent: false)]
331 | #[IsOpenWorld(openWorld: true)]
332 | class FileManagerTool
333 | {
334 | public function __invoke(FileOperation $operation): array
335 | {
336 | // Implementation...
337 | return ['status' => 'success'];
338 | }
339 | }
340 | ```
341 |
342 | ## Middleware
343 |
344 | ### Creating Custom Middleware
345 |
346 | ```php
347 | register(new LoggingMiddleware());
379 | $registry->register(new AuthenticationMiddleware());
380 | $registry->register(new RateLimitingMiddleware());
381 | }
382 | ```
383 |
384 | ## Contributing
385 |
386 | We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
387 |
388 | ### Development Setup
389 |
390 | 1. Clone the repository
391 | 2. Install dependencies: `composer install`
392 | 3. Run tests: `composer test`
393 | 4. Run static analysis: `composer psalm`
394 |
395 | ### Code Style
396 |
397 | This project follows PSR-12 coding standards. Run the code fixer:
398 |
399 | ```bash
400 | composer cs-fix
401 | ```
402 |
403 | ## License
404 |
405 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
406 |
407 | ---
408 |
409 | **Built with ❤️ by the Spiral Team**
410 |
411 | For more information about the Spiral Framework, visit [spiral.dev](https://spiral.dev)
--------------------------------------------------------------------------------
/src/Bootloader/McpServerBootloader.php:
--------------------------------------------------------------------------------
1 | static fn(): LoopInterface => Loop::get(),
70 |
71 | // Middleware Management
72 | MiddlewareRepositoryInterface::class => MiddlewareManager::class,
73 | MiddlewareRegistryInterface::class => MiddlewareManager::class,
74 |
75 | // Session Management
76 | SessionIdGeneratorInterface::class => SessionIdGenerator::class,
77 | SessionHandlerInterface::class => $this->createSessionHandler(...),
78 | SessionManager::class => $this->createSessionManager(...),
79 | SubscriptionManager::class => $this->createSubscriptionManager(...),
80 |
81 | // Cache and Storage
82 | CacheInterface::class => $this->createCache(...),
83 |
84 | // Registry and Tools
85 | ReferenceProviderInterface::class => Registry::class,
86 | ReferenceRegistryInterface::class => Registry::class,
87 | Registry::class => $this->createRegistry(...),
88 | ToolExecutorInterface::class => $this->createToolExecutor(...),
89 |
90 | // Configuration
91 | Configuration::class => $this->createMcpConfiguration(...),
92 |
93 | // Pagination
94 | Paginator::class => $this->createPaginator(...),
95 |
96 | // Routing and Dispatch
97 | DispatcherRoutesFactoryInterface::class => RoutesFactory::class,
98 | RoutesFactory::class => $this->createRoutesFactory(...),
99 | DispatcherInterface::class => Dispatcher::class,
100 | Dispatcher::class => $this->createDispatcher(...),
101 |
102 | // Protocol
103 | Protocol::class => $this->createProtocol(...),
104 |
105 | // Transport
106 | HttpServerInterface::class => $this->createHttpServer(...),
107 | ServerTransportInterface::class => $this->createTransport(...),
108 |
109 | // Main Server
110 |
111 | Server::class => $this->createServer(...),
112 | ];
113 | }
114 |
115 | private function createSessionHandler(
116 | EnvironmentInterface $env,
117 | CacheInterface $cache,
118 | ): SessionHandlerInterface {
119 | $sessionType = $env->get('MCP_SESSION_TYPE', 'array');
120 | $ttl = (int) $env->get('MCP_SESSION_TTL', 3600);
121 |
122 | return match ($sessionType) {
123 | 'cache' => new CacheSessionHandler($cache, $ttl),
124 | default => new ArraySessionHandler($ttl),
125 | };
126 | }
127 |
128 | private function createSessionManager(
129 | SessionHandlerInterface $sessionHandler,
130 | LogsInterface $logs,
131 | LoopInterface $loop,
132 | EnvironmentInterface $env,
133 | ): SessionManager {
134 | return new SessionManager(
135 | handler: $sessionHandler,
136 | logger: $logs->getLogger('mcp'),
137 | loop: $loop,
138 | ttl: (int) $env->get('MCP_SESSION_TTL', 3600),
139 | gcInterval: (float) $env->get('MCP_SESSION_GC_INTERVAL', 300),
140 | );
141 | }
142 |
143 | private function createSubscriptionManager(LogsInterface $logs): SubscriptionManager
144 | {
145 | return new SubscriptionManager($logs->getLogger('mcp'));
146 | }
147 |
148 | private function createCache(
149 | EnvironmentInterface $env,
150 | DirectoriesInterface $dirs,
151 | ): CacheInterface {
152 | $cacheType = $env->get('MCP_CACHE_TYPE', 'array');
153 |
154 | return match ($cacheType) {
155 | 'file' => new FileCache($dirs->get('runtime') . 'cache/mcp'),
156 | default => new ArrayCache(),
157 | };
158 | }
159 |
160 | private function createRegistry(LogsInterface $logs): Registry
161 | {
162 | return new Registry($logs->getLogger('mcp'));
163 | }
164 |
165 | private function createToolExecutor(
166 | ReferenceRegistryInterface $registry,
167 | LogsInterface $logs,
168 | ): ToolExecutorInterface {
169 | return new ToolExecutor($registry, $logs->getLogger('mcp'));
170 | }
171 |
172 | private function createMcpConfiguration(
173 | EnvironmentInterface $env,
174 | ): Configuration {
175 | return new Configuration(
176 | serverInfo: Implementation::make(
177 | name: trim((string) $env->get('MCP_SERVER_NAME', 'Spiral MCP Server')),
178 | version: trim((string) $env->get('MCP_SERVER_VERSION', '1.0.0')),
179 | ),
180 | capabilities: ServerCapabilities::make(
181 | tools: $env->get('MCP_ENABLE_TOOLS', true),
182 | toolsListChanged: $env->get('MCP_ENABLE_TOOLS_LIST_CHANGED', true),
183 | resources: $env->get('MCP_ENABLE_RESOURCES', true),
184 | resourcesSubscribe: $env->get('MCP_ENABLE_RESOURCES_SUBSCRIBE', true),
185 | resourcesListChanged: $env->get('MCP_ENABLE_RESOURCES_LIST_CHANGED', true),
186 | prompts: $env->get('MCP_ENABLE_PROMPTS', true),
187 | promptsListChanged: $env->get('MCP_ENABLE_PROMPTS_LIST_CHANGED', true),
188 | logging: $env->get('MCP_ENABLE_LOGGING', true),
189 | completions: $env->get('MCP_ENABLE_COMPLETIONS', true),
190 | experimental: (array) $env->get('MCP_EXPERIMENTAL_CAPABILITIES', []),
191 | ),
192 | instructions: $env->get('MCP_INSTRUCTIONS'),
193 | );
194 | }
195 |
196 | private function createPaginator(EnvironmentInterface $env): Paginator
197 | {
198 | return new Paginator(
199 | paginationLimit: (int) $env->get('MCP_PAGINATION_LIMIT', 50),
200 | );
201 | }
202 |
203 | private function createRoutesFactory(
204 | Configuration $configuration,
205 | ReferenceRegistryInterface $registry,
206 | SubscriptionManager $subscriptionManager,
207 | ToolExecutorInterface $toolExecutor,
208 | Paginator $pagination,
209 | LogsInterface $logs,
210 | ): RoutesFactory {
211 | return new RoutesFactory(
212 | configuration: $configuration,
213 | registry: $registry,
214 | subscriptionManager: $subscriptionManager,
215 | toolExecutor: $toolExecutor,
216 | pagination: $pagination,
217 | logger: $logs->getLogger('mcp'),
218 | );
219 | }
220 |
221 | private function createDispatcher(
222 | LogsInterface $logs,
223 | RoutesFactory $routesFactory,
224 | ): Dispatcher {
225 | return new Dispatcher(
226 | logger: $logs->getLogger('mcp'),
227 | routesFactory: $routesFactory,
228 | );
229 | }
230 |
231 | private function createProtocol(
232 | FactoryInterface $factory,
233 | LogsInterface $logs,
234 | ): Protocol {
235 | return $factory->make(Protocol::class, [
236 | 'logger' => $logs->getLogger('mcp'),
237 | ]);
238 | }
239 |
240 | private function createHttpServer(
241 | EnvironmentInterface $env,
242 | LoopInterface $loop,
243 | MiddlewareRepositoryInterface $middleware,
244 | LogsInterface $logs,
245 | ): HttpServerInterface {
246 | $host = $env->get('MCP_HOST', '127.0.0.1');
247 | $port = (int) $env->get('MCP_PORT', 8090);
248 | $mcpPath = $env->get('MCP_PATH', '/mcp');
249 |
250 | return new HttpServer(
251 | loop: $loop,
252 | host: $host,
253 | port: $port,
254 | mcpPath: $mcpPath,
255 | sslContext: $env->get('MCP_SSL_CONTEXT'),
256 | middleware: $middleware->all(),
257 | logger: $logs->getLogger('mcp'),
258 | runLoop: true,
259 | );
260 | }
261 |
262 | private function createTransport(
263 | ContainerInterface $container,
264 | EnvironmentInterface $env,
265 | LoopInterface $loop,
266 | SessionIdGeneratorInterface $sessionIdGenerator,
267 | LogsInterface $logs,
268 | ): ServerTransportInterface {
269 | $transportType = $env->get('MCP_TRANSPORT', 'stdio');
270 |
271 | return match ($transportType) {
272 | 'http' => $this->createHttpTransport(
273 | httpServer: $container->get(HttpServerInterface::class),
274 | sessionIdGenerator: $sessionIdGenerator,
275 | logs: $logs,
276 | ),
277 | 'streamable' => $this->createStreamableHttpTransport(
278 | httpServer: $container->get(HttpServerInterface::class),
279 | env: $env,
280 | sessionIdGenerator: $sessionIdGenerator,
281 | logs: $logs,
282 | ),
283 | 'stdio' => new StdioServerTransport(
284 | loop: $loop,
285 | logger: $logs->getLogger('mcp'),
286 | ),
287 | default => throw new \InvalidArgumentException("Unknown transport type: {$transportType}")
288 | };
289 | }
290 |
291 | private function createHttpTransport(
292 | HttpServerInterface $httpServer,
293 | SessionIdGeneratorInterface $sessionIdGenerator,
294 | LogsInterface $logs,
295 | ): HttpServerTransport {
296 | return new HttpServerTransport(
297 | httpServer: $httpServer,
298 | sessionId: $sessionIdGenerator,
299 | logger: $logs->getLogger('mcp'),
300 | );
301 | }
302 |
303 | private function createStreamableHttpTransport(
304 | HttpServerInterface $httpServer,
305 | EnvironmentInterface $env,
306 | SessionIdGeneratorInterface $sessionIdGenerator,
307 | LogsInterface $logs,
308 | ): StreamableHttpServerTransport {
309 | return new StreamableHttpServerTransport(
310 | httpServer: $httpServer,
311 | sessionId: $sessionIdGenerator,
312 | logger: $logs->getLogger('mcp'),
313 | enableJsonResponse: (bool) $env->get('MCP_ENABLE_JSON_RESPONSE', true),
314 | stateless: (bool) $env->get('MCP_STATELESS', false),
315 | );
316 | }
317 |
318 | private function createServer(
319 | Protocol $protocol,
320 | SessionManager $sessionManager,
321 | LogsInterface $logs,
322 | ): Server {
323 | return new Server(
324 | protocol: $protocol,
325 | sessionManager: $sessionManager,
326 | logger: $logs->getLogger('mcp'),
327 | );
328 | }
329 |
330 | public function init(
331 | AbstractKernel $kernel,
332 | TokenizerListenerRegistryInterface $tokenizerRegistry,
333 | ToolsLocator $toolsLocator,
334 | ConsoleBootloader $console,
335 | ): void {
336 | $tokenizerRegistry->addListener($toolsLocator);
337 |
338 | $kernel->addDispatcher(McpDispatcher::class);
339 | $console->addCommand(McpCommand::class);
340 | }
341 | }
342 |
--------------------------------------------------------------------------------