@x0333/bitrix24-mcp-server 2.2.0 → 2.3.0
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.
- package/README.md +2 -2
- package/index.js +171 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ MCP-сервер для работы с Bitrix24 через входящий в
|
|
|
16
16
|
- **`title`** — подстрока в названии (`%TITLE` в фильтре);
|
|
17
17
|
- **`stage_id`** — ID колонки канбана задач (`STAGE_ID` в фильтре).
|
|
18
18
|
Можно передать оба параметра одновременно (условия объединяются). Дополнительно: **`order`**, **`dir`** (сортировка), **`start`** (смещение для пагинации).
|
|
19
|
-
- **search_groups** — рабочие группы и проекты по подстроке имени (`
|
|
20
|
-
- **get_group** —
|
|
19
|
+
- **search_groups** — рабочие группы и проекты по подстроке имени (`sonet_group.get.json`, фильтр `%NAME`).
|
|
20
|
+
- **get_group** — данные группы или проекта по ID (`sonet_group.get.json`, фильтр `ID`).
|
|
21
21
|
- **get_kanban_stages_by_group** — стадии канбана задач для группы по её ID (`task.stages.get`, в теле запроса `entityId` = ID группы).
|
|
22
22
|
|
|
23
23
|
Все вызовы к Bitrix24 идут как `POST` на `{B24_BASE}/{метод}` с JSON-телом, как в документации REST.
|
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ if (!B24_BASE) {
|
|
|
27
27
|
const server = new Server(
|
|
28
28
|
{
|
|
29
29
|
name: "bitrix24-mcp-server",
|
|
30
|
-
version: "2.
|
|
30
|
+
version: "2.3.0",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
capabilities: {
|
|
@@ -36,11 +36,23 @@ const server = new Server(
|
|
|
36
36
|
}
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* If B24_BASE is legacy .../rest/{user}/{webhook}/ (without /api/), new task fields
|
|
41
|
+
* like chatId are returned from .../rest/api/{user}/{webhook}/ only.
|
|
42
|
+
*/
|
|
43
|
+
function bitrixRestApiBaseFromLegacy() {
|
|
44
|
+
const b = B24_BASE.replace(/\/$/, "");
|
|
45
|
+
if (/\/rest\/api\//.test(b)) return null;
|
|
46
|
+
if (/\/rest\/\d+\//.test(b)) return b.replace("/rest/", "/rest/api/");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/**
|
|
40
51
|
* Helper to call Bitrix24 REST API
|
|
52
|
+
* @param {string} [baseUrl] — override base (e.g. rest/api for tasks.task.get)
|
|
41
53
|
*/
|
|
42
|
-
async function callBitrix(method, body) {
|
|
43
|
-
const url = `${B24_BASE.replace(/\/$/, "")}/${method}`;
|
|
54
|
+
async function callBitrix(method, body, baseUrl) {
|
|
55
|
+
const url = `${(baseUrl || B24_BASE).replace(/\/$/, "")}/${method}`;
|
|
44
56
|
try {
|
|
45
57
|
const response = await axios.post(url, body, {
|
|
46
58
|
headers: { "Content-Type": "application/json" },
|
|
@@ -54,6 +66,48 @@ async function callBitrix(method, body) {
|
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
const IM_MESSAGES_LIMIT_MAX = 50;
|
|
70
|
+
|
|
71
|
+
function pickTaskItem(taskGetResult) {
|
|
72
|
+
const r = taskGetResult?.result;
|
|
73
|
+
if (!r) return null;
|
|
74
|
+
return r.item ?? r.task ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveTaskChatId(item) {
|
|
78
|
+
if (!item || typeof item !== "object") return null;
|
|
79
|
+
const cid = item.chatId ?? item.CHAT_ID ?? item.chat?.id ?? item.chat?.ID;
|
|
80
|
+
return cid != null && cid !== "" ? Number(cid) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function slimTaskChatMessages(messages) {
|
|
84
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
85
|
+
return list.map((m) => ({
|
|
86
|
+
id: m.id,
|
|
87
|
+
author_id: m.author_id,
|
|
88
|
+
text: m.text,
|
|
89
|
+
date: m.date,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slimImUsers(users) {
|
|
94
|
+
const list = Array.isArray(users) ? users : [];
|
|
95
|
+
return list.map((u) => ({
|
|
96
|
+
id: u.id,
|
|
97
|
+
name: u.name,
|
|
98
|
+
work_position: u.work_position ?? null,
|
|
99
|
+
email: u.email ?? null,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function throwIfBitrixError(data, context) {
|
|
104
|
+
if (!data || typeof data !== "object") return;
|
|
105
|
+
if (!Object.prototype.hasOwnProperty.call(data, "error") || data.error == null) return;
|
|
106
|
+
const code = typeof data.error === "object" ? JSON.stringify(data.error) : String(data.error);
|
|
107
|
+
const desc = data.error_description != null ? String(data.error_description) : "";
|
|
108
|
+
throw new Error(`${context}: ${code}${desc ? ` — ${desc}` : ""}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
/**
|
|
58
112
|
* Define available tools
|
|
59
113
|
*/
|
|
@@ -109,7 +163,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
109
163
|
type: "object",
|
|
110
164
|
properties: {
|
|
111
165
|
name: { type: "string", description: "Substring of the group name to search for" },
|
|
112
|
-
start: { type: "number", description: "Pagination offset (default: 0)" },
|
|
113
166
|
},
|
|
114
167
|
required: ["name"],
|
|
115
168
|
},
|
|
@@ -137,6 +190,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
137
190
|
required: ["id"],
|
|
138
191
|
},
|
|
139
192
|
},
|
|
193
|
+
{
|
|
194
|
+
name: "get_task_comments",
|
|
195
|
+
description:
|
|
196
|
+
"Comments for a task on the new task card (module tasks 25.700.0+): messages come from the task chat via im.dialog.messages.get, newest first when no cursors are passed. " +
|
|
197
|
+
"Arrays are trimmed: messages only id, author_id, text, date; users only id, name, work_position, email. " +
|
|
198
|
+
"author_id 0 means system/service chat messages (stage changes, time tracking, joins, etc.) — not a user comment; do not expect a matching row in users. " +
|
|
199
|
+
"For author_id > 0, match message.author_id to users[].id to resolve author name. " +
|
|
200
|
+
"ИИ: author_id 0 = системные сообщения чата (не комментарий человека), в users не сопоставлять; для author_id > 0 сопоставляй с users[].id. В теле ответа см. agent_instructions. " +
|
|
201
|
+
"Pagination: limit 1–50 (default 20). first_id = Bitrix FIRST_ID (next page of older messages). last_id = Bitrix LAST_ID (messages newer than id). Do not send both first_id and last_id.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
task_id: { type: "number", description: "Task ID" },
|
|
206
|
+
limit: {
|
|
207
|
+
type: "number",
|
|
208
|
+
description: `Page size (1–${IM_MESSAGES_LIMIT_MAX}, default 20). Bitrix im.dialog.messages.get LIMIT`,
|
|
209
|
+
},
|
|
210
|
+
first_id: {
|
|
211
|
+
type: "number",
|
|
212
|
+
description:
|
|
213
|
+
"Optional. Bitrix FIRST_ID: load messages older than this id (next page toward history). Typically set to the smallest message id from the previous response.",
|
|
214
|
+
},
|
|
215
|
+
last_id: {
|
|
216
|
+
type: "number",
|
|
217
|
+
description:
|
|
218
|
+
"Optional. Bitrix LAST_ID: load messages newer than this id. Do not combine with first_id.",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ["task_id"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
140
224
|
],
|
|
141
225
|
};
|
|
142
226
|
});
|
|
@@ -186,17 +270,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
186
270
|
|
|
187
271
|
case "search_groups": {
|
|
188
272
|
const body = {
|
|
189
|
-
|
|
190
|
-
order: { ID: "DESC" },
|
|
191
|
-
start: args.start || 0,
|
|
273
|
+
FILTER: { "%NAME": args.name },
|
|
192
274
|
};
|
|
193
|
-
const data = await callBitrix("
|
|
275
|
+
const data = await callBitrix("sonet_group.get.json", body);
|
|
194
276
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
195
277
|
}
|
|
196
278
|
|
|
197
279
|
case "get_group": {
|
|
198
|
-
const body = {
|
|
199
|
-
const data = await callBitrix("
|
|
280
|
+
const body = { FILTER: { ID: args.id } };
|
|
281
|
+
const data = await callBitrix("sonet_group.get.json", body);
|
|
200
282
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
201
283
|
}
|
|
202
284
|
|
|
@@ -206,6 +288,85 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
206
288
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
207
289
|
}
|
|
208
290
|
|
|
291
|
+
case "get_task_comments": {
|
|
292
|
+
const taskId = Number(args.task_id);
|
|
293
|
+
if (!Number.isFinite(taskId) || taskId <= 0) {
|
|
294
|
+
throw new Error("get_task_comments: task_id must be a positive number");
|
|
295
|
+
}
|
|
296
|
+
const hasFirst = args.first_id !== undefined && args.first_id !== null;
|
|
297
|
+
const hasLast = args.last_id !== undefined && args.last_id !== null;
|
|
298
|
+
if (hasFirst && hasLast) {
|
|
299
|
+
throw new Error("get_task_comments: pass only one of first_id or last_id, not both");
|
|
300
|
+
}
|
|
301
|
+
let limit = args.limit === undefined || args.limit === null ? 20 : Number(args.limit);
|
|
302
|
+
if (!Number.isFinite(limit)) limit = 20;
|
|
303
|
+
limit = Math.min(IM_MESSAGES_LIMIT_MAX, Math.max(1, Math.floor(limit)));
|
|
304
|
+
|
|
305
|
+
const taskSelect = ["id", "chatId", "chat.id"];
|
|
306
|
+
let taskRaw = await callBitrix("tasks.task.get", {
|
|
307
|
+
id: taskId,
|
|
308
|
+
select: taskSelect,
|
|
309
|
+
});
|
|
310
|
+
throwIfBitrixError(taskRaw, "tasks.task.get");
|
|
311
|
+
let item = pickTaskItem(taskRaw);
|
|
312
|
+
let chatId = resolveTaskChatId(item);
|
|
313
|
+
const apiBase = bitrixRestApiBaseFromLegacy();
|
|
314
|
+
if (!chatId && apiBase) {
|
|
315
|
+
taskRaw = await callBitrix(
|
|
316
|
+
"tasks.task.get",
|
|
317
|
+
{ id: taskId, select: taskSelect },
|
|
318
|
+
apiBase
|
|
319
|
+
);
|
|
320
|
+
throwIfBitrixError(taskRaw, "tasks.task.get (rest/api)");
|
|
321
|
+
item = pickTaskItem(taskRaw);
|
|
322
|
+
chatId = resolveTaskChatId(item);
|
|
323
|
+
}
|
|
324
|
+
if (!chatId) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
"get_task_comments: task has no chatId (new-card comments unavailable, no access, or set B24_BASE to .../rest/api/...)"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const imBody = {
|
|
331
|
+
DIALOG_ID: `chat${chatId}`,
|
|
332
|
+
LIMIT: limit,
|
|
333
|
+
};
|
|
334
|
+
if (hasFirst) imBody.FIRST_ID = Number(args.first_id);
|
|
335
|
+
if (hasLast) imBody.LAST_ID = Number(args.last_id);
|
|
336
|
+
|
|
337
|
+
const imRaw = await callBitrix("im.dialog.messages.get", imBody);
|
|
338
|
+
throwIfBitrixError(imRaw, "im.dialog.messages.get");
|
|
339
|
+
const imResult = imRaw.result || {};
|
|
340
|
+
const rawMessages = Array.isArray(imResult.messages) ? imResult.messages : [];
|
|
341
|
+
const slimMessages = slimTaskChatMessages(rawMessages);
|
|
342
|
+
const ids = slimMessages.map((m) => m.id).filter((id) => typeof id === "number");
|
|
343
|
+
const minId = ids.length ? Math.min(...ids) : null;
|
|
344
|
+
const maxId = ids.length ? Math.max(...ids) : null;
|
|
345
|
+
|
|
346
|
+
const payload = {
|
|
347
|
+
task_id: taskId,
|
|
348
|
+
chat_id: imResult.chat_id ?? chatId,
|
|
349
|
+
messages: slimMessages,
|
|
350
|
+
users: slimImUsers(imResult.users),
|
|
351
|
+
pagination: {
|
|
352
|
+
limit,
|
|
353
|
+
first_id_sent: hasFirst ? Number(args.first_id) : null,
|
|
354
|
+
last_id_sent: hasLast ? Number(args.last_id) : null,
|
|
355
|
+
suggested_next_first_id:
|
|
356
|
+
!hasLast && slimMessages.length > 0 ? minId : null,
|
|
357
|
+
suggested_next_last_id:
|
|
358
|
+
!hasFirst && slimMessages.length > 0 ? maxId : null,
|
|
359
|
+
note:
|
|
360
|
+
"If suggested_next_first_id is set and you need older messages, call again with first_id equal to that value. If suggested_next_last_id is set and you need newer messages, call again with last_id equal to that value. Empty page or short page means no more in that direction (heuristic).",
|
|
361
|
+
},
|
|
362
|
+
agent_instructions:
|
|
363
|
+
"author_id 0 — это системные сообщения чата Битрикс24 (смена стадии, учёт времени, приглашения и т.п.), а не пользовательский комментарий; не ищи для них запись в users. " +
|
|
364
|
+
"Для author_id > 0 сопоставь author_id с users[].id и бери имя/должность из найденного user. Текст сообщения в messages[].text.",
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
368
|
+
}
|
|
369
|
+
|
|
209
370
|
default:
|
|
210
371
|
throw new Error(`Unknown tool: ${name}`);
|
|
211
372
|
}
|