From 629c38be815502bf0ac4019fccdd1123d2059de3 Mon Sep 17 00:00:00 2001 From: CodeWithKyrian <48791154+CodeWithKyrian@users.noreply.github.com> Date: Thu, 26 Jun 2025 00:36:32 +0000 Subject: [PATCH 1/6] Update CHANGELOG --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f9614..c0e28f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to `php-mcp/laravel` will be documented in this file. +## v3.0.0 - 2025-06-26 + +### Major Changes + +- **Upgraded to php-mcp/server ^3.0** with latest MCP protocol support(`2025-03-26`) +- **Laravel-native session management** with multiple storage backends (file, database, cache, redis) +- **Streamable HTTP transport** with resumability and better connection handling +- **Simplified configuration** structure for capabilities and session management + +### Improvements + +- Enhanced transport layer with better error handling +- Automatic session garbage collection +- Updated documentation with migration guide + +### Fixes + +- Fixed SSE stream handling for expired sessions +- Improved transport initialization +- Better memory management for long-running servers + +### Breaking Changes + +- Requires `php-mcp/server ^3.0` +- Updated configuration structure (see migration guide in `README`) +- Transport class names changed for consistency + +### Installation + +```bash +composer require php-mcp/laravel:^3.0 +php artisan vendor:publish --provider="PhpMcp\Laravel\McpServiceProvider" + +``` +**Full Changelog**: https://github.com/php-mcp/laravel/compare/2.1.1...3.0.0 + ## v2.1.1 - 2025-06-25 ### What's Changed @@ -109,6 +145,7 @@ This release marks a **major overhaul**, bringing it into full alignment with `p + ``` * **`mcp:serve` for HTTP:** The `--transport=http` option for `mcp:serve` now launches a *dedicated* ReactPHP-based server process. For serving MCP via your main Laravel application routes, ensure the `http_integrated` transport is enabled in `config/mcp.php` and your web server is configured appropriately. * **Event Handling:** If you were directly listening to internal events from the previous version, these may have changed. Rely on the documented Laravel events (`ToolsListChanged`, etc.). @@ -174,6 +211,7 @@ php artisan vendor:publish --provider="PhpMcp\Laravel\Server\McpServiceProvider" + ``` ## Getting Started From f76a6f67747a90b3ba2e07292e86d3b2f2dc02ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Turan=20Karatu=C4=9F?= Date: Tue, 1 Jul 2025 15:33:21 +0300 Subject: [PATCH 2/6] chore: Update PHPUnit version constraint in composer.json to support version 12.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b109835..0dbf8fd 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "orchestra/testbench": "^8.0 || ^9.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.0", - "phpunit/phpunit": "^10.0 || ^11.0" + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0" }, "autoload": { "psr-4": { From 6f9ce34cc16886254b064d77e262acbe3f4f7d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Turan=20Karatu=C4=9F?= Date: Tue, 1 Jul 2025 15:33:32 +0300 Subject: [PATCH 3/6] refactor: Update handler assertions in ManualRegistrationTest - Changed assertions in ManualRegistrationTest to verify handler as an array containing the handler class and method instead of separate properties. - Updated tests to reflect the new structure for handler registration, ensuring consistency across tool, resource, and prompt registrations. --- tests/Feature/ManualRegistrationTest.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/Feature/ManualRegistrationTest.php b/tests/Feature/ManualRegistrationTest.php index 423d756..72bb15d 100644 --- a/tests/Feature/ManualRegistrationTest.php +++ b/tests/Feature/ManualRegistrationTest.php @@ -31,8 +31,7 @@ public function test_can_manually_register_a_tool() $this->assertInstanceOf(RegisteredTool::class, $tool); $this->assertEquals('manual_test_tool', $tool->schema->name); $this->assertEquals('A manually registered test tool.', $tool->schema->description); - $this->assertEquals(ManualTestHandler::class, $tool->handlerClass); - $this->assertEquals('handleTool', $tool->handlerMethod); + $this->assertEquals([ManualTestHandler::class, 'handleTool'], $tool->handler); $this->assertArrayHasKey('input', $tool->schema->inputSchema['properties']); $this->assertEquals('string', $tool->schema->inputSchema['properties']['input']['type']); } @@ -52,8 +51,7 @@ public function test_can_manually_register_tool_using_handler_only() $tool = $registry->getTool('handleTool'); $this->assertNotNull($tool); - $this->assertEquals(ManualTestHandler::class, $tool->handlerClass); - $this->assertEquals('handleTool', $tool->handlerMethod); + $this->assertEquals([ManualTestHandler::class, 'handleTool'], $tool->handler); $this->assertEquals('A sample tool handler.', $tool->schema->description); } @@ -82,8 +80,7 @@ public function test_can_manually_register_a_resource() $this->assertEquals('application/json', $resource->schema->mimeType); $this->assertEquals(1024, $resource->schema->size); $this->assertEquals(['priority' => 0.8], $resource->schema->annotations->toArray()); - $this->assertEquals(ManualTestHandler::class, $resource->handlerClass); - $this->assertEquals('handleResource', $resource->handlerMethod); + $this->assertEquals([ManualTestHandler::class, 'handleResource'], $resource->handler); } public function test_can_manually_register_a_prompt_with_invokable_class_handler() @@ -104,8 +101,7 @@ public function test_can_manually_register_a_prompt_with_invokable_class_handler $this->assertInstanceOf(RegisteredPrompt::class, $prompt); $this->assertEquals('manual_invokable_prompt', $prompt->schema->name); $this->assertEquals('A prompt handled by an invokable class.', $prompt->schema->description); - $this->assertEquals(ManualTestInvokableHandler::class, $prompt->handlerClass); - $this->assertEquals('__invoke', $prompt->handlerMethod); + $this->assertEquals(ManualTestInvokableHandler::class, $prompt->handler); } public function test_can_manually_register_a_resource_template_via_facade() @@ -130,7 +126,6 @@ public function test_can_manually_register_a_resource_template_via_facade() $this->assertEquals('manual_item_details_template', $template->schema->name); $this->assertEquals('A sample resource template handler.', $template->schema->description); $this->assertEquals('application/vnd.api+json', $template->schema->mimeType); - $this->assertEquals(ManualTestHandler::class, $template->handlerClass); - $this->assertEquals('handleTemplate', $template->handlerMethod); + $this->assertEquals([ManualTestHandler::class, 'handleTemplate'], $template->handler); } } From a6a04f9617f9c148383d7c3384b0a88960c63c9f Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Tue, 1 Jul 2025 15:06:07 +0100 Subject: [PATCH 4/6] feat: add closure handler support and custom input schema for tools - Bump php-mcp/server dependency to ^3.2 - Add support for callable/closure handlers in all MCP element types (tools, resources, resource templates, prompts) - Introduce inputSchema() method for tools to define custom JSON validation schemas Breaking: None - all existing v3.0 code remains compatible --- README.md | 120 ++++++++++++++++++- composer.json | 2 +- src/Blueprints/PromptBlueprint.php | 5 +- src/Blueprints/ResourceBlueprint.php | 6 +- src/Blueprints/ResourceTemplateBlueprint.php | 6 +- src/Blueprints/ToolBlueprint.php | 14 ++- src/Facades/Mcp.php | 8 +- src/McpRegistrar.php | 18 +-- tests/Feature/ManualRegistrationTest.php | 102 ++++++++++++++++ 9 files changed, 262 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 85ec5d4..50353ad 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,21 @@ Mcp::tool([CalculatorService::class, 'add']) Mcp::tool(EmailService::class) ->description('Send emails to users'); +// Register a closure as a tool with custom input schema +Mcp::tool(function(float $x, float $y): float { + return $x * $y; +}) + ->name('multiply') + ->description('Multiply two numbers') + ->inputSchema([ + 'type' => 'object', + 'properties' => [ + 'x' => ['type' => 'number', 'description' => 'First number'], + 'y' => ['type' => 'number', 'description' => 'Second number'], + ], + 'required' => ['x', 'y'], + ]); + // Register a resource with metadata Mcp::resource('config://app/settings', [UserService::class, 'getAppSettings']) ->name('app_settings') @@ -102,16 +117,47 @@ Mcp::resource('config://app/settings', [UserService::class, 'getAppSettings']) ->mimeType('application/json') ->size(1024); +// Register a closure as a resource +Mcp::resource('system://time', function(): string { + return now()->toISOString(); +}) + ->name('current_time') + ->description('Get current server time') + ->mimeType('text/plain'); + // Register a resource template for dynamic content Mcp::resourceTemplate('user://{userId}/profile', [UserService::class, 'getUserProfile']) ->name('user_profile') ->description('Get user profile by ID') ->mimeType('application/json'); +// Register a closure as a resource template +Mcp::resourceTemplate('file://{path}', function(string $path): string { + if (!file_exists($path) || !is_readable($path)) { + throw new \InvalidArgumentException("File not found or not readable: {$path}"); + } + return file_get_contents($path); +}) + ->name('file_reader') + ->description('Read file contents by path') + ->mimeType('text/plain'); + // Register a prompt generator Mcp::prompt([PromptService::class, 'generateWelcome']) ->name('welcome_user') ->description('Generate a personalized welcome message'); + +// Register a closure as a prompt +Mcp::prompt(function(string $topic, string $tone = 'professional'): array { + return [ + [ + 'role' => 'user', + 'content' => "Write a {$tone} summary about {$topic}. Make it informative and engaging." + ] + ]; +}) + ->name('topic_summary') + ->description('Generate topic summary prompts'); ``` **Available Fluent Methods:** @@ -120,14 +166,23 @@ Mcp::prompt([PromptService::class, 'generateWelcome']) - `name(string $name)`: Override the inferred name - `description(string $description)`: Set a custom description +**For Tools:** +- `annotations(ToolAnnotations $annotations)`: Add MCP tool annotations +- `inputSchema(array $schema)`: Define custom JSON schema for parameters + **For Resources:** - `mimeType(string $mimeType)`: Specify content type - `size(int $size)`: Set content size in bytes -- `annotations(array|Annotations $annotations)`: Add MCP annotations +- `annotations(Annotations $annotations)`: Add MCP annotations + +**For Resource Templates:** +- `mimeType(string $mimeType)`: Specify content type +- `annotations(Annotations $annotations)`: Add MCP annotations **Handler Formats:** - `[ClassName::class, 'methodName']` - Class method - `InvokableClass::class` - Invokable class with `__invoke()` method +- `function(...) { ... }` - Callables (v3.2+) ### 2. Attribute-Based Discovery @@ -717,6 +772,69 @@ Create a dedicated log channel in `config/logging.php`: ## Migration Guide +### From v3.0 to v3.1 + +**New Handler Types:** + +Laravel MCP v3.1 introduces support for closure handlers, expanding beyond just class methods and invokable classes: + +```php +// v3.0 and earlier - Class-based handlers only +Mcp::tool([CalculatorService::class, 'add']) + ->name('add_numbers'); + +Mcp::tool(EmailService::class) // Invokable class + ->name('send_email'); + +// v3.1+ - Now supports closures +Mcp::tool(function(float $x, float $y): float { + return $x * $y; +}) + ->name('multiply') + ->description('Multiply two numbers'); + +Mcp::resource('system://time', function(): string { + return now()->toISOString(); +}) + ->name('current_time'); +``` + +**Input Schema Support:** + +Tools can now define custom JSON schemas for parameter validation: + +```php +// v3.1+ - Custom input schema +Mcp::tool([CalculatorService::class, 'calculate']) + ->inputSchema([ + 'type' => 'object', + 'properties' => [ + 'operation' => [ + 'type' => 'string', + 'enum' => ['add', 'subtract', 'multiply', 'divide'] + ], + 'numbers' => [ + 'type' => 'array', + 'items' => ['type' => 'number'], + 'minItems' => 2 + ] + ], + 'required' => ['operation', 'numbers'] + ]); +``` + +**Enhanced Blueprint Methods:** + +New fluent methods available on blueprints: + +```php +->inputSchema(array $schema) // Define custom parameter schema +``` + +**No Breaking Changes:** + +All existing v3.0 code continues to work without modification. The new features are additive enhancements. + ### From v2.x to v3.x **Configuration Changes:** diff --git a/composer.json b/composer.json index 0dbf8fd..ef52894 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require": { "php": "^8.1", "laravel/framework": "^9.46 || ^10.34 || ^11.29 || ^12.0", - "php-mcp/server": "^3.1" + "php-mcp/server": "^3.2" }, "require-dev": { "laravel/pint": "^1.13", diff --git a/src/Blueprints/PromptBlueprint.php b/src/Blueprints/PromptBlueprint.php index d097a09..06b91ee 100644 --- a/src/Blueprints/PromptBlueprint.php +++ b/src/Blueprints/PromptBlueprint.php @@ -8,8 +8,11 @@ class PromptBlueprint { public ?string $description = null; + /** + * @param string|array|callable $handler + */ public function __construct( - public array|string $handler, + public mixed $handler, public ?string $name = null ) {} diff --git a/src/Blueprints/ResourceBlueprint.php b/src/Blueprints/ResourceBlueprint.php index 3501310..0e98b9e 100644 --- a/src/Blueprints/ResourceBlueprint.php +++ b/src/Blueprints/ResourceBlueprint.php @@ -4,6 +4,7 @@ namespace PhpMcp\Laravel\Blueprints; +use Closure; use PhpMcp\Schema\Annotations; class ResourceBlueprint @@ -18,9 +19,12 @@ class ResourceBlueprint public ?Annotations $annotations = null; + /** + * @param string|array|callable $handler + */ public function __construct( public string $uri, - public array|string $handler, + public mixed $handler, ) {} public function name(string $name): static diff --git a/src/Blueprints/ResourceTemplateBlueprint.php b/src/Blueprints/ResourceTemplateBlueprint.php index 14c53dd..6a50c39 100644 --- a/src/Blueprints/ResourceTemplateBlueprint.php +++ b/src/Blueprints/ResourceTemplateBlueprint.php @@ -4,6 +4,7 @@ namespace PhpMcp\Laravel\Blueprints; +use Closure; use PhpMcp\Schema\Annotations; class ResourceTemplateBlueprint @@ -16,9 +17,12 @@ class ResourceTemplateBlueprint public ?Annotations $annotations = null; + /** + * @param string|array|callable $handler + */ public function __construct( public string $uriTemplate, - public array|string $handler, + public mixed $handler, ) {} public function name(string $name): static diff --git a/src/Blueprints/ToolBlueprint.php b/src/Blueprints/ToolBlueprint.php index 4d7b26d..56b8223 100644 --- a/src/Blueprints/ToolBlueprint.php +++ b/src/Blueprints/ToolBlueprint.php @@ -4,15 +4,20 @@ namespace PhpMcp\Laravel\Blueprints; +use Closure; use PhpMcp\Schema\ToolAnnotations; class ToolBlueprint { public ?string $description = null; public ?ToolAnnotations $annotations = null; + public ?array $inputSchema = null; + /** + * @param string|array|callable $handler + */ public function __construct( - public array|string $handler, + public mixed $handler, public ?string $name = null ) {} @@ -36,4 +41,11 @@ public function annotations(ToolAnnotations $annotations): static return $this; } + + public function inputSchema(array $inputSchema): static + { + $this->inputSchema = $inputSchema; + + return $this; + } } diff --git a/src/Facades/Mcp.php b/src/Facades/Mcp.php index cd571ed..ee8e509 100644 --- a/src/Facades/Mcp.php +++ b/src/Facades/Mcp.php @@ -11,10 +11,10 @@ use PhpMcp\Laravel\Blueprints\ToolBlueprint; /** - * @method static ToolBlueprint tool(string|array $handlerOrName, array|string|null $handler = null) - * @method static ResourceBlueprint resource(string $uri, array|string $handler) - * @method static ResourceTemplateBlueprint resourceTemplate(string $uriTemplate, array|string $handler) - * @method static PromptBlueprint prompt(string|array $handlerOrName, array|string|null $handler = null) + * @method static ToolBlueprint tool(string|callable|array $handlerOrName, callable|array|string|null $handler = null) + * @method static ResourceBlueprint resource(string $uri, callable|array|string $handler) + * @method static ResourceTemplateBlueprint resourceTemplate(string $uriTemplate, callable|array|string $handler) + * @method static PromptBlueprint prompt(string|callable|array $handlerOrName, callable|array|string|null $handler = null) * * @see \PhpMcp\Laravel\McpRegistrar */ diff --git a/src/McpRegistrar.php b/src/McpRegistrar.php index 0935e0f..7fff3fa 100644 --- a/src/McpRegistrar.php +++ b/src/McpRegistrar.php @@ -34,14 +34,14 @@ public function __construct() {} * Mcp::tool('tool_name', $handler) * Mcp::tool($handler) // Name will be inferred */ - public function tool(string|array ...$args): ToolBlueprint + public function tool(string|callable|array ...$args): ToolBlueprint { $name = null; $handler = null; - if (count($args) === 1 && (is_array($args[0]) || (is_string($args[0]) && (class_exists($args[0]) || is_callable($args[0]))))) { + if (count($args) === 1 && (is_callable($args[0]) || is_array($args[0]) || (is_string($args[0]) && (class_exists($args[0]) || is_callable($args[0]))))) { $handler = $args[0]; - } elseif (count($args) === 2 && is_string($args[0]) && (is_array($args[1]) || (is_string($args[1]) && (class_exists($args[1]) || is_callable($args[1]))))) { + } elseif (count($args) === 2 && is_string($args[0]) && (is_callable($args[1]) || is_array($args[1]) || (is_string($args[1]) && (class_exists($args[1]) || is_callable($args[1]))))) { $name = $args[0]; $handler = $args[1]; } else { @@ -57,7 +57,7 @@ public function tool(string|array ...$args): ToolBlueprint /** * Register a new resource. */ - public function resource(string $uri, array|string $handler): ResourceBlueprint + public function resource(string $uri, callable|array|string $handler): ResourceBlueprint { $pendingResource = new ResourceBlueprint($uri, $handler); $this->pendingResources[] = $pendingResource; @@ -68,7 +68,7 @@ public function resource(string $uri, array|string $handler): ResourceBlueprint /** * Register a new resource template. */ - public function resourceTemplate(string $uriTemplate, array|string $handler): ResourceTemplateBlueprint + public function resourceTemplate(string $uriTemplate, callable|array|string $handler): ResourceTemplateBlueprint { $pendingResourceTemplate = new ResourceTemplateBlueprint($uriTemplate, $handler); $this->pendingResourceTemplates[] = $pendingResourceTemplate; @@ -83,14 +83,14 @@ public function resourceTemplate(string $uriTemplate, array|string $handler): Re * Mcp::prompt('prompt_name', $handler) * Mcp::prompt($handler) // Name will be inferred */ - public function prompt(string|array ...$args): PromptBlueprint + public function prompt(string|callable|array ...$args): PromptBlueprint { $name = null; $handler = null; - if (count($args) === 1 && (is_array($args[0]) || (is_string($args[0]) && (class_exists($args[0]) || is_callable($args[0]))))) { + if (count($args) === 1 && (is_callable($args[0]) || is_array($args[0]) || (is_string($args[0]) && (class_exists($args[0]) || is_callable($args[0]))))) { $handler = $args[0]; - } elseif (count($args) === 2 && is_string($args[0]) && (is_array($args[1]) || (is_string($args[1]) && (class_exists($args[1]) || is_callable($args[1]))))) { + } elseif (count($args) === 2 && is_string($args[0]) && (is_callable($args[1]) || is_array($args[1]) || (is_string($args[1]) && (class_exists($args[1]) || is_callable($args[1]))))) { $name = $args[0]; $handler = $args[1]; } else { @@ -106,7 +106,7 @@ public function prompt(string|array ...$args): PromptBlueprint public function applyBlueprints(ServerBuilder $builder): void { foreach ($this->pendingTools as $pendingTool) { - $builder->withTool($pendingTool->handler, $pendingTool->name, $pendingTool->description, $pendingTool->annotations); + $builder->withTool($pendingTool->handler, $pendingTool->name, $pendingTool->description, $pendingTool->annotations, $pendingTool->inputSchema); } foreach ($this->pendingResources as $pendingResource) { diff --git a/tests/Feature/ManualRegistrationTest.php b/tests/Feature/ManualRegistrationTest.php index 72bb15d..7f569bb 100644 --- a/tests/Feature/ManualRegistrationTest.php +++ b/tests/Feature/ManualRegistrationTest.php @@ -32,6 +32,7 @@ public function test_can_manually_register_a_tool() $this->assertEquals('manual_test_tool', $tool->schema->name); $this->assertEquals('A manually registered test tool.', $tool->schema->description); $this->assertEquals([ManualTestHandler::class, 'handleTool'], $tool->handler); + $this->assertTrue($tool->isManual); $this->assertArrayHasKey('input', $tool->schema->inputSchema['properties']); $this->assertEquals('string', $tool->schema->inputSchema['properties']['input']['type']); } @@ -52,6 +53,7 @@ public function test_can_manually_register_tool_using_handler_only() $this->assertNotNull($tool); $this->assertEquals([ManualTestHandler::class, 'handleTool'], $tool->handler); + $this->assertTrue($tool->isManual); $this->assertEquals('A sample tool handler.', $tool->schema->description); } @@ -81,6 +83,7 @@ public function test_can_manually_register_a_resource() $this->assertEquals(1024, $resource->schema->size); $this->assertEquals(['priority' => 0.8], $resource->schema->annotations->toArray()); $this->assertEquals([ManualTestHandler::class, 'handleResource'], $resource->handler); + $this->assertTrue($resource->isManual); } public function test_can_manually_register_a_prompt_with_invokable_class_handler() @@ -102,6 +105,7 @@ public function test_can_manually_register_a_prompt_with_invokable_class_handler $this->assertEquals('manual_invokable_prompt', $prompt->schema->name); $this->assertEquals('A prompt handled by an invokable class.', $prompt->schema->description); $this->assertEquals(ManualTestInvokableHandler::class, $prompt->handler); + $this->assertTrue($prompt->isManual); } public function test_can_manually_register_a_resource_template_via_facade() @@ -127,5 +131,103 @@ public function test_can_manually_register_a_resource_template_via_facade() $this->assertEquals('A sample resource template handler.', $template->schema->description); $this->assertEquals('application/vnd.api+json', $template->schema->mimeType); $this->assertEquals([ManualTestHandler::class, 'handleTemplate'], $template->handler); + $this->assertTrue($template->isManual); + } + + public function test_can_manually_register_closure_handlers_and_custom_input_schema() + { + $definitionsContent = <<<'PHP' + name('multiply') + ->description('Multiply two numbers') + ->inputSchema([ + 'type' => 'object', + 'properties' => [ + 'x' => ['type' => 'number', 'description' => 'First number'], + 'y' => ['type' => 'number', 'description' => 'Second number'], + ], + 'required' => ['x', 'y'], + ]); + + // Test closure resource + Mcp::resource('system://time', function(): string { + return now()->toISOString(); + }) + ->name('current_time') + ->description('Get current server time') + ->mimeType('text/plain'); + + // Test closure resource template + Mcp::resourceTemplate('calculation://{operation}', function(string $operation): string { + return "Result of {$operation}"; + }) + ->name('calculator') + ->description('Perform calculations') + ->mimeType('text/plain'); + + // Test closure prompt + Mcp::prompt(function(string $topic): array { + return [ + [ + 'role' => 'user', + 'content' => "Write about {$topic}", + ] + ]; + }) + ->name('write_about') + ->description('Generate writing prompts'); + PHP; + $this->setMcpDefinitions($definitionsContent); + + $registry = $this->app->make('mcp.registry'); + + // Test closure tool + $tool = $registry->getTool('multiply'); + $this->assertInstanceOf(RegisteredTool::class, $tool); + $this->assertEquals('multiply', $tool->schema->name); + $this->assertEquals('Multiply two numbers', $tool->schema->description); + $this->assertInstanceOf(\Closure::class, $tool->handler); + $this->assertTrue($tool->isManual); + + // Test custom input schema + $schema = $tool->schema->inputSchema; + $this->assertEquals('object', $schema['type']); + $this->assertArrayHasKey('x', $schema['properties']); + $this->assertArrayHasKey('y', $schema['properties']); + $this->assertEquals('number', $schema['properties']['x']['type']); + $this->assertEquals('number', $schema['properties']['y']['type']); + $this->assertEquals(['x', 'y'], $schema['required']); + + // Test closure resource + $resource = $registry->getResource('system://time'); + $this->assertInstanceOf(RegisteredResource::class, $resource); + $this->assertEquals('current_time', $resource->schema->name); + $this->assertEquals('Get current server time', $resource->schema->description); + $this->assertEquals('text/plain', $resource->schema->mimeType); + $this->assertInstanceOf(\Closure::class, $resource->handler); + $this->assertTrue($resource->isManual); + + // Test closure resource template + $template = $registry->getResource('calculation://add'); + $this->assertInstanceOf(RegisteredResourceTemplate::class, $template); + $this->assertEquals('calculator', $template->schema->name); + $this->assertEquals('Perform calculations', $template->schema->description); + $this->assertEquals('text/plain', $template->schema->mimeType); + $this->assertInstanceOf(\Closure::class, $template->handler); + $this->assertTrue($template->isManual); + + // Test closure prompt + $prompt = $registry->getPrompt('write_about'); + $this->assertInstanceOf(RegisteredPrompt::class, $prompt); + $this->assertEquals('write_about', $prompt->schema->name); + $this->assertEquals('Generate writing prompts', $prompt->schema->description); + $this->assertInstanceOf(\Closure::class, $prompt->handler); + $this->assertTrue($prompt->isManual); } } From 941f767dd3d46e3ef4949a9304c631bffe128fdc Mon Sep 17 00:00:00 2001 From: WSL PC Sergio Date: Wed, 9 Jul 2025 10:49:31 +0200 Subject: [PATCH 5/6] Fix: correct argument order in response()->stream for legacy SSE mode --- src/Transports/HttpServerTransport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports/HttpServerTransport.php b/src/Transports/HttpServerTransport.php index 35d3323..f51db35 100644 --- a/src/Transports/HttpServerTransport.php +++ b/src/Transports/HttpServerTransport.php @@ -169,7 +169,7 @@ public function handleSseRequest(Request $request): StreamedResponse } $this->emit('client_disconnected', [$sessionId, 'SSE stream closed']); - }, [ + }, headers: [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', From 6380d27a78f0dff825c731dc240b92db5baebdc5 Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Sun, 13 Jul 2025 01:00:32 +0100 Subject: [PATCH 6/6] fix: redirect ServeCommand output to STDERR for stdio transport Console output methods sometimes write to STDOUT, which interferes with JSON-RPC communication in stdio mode. This redirects startup messages to STDERR when available. --- src/Commands/ServeCommand.php | 13 +++++++++---- tests/Feature/Commands/ServeCommandTest.php | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Commands/ServeCommand.php b/src/Commands/ServeCommand.php index 77e3c35..da1dbab 100644 --- a/src/Commands/ServeCommand.php +++ b/src/Commands/ServeCommand.php @@ -10,6 +10,7 @@ use PhpMcp\Server\Transports\HttpServerTransport; use PhpMcp\Server\Transports\StdioServerTransport; use PhpMcp\Server\Transports\StreamableHttpServerTransport; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use function Laravel\Prompts\select; @@ -80,10 +81,14 @@ private function handleStdioTransport(Server $server): int return Command::FAILURE; } - $this->info('Starting MCP server'); - $this->line(" - Transport: STDIO"); - $this->line(" - Communication: STDIN/STDOUT"); - $this->line(" - Mode: JSON-RPC over Standard I/O"); + $output = $this->output->getOutput(); + + if ($output instanceof ConsoleOutputInterface) { + $output->getErrorOutput()->writeln("Starting MCP server"); + $output->getErrorOutput()->writeln(" - Transport: STDIO"); + $output->getErrorOutput()->writeln(" - Communication: STDIN/STDOUT"); + $output->getErrorOutput()->writeln(" - Mode: JSON-RPC over Standard I/O"); + } try { $transport = new StdioServerTransport; diff --git a/tests/Feature/Commands/ServeCommandTest.php b/tests/Feature/Commands/ServeCommandTest.php index 97ed523..270054f 100644 --- a/tests/Feature/Commands/ServeCommandTest.php +++ b/tests/Feature/Commands/ServeCommandTest.php @@ -45,8 +45,8 @@ public function test_serve_command_defaults_to_stdio_and_calls_server_listen() ); $this->artisan('mcp:serve --transport=stdio') - ->expectsOutputToContain('Starting MCP server') - ->expectsOutputToContain('Transport: STDIO') + ->doesntExpectOutputToContain('Starting MCP server') + ->doesntExpectOutputToContain('Transport: STDIO') ->assertSuccessful(); }