@retensy/mcp 0.50.0 → 0.51.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.50.0",
3
+ "version": "0.51.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": {
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "start": "node src/index.mjs",
23
23
  "check": "node --check src/index.mjs",
24
- "test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/agent-write-tools.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs && node scripts/secondary-graphs.test.mjs && node scripts/graph-versions.test.mjs && node scripts/templates-filter.test.mjs && node scripts/agent-tests.test.mjs && node scripts/catalog.test.mjs",
24
+ "test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/agent-write-tools.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs && node scripts/secondary-graphs.test.mjs && node scripts/graph-versions.test.mjs && node scripts/templates-filter.test.mjs && node scripts/agent-tests.test.mjs && node scripts/catalog.test.mjs && node scripts/confirm-409.test.mjs",
25
25
  "validate": "node skills/build-bot-funnel/validate.mjs"
26
26
  },
27
27
  "engines": {
package/src/catalog.mjs CHANGED
@@ -150,14 +150,27 @@ function hash(s) {
150
150
  return h.toString(16).padStart(8, "0");
151
151
  }
152
152
 
153
+ /** Что изменится: summary.text от сервера (diff узлов/блоков, получатели), иначе общий message. */
154
+ function changeSummary(data) {
155
+ const sum = data?.step?.confirm?.summary;
156
+ if (!sum || typeof sum !== "object") return "";
157
+ const parts = [];
158
+ if (typeof sum.text === "string" && sum.text) parts.push(sum.text);
159
+ if (typeof sum.recipients === "number") parts.push(`Получателей: ${sum.recipients}.`);
160
+ if (sum.irreversible === true) parts.push("Действие необратимо.");
161
+ return parts.join(" ");
162
+ }
163
+
153
164
  /** Ответ invoke → результат MCP. 409 needsConfirmation — не ошибка сервиса, а просьба спросить человека. */
154
165
  export function confirmationText(data) {
155
166
  if (data && typeof data === "object" && data.needsConfirmation === true) {
156
167
  const msg = typeof data.message === "string" ? data.message : "";
168
+ const what = changeSummary(data);
169
+ const head = ["Нужно подтверждение пользователя.", what && !msg.includes(what) ? `Что изменится: ${what}` : "", msg].filter(Boolean).join(" ");
157
170
  if (typeof data.confirmToken === "string" && data.confirmToken) {
158
- return `⚠️ Нужно подтверждение пользователя. ${msg} Опиши пользователю, что именно будет сделано, получи его явное согласие и повтори вызов с ТЕМИ ЖЕ аргументами плюс confirmToken: "${data.confirmToken}" (токен привязан к этим аргументам и действует 5 минут).`.trim();
171
+ return `⚠️ ${head} Опиши пользователю, что именно будет сделано, получи его явное согласие и повтори вызов с ТЕМИ ЖЕ аргументами плюс confirmToken: "${data.confirmToken}" (токен одноразовый, привязан к этим аргументам и действует 5 минут; если состояние изменилось, придёт новый запрос — покажи пользователю новое описание).`.trim();
159
172
  }
160
- return `⚠️ Нужно подтверждение пользователя. ${msg}`.trim();
173
+ return `⚠️ ${head}`.trim();
161
174
  }
162
175
  return null;
163
176
  }
package/src/index.mjs CHANGED
@@ -1466,8 +1466,9 @@ async function handleCall(params) {
1466
1466
  return okResult(await api(`/api/bots/graphs/${a.graphId}/dry-run`, { method: "POST", body: { kind: a.kind, value, fromUsername: a.fromUsername, presetVariables: a.presetVariables, presetTags: a.presetTags } }));
1467
1467
  }
1468
1468
  case "publish_graph": {
1469
- const qs = a.secondary === true ? "?secondary=true" : "";
1470
- const pub = await api(`/api/bots/graphs/${a.graphId}/publish${qs}`, { method: "POST" });
1469
+ const g = await invokeGuarded("graph_publish", { graphId: a.graphId, ...(a.secondary === true ? { secondary: true } : {}) }, a);
1470
+ if (g.pending) return g.pending;
1471
+ const pub = g.data;
1471
1472
  // errors[] приходит с HTTP 200, но для пользователя это «не получилось» — и самый
1472
1473
  // ценный сигнал: видно, какого узла/возможности ему не хватило.
1473
1474
  if (Array.isArray(pub?.errors) && pub.errors.length) {
@@ -1524,8 +1525,11 @@ async function handleCall(params) {
1524
1525
  case "copy_graph":
1525
1526
  return okResult(await api(`/api/bots/graphs/${a.graphId}/copy`, { method: "POST", body: { targetBotId: a.targetBotId, preview: a.preview === true } }));
1526
1527
  case "delete_graph":
1527
- await api(`/api/bots/graphs/${a.graphId}`, { method: "DELETE" });
1528
+ {
1529
+ const g = await invokeGuarded("graph_delete", { graphId: a.graphId }, a);
1530
+ if (g.pending) return g.pending;
1528
1531
  return okResult(`🗑️ Граф ${a.graphId} удалён.`);
1532
+ }
1529
1533
  case "set_active_graph":
1530
1534
  await api(`/api/bots/${a.botId}/active-graph`, { method: "POST", body: { graphId: a.graphId } });
1531
1535
  return okResult(`✅ Активный граф бота ${a.botId} → ${a.graphId}.`);
@@ -1571,10 +1575,13 @@ async function handleCall(params) {
1571
1575
  if (!hasOps && !a.init) throw new Error("Передай ops — массив операций (см. site_schema) — или init, чтобы только создать черновик.");
1572
1576
  if (a.init != null && !SITE_INITS.includes(a.init)) throw new Error(`init — одно из: ${SITE_INITS.join(", ")}.`);
1573
1577
  const ops = hasOps ? a.ops : SITE_NOOP_OPS; // бэкенд отвечает 422 на пустой ops[]
1574
- return okResult(await api(`/api/bots/pages/${a.siteId}/document/ops`, { method: "POST", body: { ops, revision: a.revision, init: a.init } }));
1578
+ const g = await invokeGuarded("site_edit", { siteId: a.siteId, ops, revision: a.revision, init: a.init }, a);
1579
+ return g.pending || okResult(g.data);
1575
1580
  }
1576
1581
  case "site_publish": {
1577
- const r = await api(`/api/bots/pages/${a.siteId}/publish`, { method: "POST" });
1582
+ const g = await invokeGuarded("site_publish", { siteId: a.siteId }, a);
1583
+ if (g.pending) return g.pending;
1584
+ const r = g.data;
1578
1585
  return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
1579
1586
  }
1580
1587
  case "site_upload_asset": return okResult(await uploadSiteAsset(a.siteId, { filePath: a.path, url: a.url }));
@@ -1587,7 +1594,9 @@ async function handleCall(params) {
1587
1594
  }
1588
1595
  case "site_rollback": {
1589
1596
  if (typeof a.revision !== "number") throw new Error("Передай revision — номер публикации (site_get → versions[]).");
1590
- const r = await api(`/api/bots/pages/${a.siteId}/publish/rollback`, { method: "POST", body: { revision: a.revision } });
1597
+ const g = await invokeGuarded("site_rollback", { siteId: a.siteId, revision: a.revision }, a);
1598
+ if (g.pending) return g.pending;
1599
+ const r = g.data;
1591
1600
  return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
1592
1601
  }
1593
1602
  case "site_domains": {
@@ -1629,9 +1638,13 @@ async function handleCall(params) {
1629
1638
  const errs = validateAgentTools(a.patch.tools);
1630
1639
  if (errs.length) throw new Error("Инструменты агента не прошли проверку:\n- " + errs.join("\n- "));
1631
1640
  }
1632
- return okResult(await api(`/api/bots/agents/${a.agentId}`, { method: "PATCH", body: a.patch }));
1641
+ const g = await invokeGuarded("agent_update", { agentId: a.agentId, patch: a.patch }, a);
1642
+ return g.pending || okResult(g.data);
1643
+ }
1644
+ case "agent_publish": {
1645
+ const g = await invokeGuarded("agent_publish", { agentId: a.agentId }, a);
1646
+ return g.pending || okResult(g.data);
1633
1647
  }
1634
- case "agent_publish": return okResult(await api(`/api/bots/agents/${a.agentId}/publish`, { method: "POST" }));
1635
1648
  case "agent_health": return okResult(await api(`/api/bots/agents/${a.agentId}/health`));
1636
1649
  case "agent_test_chat": {
1637
1650
  if (!a.question || !String(a.question).trim()) throw new Error("Передай question.");
@@ -1655,7 +1668,10 @@ async function handleCall(params) {
1655
1668
  }
1656
1669
  case "kb_add_site": {
1657
1670
  if (!a.url) throw new Error("Передай url.");
1658
- return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
1671
+ // schedule в реестре нет — такой вызов остаётся прямым REST
1672
+ if (a.schedule) return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
1673
+ const g = await invokeGuarded("kb_add_site", { kbId: a.kbId, url: a.url }, a);
1674
+ return g.pending || okResult(g.data);
1659
1675
  }
1660
1676
  case "kb_reindex": return okResult(await api(`/api/bots/kb/${a.kbId}/docs/${a.docId}/reindex`, { method: "POST", body: { headerRow: a.headerRow } }));
1661
1677
  case "analytics_ask": {
@@ -1710,7 +1726,8 @@ async function handleCall(params) {
1710
1726
  case "dialog_reply": {
1711
1727
  const text = String(a.text ?? "").trim();
1712
1728
  if (!text) throw new Error("Передай text — текст ответа.");
1713
- return okResult(await api(`/api/bots/${a.botId}/users/${chatIdArg(a.chatId)}/messages`, { method: "POST", body: { text } }));
1729
+ const g = await invokeGuarded("dialog_reply", { botId: a.botId, chatId: chatIdArg(a.chatId), text }, a);
1730
+ return g.pending || okResult(g.data);
1714
1731
  }
1715
1732
  // ---- Запись на слоты ----
1716
1733
  case "booking_calendar_list": return okResult(await api("/api/bots/booking/calendars"));
@@ -1775,7 +1792,8 @@ async function handleCall(params) {
1775
1792
  case "booking_cancel": {
1776
1793
  const id = uuidArg(a.calendarId, "calendarId");
1777
1794
  const bookingId = uuidArg(a.bookingId, "bookingId");
1778
- await api(`/api/bots/booking/calendars/${id}/bookings/${bookingId}/cancel`, { method: "POST" });
1795
+ const g = await invokeGuarded("booking_cancel", { calendarId: id, bookingId }, a);
1796
+ if (g.pending) return g.pending;
1779
1797
  return okResult(`✅ Бронь ${bookingId} отменена, слот свободен.`);
1780
1798
  }
1781
1799
  case "booking_confirm":
@@ -1870,8 +1888,8 @@ async function handleCall(params) {
1870
1888
  case "scenario_runs": return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/runs${pageQs(a)}`));
1871
1889
  case "bot_delete": {
1872
1890
  const botId = uuidArg(a.botId, "botId");
1873
- if (a.confirm !== true) throw new Error("Удаление бота необратимо. Спроси пользователя и повтори с confirm:true.");
1874
- await api(`/api/bots/${botId}`, { method: "DELETE" });
1891
+ const g = await invokeGuarded("bot_delete", { botId }, a);
1892
+ if (g.pending) return g.pending;
1875
1893
  return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
1876
1894
  }
1877
1895
  // ---- Паритет платформы, часть 2 ----
@@ -2156,10 +2174,15 @@ async function handleCall(params) {
2156
2174
  const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
2157
2175
  let bot;
2158
2176
  try {
2159
- bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
2177
+ if (inactive) bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
2178
+ else {
2179
+ const g = await invokeGuarded("bot_connect", { platform, token }, a);
2180
+ if (g.pending) return g.pending;
2181
+ bot = g.data;
2182
+ }
2160
2183
  } catch (e) {
2161
2184
  // 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
2162
- if (e.status === 400) throw new Error(`Бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
2185
+ if (e.status === 400) throw new Error(`Бот не подключён: ${bodyReason(e.data) || "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
2163
2186
  throw e;
2164
2187
  }
2165
2188
  if (a.name && bot?.id) {
@@ -2181,7 +2204,10 @@ async function handleCall(params) {
2181
2204
  case "vk_landing_delete":
2182
2205
  await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "DELETE" });
2183
2206
  return okResult({ deleted: true });
2184
- case "bot_stop": return okResult(await api(`/api/bots/${a.botId}/stop`, { method: "POST" }));
2207
+ case "bot_stop": {
2208
+ const g = await invokeGuarded("bot_stop", { botId: a.botId }, a);
2209
+ return g.pending || okResult(g.data);
2210
+ }
2185
2211
  case "bot_resume": return okResult(await api(`/api/bots/${a.botId}/resume`, { method: "POST" }));
2186
2212
  // ---- Подключения ----
2187
2213
  case "connect_integration": {
@@ -2341,7 +2367,9 @@ async function handleCall(params) {
2341
2367
  if (a.scheduledAt !== undefined) throw new Error(`scheduledAt ${body.scheduledAt} уже прошло — укажи будущее время или не передавай его (отправить сейчас).`);
2342
2368
  body.scheduledAt = null;
2343
2369
  }
2344
- const r = await api("/api/bots/broadcasts/direct", { method: "POST", body });
2370
+ const g = await invokeGuarded("broadcast_send_now", body, a);
2371
+ if (g.pending) return g.pending;
2372
+ const r = g.data;
2345
2373
  let draftDeleted = false;
2346
2374
  if (a.draftId) { try { await api(`/api/bots/broadcasts/drafts/${a.draftId}`, { method: "DELETE" }); draftDeleted = true; } catch { /* не критично */ } }
2347
2375
  // dedup/stagger → бэкенд создаёт BroadcastGroup вместо прямых TgBroadcast (другая форма ответа: id/perBot[]/status вместо broadcastIds/totalAudience).
@@ -2471,9 +2499,11 @@ async function handleCall(params) {
2471
2499
  }
2472
2500
  return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops/preview`, { method: "POST", body }));
2473
2501
  }
2474
- case "broadcast_cancel":
2475
- await api(`/api/bots/broadcasts/${a.broadcastId}/cancel`, { method: "POST" });
2502
+ case "broadcast_cancel": {
2503
+ const g = await invokeGuarded("broadcast_cancel", { broadcastId: a.broadcastId }, a);
2504
+ if (g.pending) return g.pending;
2476
2505
  return okResult(`⏹️ Рассылка ${a.broadcastId} отменяется (CANCELLING → CANCELLED).`);
2506
+ }
2477
2507
  case "broadcast_recurring": {
2478
2508
  const base = "/api/bots/broadcasts/recurring";
2479
2509
  if (a.action === "list") return okResult(await api(base));
@@ -2584,7 +2614,10 @@ async function handleCall(params) {
2584
2614
  await api(`/api/bots/chains/${chainId}`, { method: "DELETE" });
2585
2615
  return okResult(`🗑️ Цепочка ${chainId} удалена.`);
2586
2616
  }
2587
- case "chain_enable": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/enable`, { method: "POST" }));
2617
+ case "chain_enable": {
2618
+ const g = await invokeGuarded("chain_enable", { chainId: uuidArg(a.chainId, "chainId") }, a);
2619
+ return g.pending || okResult(g.data);
2620
+ }
2588
2621
  case "chain_pause": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/pause`, { method: "POST" }));
2589
2622
  case "chain_runs": {
2590
2623
  const chainId = uuidArg(a.chainId, "chainId");
@@ -2780,6 +2813,32 @@ async function callCatalog(params) {
2780
2813
  }
2781
2814
  }
2782
2815
 
2816
+ /**
2817
+ * Опасное действие через единый реестр: сервер сам требует подтверждение (409 + confirmToken) и пишет вызов в журнал.
2818
+ * { pending } — готовый MCP-результат «спроси пользователя» (с описанием изменений и токеном), { data } — выполнено.
2819
+ * Повтор: те же аргументы + confirmToken (параметр самого инструмента).
2820
+ */
2821
+ async function invokeGuarded(name, args, a) {
2822
+ const clean = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
2823
+ if (typeof a?.confirmToken === "string" && a.confirmToken) clean.confirmToken = a.confirmToken;
2824
+ try {
2825
+ return { data: await api("/api/agent-tools/invoke", { method: "POST", body: { name, args: clean } }) };
2826
+ } catch (e) {
2827
+ const text = e?.status === 409 ? confirmationText(e.data) : null;
2828
+ if (text) return { pending: okResult(text) };
2829
+ throw e;
2830
+ }
2831
+ }
2832
+ /** Обратимые правки: сервер обычно не требует подтверждения, токен нужен лишь при явном 409. */
2833
+ const SOFT_CONFIRM_TOOLS = new Set(["site_edit", "agent_update", "kb_add_site", "chain_enable"]);
2834
+ const GUARDED_TOOLS = new Set(["publish_graph", "site_publish", "site_rollback", "agent_publish", "booking_cancel", "bot_delete", "broadcast_cancel", "broadcast_send",
2835
+ "delete_graph", "agent_update", "chain_enable", "kb_add_site", "create_bot", "bot_stop", "dialog_reply", "site_edit"]);
2836
+ for (const t of TOOLS) {
2837
+ if (!GUARDED_TOOLS.has(t.name)) continue;
2838
+ const soft = SOFT_CONFIRM_TOOLS.has(t.name);
2839
+ t.inputSchema.properties = { ...(t.inputSchema.properties || {}), confirmToken: { type: "string", description: soft ? "Только если сервер вернул запрос подтверждения (409): токен одноразовый, при новом 409 используй новый токен и покажи пользователю новое описание изменений. Обычно не нужен." : "Токен подтверждения из ответа «Нужно подтверждение пользователя»: повтори вызов с теми же аргументами после явного согласия человека. Без него сервер ничего не выполнит." } };
2840
+ }
2841
+
2783
2842
  function notifyListChanged() { send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }); }
2784
2843
  /** Обновить каталог в фоне; изменился набор — сказать клиенту перечитать tools/list. */
2785
2844
  function refreshCatalog() {