@retensy/mcp 0.15.2 → 0.16.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/package.json +1 -1
- package/src/index.mjs +54 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "MCP server to build and publish Telegram and MAX bot funnels/automations, broadcasts, service integrations, block-based websites (incl. free-layout Zero blocks, designs, templates, custom domains, form leads) and Markdown blog articles in the Retensy Bots service. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import os from "node:os";
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
|
|
30
|
-
const VERSION = "0.
|
|
30
|
+
const VERSION = "0.16.0";
|
|
31
31
|
/** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
|
|
32
32
|
const SITE_INITS = ["starter", "blank", "mini-landing"];
|
|
33
33
|
/** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
|
|
@@ -648,7 +648,7 @@ const TOOLS = [
|
|
|
648
648
|
{ name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
649
649
|
{ name: "bot_resume", description: "Запустить остановленного бота или бота, приостановленного лимитом тарифа (POST /api/bots/{botId}/resume). Если лимит ботов тарифа исчерпан — HTTP 402 со ссылкой на смену тарифа.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
650
650
|
// ---- Подключения ----
|
|
651
|
-
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля
|
|
651
|
+
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Остальные ~25 сервисов (smsru, retailcrm, cloudpayments, rest_api, yandex_market и т.д.) берутся из каталога Integration Core (integration_catalog) — provider передавай его provider-ключом в НИЖНЕМ регистре, нужные creds — из configSchema каждого провайдера (hint подскажет, где взять; поле с «необязательно»/optional в hint можно не слать). Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Провайдер каталога со статусом COMING_SOON/IN_DEVELOPMENT подключить нельзя (вернёт ошибку). Если каталог недоступен (офлайн/выключен) — работают только 5 легаси-провайдеров выше. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM, либо провайдер из integration_catalog (его ключ в нижнем регистре, напр. smsru, retailcrm, cloudpayments)" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля провайдера: для легаси — см. описание, для остальных — ключи из configSchema провайдера в integration_catalog" }, connectionId: { type: "string", description: "id существующего подключения (list_integrations) — обновить его" } }, required: ["provider"] } },
|
|
652
652
|
{ name: "disconnect_integration", description: "Удалить подключение сервиса по id из list_integrations (DELETE /api/bots/integrations/{id}). Действия сценария с этим connectionId перестанут работать.", inputSchema: { type: "object", properties: { connectionId: { type: "string" } }, required: ["connectionId"] } },
|
|
653
653
|
// ---- Рассылки ----
|
|
654
654
|
{ name: "broadcast_list", description: "Рассылки. Без botId — по всем ботам постранично (GET /api/bots/broadcasts): {counts: {drafts, scheduled, sent, recurring}, page: {content: [{id, botId, botUsername, name, status, direct, totalJobs, sentJobs, failedJobs, skippedByQuota, scheduledAt, createdAt}], totalElements…}}; group: scheduled (ещё не начали) | sent (идут/завершены). С botId — полная история одного бота. status: EXPANDING/MATERIALIZING/READY (ждёт) → RUNNING → DONE | CANCELLING → CANCELLED | FAILED. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, group: { type: "string", enum: ["scheduled", "sent"] }, page: { type: "number" }, size: { type: "number", description: "до 100, по умолчанию 20" } } } },
|
|
@@ -1000,26 +1000,69 @@ async function handleCall(params) {
|
|
|
1000
1000
|
{ connectedGoogleAccounts: connected });
|
|
1001
1001
|
}
|
|
1002
1002
|
const spec = PROVIDER_FIELDS[provider];
|
|
1003
|
-
if (
|
|
1003
|
+
if (spec) {
|
|
1004
|
+
// Легаси-провайдер (одна из исходных 5) — шлём enum `provider` как раньше, без каталога.
|
|
1005
|
+
const creds = a.creds && typeof a.creds === "object" ? Object.fromEntries(
|
|
1006
|
+
Object.entries(a.creds).filter(([, v]) => v != null && String(v).trim() !== "").map(([k, v]) => [k, String(v).trim()])) : {};
|
|
1007
|
+
const missing = Object.keys(spec.fields).filter((k) => !creds[k]);
|
|
1008
|
+
if (missing.length && (!a.connectionId || Object.keys(creds).length)) {
|
|
1009
|
+
return okResult({
|
|
1010
|
+
connected: false,
|
|
1011
|
+
provider,
|
|
1012
|
+
need: Object.fromEntries(missing.map((k) => [k, spec.fields[k]])),
|
|
1013
|
+
instruction: `Для ${spec.name} не хватает полей creds: ${missing.join(", ")}. Попроси их у пользователя и вызови connect_integration ещё раз. Или пусть подключит сам в кабинете: ${CONNECT_PAGE}`,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
const title = a.title || spec.name;
|
|
1017
|
+
try {
|
|
1018
|
+
const saved = a.connectionId
|
|
1019
|
+
? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
|
|
1020
|
+
: await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
|
|
1021
|
+
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings (amoConnectionId или coreDelivery.connectionId). Проверить ключ — integration_test." });
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
if (e.status === 400) throw new Error(`${spec.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${Object.entries(spec.fields).map(([k, v]) => `${k} — ${v}`).join("; ")}.`);
|
|
1024
|
+
throw e;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Провайдер из каталога Integration Core (~25 сервисов, GET /api/integrations/catalog) —
|
|
1029
|
+
// ключ в запросе на создание ВСЁ ЕЩЁ называется `provider`, но в нижнем регистре (bots/integrations
|
|
1030
|
+
// одинаково принимает enum-провайдеров и свободные providerKey из реестра, см. IntegrationConnectionService.create).
|
|
1031
|
+
const catalogKey = String(a.provider || "").trim().toLowerCase().replace(/[\s.-]+/g, "_");
|
|
1032
|
+
let catalog;
|
|
1033
|
+
try { catalog = await api("/api/integrations/catalog"); } catch { /* офлайн — фолбэк на статическую таблицу ниже */ }
|
|
1034
|
+
if (!Array.isArray(catalog)) {
|
|
1035
|
+
throw new Error(`Неизвестный provider «${a.provider}». Бывают: ${Object.keys(PROVIDER_FIELDS).join(", ")}, GOOGLE_SHEETS, INSTAGRAM (каталог Integration Core сейчас недоступен — офлайн-режим). Каталог: ${CONNECT_PAGE}`);
|
|
1036
|
+
}
|
|
1037
|
+
const entry = catalog.find((p) => String(p?.provider || "").toLowerCase() === catalogKey);
|
|
1038
|
+
if (!entry) {
|
|
1039
|
+
const names = catalog.map((p) => p.provider).filter(Boolean).join(", ");
|
|
1040
|
+
throw new Error(`Неизвестный provider «${a.provider}». Бывают: ${Object.keys(PROVIDER_FIELDS).join(", ")}, GOOGLE_SHEETS, INSTAGRAM, ${names}. Каталог: ${CONNECT_PAGE}`);
|
|
1041
|
+
}
|
|
1042
|
+
if (entry.status === "COMING_SOON" || entry.status === "IN_DEVELOPMENT") {
|
|
1043
|
+
throw new Error(`${entry.name} (${entry.provider}) пока нельзя подключить: статус ${entry.status}. Каталог: ${CONNECT_PAGE}`);
|
|
1044
|
+
}
|
|
1045
|
+
const schema = Array.isArray(entry.configSchema) ? entry.configSchema : [];
|
|
1004
1046
|
const creds = a.creds && typeof a.creds === "object" ? Object.fromEntries(
|
|
1005
1047
|
Object.entries(a.creds).filter(([, v]) => v != null && String(v).trim() !== "").map(([k, v]) => [k, String(v).trim()])) : {};
|
|
1006
|
-
const
|
|
1048
|
+
const requiredFields = schema.filter((f) => !/необязательно|optional/i.test(f?.hint || ""));
|
|
1049
|
+
const missing = requiredFields.map((f) => f.key).filter((k) => !creds[k]);
|
|
1007
1050
|
if (missing.length && (!a.connectionId || Object.keys(creds).length)) {
|
|
1008
1051
|
return okResult({
|
|
1009
1052
|
connected: false,
|
|
1010
|
-
provider,
|
|
1011
|
-
need: Object.fromEntries(missing.map((
|
|
1012
|
-
instruction: `Для ${
|
|
1053
|
+
provider: entry.provider,
|
|
1054
|
+
need: Object.fromEntries(schema.filter((f) => missing.includes(f.key)).map((f) => [f.key, f.hint || f.label])),
|
|
1055
|
+
instruction: `Для ${entry.name} не хватает полей creds: ${missing.join(", ")}. Попроси их у пользователя и вызови connect_integration ещё раз. Или пусть подключит сам в кабинете: ${CONNECT_PAGE}`,
|
|
1013
1056
|
});
|
|
1014
1057
|
}
|
|
1015
|
-
const title = a.title ||
|
|
1058
|
+
const title = a.title || entry.name;
|
|
1016
1059
|
try {
|
|
1017
1060
|
const saved = a.connectionId
|
|
1018
1061
|
? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
|
|
1019
|
-
: await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
|
|
1020
|
-
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (
|
|
1062
|
+
: await api("/api/bots/integrations", { method: "POST", body: { provider: entry.provider, title, creds } });
|
|
1063
|
+
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (actions из integration_catalog) и в site_lead_settings (coreDelivery.connectionId). Проверить ключ — integration_test." });
|
|
1021
1064
|
} catch (e) {
|
|
1022
|
-
if (e.status === 400) throw new Error(`${
|
|
1065
|
+
if (e.status === 400) throw new Error(`${entry.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${schema.map((f) => `${f.key} — ${f.hint || f.label}`).join("; ")}.`);
|
|
1023
1066
|
throw e;
|
|
1024
1067
|
}
|
|
1025
1068
|
}
|