php-framework/docs/packages/mcp/tools.md

12 KiB

Creating MCP Tools

Learn how to create custom MCP tools for AI agents with parameter validation, dependency management, and workspace context.

Tool Structure

Every MCP tool extends BaseTool:

<?php

namespace Mod\Blog\Tools;

use Core\Mcp\Tools\BaseTool;

class ListPostsTool extends BaseTool
{
    public function getName(): string
    {
        return 'blog:list-posts';
    }

    public function getDescription(): string
    {
        return 'List all blog posts with optional filters';
    }

    public function getParameters(): array
    {
        return [
            'status' => [
                'type' => 'string',
                'description' => 'Filter by status',
                'enum' => ['published', 'draft', 'archived'],
                'required' => false,
            ],
            'limit' => [
                'type' => 'integer',
                'description' => 'Number of posts to return',
                'default' => 10,
                'min' => 1,
                'max' => 100,
                'required' => false,
            ],
        ];
    }

    public function execute(array $params): array
    {
        $query = Post::query();

        if (isset($params['status'])) {
            $query->where('status', $params['status']);
        }

        $posts = $query->limit($params['limit'] ?? 10)->get();

        return [
            'success' => true,
            'posts' => $posts->map(fn ($post) => [
                'id' => $post->id,
                'title' => $post->title,
                'slug' => $post->slug,
                'status' => $post->status,
                'created_at' => $post->created_at->toIso8601String(),
            ])->toArray(),
            'count' => $posts->count(),
        ];
    }
}

Registering Tools

Register tools in your module's Boot.php:

<?php

namespace Mod\Blog;

use Core\Events\McpToolsRegistering;
use Mod\Blog\Tools\ListPostsTool;
use Mod\Blog\Tools\CreatePostTool;

class Boot
{
    public static array $listens = [
        McpToolsRegistering::class => 'onMcpTools',
    ];

    public function onMcpTools(McpToolsRegistering $event): void
    {
        $event->tool('blog:list-posts', ListPostsTool::class);
        $event->tool('blog:create-post', CreatePostTool::class);
        $event->tool('blog:get-post', GetPostTool::class);
    }
}

Parameter Validation

Parameter Types

public function getParameters(): array
{
    return [
        // String
        'title' => [
            'type' => 'string',
            'description' => 'Post title',
            'minLength' => 1,
            'maxLength' => 255,
            'required' => true,
        ],

        // Integer
        'views' => [
            'type' => 'integer',
            'description' => 'Number of views',
            'min' => 0,
            'max' => 1000000,
            'required' => false,
        ],

        // Boolean
        'published' => [
            'type' => 'boolean',
            'description' => 'Is published',
            'required' => false,
        ],

        // Enum
        'status' => [
            'type' => 'string',
            'enum' => ['draft', 'published', 'archived'],
            'description' => 'Post status',
            'required' => true,
        ],

        // Array
        'tags' => [
            'type' => 'array',
            'description' => 'Post tags',
            'items' => ['type' => 'string'],
            'required' => false,
        ],

        // Object
        'metadata' => [
            'type' => 'object',
            'description' => 'Additional metadata',
            'properties' => [
                'featured' => ['type' => 'boolean'],
                'views' => ['type' => 'integer'],
            ],
            'required' => false,
        ],
    ];
}

Default Values

'limit' => [
    'type' => 'integer',
    'default' => 10,  // Used if not provided
    'required' => false,
]

Custom Validation

public function execute(array $params): array
{
    // Additional validation
    if (isset($params['email']) && !filter_var($params['email'], FILTER_VALIDATE_EMAIL)) {
        return [
            'success' => false,
            'error' => 'Invalid email address',
            'code' => 'INVALID_EMAIL',
        ];
    }

    // Tool logic...
}

Workspace Context

Requiring Workspace

Use the RequiresWorkspaceContext trait:

<?php

namespace Mod\Blog\Tools;

use Core\Mcp\Tools\BaseTool;
use Core\Mcp\Tools\Concerns\RequiresWorkspaceContext;

class CreatePostTool extends BaseTool
{
    use RequiresWorkspaceContext;

    public function execute(array $params): array
    {
        // Workspace automatically validated and available
        $workspace = $this->getWorkspaceContext();

        $post = Post::create([
            'title' => $params['title'],
            'content' => $params['content'],
            'workspace_id' => $workspace->id,
        ]);

        return [
            'success' => true,
            'post_id' => $post->id,
        ];
    }
}

Optional Workspace

public function execute(array $params): array
{
    $workspace = $this->getWorkspaceContext(); // May be null

    $query = Post::query();

    if ($workspace) {
        $query->where('workspace_id', $workspace->id);
    }

    return ['posts' => $query->get()];
}

Tool Dependencies

Declaring Dependencies

<?php

namespace Mod\Blog\Tools;

use Core\Mcp\Tools\BaseTool;
use Core\Mcp\Dependencies\HasDependencies;
use Core\Mcp\Dependencies\ToolDependency;
use Core\Mcp\Dependencies\DependencyType;

class ImportPostsTool extends BaseTool
{
    use HasDependencies;

