From f3208a481f651dd93551abd78cd3b85744d6e967 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 9 Apr 2026 09:55:36 +0200 Subject: [PATCH] feat : add collaborators to all MCP task tools Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Mcp/Tool/Task/CreateTaskTool.php | 14 +++++++++++++ src/Mcp/Tool/Task/GetTaskTool.php | 27 +++++++++++++------------ src/Mcp/Tool/Task/ListTasksTool.php | 30 +++++++++++++++++----------- src/Mcp/Tool/Task/UpdateTaskTool.php | 18 +++++++++++++++++ 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Mcp/Tool/Task/CreateTaskTool.php b/src/Mcp/Tool/Task/CreateTaskTool.php index 3c74934..922e8aa 100644 --- a/src/Mcp/Tool/Task/CreateTaskTool.php +++ b/src/Mcp/Tool/Task/CreateTaskTool.php @@ -51,6 +51,7 @@ class CreateTaskTool ?int $assigneeId = null, ?int $groupId = null, ?array $tagIds = null, + ?array $collaboratorIds = null, ?string $scheduledStart = null, ?string $scheduledEnd = null, ?string $deadline = null, @@ -116,6 +117,18 @@ class CreateTaskTool $task->addTag($tag); } } + if (null !== $collaboratorIds) { + foreach ($collaboratorIds as $collaboratorId) { + $collaborator = $this->userRepository->find($collaboratorId); + if (null === $collaborator) { + throw new InvalidArgumentException(sprintf('User with ID %d not found.', $collaboratorId)); + } + if (null !== $assigneeId && $collaboratorId === $assigneeId) { + throw new InvalidArgumentException('A collaborator cannot be the assignee.'); + } + $task->addCollaborator($collaborator); + } + } if (null !== $scheduledStart) { $task->setScheduledStart(new DateTimeImmutable($scheduledStart)); } @@ -147,6 +160,7 @@ class CreateTaskTool 'priority' => Serializer::priority($task->getPriority()), 'effort' => Serializer::effort($task->getEffort()), 'assignee' => Serializer::user($task->getAssignee()), + 'collaborators' => Serializer::users($task->getCollaborators()), 'group' => Serializer::groupRef($task->getGroup()), 'project' => Serializer::projectRef($project), 'tags' => Serializer::tags($task->getTags()), diff --git a/src/Mcp/Tool/Task/GetTaskTool.php b/src/Mcp/Tool/Task/GetTaskTool.php index 7a3440c..94f0bfb 100644 --- a/src/Mcp/Tool/Task/GetTaskTool.php +++ b/src/Mcp/Tool/Task/GetTaskTool.php @@ -34,19 +34,20 @@ class GetTaskTool } return json_encode([ - 'id' => $task->getId(), - 'number' => $task->getNumber(), - 'title' => $task->getTitle(), - 'description' => $task->getDescription(), - 'status' => Serializer::statusFull($task->getStatus()), - 'priority' => Serializer::priority($task->getPriority()), - 'effort' => Serializer::effort($task->getEffort()), - 'assignee' => Serializer::user($task->getAssignee()), - 'group' => Serializer::group($task->getGroup()), - 'project' => Serializer::projectRef($task->getProject()), - 'tags' => Serializer::tagsWithColor($task->getTags()), - 'documents' => Serializer::documents($task->getDocuments()), - 'archived' => $task->isArchived(), + 'id' => $task->getId(), + 'number' => $task->getNumber(), + 'title' => $task->getTitle(), + 'description' => $task->getDescription(), + 'status' => Serializer::statusFull($task->getStatus()), + 'priority' => Serializer::priority($task->getPriority()), + 'effort' => Serializer::effort($task->getEffort()), + 'assignee' => Serializer::user($task->getAssignee()), + 'collaborators' => Serializer::users($task->getCollaborators()), + 'group' => Serializer::group($task->getGroup()), + 'project' => Serializer::projectRef($task->getProject()), + 'tags' => Serializer::tagsWithColor($task->getTags()), + 'documents' => Serializer::documents($task->getDocuments()), + 'archived' => $task->isArchived(), ]); } } diff --git a/src/Mcp/Tool/Task/ListTasksTool.php b/src/Mcp/Tool/Task/ListTasksTool.php index 92de120..88096ef 100644 --- a/src/Mcp/Tool/Task/ListTasksTool.php +++ b/src/Mcp/Tool/Task/ListTasksTool.php @@ -10,7 +10,7 @@ use Mcp\Capability\Attribute\McpTool; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -#[McpTool(name: 'list-tasks', description: 'List tasks with optional filters by project, status, assignee, priority, group, tags, and archive state. Returns max 100 results by default, use filters to narrow down.')] +#[McpTool(name: 'list-tasks', description: 'List tasks with optional filters by project, status, assignee, collaborator, priority, group, tags, and archive state. Returns max 100 results by default, use filters to narrow down.')] class ListTasksTool { public function __construct( @@ -22,6 +22,7 @@ class ListTasksTool ?int $projectId = null, ?int $statusId = null, ?int $assigneeId = null, + ?int $collaboratorId = null, ?int $priorityId = null, ?int $groupId = null, ?array $tagIds = null, @@ -38,6 +39,7 @@ class ListTasksTool ->leftJoin('t.status', 's')->addSelect('s') ->leftJoin('t.priority', 'p')->addSelect('p') ->leftJoin('t.assignee', 'a')->addSelect('a') + ->leftJoin('t.collaborators', 'collab')->addSelect('collab') ->leftJoin('t.project', 'pr')->addSelect('pr') ->leftJoin('t.effort', 'e')->addSelect('e') ->leftJoin('t.group', 'g')->addSelect('g') @@ -57,6 +59,9 @@ class ListTasksTool if (null !== $assigneeId) { $qb->andWhere('a.id = :assigneeId')->setParameter('assigneeId', $assigneeId); } + if (null !== $collaboratorId) { + $qb->andWhere('collab.id = :collaboratorId')->setParameter('collaboratorId', $collaboratorId); + } if (null !== $priorityId) { $qb->andWhere('p.id = :priorityId')->setParameter('priorityId', $priorityId); } @@ -75,17 +80,18 @@ class ListTasksTool } return json_encode(array_map(fn ($task) => [ - 'id' => $task->getId(), - 'number' => $task->getNumber(), - 'title' => $task->getTitle(), - 'status' => Serializer::status($task->getStatus()), - 'priority' => Serializer::priority($task->getPriority()), - 'assignee' => Serializer::user($task->getAssignee()), - 'effort' => Serializer::effort($task->getEffort()), - 'group' => Serializer::groupRef($task->getGroup()), - 'project' => Serializer::projectRef($task->getProject()), - 'tags' => Serializer::tags($task->getTags()), - 'archived' => $task->isArchived(), + 'id' => $task->getId(), + 'number' => $task->getNumber(), + 'title' => $task->getTitle(), + 'status' => Serializer::status($task->getStatus()), + 'priority' => Serializer::priority($task->getPriority()), + 'assignee' => Serializer::user($task->getAssignee()), + 'collaborators' => Serializer::users($task->getCollaborators()), + 'effort' => Serializer::effort($task->getEffort()), + 'group' => Serializer::groupRef($task->getGroup()), + 'project' => Serializer::projectRef($task->getProject()), + 'tags' => Serializer::tags($task->getTags()), + 'archived' => $task->isArchived(), ], array_values($tasks))); } } diff --git a/src/Mcp/Tool/Task/UpdateTaskTool.php b/src/Mcp/Tool/Task/UpdateTaskTool.php index ed67b72..f078cd6 100644 --- a/src/Mcp/Tool/Task/UpdateTaskTool.php +++ b/src/Mcp/Tool/Task/UpdateTaskTool.php @@ -48,6 +48,7 @@ class UpdateTaskTool ?int $assigneeId = null, ?int $groupId = null, ?array $tagIds = null, + ?array $collaboratorIds = null, ?bool $archived = null, ?string $scheduledStart = null, ?string $scheduledEnd = null, @@ -118,6 +119,22 @@ class UpdateTaskTool $task->addTag($tag); } } + if (null !== $collaboratorIds) { + foreach ($task->getCollaborators()->toArray() as $existing) { + $task->removeCollaborator($existing); + } + $assignee = $task->getAssignee(); + foreach ($collaboratorIds as $collaboratorId) { + $collaborator = $this->userRepository->find($collaboratorId); + if (null === $collaborator) { + throw new InvalidArgumentException(sprintf('User with ID %d not found.', $collaboratorId)); + } + if (null !== $assignee && $collaborator->getId() === $assignee->getId()) { + throw new InvalidArgumentException('A collaborator cannot be the assignee.'); + } + $task->addCollaborator($collaborator); + } + } if (null !== $archived) { $task->setArchived($archived); } @@ -147,6 +164,7 @@ class UpdateTaskTool 'priority' => Serializer::priority($task->getPriority()), 'effort' => Serializer::effort($task->getEffort()), 'assignee' => Serializer::user($task->getAssignee()), + 'collaborators' => Serializer::users($task->getCollaborators()), 'group' => Serializer::groupRef($task->getGroup()), 'project' => Serializer::projectRef($task->getProject()), 'tags' => Serializer::tags($task->getTags()),