Skip to content

feat: Add Closure Handler Support and Custom Input Schema for Tools #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 119 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,69 @@ 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')
->description('Application configuration settings')
->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:**
Expand All @@ -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

Expand Down Expand Up @@ -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:**
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion src/Blueprints/PromptBlueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}

Expand Down
6 changes: 5 additions & 1 deletion src/Blueprints/ResourceBlueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpMcp\Laravel\Blueprints;

use Closure;
use PhpMcp\Schema\Annotations;

class ResourceBlueprint
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/Blueprints/ResourceTemplateBlueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpMcp\Laravel\Blueprints;

use Closure;
use PhpMcp\Schema\Annotations;

class ResourceTemplateBlueprint
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/Blueprints/ToolBlueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}

Expand All @@ -36,4 +41,11 @@ public function annotations(ToolAnnotations $annotations): static

return $this;
}

public function inputSchema(array $inputSchema): static
{
$this->inputSchema = $inputSchema;

return $this;
}
}
8 changes: 4 additions & 4 deletions src/Facades/Mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
18 changes: 9 additions & 9 deletions src/McpRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
Loading