bitrix24-tasks-mcp-server 1.5.2 → 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;
@@ -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: {
@@ -403,9 +695,21 @@ export const moveTaskToStageTool = {
403
695
  description: 'Move a task to another Kanban/My Plan stage using task.stages.movetask. Provide stageId directly or stageName with entityId.',
404
696
  inputSchema: {
405
697
  type: 'object',
698
+ additionalProperties: false,
406
699
  properties: {
407
- taskId: { type: 'string', description: 'Task ID' },
408
- stageId: { type: 'string', description: 'Target stage ID' },
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
+ },
409
713
  stageName: {
410
714
  type: 'string',
411
715
  description: 'Target stage title. Used to resolve stageId when stageId is omitted.'
@@ -695,19 +999,24 @@ export const resolveUserNamesTool = {
695
999
  required: ['userIds']
696
1000
  }
697
1001
  };
698
- /** Tools exposed via MCP ListTools (comments are included in bitrix24_get_task). */
699
- export const allTools = [
1002
+ const allToolCatalog = [
700
1003
  createTaskTool,
1004
+ getTaskSummaryTool,
1005
+ getTaskStageTool,
701
1006
  getTaskTool,
702
1007
  listTasksTool,
703
1008
  getLatestTasksTool,
704
1009
  getTasksFromDateRangeTool,
1010
+ getTaskCommentsTool,
1011
+ getTaskCommentsAfterTool,
705
1012
  updateTaskTool,
706
1013
  getTaskStagesTool,
707
1014
  canMoveTaskInStagesTool,
708
1015
  moveTaskToStageTool,
709
1016
  getTaskFilesTool,
1017
+ getTaskFilesMetadataTool,
710
1018
  addTaskCommentTool,
1019
+ publishTaskCompositeTool,
711
1020
  attachFilesToTaskTool,
712
1021
  addTaskChecklistItemTool,
713
1022
  listTaskChecklistItemsTool,
@@ -727,8 +1036,35 @@ export const allTools = [
727
1036
  getAllUsersTool,
728
1037
  resolveUserNamesTool
729
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();
730
1061
  export async function executeToolCall(name, args) {
731
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 ?? {});
732
1068
  switch (name) {
733
1069
  case 'bitrix24_create_task': {
734
1070
  const task = {
@@ -795,6 +1131,88 @@ export async function executeToolCall(name, args) {
795
1131
  message: `Task created with ID: ${taskId}`
796
1132
  };
797
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
+ }
798
1216
  case 'bitrix24_get_task_comments': {
799
1217
  const order = {};
800
1218
  order[args.orderBy || 'POST_DATE'] = args.orderDirection || 'asc';
@@ -892,6 +1310,25 @@ export async function executeToolCall(name, args) {
892
1310
  : `Retrieved ${filesResult.files.length} file(s) for task ${args.taskId}`
893
1311
  };
894
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
+ }
895
1332
  case 'bitrix24_attach_files_to_task': {
896
1333
  const attachmentResult = await bitrix24Client.attachLocalFilesToTask(args.taskId, args.filePaths, args.diskFolderId);
897
1334
  return {
@@ -904,6 +1341,22 @@ export async function executeToolCall(name, args) {
904
1341
  : `Attached ${attachmentResult.attached.length} file(s) with ${attachmentResult.errors.length} error(s)`
905
1342
  };
906
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
+ }
907
1360
  case 'bitrix24_get_task': {
908
1361
  const details = await bitrix24Client.getTaskDetails(args.id, {
909
1362
  includeComments: args.includeComments,