bitrix24-tasks-mcp-server 1.5.2 → 1.6.3

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;
@@ -16,6 +19,42 @@ function collectFilesWithContent(...groups) {
16
19
  function normalizeStageTitle(value) {
17
20
  return String(value ?? '').trim().toLocaleLowerCase('ru-RU');
18
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
+ }
19
58
  // Task Management Tools
20
59
  export const createTaskTool = {
21
60
  name: 'bitrix24_create_task',
@@ -125,6 +164,65 @@ export const getTaskTool = {
125
164
  required: ['id']
126
165
  }
127
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
+ };
128
226
  export const listTasksTool = {
129
227
  name: 'bitrix24_list_tasks',
130
228
  description: 'List tasks with optional filtering and ordering',
@@ -206,7 +304,7 @@ export const getTaskCommentsTool = {
206
304
  description: 'Legacy comment FILTER (task.commentitem.getlist only)'
207
305
  },
208
306
  limit: {
209
- type: 'number',
307
+ type: 'integer',
210
308
  description: 'Max messages for chat API (1-50, default 50)',
211
309
  default: 50
212
310
  },
@@ -250,17 +348,86 @@ export const getTaskCommentsTool = {
250
348
  required: ['taskId']
251
349
  }
252
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
+ };
253
408
  export const addTaskCommentTool = {
254
409
  name: 'bitrix24_add_task_comment',
255
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.',
256
411
  inputSchema: {
257
412
  type: 'object',
413
+ additionalProperties: false,
258
414
  properties: {
259
- taskId: { type: 'string', description: 'Task ID' },
260
- 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
+ },
261
427
  filePaths: {
262
428
  type: 'array',
263
- items: { type: 'string' },
429
+ maxItems: 12,
430
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
264
431
  description: 'Local files to upload and attach to the comment/message'
265
432
  },
266
433
  diskFolderId: {
@@ -311,16 +478,141 @@ export const getTaskFilesTool = {
311
478
  required: ['taskId']
312
479
  }
313
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
+ };
314
597
  export const attachFilesToTaskTool = {
315
598
  name: 'bitrix24_attach_files_to_task',
316
599
  description: 'Upload local files (images, documents) to Bitrix24 Disk and attach them to an existing task',
317
600
  inputSchema: {
318
601
  type: 'object',
602
+ additionalProperties: false,
319
603
  properties: {
320
- 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
+ },
321
611
  filePaths: {
322
612
  type: 'array',
323
- items: { type: 'string' },
613
+ minItems: 1,
614
+ maxItems: 12,
615
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
324
616
  description: 'Local file paths to upload and attach'
325
617
  },
326
618
  diskFolderId: {
@@ -358,6 +650,32 @@ export const updateTaskTool = {
358
650
  required: ['id']
359
651
  }
360
652
  };
653
+ export const updateTaskCustomFieldsTool = {
654
+ name: 'bitrix24_update_task_custom_fields',
655
+ description: 'Update one or more Bitrix24 task custom fields (UF_TASK_*). Reads current values first, protects populated fields by default, and verifies the write.',
656
+ inputSchema: {
657
+ type: 'object',
658
+ additionalProperties: false,
659
+ properties: {
660
+ taskId: {
661
+ type: 'string',
662
+ minLength: 1,
663
+ maxLength: 32,
664
+ pattern: '^\\d+$',
665
+ description: 'Bitrix24 task ID'
666
+ },
667
+ fields: {
668
+ type: 'object',
669
+ description: 'Map of UF_TASK_* field codes to Bitrix24-compatible scalar, array, object, or null values (1-20 fields)'
670
+ },
671
+ overwriteExisting: {
672
+ type: 'boolean',
673
+ description: 'Allow replacing non-empty current values. Defaults to false.'
674
+ }
675
+ },
676
+ required: ['taskId', 'fields']
677
+ }
678
+ };
361
679
  export const getTaskStagesTool = {
362
680
  name: 'bitrix24_get_task_stages',
363
681
  description: 'Get Kanban/My Plan stages for a Bitrix24 task group or user plan',
@@ -403,9 +721,21 @@ export const moveTaskToStageTool = {
403
721
  description: 'Move a task to another Kanban/My Plan stage using task.stages.movetask. Provide stageId directly or stageName with entityId.',
404
722
  inputSchema: {
405
723
  type: 'object',
724
+ additionalProperties: false,
406
725
  properties: {
407
- taskId: { type: 'string', description: 'Task ID' },
408
- stageId: { type: 'string', description: 'Target stage ID' },
726
+ taskId: {
727
+ type: 'string',
728
+ minLength: 1,
729
+ maxLength: 32,
730
+ pattern: '^\\d+$',
731
+ description: 'Task ID'
732
+ },
733
+ stageId: {
734
+ type: 'string',
735
+ minLength: 1,
736
+ maxLength: 64,
737
+ description: 'Target stage ID'
738
+ },
409
739
  stageName: {
410
740
  type: 'string',
411
741
  description: 'Target stage title. Used to resolve stageId when stageId is omitted.'
@@ -695,19 +1025,25 @@ export const resolveUserNamesTool = {
695
1025
  required: ['userIds']
696
1026
  }
697
1027
  };
698
- /** Tools exposed via MCP ListTools (comments are included in bitrix24_get_task). */
699
- export const allTools = [
1028
+ const allToolCatalog = [
700
1029
  createTaskTool,
1030
+ getTaskSummaryTool,
1031
+ getTaskStageTool,
701
1032
  getTaskTool,
702
1033
  listTasksTool,
703
1034
  getLatestTasksTool,
704
1035
  getTasksFromDateRangeTool,
1036
+ getTaskCommentsTool,
1037
+ getTaskCommentsAfterTool,
705
1038
  updateTaskTool,
1039
+ updateTaskCustomFieldsTool,
706
1040
  getTaskStagesTool,
707
1041
  canMoveTaskInStagesTool,
708
1042
  moveTaskToStageTool,
709
1043
  getTaskFilesTool,
1044
+ getTaskFilesMetadataTool,
710
1045
  addTaskCommentTool,
1046
+ publishTaskCompositeTool,
711
1047
  attachFilesToTaskTool,
712
1048
  addTaskChecklistItemTool,
713
1049
  listTaskChecklistItemsTool,
@@ -727,8 +1063,36 @@ export const allTools = [
727
1063
  getAllUsersTool,
728
1064
  resolveUserNamesTool
729
1065
  ];
1066
+ const WORKFLOW_TOOL_NAMES = new Set([
1067
+ 'bitrix24_get_task_summary',
1068
+ 'bitrix24_get_task_stage',
1069
+ 'bitrix24_get_comments_after',
1070
+ 'bitrix24_get_task_files_metadata',
1071
+ 'bitrix24_publish_task_composite',
1072
+ 'bitrix24_add_task_comment',
1073
+ 'bitrix24_attach_files_to_task',
1074
+ 'bitrix24_move_task_to_stage',
1075
+ 'bitrix24_update_task_custom_fields'
1076
+ ]);
1077
+ export function toolsForProfile(profile = process.env.BITRIX24_TOOL_PROFILE) {
1078
+ const normalized = profile?.trim().toLowerCase();
1079
+ if (!normalized || normalized === 'full') {
1080
+ return [...allToolCatalog];
1081
+ }
1082
+ if (normalized === 'workflow') {
1083
+ return allToolCatalog.filter((tool) => WORKFLOW_TOOL_NAMES.has(tool.name));
1084
+ }
1085
+ throw new Error(`Unsupported BITRIX24_TOOL_PROFILE ${JSON.stringify(profile)}; expected full or workflow`);
1086
+ }
1087
+ /** Tools exposed via MCP ListTools for the configured deployment profile. */
1088
+ export const allTools = toolsForProfile();
730
1089
  export async function executeToolCall(name, args) {
731
1090
  try {
1091
+ const tool = allTools.find((candidate) => candidate.name === name);
1092
+ if (!tool) {
1093
+ throw new Error(`Tool is not exposed by the active profile: ${name}`);
1094
+ }
1095
+ args = validateToolArguments(tool, args ?? {});
732
1096
  switch (name) {
733
1097
  case 'bitrix24_create_task': {
734
1098
  const task = {
@@ -795,6 +1159,88 @@ export async function executeToolCall(name, args) {
795
1159
  message: `Task created with ID: ${taskId}`
796
1160
  };
797
1161
  }
1162
+ case 'bitrix24_get_task_summary': {
1163
+ const task = await bitrix24Client.getTaskSummary(args.taskId, Array.isArray(args.select) && args.select.length > 0
1164
+ ? args.select
1165
+ : undefined);
1166
+ return {
1167
+ success: true,
1168
+ task
1169
+ };
1170
+ }
1171
+ case 'bitrix24_get_task_stage': {
1172
+ const stage = await bitrix24Client.getTaskStage(args.taskId);
1173
+ return {
1174
+ success: true,
1175
+ ...stage
1176
+ };
1177
+ }
1178
+ case 'bitrix24_get_comments_after': {
1179
+ const afterCommentId = String(args.afterCommentId);
1180
+ const afterCommentNumber = Number(afterCommentId);
1181
+ if (!Number.isSafeInteger(afterCommentNumber)) {
1182
+ throw new Error('afterCommentId exceeds the supported safe integer range');
1183
+ }
1184
+ const limit = Math.min(20, Math.max(1, Math.trunc(Number(args.limit ?? 20))));
1185
+ const maxCommentChars = Math.min(8_000, Math.max(256, Number(args.maxCommentChars ?? 4_000)));
1186
+ const maxTotalChars = Math.min(32_000, Math.max(512, Number(args.maxTotalChars ?? 16_000)));
1187
+ const commentsResult = await bitrix24Client.getTaskComments(args.taskId, {
1188
+ preferApi: args.preferApi,
1189
+ firstId: afterCommentNumber,
1190
+ limit: Math.min(50, limit + 1),
1191
+ includeSystemMessages: args.includeSystemMessages === true,
1192
+ includeAttachments: false,
1193
+ includeContent: false
1194
+ });
1195
+ const candidates = commentsResult.comments
1196
+ .filter((comment) => /^\d{1,32}$/.test(String(comment.id)))
1197
+ .filter((comment) => compareNumericIds(comment.id, afterCommentId) > 0)
1198
+ .sort((left, right) => compareNumericIds(left.id, right.id));
1199
+ const comments = [];
1200
+ let totalCommentChars = 0;
1201
+ for (const comment of candidates) {
1202
+ if (comments.length >= limit) {
1203
+ break;
1204
+ }
1205
+ const remainingChars = maxTotalChars - totalCommentChars;
1206
+ if (remainingChars <= 0) {
1207
+ break;
1208
+ }
1209
+ const effectiveCap = Math.max(1, Math.min(maxCommentChars, remainingChars));
1210
+ const compacted = compactText(comment.postMessage, effectiveCap);
1211
+ const textLength = compacted.text?.length ?? 0;
1212
+ if (comments.length > 0 && textLength > remainingChars) {
1213
+ break;
1214
+ }
1215
+ comments.push({
1216
+ id: comment.id,
1217
+ authorId: boundedOptionalString(comment.authorId, 64),
1218
+ authorName: boundedOptionalString(comment.authorName, 256),
1219
+ postDate: boundedOptionalString(comment.postDate, 128),
1220
+ postMessage: compacted.text,
1221
+ truncated: compacted.truncated,
1222
+ originalChars: compacted.originalChars,
1223
+ sha256: compacted.sha256
1224
+ });
1225
+ totalCommentChars += textLength;
1226
+ }
1227
+ const nextAfterCommentId = comments.length > 0
1228
+ ? String(comments[comments.length - 1].id)
1229
+ : afterCommentId;
1230
+ const boundedErrors = boundErrors(commentsResult.errors);
1231
+ return {
1232
+ success: commentsResult.errors.length === 0,
1233
+ taskId: commentsResult.taskId,
1234
+ apiMode: commentsResult.apiMode,
1235
+ afterCommentId,
1236
+ nextAfterCommentId,
1237
+ hasMore: candidates.some((comment) => compareNumericIds(comment.id, nextAfterCommentId) > 0),
1238
+ pageSize: comments.length,
1239
+ totalCommentChars,
1240
+ comments,
1241
+ ...boundedErrors
1242
+ };
1243
+ }
798
1244
  case 'bitrix24_get_task_comments': {
799
1245
  const order = {};
800
1246
  order[args.orderBy || 'POST_DATE'] = args.orderDirection || 'asc';
@@ -892,6 +1338,25 @@ export async function executeToolCall(name, args) {
892
1338
  : `Retrieved ${filesResult.files.length} file(s) for task ${args.taskId}`
893
1339
  };
894
1340
  }
1341
+ case 'bitrix24_get_task_files_metadata': {
1342
+ const filesResult = await bitrix24Client.getTaskFilesMetadata(args.taskId, {
1343
+ diskFileIds: args.diskFileIds,
1344
+ imagesOnly: args.imagesOnly === true,
1345
+ afterFileId: args.afterFileId,
1346
+ limit: args.limit
1347
+ });
1348
+ const boundedErrors = boundErrors(filesResult.errors);
1349
+ return {
1350
+ success: filesResult.errors.length === 0,
1351
+ taskId: filesResult.taskId,
1352
+ afterFileId: filesResult.afterFileId,
1353
+ nextAfterFileId: filesResult.nextAfterFileId,
1354
+ hasMore: filesResult.hasMore,
1355
+ pageSize: filesResult.files.length,
1356
+ files: filesResult.files,
1357
+ ...boundedErrors
1358
+ };
1359
+ }
895
1360
  case 'bitrix24_attach_files_to_task': {
896
1361
  const attachmentResult = await bitrix24Client.attachLocalFilesToTask(args.taskId, args.filePaths, args.diskFolderId);
897
1362
  return {
@@ -904,6 +1369,22 @@ export async function executeToolCall(name, args) {
904
1369
  : `Attached ${attachmentResult.attached.length} file(s) with ${attachmentResult.errors.length} error(s)`
905
1370
  };
906
1371
  }
1372
+ case 'bitrix24_publish_task_composite': {
1373
+ const receipt = await publishTaskComposite(bitrix24Client, {
1374
+ taskId: args.taskId,
1375
+ runId: args.runId,
1376
+ evidenceSha256: args.evidenceSha256,
1377
+ comments: args.comments,
1378
+ targetStageId: args.targetStageId,
1379
+ expectedCurrentStageId: args.expectedCurrentStageId,
1380
+ preferApi: args.preferApi,
1381
+ diskFolderId: args.diskFolderId
1382
+ });
1383
+ return {
1384
+ success: true,
1385
+ receipt
1386
+ };
1387
+ }
907
1388
  case 'bitrix24_get_task': {
908
1389
  const details = await bitrix24Client.getTaskDetails(args.id, {
909
1390
  includeComments: args.includeComments,
@@ -1004,6 +1485,16 @@ export async function executeToolCall(name, args) {
1004
1485
  const updated = await bitrix24Client.updateTask(args.id, updateTask);
1005
1486
  return { success: true, updated, message: `Task ${args.id} updated successfully` };
1006
1487
  }
1488
+ case 'bitrix24_update_task_custom_fields': {
1489
+ const update = await bitrix24Client.updateTaskCustomFields(args.taskId, args.fields, args.overwriteExisting === true);
1490
+ return {
1491
+ success: true,
1492
+ update,
1493
+ message: update.changed
1494
+ ? `Updated ${update.updatedFields.length} custom field(s) on task ${update.taskId}`
1495
+ : `Requested custom fields on task ${update.taskId} were already up to date`
1496
+ };
1497
+ }
1007
1498
  case 'bitrix24_get_task_stages': {
1008
1499
  const entityType = args.entityType === 'U' ? 'U' : 'G';
1009
1500
  const stages = await bitrix24Client.getTaskStages(args.entityId, entityType);