PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / app / Modules / MCP / AbilitiesRegistrar.php

AbilitiesRegistrar.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at app/Modules/MCP/AbilitiesRegistrar.php

581 lines 33.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Modules\MCP;
4
5 use FluentBoards\App\Modules\MCP\Tools\BoardTools;
6 use FluentBoards\App\Modules\MCP\Tools\CommentTools;
7 use FluentBoards\App\Modules\MCP\Tools\ContextTools;
8 use FluentBoards\App\Modules\MCP\Tools\LabelTools;
9 use FluentBoards\App\Modules\MCP\Tools\StageTools;
10 use FluentBoards\App\Modules\MCP\Tools\TaskQueryTools;
11 use FluentBoards\App\Modules\MCP\Tools\TaskTools;
12 use FluentBoards\App\Services\Constant;
13 use FluentBoards\App\Services\PermissionManager;
14
15 /**
16 * Single source of truth for Fluent Boards MCP abilities.
17 */
18 class AbilitiesRegistrar
19 {
20 public static function getDefinitions()
21 {
22 return [
23 'fluent-boards/get-fluentboards-context' => [
24 'label' => __('Get FluentBoards Context', 'fluent-boards'),
25 'description' => __('Discovery. Returns current user, site metadata, permissions, enums, safety levels, rate hints, and usage guidelines. Call once per session.', 'fluent-boards'),
26 'input_schema' => [
27 'type' => 'object',
28 'properties' => new \stdClass(),
29 ],
30 'execute_callback' => [ContextTools::class, 'getContext'],
31 'permission_callback' => function () {
32 return PermissionManager::hasAppAccess();
33 },
34 'annotations' => ['readonly' => true],
35 ],
36
37 'fluent-boards/list-boards' => [
38 'label' => __('List Boards', 'fluent-boards'),
39 'description' => __('List boards visible to the current user. Supports search, type, archived filter, pagination, and compact counts.', 'fluent-boards'),
40 'input_schema' => [
41 'type' => 'object',
42 'properties' => [
43 'search' => ['type' => 'string'],
44 'type' => ['type' => 'string', 'enum' => ['to-do', 'roadmap']],
45 'include_archived' => ['type' => 'boolean', 'default' => false],
46 'sort_by' => ['type' => 'string', 'enum' => ['id', 'title', 'created_at', 'updated_at'], 'default' => 'created_at'],
47 'sort_type' => ['type' => 'string', 'enum' => ['ASC', 'DESC'], 'default' => 'DESC'],
48 'page' => ['type' => 'integer', 'default' => 1],
49 'per_page' => ['type' => 'integer', 'default' => 20, 'description' => 'Max 100.'],
50 ],
51 ],
52 'execute_callback' => [BoardTools::class, 'listBoards'],
53 'permission_callback' => function () {
54 return PermissionManager::userHasAnyBoardAccess();
55 },
56 'annotations' => ['readonly' => true],
57 ],
58
59 'fluent-boards/get-board' => [
60 'label' => __('Get Board', 'fluent-boards'),
61 'description' => __('Board details. Includes stages, labels, members, and optional task summary for one board.', 'fluent-boards'),
62 'input_schema' => [
63 'type' => 'object',
64 'properties' => [
65 'board_id' => ['type' => 'integer'],
66 'include_tasks' => ['type' => 'boolean', 'default' => false],
67 ],
68 'required' => ['board_id'],
69 ],
70 'execute_callback' => [BoardTools::class, 'getBoard'],
71 'permission_callback' => function ($params = []) {
72 return BoardTools::canReadBoard($params);
73 },
74 'annotations' => ['readonly' => true],
75 ],
76
77 'fluent-boards/create-board' => [
78 'label' => __('Create Board', 'fluent-boards'),
79 'description' => __('Create a board using Fluent Boards board-creation permission. Accepts custom stages, labels and members; falls back to the default stages and labels when they are omitted. Fires native board-created hooks.', 'fluent-boards'),
80 'input_schema' => [
81 'type' => 'object',
82 'properties' => [
83 'title' => ['type' => 'string'],
84 'description' => ['type' => 'string'],
85 'type' => ['type' => 'string', 'enum' => ['to-do', 'roadmap'], 'default' => 'to-do'],
86 'currency' => ['type' => 'string', 'default' => 'USD'],
87 'crm_contact_id' => ['type' => 'integer'],
88 'folder_id' => ['type' => 'integer', 'description' => 'Optional folder id.'],
89 'stages' => [
90 'type' => 'array',
91 'description' => 'Optional stages, in order, for both board types. Omit to get the default stages.',
92 'items' => [
93 'type' => 'object',
94 'properties' => [
95 'title' => ['type' => 'string'],
96 'slug' => ['type' => 'string'],
97 'position' => ['type' => 'integer'],
98 'default_task_status' => [
99 'type' => 'string',
100 'enum' => ['open', 'closed'],
101 'description' => 'Status given to tasks landing in this stage. Defaults to open, or closed for stages titled Completed/Done. Ignored for roadmap boards.',
102 ],
103 ],
104 'required' => ['title'],
105 ],
106 ],
107 'labels' => [
108 'type' => 'array',
109 'description' => 'Optional named labels. Omit to get the five default colour labels.',
110 'items' => [
111 'type' => 'object',
112 'properties' => [
113 'title' => ['type' => 'string'],
114 'bg_color' => ['type' => 'string', 'description' => 'Hex background colour. Defaults to #f3f4f6.'],
115 'color' => ['type' => 'string', 'description' => 'Hex text colour. Defaults to #1B2533.'],
116 Constant::LABEL_COLOR_PRESET_SETTING => [
117 'type' => 'string',
118 'enum' => self::getLabelColorPresetIds(),
119 'description' => 'Optional predefined label colour. Use an empty string to clear a preset when updating a label.',
120 ],
121 ],
122 'required' => ['title'],
123 ],
124 ],
125 'member_ids' => [
126 'type' => 'array',
127 'description' => 'Optional WordPress user ids to add as board members. Max 50. The creator is always a member.',
128 'items' => ['type' => 'integer'],
129 'maxItems' => 50,
130 ],
131 ],
132 'required' => ['title'],
133 ],
134 'execute_callback' => [BoardTools::class, 'createBoard'],
135 'permission_callback' => function () {
136 return PermissionManager::userHasBoardCreationPermission();
137 },
138 ],
139
140 'fluent-boards/list-tasks' => [
141 'label' => __('List Tasks', 'fluent-boards'),
142 'description' => __('List/filter tasks in a board. Supports search, stage, status, priority, assignee, labels, due date, archived filter, and pagination.', 'fluent-boards'),
143 'input_schema' => [
144 'type' => 'object',
145 'properties' => [
146 'board_id' => ['type' => 'integer'],
147 'search' => ['type' => 'string'],
148 'stage' => ['type' => 'array', 'items' => ['type' => ['string', 'integer']]],
149 'task_status' => ['type' => 'array', 'items' => ['type' => 'string']],
150 'priority' => ['type' => 'array', 'items' => ['type' => 'string']],
151 'assignee' => ['type' => 'array', 'items' => ['type' => ['string', 'integer']]],
152 'labels' => ['type' => 'array', 'items' => ['type' => ['string', 'integer']]],
153 'due_date' => ['type' => 'array', 'items' => ['type' => 'string']],
154 'include_archived' => ['type' => 'boolean', 'default' => false],
155 'sort_by' => ['type' => 'string', 'enum' => ['title', 'status', 'created_at', 'position'], 'default' => 'position'],
156 'sort_direction' => ['type' => 'string', 'enum' => ['asc', 'desc'], 'default' => 'asc'],
157 'page' => ['type' => 'integer', 'default' => 1],
158 'per_page' => ['type' => 'integer', 'default' => 20, 'description' => 'Max 100.'],
159 ],
160 'required' => ['board_id'],
161 ],
162 'execute_callback' => [TaskTools::class, 'listTasks'],
163 'permission_callback' => function ($params = []) {
164 return BoardTools::canReadBoard($params);
165 },
166 'annotations' => ['readonly' => true],
167 ],
168
169 'fluent-boards/get-task' => [
170 'label' => __('Get Task', 'fluent-boards'),
171 'description' => __('Full task details. Includes board, stage, labels, assignees, watchers, comments, and recent activities.', 'fluent-boards'),
172 'input_schema' => [
173 'type' => 'object',
174 'properties' => [
175 'board_id' => ['type' => 'integer'],
176 'task_id' => ['type' => 'integer'],
177 ],
178 'required' => ['board_id', 'task_id'],
179 ],
180 'execute_callback' => [TaskTools::class, 'getTask'],
181 'permission_callback' => function ($params = []) {
182 return BoardTools::canReadBoard($params);
183 },
184 'annotations' => ['readonly' => true],
185 ],
186
187 'fluent-boards/create-task' => [
188 'label' => __('Create Task', 'fluent-boards'),
189 'description' => __('Create a task in a board stage. Fires native Fluent Boards task-created hooks and default assignee/watchers logic.', 'fluent-boards'),
190 'input_schema' => [
191 'type' => 'object',
192 'properties' => [
193 'board_id' => ['type' => 'integer'],
194 'stage_id' => ['type' => 'integer'],
195 'title' => ['type' => 'string'],
196 'description' => ['type' => 'string'],
197 'priority' => ['type' => 'string', 'description' => 'Priority key. Built-ins: urgent, high, medium, low. Custom priority keys registered via fluent_boards/task_priorities are allowed. Pass an empty string for no priority.'],
198 'due_at' => ['type' => 'string', 'description' => 'Date/time parseable by WordPress. Site timezone.'],
199 'started_at' => ['type' => 'string', 'description' => 'Date/time parseable by WordPress. Site timezone.'],
200 'crm_contact_id' => ['type' => 'integer'],
201 ],
202 'required' => ['board_id', 'stage_id', 'title'],
203 ],
204 'execute_callback' => [TaskTools::class, 'createTask'],
205 'permission_callback' => function ($params = []) {
206 return BoardTools::canWriteBoard($params);
207 },
208 ],
209
210 'fluent-boards/update-task' => [
211 'label' => __('Update Task', 'fluent-boards'),
212 'description' => __('Update task fields: title, description, status, priority, due_at, started_at, assignees, crm_contact_id, or settings.', 'fluent-boards'),
213 'input_schema' => [
214 'type' => 'object',
215 'properties' => [
216 'board_id' => ['type' => 'integer'],
217 'task_id' => ['type' => 'integer'],
218 'title' => ['type' => 'string'],
219 'description' => ['type' => 'string'],
220 'status' => ['type' => 'string', 'enum' => ['open', 'closed']],
221 'priority' => ['type' => 'string', 'description' => 'Omit to leave unchanged. Pass an empty string to clear. Custom priority keys registered via fluent_boards/task_priorities are allowed.'],
222 'due_at' => ['type' => 'string', 'description' => 'Omit to leave unchanged. Pass an empty string to clear.'],
223 'started_at' => ['type' => 'string', 'description' => 'Omit to leave unchanged. Pass an empty string to clear.'],
224 'assignees' => [
225 'type' => 'array',
226 'items' => ['type' => 'integer'],
227 'description' => __('Replaces current assignees. Newly added user ids must identify board members or global administrators. Pass an empty array to clear all assignees.', 'fluent-boards'),
228 ],
229 'crm_contact_id' => ['type' => 'integer', 'description' => 'Omit to leave unchanged. Pass 0 to clear.'],
230 'settings' => ['type' => 'object'],
231 ],
232 'required' => ['board_id', 'task_id'],
233 ],
234 'execute_callback' => [TaskTools::class, 'updateTask'],
235 'permission_callback' => function ($params = []) {
236 return BoardTools::canWriteBoard($params);
237 },
238 ],
239
240 'fluent-boards/move-task' => [
241 'label' => __('Move Task', 'fluent-boards'),
242 'description' => __('Move a task to another stage or board. Provide position or neighboring task ids for ordering.', 'fluent-boards'),
243 'input_schema' => [
244 'type' => 'object',
245 'properties' => [
246 'board_id' => ['type' => 'integer', 'description' => 'Current board id.'],
247 'task_id' => ['type' => 'integer'],
248 'target_board_id' => ['type' => 'integer', 'description' => 'Optional. Defaults to current board.'],
249 'target_stage_id' => ['type' => 'integer'],
250 'position' => ['type' => 'integer', 'description' => '1-based fallback position.'],
251 'previous_task_id' => ['type' => 'integer'],
252 'next_task_id' => ['type' => 'integer'],
253 ],
254 'required' => ['board_id', 'task_id', 'target_stage_id'],
255 ],
256 'execute_callback' => [TaskTools::class, 'moveTask'],
257 'permission_callback' => function ($params = []) {
258 return TaskTools::canMoveTask($params);
259 },
260 ],
261
262 'fluent-boards/archive-task' => [
263 'label' => __('Archive or Restore Task', 'fluent-boards'),
264 'description' => __('Archive or restore one task. Set archived=true to archive, false to restore.', 'fluent-boards'),
265 'input_schema' => [
266 'type' => 'object',
267 'properties' => [
268 'board_id' => ['type' => 'integer'],
269 'task_id' => ['type' => 'integer'],
270 'archived' => ['type' => 'boolean', 'default' => true],
271 ],
272 'required' => ['board_id', 'task_id'],
273 ],
274 'execute_callback' => [TaskTools::class, 'archiveTask'],
275 'permission_callback' => function ($params = []) {
276 return BoardTools::canWriteBoard($params);
277 },
278 ],
279
280 'fluent-boards/add-comment' => [
281 'label' => __('Add Comment', 'fluent-boards'),
282 'description' => __('Add a private or public comment to a task. Fires native comment-created hooks and notifications.', 'fluent-boards'),
283 'input_schema' => [
284 'type' => 'object',
285 'properties' => [
286 'board_id' => ['type' => 'integer'],
287 'task_id' => ['type' => 'integer'],
288 'description' => ['type' => 'string'],
289 'privacy' => ['type' => 'string', 'enum' => ['private', 'public'], 'default' => 'private'],
290 'mentioned_ids'=> ['type' => 'array', 'items' => ['type' => 'integer']],
291 ],
292 'required' => ['board_id', 'task_id', 'description'],
293 ],
294 'execute_callback' => [CommentTools::class, 'addComment'],
295 'permission_callback' => function ($params = []) {
296 return BoardTools::canWriteBoard($params);
297 },
298 ],
299
300 'fluent-boards/list-labels' => [
301 'label' => __('List Labels', 'fluent-boards'),
302 'description' => __('List the labels of a board, optionally only those already used by tasks.', 'fluent-boards'),
303 'input_schema' => [
304 'type' => 'object',
305 'properties' => [
306 'board_id' => ['type' => 'integer'],
307 'only_used' => ['type' => 'boolean', 'default' => false, 'description' => 'Return only labels currently attached to at least one task.'],
308 ],
309 'required' => ['board_id'],
310 ],
311 'execute_callback' => [LabelTools::class, 'listLabels'],
312 'permission_callback' => function ($params = []) {
313 return BoardTools::canReadBoard($params);
314 },
315 'annotations' => ['readonly' => true],
316 ],
317
318 'fluent-boards/save-label' => [
319 'label' => __('Create or Update Label', 'fluent-boards'),
320 'description' => __('Create a board label, or update an existing one when label_id is given. Only supplied fields change. Fires native board-label hooks.', 'fluent-boards'),
321 'input_schema' => [
322 'type' => 'object',
323 'properties' => [
324 'board_id' => ['type' => 'integer'],
325 'label_id' => ['type' => 'integer', 'description' => 'Omit to create a new label.'],
326 'title' => ['type' => 'string', 'description' => 'Required when creating unless bg_color is given.'],
327 'bg_color' => ['type' => 'string', 'description' => 'Hex background colour. Defaults to #f3f4f6 on create.'],
328 'color' => ['type' => 'string', 'description' => 'Hex text colour. Defaults to #1B2533 on create.'],
329 Constant::LABEL_COLOR_PRESET_SETTING => [
330 'type' => 'string',
331 'enum' => self::getLabelColorPresetIds(),
332 'description' => 'Optional predefined label colour. Omit to retain the current preset; use an empty string to clear it.',
333 ],
334 ],
335 'required' => ['board_id'],
336 ],
337 'execute_callback' => [LabelTools::class, 'saveLabel'],
338 'permission_callback' => function ($params = []) {
339 return BoardTools::canWriteBoard($params);
340 },
341 ],
342
343 'fluent-boards/delete-label' => [
344 'label' => __('Delete Label', 'fluent-boards'),
345 'description' => __('Delete a board label and detach it from every task that carries it.', 'fluent-boards'),
346 'input_schema' => [
347 'type' => 'object',
348 'properties' => [
349 'board_id' => ['type' => 'integer'],
350 'label_id' => ['type' => 'integer'],
351 ],
352 'required' => ['board_id', 'label_id'],
353 ],
354 'execute_callback' => [LabelTools::class, 'deleteLabel'],
355 'permission_callback' => function ($params = []) {
356 return BoardTools::canWriteBoard($params);
357 },
358 ],
359
360 'fluent-boards/add-task-label' => [
361 'label' => __('Add Task Label', 'fluent-boards'),
362 'description' => __('Attach one or more existing board labels to a task, by id or by title.', 'fluent-boards'),
363 'input_schema' => [
364 'type' => 'object',
365 'properties' => [
366 'board_id' => ['type' => 'integer'],
367 'task_id' => ['type' => 'integer'],
368 'label_id' => ['type' => 'integer', 'description' => 'Single label id. Combine with label_ids/label_titles if needed.'],
369 'label_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'maxItems' => 50],
370 'label_titles' => [
371 'type' => 'array',
372 'items' => ['type' => 'string'],
373 'maxItems' => 50,
374 'description' => 'Label titles on the same board, matched case-insensitively. Unknown titles are an error; create them with create-label first.',
375 ],
376 ],
377 'required' => ['board_id', 'task_id'],
378 ],
379 'execute_callback' => [LabelTools::class, 'addTaskLabel'],
380 'permission_callback' => function ($params = []) {
381 return BoardTools::canWriteBoard($params);
382 },
383 ],
384
385 'fluent-boards/remove-task-label' => [
386 'label' => __('Remove Task Label', 'fluent-boards'),
387 'description' => __('Detach one or more labels from a task, by id or by title. The board label itself is kept.', 'fluent-boards'),
388 'input_schema' => [
389 'type' => 'object',
390 'properties' => [
391 'board_id' => ['type' => 'integer'],
392 'task_id' => ['type' => 'integer'],
393 'label_id' => ['type' => 'integer'],
394 'label_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'maxItems' => 50],
395 'label_titles' => ['type' => 'array', 'items' => ['type' => 'string'], 'maxItems' => 50],
396 ],
397 'required' => ['board_id', 'task_id'],
398 ],
399 'execute_callback' => [LabelTools::class, 'removeTaskLabel'],
400 'permission_callback' => function ($params = []) {
401 return BoardTools::canWriteBoard($params);
402 },
403 ],
404
405 'fluent-boards/list-my-tasks' => [
406 'label' => __('List My Tasks', 'fluent-boards'),
407 'description' => __('List tasks across every board the caller can see, grouped by the dashboard categories: assigned, mentioned, upcoming, due today, overdue, completed, or others (no due date).', 'fluent-boards'),
408 'input_schema' => [
409 'type' => 'object',
410 'properties' => [
411 'user_id' => ['type' => 'integer', 'description' => 'Defaults to the current user. Results stay limited to boards the caller can access.'],
412 'task_type' => [
413 'type' => 'string',
414 'enum' => TaskQueryTools::TASK_TYPES,
415 'default' => 'assigned',
416 ],
417 'board_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Optional board filter.'],
418 'order_by' => ['type' => 'string', 'enum' => TaskQueryTools::ORDER_BY, 'default' => 'due_at'],
419 'order' => ['type' => 'string', 'enum' => ['ASC', 'DESC'], 'default' => 'ASC'],
420 'page' => ['type' => 'integer', 'default' => 1],
421 'per_page' => ['type' => 'integer', 'default' => 20, 'description' => 'Max 50.'],
422 ],
423 ],
424 'execute_callback' => [TaskQueryTools::class, 'listMyTasks'],
425 'permission_callback' => function () {
426 return PermissionManager::hasAppAccess();
427 },
428 'annotations' => ['readonly' => true],
429 ],
430
431 'fluent-boards/search-tasks' => [
432 'label' => __('Search Tasks', 'fluent-boards'),
433 'description' => __('Search tasks by title across every board the caller can see. Terms need at least three characters. Supports the "id:123" exact lookup and "archived:term" prefixes used by the product search box.', 'fluent-boards'),
434 'input_schema' => [
435 'type' => 'object',
436 'properties' => [
437 'query' => ['type' => 'string', 'minLength' => 3, 'description' => 'At least three characters, or an "id:123" lookup.'],
438 'board_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Optional board filter.'],
439 'include_archived' => ['type' => 'boolean', 'default' => false],
440 'page' => ['type' => 'integer', 'default' => 1],
441 'per_page' => ['type' => 'integer', 'default' => 20, 'description' => 'Max 50.'],
442 ],
443 'required' => ['query'],
444 ],
445 'execute_callback' => [TaskQueryTools::class, 'searchTasks'],
446 'permission_callback' => function () {
447 return PermissionManager::hasAppAccess();
448 },
449 'annotations' => ['readonly' => true],
450 ],
451
452 'fluent-boards/save-stage' => [
453 'label' => __('Create or Update Stage', 'fluent-boards'),
454 'description' => __('Create a stage, or update an existing one when stage_id is given. Only supplied fields change.', 'fluent-boards'),
455 'input_schema' => [
456 'type' => 'object',
457 'properties' => [
458 'board_id' => ['type' => 'integer'],
459 'stage_id' => ['type' => 'integer', 'description' => 'Omit to create a new stage.'],
460 'title' => ['type' => 'string', 'description' => 'Required when creating.'],
461 'position' => ['type' => 'integer', 'description' => '1-based position on the board.'],
462 'default_task_status' => ['type' => 'string', 'enum' => ['open', 'closed'], 'description' => 'Status given to tasks landing in this stage.'],
463 'bg_color' => ['type' => 'string'],
464 'color' => ['type' => 'string'],
465 ],
466 'required' => ['board_id'],
467 ],
468 'execute_callback' => [StageTools::class, 'saveStage'],
469 'permission_callback' => function ($params = []) {
470 return BoardTools::canWriteBoard($params);
471 },
472 ],
473
474 'fluent-boards/archive-stage' => [
475 'label' => __('Archive Stage', 'fluent-boards'),
476 'description' => __('Archive or restore a stage. Archived stages keep their tasks; set archived=false to restore at the end of the board.', 'fluent-boards'),
477 'input_schema' => [
478 'type' => 'object',
479 'properties' => [
480 'board_id' => ['type' => 'integer'],
481 'stage_id' => ['type' => 'integer'],
482 'archived' => ['type' => 'boolean', 'default' => true],
483 ],
484 'required' => ['board_id', 'stage_id'],
485 ],
486 'execute_callback' => [StageTools::class, 'archiveStage'],
487 'permission_callback' => function ($params = []) {
488 return BoardTools::canWriteBoard($params);
489 },
490 ],
491
492 'fluent-boards/assign-task' => [
493 'label' => __('Assign Task', 'fluent-boards'),
494 'description' => __('Add, remove, or sync task assignees. Newly added user ids must identify existing WordPress users who are board members or global administrators. Adding someone already assigned is a no-op. Assignees also become task watchers.', 'fluent-boards'),
495 'input_schema' => [
496 'type' => 'object',
497 'properties' => [
498 'board_id' => ['type' => 'integer'],
499 'task_id' => ['type' => 'integer'],
500 'user_id' => ['type' => 'integer', 'description' => 'Single user id. Ignored when user_ids is provided.'],
501 'user_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'maxItems' => 50],
502 'mode' => [
503 'type' => 'string',
504 'enum' => ['add', 'remove', 'sync'],
505 'default' => 'add',
506 'description' => 'sync makes the assignee list exactly match the ids given. Pass user_ids: [] with sync to clear all assignees.',
507 ],
508 ],
509 'required' => ['board_id', 'task_id'],
510 ],
511 'execute_callback' => [TaskTools::class, 'assignTask'],
512 'permission_callback' => function ($params = []) {
513 return BoardTools::canWriteBoard($params);
514 },
515 ],
516 ];
517 }
518
519 public static function register()
520 {
521 foreach (self::getDefinitions() as $name => $definition) {
522 $args = [
523 'label' => $definition['label'],
524 'description' => $definition['description'],
525 'category' => 'fluent-boards',
526 'execute_callback' => self::wrapExecuteCallback($name, $definition['execute_callback']),
527 'permission_callback' => $definition['permission_callback'],
528 'meta' => [
529 'show_in_rest' => true,
530 'mcp' => [
531 'public' => true,
532 ],
533 ],
534 ];
535
536 if (!empty($definition['input_schema'])) {
537 $args['input_schema'] = $definition['input_schema'];
538 }
539
540 if (!empty($definition['annotations'])) {
541 $args['meta']['annotations'] = $definition['annotations'];
542 }
543
544 wp_register_ability($name, $args);
545 }
546 }
547
548 private static function wrapExecuteCallback($toolName, $callback)
549 {
550 return function ($params) use ($toolName, $callback) {
551 try {
552 return call_user_func($callback, is_array($params) ? $params : []);
553 } catch (\Throwable $e) {
554 do_action('fluent_boards/mcp_tool_exception', $e, $toolName, $params);
555
556 $details = [
557 'tool' => $toolName,
558 'exception' => get_class($e),
559 ];
560
561 if (defined('WP_DEBUG') && WP_DEBUG) {
562 $details['file'] = $e->getFile() . ':' . $e->getLine();
563 $details['trace'] = array_slice(explode("\n", $e->getTraceAsString()), 0, 5);
564 }
565
566 return new \WP_Error('failed', $e->getMessage(), $details);
567 }
568 };
569 }
570
571 /**
572 * Return every supported preset id plus the explicit value that clears one.
573 *
574 * @return array
575 */
576 private static function getLabelColorPresetIds()
577 {
578 return array_merge([''], array_column(Constant::LABEL_COLOR_PRESETS, 'id'));
579 }
580 }
581