├── .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 | --------------------------------------------------------------------------------