# 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 [ '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 '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 ```php 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 ```php 'limit' => [ 'type' => 'integer', 'default' => 10, // Used if not provided 'required' => false, ] ``` ### Custom Validation ```php 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 getWorkspaceContext(); $post = Post::create([ 'title' => $params['title'], 'content' => $params['content'], 'workspace_id' => $workspace->id, ]); return [ 'success' => true, 'post_id' => $post->id, ]; } } ``` ### Optional Workspace ```php 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 true, 'data' => $result, ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => 'TOOL_EXECUTION_FAILED', ]; } } ``` ### Specific Error Codes ```php // 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 ```php 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 ```php 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 ```php 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 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 ```php // ✅ Good - descriptive name 'blog:create-post' 'blog:list-published-posts' 'blog:delete-post' // ❌ Bad - vague name 'blog:action' 'do-thing' ``` ### 2. Detailed Descriptions ```php // ✅ 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 ```php // ✅ Good - strict validation public function getParameters(): array { return [ 'title' => [ 'type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 255, ], ]; } ``` ### 4. Return Consistent Format ```php // ✅ Good - always includes success return [ 'success' => true, 'data' => $result, ]; return [ 'success' => false, 'error' => $message, 'code' => $code, ]; ``` ## Learn More - [Query Database →](/packages/mcp/query-database) - [Workspace Context →](/packages/mcp/workspace) - [Tool Analytics →](/packages/mcp/analytics)