    public function getDependencies(): array
    {
        return [
            // Required dependency
            new ToolDependency(
                'blog:list-posts',
                DependencyType::REQUIRED
            ),

            // Optional dependency
            new ToolDependency(
                'media:upload',
                DependencyType::OPTIONAL
            ),
        ];
    }

    public function execute(array $params): array
    {
        // Dependencies automatically validated before execution
        // ...
    }
}

Dependency Types

  • DependencyType::REQUIRED - Tool cannot execute without this
  • DependencyType::OPTIONAL - Tool works better with this but not required

Error Handling

Standard Error Format

public function execute(array $params): array
{
    try {
        // Tool logic...

        return [
            'success' => true,
            'data' => $result,
        ];

    } catch (\Exception $e) {
        return [
            'success' => false,
            'error' => $e->getMessage(),
            'code' => 'TOOL_EXECUTION_FAILED',
        ];
    }
}

Specific Error Codes

// Validation error
return [
    'success' => false,
    'error' => 'Title is required',
    'code' => 'VALIDATION_ERROR',
    'field' => 'title',
];

// Not found
return [
    'success' => false,
    'error' => 'Post not found',
    'code' => 'NOT_FOUND',
    'resource_id' => $params['id'],
];

// Forbidden
return [
    'success' => false,
    'error' => 'Insufficient permissions',
    'code' => 'FORBIDDEN',
    'required_permission' => 'posts.create',
];

Advanced Patterns

Tool with File Processing

public function execute(array $params): array
{
    $csvPath = $params['csv_path'];

    if (!file_exists($csvPath)) {
        return [
            'success' => false,
            'error' => 'CSV file not found',
            'code' => 'FILE_NOT_FOUND',
        ];
    }

    $imported = 0;
    $errors = [];

    if (($handle = fopen($csvPath, 'r')) !== false) {
        while (($data = fgetcsv($handle)) !== false) {
            try {
                Post::create([
                    'title' => $data[0],
                    'content' => $data[1],
                ]);
                $imported++;
            } catch (\Exception $e) {
                $errors[] = "Row {$imported}: {$e->getMessage()}";
            }
        }
        fclose($handle);
    }

    return [
        'success' => true,
        'imported' => $imported,
        'errors' => $errors,
    ];
}

Tool with Pagination

public function execute(array $params): array
{
    $page = $params['page'] ?? 1;
    $perPage = $params['per_page'] ?? 15;

    $posts = Post::paginate($perPage, ['*'], 'page', $page);

    return [
        'success' => true,
        'posts' => $posts->items(),
        'pagination' => [
            'current_page' => $posts->currentPage(),
            'last_page' => $posts->lastPage(),
            'per_page' => $posts->perPage(),
            'total' => $posts->total(),
        ],
    ];
}

Tool with Progress Tracking

public function execute(array $params): array
{
    $postIds = $params['post_ids'];
    $total = count($postIds);
    $processed = 0;

    foreach ($postIds as $postId) {
        $post = Post::find($postId);

        if ($post) {
            $post->publish();
            $processed++;

            // Emit progress event
            event(new ToolProgress(
                tool: $this->getName(),
                progress: ($processed / $total) * 100,
                message: "Published post {$postId}"
            ));
        }
    }

    return [
        'success' => true,
        'processed' => $processed,
        'total' => $total,
    ];
}

Testing Tools

<?php

namespace Tests\Feature\Mcp;

use Tests\TestCase;
use Mod\Blog\Tools\ListPostsTool;
use Mod\Blog\Models\Post;

class ListPostsToolTest extends TestCase
{
    public function test_lists_all_posts(): void
    {
        Post::factory()->count(5)->create();

        $tool = new ListPostsTool();

        $result = $tool->execute([]);

        $this->assertTrue($result['success']);
        $this->assertCount(5, $result['posts']);
    }

    public function test_filters_by_status(): void
    {
        Post::factory()->count(3)->create(['status' => 'published']);
        Post::factory()->count(2)->create(['status' => 'draft']);

        $tool = new ListPostsTool();

        $result = $tool->execute([
            'status' => 'published',
        ]);

        $this->assertCount(3, $result['posts']);
    }

    public function test_respects_limit(): void
    {
        Post::factory()->count(20)->create();

        $tool = new ListPostsTool();

        $result = $tool->execute([
            'limit' => 5,
        ]);

        $this->assertCount(5, $result['posts']);
    }
}

Best Practices

1. Clear Naming

// ✅ Good - descriptive name
'blog:create-post'
'blog:list-published-posts'
'blog:delete-post'

// ❌ Bad - vague name
'blog:action'
'do-thing'

2. Detailed Descriptions

// ✅ Good - explains what and why
public function getDescription(): string
{
    return 'Create a new blog post with title, content, and optional metadata. '
         . 'Requires workspace context. Validates entitlements before creation.';
}

// ❌ Bad - too brief
public function getDescription(): string
{
    return 'Creates post';
}

3. Validate Parameters

// ✅ Good - strict validation
public function getParameters(): array
{
    return [
        'title' => [
            'type' => 'string',
            'required' => true,
            'minLength' => 1,
            'maxLength' => 255,
        ],
    ];
}

4. Return Consistent Format

// ✅ Good - always includes success
return [
    'success' => true,
    'data' => $result,
];

return [
    'success' => false,
    'error' => $message,
    'code' => $code,
];

Learn More