bitrix24-tasks-mcp-server 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { bitrix24Client } from '../bitrix24/client.js';
3
+ import { publishTaskComposite } from '../bitrix24/compositePublisher.js';
4
+ import { validateToolArguments } from './validation.js';
2
5
  function stripBase64FromDownloadedFile(file) {
3
6
  if (!file.base64) {
4
7
  return file;
@@ -13,6 +16,45 @@ function stripBase64FromDownloadedFile(file) {
13
16
  function collectFilesWithContent(...groups) {
14
17
  return groups.flat().filter((file) => file.base64);
15
18
  }
19
+ function normalizeStageTitle(value) {
20
+ return String(value ?? '').trim().toLocaleLowerCase('ru-RU');
21
+ }
22
+ function compareNumericIds(left, right) {
23
+ if (!/^\d+$/.test(left) || !/^\d+$/.test(right)) {
24
+ return left.localeCompare(right, 'en', { numeric: true });
25
+ }
26
+ const leftValue = BigInt(left);
27
+ const rightValue = BigInt(right);
28
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
29
+ }
30
+ function compactText(value, maxChars) {
31
+ if (value === undefined || value.length <= maxChars) {
32
+ return {
33
+ text: value,
34
+ truncated: false,
35
+ originalChars: value?.length ?? 0
36
+ };
37
+ }
38
+ return {
39
+ text: value.slice(0, maxChars),
40
+ truncated: true,
41
+ originalChars: value.length,
42
+ sha256: createHash('sha256').update(value).digest('hex')
43
+ };
44
+ }
45
+ function boundedOptionalString(value, maxChars) {
46
+ if (value === undefined || value === null) {
47
+ return undefined;
48
+ }
49
+ return String(value).slice(0, maxChars);
50
+ }
51
+ function boundErrors(errors) {
52
+ return {
53
+ errors: errors.slice(0, 10).map((error) => String(error).slice(0, 512)),
54
+ errorsTruncated: errors.length > 10 || errors.some((error) => String(error).length > 512),
55
+ originalErrorCount: errors.length
56
+ };
57
+ }
16
58
  // Task Management Tools
17
59
  export const createTaskTool = {
18
60
  name: 'bitrix24_create_task',
@@ -122,6 +164,65 @@ export const getTaskTool = {
122
164
  required: ['id']
123
165
  }
124
166
  };
167
+ export const getTaskSummaryTool = {
168
+ name: 'bitrix24_get_task_summary',
169
+ description: 'Read a strict bounded task projection for workflow intake and routing. DESCRIPTION is capped at 12000 characters; TITLE is display metadata and must not be used as a routing signal.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ additionalProperties: false,
173
+ properties: {
174
+ taskId: {
175
+ type: 'string',
176
+ minLength: 1,
177
+ maxLength: 32,
178
+ pattern: '^\\d+$',
179
+ description: 'Task ID'
180
+ },
181
+ select: {
182
+ type: 'array',
183
+ minItems: 1,
184
+ maxItems: 12,
185
+ items: {
186
+ type: 'string',
187
+ enum: [
188
+ 'ID',
189
+ 'TITLE',
190
+ 'DESCRIPTION',
191
+ 'GROUP_ID',
192
+ 'STAGE_ID',
193
+ 'STATUS',
194
+ 'CHANGED_DATE',
195
+ 'CREATED_DATE',
196
+ 'RESPONSIBLE_ID',
197
+ 'DEADLINE',
198
+ 'PRIORITY',
199
+ 'PARENT_ID'
200
+ ]
201
+ },
202
+ description: 'Optional Bitrix task fields. Defaults to ID, TITLE, DESCRIPTION, GROUP_ID, STAGE_ID, STATUS, CHANGED_DATE, RESPONSIBLE_ID.'
203
+ }
204
+ },
205
+ required: ['taskId']
206
+ }
207
+ };
208
+ export const getTaskStageTool = {
209
+ name: 'bitrix24_get_task_stage',
210
+ description: 'Read the current task stage and minimal routing metadata without loading comments, description, or files.',
211
+ inputSchema: {
212
+ type: 'object',
213
+ additionalProperties: false,
214
+ properties: {
215
+ taskId: {
216
+ type: 'string',
217
+ minLength: 1,
218
+ maxLength: 32,
219
+ pattern: '^\\d+$',
220
+ description: 'Task ID'
221
+ }
222
+ },
223
+ required: ['taskId']
224
+ }
225
+ };
125
226
  export const listTasksTool = {
126
227
  name: 'bitrix24_list_tasks',
127
228
  description: 'List tasks with optional filtering and ordering',
@@ -203,7 +304,7 @@ export const getTaskCommentsTool = {
203
304
  description: 'Legacy comment FILTER (task.commentitem.getlist only)'
204
305
  },
205
306
  limit: {
206
- type: 'number',
307
+ type: 'integer',
207
308
  description: 'Max messages for chat API (1-50, default 50)',
208
309
  default: 50
209
310
  },
@@ -247,17 +348,86 @@ export const getTaskCommentsTool = {
247
348
  required: ['taskId']
248
349
  }
249
350
  };
351
+ export const getTaskCommentsAfterTool = {
352
+ name: 'bitrix24_get_comments_after',
353
+ description: 'Read compact comments newer than a known comment/message ID. Returns text and metadata only; never downloads attachments or file bytes.',
354
+ inputSchema: {
355
+ type: 'object',
356
+ additionalProperties: false,
357
+ properties: {
358
+ taskId: {
359
+ type: 'string',
360
+ minLength: 1,
361
+ maxLength: 32,
362
+ pattern: '^\\d+$',
363
+ description: 'Task ID'
364
+ },
365
+ afterCommentId: {
366
+ type: 'string',
367
+ minLength: 1,
368
+ maxLength: 32,
369
+ pattern: '^\\d+$',
370
+ description: 'Return only comments/messages whose numeric ID is greater than this watermark'
371
+ },
372
+ preferApi: {
373
+ type: 'string',
374
+ enum: ['auto', 'chat', 'legacy'],
375
+ description: 'API strategy',
376
+ default: 'auto'
377
+ },
378
+ limit: {
379
+ type: 'integer',
380
+ minimum: 1,
381
+ maximum: 20,
382
+ description: 'Maximum comments to inspect and return',
383
+ default: 20
384
+ },
385
+ includeSystemMessages: {
386
+ type: 'boolean',
387
+ description: 'Include system messages',
388
+ default: false
389
+ },
390
+ maxCommentChars: {
391
+ type: 'integer',
392
+ minimum: 256,
393
+ maximum: 8000,
394
+ description: 'Per-comment text cap. Truncated comments include originalChars and sha256.',
395
+ default: 4000
396
+ },
397
+ maxTotalChars: {
398
+ type: 'integer',
399
+ minimum: 512,
400
+ maximum: 32000,
401
+ description: 'Hard cap for text across the returned page.',
402
+ default: 16000
403
+ }
404
+ },
405
+ required: ['taskId', 'afterCommentId']
406
+ }
407
+ };
250
408
  export const addTaskCommentTool = {
251
409
  name: 'bitrix24_add_task_comment',
252
410
  description: 'Post a comment/message to a task. Modern path: tasks.task.chat.message.send + im.disk.file.commit; legacy fallback: task.commentitem.add.',
253
411
  inputSchema: {
254
412
  type: 'object',
413
+ additionalProperties: false,
255
414
  properties: {
256
- taskId: { type: 'string', description: 'Task ID' },
257
- text: { type: 'string', description: 'Comment text (required if no filePaths)' },
415
+ taskId: {
416
+ type: 'string',
417
+ minLength: 1,
418
+ maxLength: 32,
419
+ pattern: '^\\d+$',
420
+ description: 'Task ID'
421
+ },
422
+ text: {
423
+ type: 'string',
424
+ maxLength: 32000,
425
+ description: 'Comment text (required if no filePaths)'
426
+ },
258
427
  filePaths: {
259
428
  type: 'array',
260
- items: { type: 'string' },
429
+ maxItems: 12,
430
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
261
431
  description: 'Local files to upload and attach to the comment/message'
262
432
  },
263
433
  diskFolderId: {
@@ -308,16 +478,141 @@ export const getTaskFilesTool = {
308
478
  required: ['taskId']
309
479
  }
310
480
  };
481
+ export const getTaskFilesMetadataTool = {
482
+ name: 'bitrix24_get_task_files_metadata',
483
+ description: 'Read one bounded page of task attachment metadata without downloading file bytes or emitting base64.',
484
+ inputSchema: {
485
+ type: 'object',
486
+ additionalProperties: false,
487
+ properties: {
488
+ taskId: {
489
+ type: 'string',
490
+ minLength: 1,
491
+ maxLength: 32,
492
+ pattern: '^\\d+$',
493
+ description: 'Task ID'
494
+ },
495
+ diskFileIds: {
496
+ type: 'array',
497
+ maxItems: 50,
498
+ items: { type: 'string', minLength: 1, maxLength: 64 },
499
+ description: 'Optional exact disk file IDs; omitted means all task file references'
500
+ },
501
+ imagesOnly: {
502
+ type: 'boolean',
503
+ description: 'Return image metadata only',
504
+ default: false
505
+ },
506
+ afterFileId: {
507
+ type: 'string',
508
+ minLength: 1,
509
+ maxLength: 64,
510
+ description: 'Pagination watermark from nextAfterFileId'
511
+ },
512
+ limit: {
513
+ type: 'integer',
514
+ minimum: 1,
515
+ maximum: 25,
516
+ default: 25,
517
+ description: 'Maximum metadata records in one page'
518
+ }
519
+ },
520
+ required: ['taskId']
521
+ }
522
+ };
523
+ export const publishTaskCompositeTool = {
524
+ name: 'bitrix24_publish_task_composite',
525
+ description: 'Idempotently publish a bounded ordered set of final comments/evidence attachments and then move the task to one exact stage with read-back verification. Use one call for final workflow publication.',
526
+ inputSchema: {
527
+ type: 'object',
528
+ additionalProperties: false,
529
+ properties: {
530
+ taskId: {
531
+ type: 'string',
532
+ minLength: 1,
533
+ maxLength: 32,
534
+ pattern: '^\\d+$',
535
+ description: 'Task ID'
536
+ },
537
+ runId: {
538
+ type: 'string',
539
+ minLength: 1,
540
+ maxLength: 256,
541
+ description: 'Stable workflow run/dispatch ID'
542
+ },
543
+ evidenceSha256: {
544
+ type: 'string',
545
+ pattern: '^[a-fA-F0-9]{64}$',
546
+ description: 'SHA-256 of the evidence manifest being published'
547
+ },
548
+ comments: {
549
+ type: 'array',
550
+ maxItems: 12,
551
+ items: {
552
+ type: 'object',
553
+ additionalProperties: false,
554
+ properties: {
555
+ text: { type: 'string', minLength: 1, maxLength: 32000 },
556
+ filePaths: {
557
+ type: 'array',
558
+ maxItems: 12,
559
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
560
+ description: 'Optional local evidence files for this comment'
561
+ }
562
+ },
563
+ required: ['text']
564
+ }
565
+ },
566
+ targetStageId: {
567
+ type: 'string',
568
+ minLength: 1,
569
+ maxLength: 64,
570
+ description: 'Exact target stage ID'
571
+ },
572
+ expectedCurrentStageId: {
573
+ type: 'string',
574
+ minLength: 1,
575
+ maxLength: 64,
576
+ description: 'Optional compare-and-set guard for the current stage'
577
+ },
578
+ preferApi: {
579
+ type: 'string',
580
+ enum: ['auto', 'chat', 'legacy'],
581
+ default: 'auto'
582
+ },
583
+ diskFolderId: {
584
+ type: 'string',
585
+ description: 'Optional Bitrix Disk folder for evidence uploads'
586
+ }
587
+ },
588
+ required: [
589
+ 'taskId',
590
+ 'runId',
591
+ 'evidenceSha256',
592
+ 'comments',
593
+ 'targetStageId'
594
+ ]
595
+ }
596
+ };
311
597
  export const attachFilesToTaskTool = {
312
598
  name: 'bitrix24_attach_files_to_task',
313
599
  description: 'Upload local files (images, documents) to Bitrix24 Disk and attach them to an existing task',
314
600
  inputSchema: {
315
601
  type: 'object',
602
+ additionalProperties: false,
316
603
  properties: {
317
- taskId: { type: 'string', description: 'Task ID' },
604
+ taskId: {
605
+ type: 'string',
606
+ minLength: 1,
607
+ maxLength: 32,
608
+ pattern: '^\\d+$',
609
+ description: 'Task ID'
610
+ },
318
611
  filePaths: {
319
612
  type: 'array',
320
- items: { type: 'string' },
613
+ minItems: 1,
614
+ maxItems: 12,
615
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
321
616
  description: 'Local file paths to upload and attach'
322
617
  },
323
618
  diskFolderId: {
@@ -355,6 +650,92 @@ export const updateTaskTool = {
355
650
  required: ['id']
356
651
  }
357
652
  };
653
+ export const getTaskStagesTool = {
654
+ name: 'bitrix24_get_task_stages',
655
+ description: 'Get Kanban/My Plan stages for a Bitrix24 task group or user plan',
656
+ inputSchema: {
657
+ type: 'object',
658
+ properties: {
659
+ entityId: {
660
+ type: 'string',
661
+ description: 'Group/project ID for Kanban stages, or user ID for My Plan stages'
662
+ },
663
+ entityType: {
664
+ type: 'string',
665
+ enum: ['G', 'U'],
666
+ description: 'G=group/project Kanban, U=user My Plan',
667
+ default: 'G'
668
+ }
669
+ },
670
+ required: ['entityId']
671
+ }
672
+ };
673
+ export const canMoveTaskInStagesTool = {
674
+ name: 'bitrix24_can_move_task_in_stages',
675
+ description: 'Check whether the webhook user can move tasks in a Bitrix24 Kanban/My Plan object',
676
+ inputSchema: {
677
+ type: 'object',
678
+ properties: {
679
+ entityId: {
680
+ type: 'string',
681
+ description: 'Group/project ID for Kanban stages, or user ID for My Plan stages'
682
+ },
683
+ entityType: {
684
+ type: 'string',
685
+ enum: ['G', 'U'],
686
+ description: 'G=group/project Kanban, U=user My Plan',
687
+ default: 'G'
688
+ }
689
+ },
690
+ required: ['entityId']
691
+ }
692
+ };
693
+ export const moveTaskToStageTool = {
694
+ name: 'bitrix24_move_task_to_stage',
695
+ description: 'Move a task to another Kanban/My Plan stage using task.stages.movetask. Provide stageId directly or stageName with entityId.',
696
+ inputSchema: {
697
+ type: 'object',
698
+ additionalProperties: false,
699
+ properties: {
700
+ taskId: {
701
+ type: 'string',
702
+ minLength: 1,
703
+ maxLength: 32,
704
+ pattern: '^\\d+$',
705
+ description: 'Task ID'
706
+ },
707
+ stageId: {
708
+ type: 'string',
709
+ minLength: 1,
710
+ maxLength: 64,
711
+ description: 'Target stage ID'
712
+ },
713
+ stageName: {
714
+ type: 'string',
715
+ description: 'Target stage title. Used to resolve stageId when stageId is omitted.'
716
+ },
717
+ entityId: {
718
+ type: 'string',
719
+ description: 'Group/project ID for Kanban stages, or user ID for My Plan stages. Required when using stageName.'
720
+ },
721
+ entityType: {
722
+ type: 'string',
723
+ enum: ['G', 'U'],
724
+ description: 'G=group/project Kanban, U=user My Plan',
725
+ default: 'G'
726
+ },
727
+ before: {
728
+ type: 'string',
729
+ description: 'Optional task ID before which this task should be placed'
730
+ },
731
+ after: {
732
+ type: 'string',
733
+ description: 'Optional task ID after which this task should be placed'
734
+ }
735
+ },
736
+ required: ['taskId']
737
+ }
738
+ };
358
739
  export const addTaskChecklistItemTool = {
359
740
  name: 'bitrix24_add_task_checklist_item',
360
741
  description: 'Add a checklist item to a task',
@@ -618,16 +999,24 @@ export const resolveUserNamesTool = {
618
999
  required: ['userIds']
619
1000
  }
620
1001
  };
621
- /** Tools exposed via MCP ListTools (comments are included in bitrix24_get_task). */
622
- export const allTools = [
1002
+ const allToolCatalog = [
623
1003
  createTaskTool,
1004
+ getTaskSummaryTool,
1005
+ getTaskStageTool,
624
1006
  getTaskTool,
625
1007
  listTasksTool,
626
1008
  getLatestTasksTool,
627
1009
  getTasksFromDateRangeTool,
1010
+ getTaskCommentsTool,
1011
+ getTaskCommentsAfterTool,
628
1012
  updateTaskTool,
1013
+ getTaskStagesTool,
1014
+ canMoveTaskInStagesTool,
1015
+ moveTaskToStageTool,
629
1016
  getTaskFilesTool,
1017
+ getTaskFilesMetadataTool,
630
1018
  addTaskCommentTool,
1019
+ publishTaskCompositeTool,
631
1020
  attachFilesToTaskTool,
632
1021
  addTaskChecklistItemTool,
633
1022
  listTaskChecklistItemsTool,
@@ -647,8 +1036,35 @@ export const allTools = [
647
1036
  getAllUsersTool,
648
1037
  resolveUserNamesTool
649
1038
  ];
1039
+ const WORKFLOW_TOOL_NAMES = new Set([
1040
+ 'bitrix24_get_task_summary',
1041
+ 'bitrix24_get_task_stage',
1042
+ 'bitrix24_get_comments_after',
1043
+ 'bitrix24_get_task_files_metadata',
1044
+ 'bitrix24_publish_task_composite',
1045
+ 'bitrix24_add_task_comment',
1046
+ 'bitrix24_attach_files_to_task',
1047
+ 'bitrix24_move_task_to_stage'
1048
+ ]);
1049
+ export function toolsForProfile(profile = process.env.BITRIX24_TOOL_PROFILE) {
1050
+ const normalized = profile?.trim().toLowerCase();
1051
+ if (!normalized || normalized === 'full') {
1052
+ return [...allToolCatalog];
1053
+ }
1054
+ if (normalized === 'workflow') {
1055
+ return allToolCatalog.filter((tool) => WORKFLOW_TOOL_NAMES.has(tool.name));
1056
+ }
1057
+ throw new Error(`Unsupported BITRIX24_TOOL_PROFILE ${JSON.stringify(profile)}; expected full or workflow`);
1058
+ }
1059
+ /** Tools exposed via MCP ListTools for the configured deployment profile. */
1060
+ export const allTools = toolsForProfile();
650
1061
  export async function executeToolCall(name, args) {
651
1062
  try {
1063
+ const tool = allTools.find((candidate) => candidate.name === name);
1064
+ if (!tool) {
1065
+ throw new Error(`Tool is not exposed by the active profile: ${name}`);
1066
+ }
1067
+ args = validateToolArguments(tool, args ?? {});
652
1068
  switch (name) {
653
1069
  case 'bitrix24_create_task': {
654
1070
  const task = {
@@ -715,6 +1131,88 @@ export async function executeToolCall(name, args) {
715
1131
  message: `Task created with ID: ${taskId}`
716
1132
  };
717
1133
  }
1134
+ case 'bitrix24_get_task_summary': {
1135
+ const task = await bitrix24Client.getTaskSummary(args.taskId, Array.isArray(args.select) && args.select.length > 0
1136
+ ? args.select
1137
+ : undefined);
1138
+ return {
1139
+ success: true,
1140
+ task
1141
+ };
1142
+ }
1143
+ case 'bitrix24_get_task_stage': {
1144
+ const stage = await bitrix24Client.getTaskStage(args.taskId);
1145
+ return {
1146
+ success: true,
1147
+ ...stage
1148
+ };
1149
+ }
1150
+ case 'bitrix24_get_comments_after': {
1151
+ const afterCommentId = String(args.afterCommentId);
1152
+ const afterCommentNumber = Number(afterCommentId);
1153
+ if (!Number.isSafeInteger(afterCommentNumber)) {
1154
+ throw new Error('afterCommentId exceeds the supported safe integer range');
1155
+ }
1156
+ const limit = Math.min(20, Math.max(1, Math.trunc(Number(args.limit ?? 20))));
1157
+ const maxCommentChars = Math.min(8_000, Math.max(256, Number(args.maxCommentChars ?? 4_000)));
1158
+ const maxTotalChars = Math.min(32_000, Math.max(512, Number(args.maxTotalChars ?? 16_000)));
1159
+ const commentsResult = await bitrix24Client.getTaskComments(args.taskId, {
1160
+ preferApi: args.preferApi,
1161
+ firstId: afterCommentNumber,
1162
+ limit: Math.min(50, limit + 1),
1163
+ includeSystemMessages: args.includeSystemMessages === true,
1164
+ includeAttachments: false,
1165
+ includeContent: false
1166
+ });
1167
+ const candidates = commentsResult.comments
1168
+ .filter((comment) => /^\d{1,32}$/.test(String(comment.id)))
1169
+ .filter((comment) => compareNumericIds(comment.id, afterCommentId) > 0)
1170
+ .sort((left, right) => compareNumericIds(left.id, right.id));
1171
+ const comments = [];
1172
+ let totalCommentChars = 0;
1173
+ for (const comment of candidates) {
1174
+ if (comments.length >= limit) {
1175
+ break;
1176
+ }
1177
+ const remainingChars = maxTotalChars - totalCommentChars;
1178
+ if (remainingChars <= 0) {
1179
+ break;
1180
+ }
1181
+ const effectiveCap = Math.max(1, Math.min(maxCommentChars, remainingChars));
1182
+ const compacted = compactText(comment.postMessage, effectiveCap);
1183
+ const textLength = compacted.text?.length ?? 0;
1184
+ if (comments.length > 0 && textLength > remainingChars) {
1185
+ break;
1186
+ }
1187
+ comments.push({
1188
+ id: comment.id,
1189
+ authorId: boundedOptionalString(comment.authorId, 64),
1190
+ authorName: boundedOptionalString(comment.authorName, 256),
1191
+ postDate: boundedOptionalString(comment.postDate, 128),
1192
+ postMessage: compacted.text,
1193
+ truncated: compacted.truncated,
1194
+ originalChars: compacted.originalChars,
1195
+ sha256: compacted.sha256
1196
+ });
1197
+ totalCommentChars += textLength;
1198
+ }
1199
+ const nextAfterCommentId = comments.length > 0
1200
+ ? String(comments[comments.length - 1].id)
1201
+ : afterCommentId;
1202
+ const boundedErrors = boundErrors(commentsResult.errors);
1203
+ return {
1204
+ success: commentsResult.errors.length === 0,
1205
+ taskId: commentsResult.taskId,
1206
+ apiMode: commentsResult.apiMode,
1207
+ afterCommentId,
1208
+ nextAfterCommentId,
1209
+ hasMore: candidates.some((comment) => compareNumericIds(comment.id, nextAfterCommentId) > 0),
1210
+ pageSize: comments.length,
1211
+ totalCommentChars,
1212
+ comments,
1213
+ ...boundedErrors
1214
+ };
1215
+ }
718
1216
  case 'bitrix24_get_task_comments': {
719
1217
  const order = {};
720
1218
  order[args.orderBy || 'POST_DATE'] = args.orderDirection || 'asc';
@@ -812,6 +1310,25 @@ export async function executeToolCall(name, args) {
812
1310
  : `Retrieved ${filesResult.files.length} file(s) for task ${args.taskId}`
813
1311
  };
814
1312
  }
1313
+ case 'bitrix24_get_task_files_metadata': {
1314
+ const filesResult = await bitrix24Client.getTaskFilesMetadata(args.taskId, {
1315
+ diskFileIds: args.diskFileIds,
1316
+ imagesOnly: args.imagesOnly === true,
1317
+ afterFileId: args.afterFileId,
1318
+ limit: args.limit
1319
+ });
1320
+ const boundedErrors = boundErrors(filesResult.errors);
1321
+ return {
1322
+ success: filesResult.errors.length === 0,
1323
+ taskId: filesResult.taskId,
1324
+ afterFileId: filesResult.afterFileId,
1325
+ nextAfterFileId: filesResult.nextAfterFileId,
1326
+ hasMore: filesResult.hasMore,
1327
+ pageSize: filesResult.files.length,
1328
+ files: filesResult.files,
1329
+ ...boundedErrors
1330
+ };
1331
+ }
815
1332
  case 'bitrix24_attach_files_to_task': {
816
1333
  const attachmentResult = await bitrix24Client.attachLocalFilesToTask(args.taskId, args.filePaths, args.diskFolderId);
817
1334
  return {
@@ -824,6 +1341,22 @@ export async function executeToolCall(name, args) {
824
1341
  : `Attached ${attachmentResult.attached.length} file(s) with ${attachmentResult.errors.length} error(s)`
825
1342
  };
826
1343
  }
1344
+ case 'bitrix24_publish_task_composite': {
1345
+ const receipt = await publishTaskComposite(bitrix24Client, {
1346
+ taskId: args.taskId,
1347
+ runId: args.runId,
1348
+ evidenceSha256: args.evidenceSha256,
1349
+ comments: args.comments,
1350
+ targetStageId: args.targetStageId,
1351
+ expectedCurrentStageId: args.expectedCurrentStageId,
1352
+ preferApi: args.preferApi,
1353
+ diskFolderId: args.diskFolderId
1354
+ });
1355
+ return {
1356
+ success: true,
1357
+ receipt
1358
+ };
1359
+ }
827
1360
  case 'bitrix24_get_task': {
828
1361
  const details = await bitrix24Client.getTaskDetails(args.id, {
829
1362
  includeComments: args.includeComments,
@@ -924,6 +1457,70 @@ export async function executeToolCall(name, args) {
924
1457
  const updated = await bitrix24Client.updateTask(args.id, updateTask);
925
1458
  return { success: true, updated, message: `Task ${args.id} updated successfully` };
926
1459
  }
1460
+ case 'bitrix24_get_task_stages': {
1461
+ const entityType = args.entityType === 'U' ? 'U' : 'G';
1462
+ const stages = await bitrix24Client.getTaskStages(args.entityId, entityType);
1463
+ return {
1464
+ success: true,
1465
+ entityId: args.entityId,
1466
+ entityType,
1467
+ stages,
1468
+ message: `Found ${stages.length} stage(s) for ${entityType}:${args.entityId}`
1469
+ };
1470
+ }
1471
+ case 'bitrix24_can_move_task_in_stages': {
1472
+ const entityType = args.entityType === 'U' ? 'U' : 'G';
1473
+ const canMove = await bitrix24Client.canMoveTaskInStages(args.entityId, entityType);
1474
+ return {
1475
+ success: true,
1476
+ entityId: args.entityId,
1477
+ entityType,
1478
+ canMove,
1479
+ message: canMove
1480
+ ? `Tasks can be moved in ${entityType}:${args.entityId}`
1481
+ : `Tasks cannot be moved in ${entityType}:${args.entityId}`
1482
+ };
1483
+ }
1484
+ case 'bitrix24_move_task_to_stage': {
1485
+ const entityType = args.entityType === 'U' ? 'U' : 'G';
1486
+ let stageId = args.stageId;
1487
+ let resolvedStage;
1488
+ if (!stageId) {
1489
+ if (!args.stageName) {
1490
+ throw new Error('stageId or stageName is required');
1491
+ }
1492
+ if (!args.entityId) {
1493
+ throw new Error('entityId is required when resolving stageName');
1494
+ }
1495
+ const stages = await bitrix24Client.getTaskStages(args.entityId, entityType);
1496
+ const normalizedStageName = normalizeStageTitle(args.stageName);
1497
+ const matches = stages.filter((stage) => normalizeStageTitle(stage.TITLE) === normalizedStageName);
1498
+ if (matches.length === 0) {
1499
+ throw new Error(`Stage "${args.stageName}" not found in ${entityType}:${args.entityId}`);
1500
+ }
1501
+ if (matches.length > 1) {
1502
+ throw new Error(`Stage "${args.stageName}" is ambiguous in ${entityType}:${args.entityId}`);
1503
+ }
1504
+ resolvedStage = matches[0];
1505
+ stageId = resolvedStage.ID;
1506
+ }
1507
+ const moved = await bitrix24Client.moveTaskToStage({
1508
+ taskId: args.taskId,
1509
+ stageId,
1510
+ before: args.before,
1511
+ after: args.after
1512
+ });
1513
+ return {
1514
+ success: true,
1515
+ taskId: args.taskId,
1516
+ stageId,
1517
+ stageName: resolvedStage?.TITLE ?? args.stageName,
1518
+ moved,
1519
+ message: moved
1520
+ ? `Task ${args.taskId} moved to stage ${stageId}`
1521
+ : `Move request for task ${args.taskId} returned false`
1522
+ };
1523
+ }
927
1524
  case 'bitrix24_add_task_checklist_item': {
928
1525
  const checklistItemId = await bitrix24Client.addChecklistItem(args.taskId, {
929
1526
  TITLE: args.title,