# fluent-boards/2.0.0/app/Modules/MCP/Tools/TaskTools.php

FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration, version 2.0.0. 485 lines.

- Page: https://pluginprobe.com/plugins/fluent-boards/2.0.0/code/app/Modules/MCP/Tools/TaskTools.php
- Raw: https://pluginprobe.com/plugins/fluent-boards/2.0.0/raw/app/Modules/MCP/Tools/TaskTools.php
- Modified: 2026-07-30T14:27:06+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-boards/2.0.0/code/app/Modules/MCP/Tools/TaskTools.php#L10-L20`.

```php
<?php

namespace FluentBoards\App\Modules\MCP\Tools;

use FluentBoards\App\Models\Stage;
use FluentBoards\App\Models\Task;
use FluentBoards\App\Modules\MCP\Helpers\MCPHelper;
use FluentBoards\App\Services\Constant;
use FluentBoards\App\Services\TaskService;

/**
 * Task read/write MCP tools.
 */
class TaskTools
{
    const MAX_ASSIGNEES_PER_CALL = 50;

    public static function canMoveTask($params = [])
    {
        $sourceBoardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
        $targetBoardId = !empty($params['target_board_id']) ? absint($params['target_board_id']) : $sourceBoardId;

        if (!$sourceBoardId || !$targetBoardId) {
            return false;
        }

        return MCPHelper::canWriteBoard($sourceBoardId) && MCPHelper::canWriteBoard($targetBoardId);
    }

    public static function listTasks($params = [])
    {
        $board = MCPHelper::resolveBoard($params);
        if (is_wp_error($board)) {
            return $board;
        }

        if (!MCPHelper::canReadBoard($board->id)) {
            return MCPHelper::error('forbidden', __('You do not have access to this board', 'fluent-boards'));
        }

        $pagination = MCPHelper::normalizePagination($params);
        $args = [
            'page'             => $pagination['page'],
            'per_page'         => $pagination['per_page'],
            'sort_by'          => !empty($params['sort_by']) ? sanitize_text_field($params['sort_by']) : 'position',
            'sort_direction'   => !empty($params['sort_direction']) ? sanitize_text_field($params['sort_direction']) : 'asc',
            'search'           => !empty($params['search']) ? sanitize_text_field($params['search']) : '',
            'include_archived' => !empty($params['include_archived']),
            'stage'            => isset($params['stage']) ? (array) $params['stage'] : [],
            'task_status'      => isset($params['task_status']) ? (array) $params['task_status'] : [],
            'priority'         => isset($params['priority']) ? (array) $params['priority'] : [],
            'assignee'         => isset($params['assignee']) ? (array) $params['assignee'] : [],
            'labels'           => isset($params['labels']) ? (array) $params['labels'] : [],
            'due_date'         => isset($params['due_date']) ? (array) $params['due_date'] : [],
        ];

        $paginated = (new TaskService())->getTableTasks($board->id, $args);

        return [
            'items'      => MCPHelper::formatTaskList($paginated->items()),
            'pagination' => [
                'total'        => (int) $paginated->total(),
                'current_page' => (int) $paginated->currentPage(),
                'per_page'     => (int) $paginated->perPage(),
                'last_page'    => (int) $paginated->lastPage(),
            ],
        ];
    }

    public static function getTask($params = [])
    {
        $task = MCPHelper::resolveTask($params);
        if (is_wp_error($task)) {
            return $task;
        }

        if (!MCPHelper::canReadBoard($task->board_id)) {
            return MCPHelper::error('forbidden', __('You do not have access to this task', 'fluent-boards'));
        }

        return [
            'task' => MCPHelper::formatTask($task),
        ];
    }

    public static function createTask($params = [])
    {
        $board = MCPHelper::resolveBoard($params);
        if (is_wp_error($board)) {
            return $board;
        }

        if (!MCPHelper::canWriteBoard($board->id)) {
            return MCPHelper::error('forbidden', __('You do not have permission to create tasks on this board', 'fluent-boards'));
        }

        $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
        if ($title === '') {
            return MCPHelper::error('invalid_param', __('Task title is required', 'fluent-boards'));
        }

        $stage = MCPHelper::assertStageBelongsToBoard($params['stage_id'] ?? 0, $board->id);
        if (is_wp_error($stage)) {
            return $stage;
        }

        $taskData = [
            'title'    => $title,
            'board_id' => (int) $board->id,
            'stage_id' => (int) $stage->id,
        ];

        if (!empty($params['priority'])) {
            if (!in_array($params['priority'], self::getAllowedTaskPriorities(), true)) {
                return MCPHelper::error('invalid_param', __('Invalid task priority', 'fluent-boards'), [
                    'allowed' => self::getAllowedTaskPriorities(),
                ]);
            }
            $taskData['priority'] = sanitize_text_field($params['priority']);
        }

        foreach (['due_at', 'started_at'] as $field) {
            if (!empty($params[$field])) {
                $taskData[$field] = sanitize_text_field($params[$field]);
            }
        }

        if (!empty($params['description'])) {
            $taskData['description'] = MCPHelper::sanitizeMarkdown($params['description']);
        }

        if (!empty($params['crm_contact_id'])) {
            $taskData['crm_contact_id'] = absint($params['crm_contact_id']);
        }

        $task = (new TaskService())->createTask($taskData, $board->id);
        MCPHelper::loadTaskDetails($task);

        return [
            'task'    => MCPHelper::formatTask($task),
            'message' => __('Task has been successfully created', 'fluent-boards'),
        ];
    }

    public static function updateTask($params = [])
    {
        $task = MCPHelper::resolveTask($params);
        if (is_wp_error($task)) {
            return $task;
        }

        if (!MCPHelper::canWriteBoard($task->board_id)) {
            return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards'));
        }

        $service = new TaskService();
        $updatable = [
            'title'          => 'text',
            'description'    => 'markdown',
            'status'         => 'text',
            'priority'       => 'nullable_text',
            'due_at'         => 'nullable_text',
            'started_at'     => 'nullable_text',
            'crm_contact_id' => 'nullable_int',
            'settings'       => 'array',
        ];

        foreach ($updatable as $field => $type) {
            if (!array_key_exists($field, $params)) {
                continue;
            }

            $validationError = self::validateUpdateField($field, $params[$field]);
            if (is_wp_error($validationError)) {
                return $validationError;
            }

            $value = self::sanitizeUpdateValue($params[$field], $type);
            $task = $service->updateTaskProperty($field, $value, $task);
        }

        if (array_key_exists('assignees', $params)) {
            $task = self::syncAssignees($task, MCPHelper::sanitizeIdArray($params['assignees']));
        }

        MCPHelper::loadTaskDetails($task);

        return [
            'task'    => MCPHelper::formatTask($task),
            'message' => __('Task has been updated', 'fluent-boards'),
        ];
    }

    public static function moveTask($params = [])
    {
        $task = MCPHelper::resolveTask($params);
        if (is_wp_error($task)) {
            return $task;
        }

        $sourceBoardId = (int) $task->board_id;
        $targetBoardId = !empty($params['target_board_id']) ? absint($params['target_board_id']) : $sourceBoardId;
        $targetStageId = !empty($params['target_stage_id']) ? absint($params['target_stage_id']) : 0;

        if (!MCPHelper::canWriteBoard($sourceBoardId) || !MCPHelper::canWriteBoard($targetBoardId)) {
            return MCPHelper::error('forbidden', __('You do not have permission to move this task', 'fluent-boards'));
        }

        $stage = MCPHelper::assertStageBelongsToBoard($targetStageId, $targetBoardId);
        if (is_wp_error($stage)) {
            return $stage;
        }

        $service = new TaskService();
        $oldStageId = (int) $task->stage_id;

        if ($targetBoardId !== $sourceBoardId) {
            $task = $service->changeBoardByTask($task, $targetBoardId);
        }

        if ($oldStageId !== $targetStageId) {
            \FluentBoards\App\Models\TaskMeta::where('task_id', $task->id)
                ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
                ->delete();
        }

        $task->stage_id = $targetStageId;
        $task->save();

        $previousTaskId = !empty($params['previous_task_id']) ? absint($params['previous_task_id']) : 0;
        $nextTaskId = !empty($params['next_task_id']) ? absint($params['next_task_id']) : 0;
        if ($previousTaskId || $nextTaskId) {
            $task = $task->moveBetweenTasks($previousTaskId, $nextTaskId);
        } else {
            $position = !empty($params['position']) ? absint($params['position']) : 1;
            $task = $task->moveToNewPosition($position);
        }

        if ($oldStageId !== $targetStageId) {
            $service->manageDefaultAssignees($task, $targetStageId);

            if ($stage->defaultTaskStatus() === 'closed' && $task->status !== 'closed') {
                $task = $task->close();
            } elseif ($stage->defaultTaskStatus() === 'open' && $task->status === 'closed') {
                $task = $task->reopen();
            }

            do_action('fluent_boards/task_stage_updated', $task, $oldStageId);
        }

        do_action('fluent_boards/task_updated', $task, 'position');
        MCPHelper::loadTaskDetails($task);

        return [
            'task'    => MCPHelper::formatTask($task),
            'message' => __('Task has been moved', 'fluent-boards'),
        ];
    }

    /**
     * Core assignee management. Pro also ships add-task-assignee/remove-task-assignee; this works
     * without Pro and adds a sync mode.
     */
    public static function assignTask($params = [])
    {
        $task = MCPHelper::resolveTask($params);
        if (is_wp_error($task)) {
            return $task;
        }

        if (!MCPHelper::canWriteBoard($task->board_id)) {
            return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards'));
        }

        $mode = !empty($params['mode']) ? sanitize_text_field($params['mode']) : 'add';
        if (!in_array($mode, ['add', 'remove', 'sync'], true)) {
            return MCPHelper::error('invalid_param', __('Invalid assignment mode', 'fluent-boards'), [
                'allowed' => ['add', 'remove', 'sync'],
            ]);
        }

        $hasUserIds = array_key_exists('user_ids', $params);
        $userIds = MCPHelper::sanitizeIdArray($params['user_ids'] ?? []);
        if (!$hasUserIds && !empty($params['user_id'])) {
            $userIds = [absint($params['user_id'])];
        }

        if (!$userIds && !($mode === 'sync' && $hasUserIds && is_array($params['user_ids']))) {
            return MCPHelper::error('invalid_param', __('Provide user_id or user_ids', 'fluent-boards'));
        }

        if (count($userIds) > self::MAX_ASSIGNEES_PER_CALL) {
            return MCPHelper::error('invalid_param', __('Too many users in one call', 'fluent-boards'), [
                'max' => self::MAX_ASSIGNEES_PER_CALL,
            ]);
        }

        // fbs_relations has no foreign key, so an unknown id would persist as an assignee
        // and a watcher that no user can ever be loaded for.
        $unknownIds = self::findUnknownUserIds($userIds);
        if ($unknownIds) {
            return MCPHelper::error('not_found', __('Some users do not exist', 'fluent-boards'), [
                'unknown_user_ids' => $unknownIds,
            ]);
        }

        $task->load('assignees');
        $currentIds = [];
        foreach ($task->assignees as $assignee) {
            $currentIds[] = (int) $assignee->ID;
        }

        // updateAssignee() toggles, so only feed it genuine changes.
        if ($mode === 'add') {
            $toAdd = array_diff($userIds, $currentIds);
            $toRemove = [];
        } elseif ($mode === 'remove') {
            $toAdd = [];
            $toRemove = array_intersect($userIds, $currentIds);
        } else {
            $toAdd = array_diff($userIds, $currentIds);
            $toRemove = array_diff($currentIds, $userIds);
        }

        $service = new TaskService();

        foreach (array_merge($toRemove, $toAdd) as $userId) {
            $service->updateAssignee($userId, $task);
            $task->load('assignees');
        }

        MCPHelper::loadTaskDetails($task);

        return [
            'task_id'   => (int) $task->id,
            'board_id'  => (int) $task->board_id,
            'mode'      => $mode,
            'added'     => array_values($toAdd),
            'removed'   => array_values($toRemove),
            'assignees' => MCPHelper::formatUserList($task->assignees),
            'message'   => __('Task assignees have been updated', 'fluent-boards'),
        ];
    }

    public static function archiveTask($params = [])
    {
        $task = MCPHelper::resolveTask($params);
        if (is_wp_error($task)) {
            return $task;
        }

        if (!MCPHelper::canWriteBoard($task->board_id)) {
            return MCPHelper::error('forbidden', __('You do not have permission to archive this task', 'fluent-boards'));
        }

        $archived = array_key_exists('archived', $params) ? (bool) $params['archived'] : true;
        $value = $archived ? current_time('mysql') : null;

        $task = (new TaskService())->updateTaskProperty('archived_at', $value, $task);
        MCPHelper::loadTaskDetails($task);

        return [
            'task'    => MCPHelper::formatTask($task),
            'message' => $archived ? __('Task has been archived', 'fluent-boards') : __('Task has been restored', 'fluent-boards'),
        ];
    }

    /**
     * @return array Ids with no matching WordPress user, in the order supplied.
     */
    private static function findUnknownUserIds($userIds)
    {
        $found = get_users([
            'include' => $userIds,
            'fields'  => 'ID',
            'number'  => count($userIds),
        ]);

        $found = array_map('intval', (array) $found);

        return array_values(array_diff($userIds, $found));
    }

    private static function sanitizeUpdateValue($value, $type)
    {
        if ($type === 'html') {
            return wp_kses_post((string) $value);
        }

        if ($type === 'markdown') {
            return MCPHelper::sanitizeMarkdown($value);
        }

        if ($type === 'array') {
            return is_array($value) ? self::sanitizeArray($value) : [];
        }

        if ($type === 'nullable_int') {
            return $value === null || $value === '' ? null : absint($value);
        }

        if ($type === 'nullable_text') {
            return $value === null ? null : sanitize_text_field((string) $value);
        }

        return sanitize_text_field((string) $value);
    }

    private static function validateUpdateField($field, $value)
    {
        if ($field === 'status' && !in_array($value, ['open', 'closed'], true)) {
            return MCPHelper::error('invalid_param', __('Invalid task status', 'fluent-boards'), [
                'allowed' => ['open', 'closed'],
            ]);
        }

        if ($field === 'priority' && $value !== '' && $value !== null && !in_array($value, self::getAllowedTaskPriorities(), true)) {
            return MCPHelper::error('invalid_param', __('Invalid task priority', 'fluent-boards'), [
                'allowed' => self::getAllowedTaskPriorities(),
            ]);
        }

        return true;
    }

    /**
     * Get priority keys allowed by the task priority filter.
     *
     * @return array
     */
    private static function getAllowedTaskPriorities()
    {
        return array_map('strval', array_keys(apply_filters('fluent_boards/task_priorities', [
            ''       => __('No priority', 'fluent-boards'),
            'urgent' => __('Urgent', 'fluent-boards'),
            'high'   => __('High', 'fluent-boards'),
            'medium' => __('Medium', 'fluent-boards'),
            'low'    => __('Low', 'fluent-boards'),
        ])));
    }

    private static function sanitizeArray($value)
    {
        $sanitized = [];
        foreach ((array) $value as $key => $item) {
            $safeKey = sanitize_key((string) $key);
            if ($safeKey === '') {
                continue;
            }

            if (is_array($item)) {
                $sanitized[$safeKey] = self::sanitizeArray($item);
            } elseif (is_bool($item) || is_int($item) || is_float($item) || $item === null) {
                $sanitized[$safeKey] = $item;
            } else {
                $sanitized[$safeKey] = sanitize_text_field((string) $item);
            }
        }

        return $sanitized;
    }

    private static function syncAssignees($task, $newAssigneeIds)
    {
        $task->load('assignees');
        $currentIds = [];
        foreach ($task->assignees as $assignee) {
            $currentIds[] = (int) $assignee->ID;
        }

        $service = new TaskService();
        foreach (array_diff($currentIds, $newAssigneeIds) as $removeId) {
            $service->updateAssignee($removeId, $task);
            $task->load('assignees');
        }

        foreach (array_diff($newAssigneeIds, $currentIds) as $addId) {
            $service->updateAssignee($addId, $task);
            $task->load('assignees');
        }

        return $task;
    }
}

```
