@retensy/mcp 0.50.0 → 0.52.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 CHANGED
@@ -224,8 +224,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
224
224
  | `site_list()` | сайты пользователя (id, mode, url, publishedRevision, status — `BLOCKED` у заблокированных модерацией, blockedReason) |
225
225
  | `site_create(title, slug?, template?)` | новый сайт из блоков → `id`; `template` (`starter`/`blank`/`mini-landing`/`shop-starter`) — сразу черновик из шаблона; число сайтов ограничено тарифом — сверх лимита HTTP 402 с причиной и ссылкой на смену тарифа |
226
226
  | `site_get(siteId, saveToFile?)` | модель сайта (`revision`, `draft`, `versions[]` публикаций) |
227
- | `site_schema()` | JSON Schema модели и операций — читать перед правкой |
228
- | `site_edit(siteId, ops[]?, revision?, init?)` | правка операциями (`init`: `starter`/`blank`/`mini-landing`/`shop-starter`; только `init` — черновик из шаблона), всё или ничего: страницы и папки, блоки, Zero-элементы, код блока (`get/set/add_block_code`), дизайны и их кадры, шаблоны (`add_template`), тема, попапы; пиксели аналитики (GA4/GTM/Meta/VK/Яндекс.Метрика) — тоже сюда, `set_settings {analytics: {...}}` (отдельного инструмента нет — те же `settings.analytics` модели сайта, валидируются форматом id на сервере) |
227
+ | `site_schema(full?)` | компактный справочник модели и операций (~10 КБ); `full:true` — полная JSON Schema — читать перед правкой |
228
+ | `site_edit(siteId, ops[]?, revision?, init?, reset?)` | правка операциями (`init`: `starter`/`blank`/`mini-landing`/`shop-starter`; только `init` — черновик из шаблона; `reset:true` с `init` и `revision` — пересобрать черновик из шаблона, деструктивно), всё или ничего: страницы и папки, блоки, Zero-элементы, код блока (`get/set/add_block_code`), дизайны и их кадры, шаблоны (`add_template`), тема, попапы; пиксели аналитики (GA4/GTM/Meta/VK/Яндекс.Метрика) — тоже сюда, `set_settings {analytics: {...}}` (отдельного инструмента нет — те же `settings.analytics` модели сайта, валидируются форматом id на сервере) |
229
229
  | `site_templates(category?, full?)` | библиотека шаблонов блоков для `add_template` |
230
230
  | `site_publish(siteId)` | опубликовать черновик → `url` |
231
231
  | `site_rollback(siteId, revision)` | вернуть прошлую публикацию |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.50.0",
3
+ "version": "0.52.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
@@ -1079,7 +1079,7 @@ const TOOLS = [
1079
1079
  { name: "site_list", description: "Сайты (раздел «Страницы», GET /api/bots/pages): id, title, mode (BLOCKS — сайт из блоков, CODE — файлы/Mini App), url (основной адрес), publishedRevision, status (ACTIVE | BLOCKED — заблокирован модерацией: посетители видят HTTP 451, удалить такой сайт нельзя — 409) и blockedReason. Включает и свои сайты, и сайты команды проекта (привязанные к боту, где тебя пригласили) — у каждого accessRole (OWNER/ADMIN/EDITOR/VIEWER), у чужих — ownerName. Доступ проверяется на каждый вызов. Read-only.", inputSchema: { type: "object", properties: {} } },
1080
1080
  { name: "site_create", description: "Создать сайт из блоков (POST /api/bots/pages, mode=BLOCKS). Возвращает id. Дальше: site_edit (init=starter — стартовый лендинг, init=blank — пустая главная, init=mini-landing — мини-лендинг с кнопками мессенджеров, init=shop-starter — многостраничный магазин: главная, каталог, о нас, доставка, контакты, политика конфиденциальности, оферта, корзина; общие шапка и подвал) → site_publish. template (starter|blank|mini-landing|shop-starter) — сразу создать черновик из шаблона (как «Мини-лендинг» в редакторе), в ответе revision. slug — «название» в адресе pages.retensy.com/<id>/<slug>/ (необязательно, по умолчанию транслит title). Число сайтов ограничено тарифом (free — 1, «Старт» — 3, «Про» — 10, с «Бизнеса» — без лимита): сверх лимита — HTTP 402 с причиной и upgradeUrl (смена тарифа только в браузере, передай ссылку пользователю). Заблокированный модерацией сайт в site_list имеет status=BLOCKED и blockedReason, посетители видят HTTP 451.", inputSchema: { type: "object", properties: { title: { type: "string" }, slug: { type: "string" }, template: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] } }, required: ["title"] } },
1081
1081
  { name: "site_get", description: "Модель сайта из блоков (GET /api/bots/pages/{siteId}/document): revision, draft (SiteModel: theme, globals.header/footer, pages[].blocks[], popups[]) — id страниц/блоков/попапов нужны для site_edit. draft=null — сайт пуст (первый site_edit создаст его). saveToFile — записать модель на диск и вернуть путь.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, saveToFile: { type: "string" } }, required: ["siteId"] } },
1082
- { name: "site_schema", description: "JSON Schema модели сайта (model) и операций правки (ops) — какие блоки и поля бывают (GET /api/bots/pages/schema). Читай перед первой правкой.", inputSchema: { type: "object", properties: {} } },
1082
+ { name: "site_schema", description: "Компактный справочник модели сайта и операций правки (ops) — какие блоки и поля бывают (GET /api/bots/pages/schema, ~10 КБ). Читай перед первой правкой. full:true — полная JSON Schema (GET ?full=true), большая.", inputSchema: { type: "object", properties: { full: { type: "boolean", description: "true = полная JSON Schema вместо компактного справочника" } } } },
1083
1083
  { name: "site_edit", description: "Правка сайта операциями — всё или ничего (POST /api/bots/pages/{siteId}/document/ops). " +
1084
1084
  "Страницы: add_page{title} · update_page{pageId,patch: {title?, path?, seo?{title,description,noindex,ogTitle,ogDescription,ogImage}, showHeader?, showFooter?, folder?}} · remove_page{pageId} · move_page{pageId,delta}. " +
1085
1085
  "Папки страниц: add_folder{name} (id в results) · rename_folder{folderId,name} · remove_folder{folderId}; страница в папку — update_page{pageId, patch:{folder: folderId}}. " +
@@ -1091,8 +1091,8 @@ const TOOLS = [
1091
1091
  "Zero-kinds form/product-list/product: form — props.form как у блока form (fields, submitText); product-list — props: categoryId?, limit, columns (значения по экранам {d, t?, m?}); product — productId обязателен (товар из каталога). Новые Zero-блоки создаются с артбордами 1440/768/390 (d/t/m). " +
1092
1092
  "Сайт: set_global{slot: header|footer, on} · set_theme{theme} · set_settings{settings} · add_popup{name} · update_popup{popupId,name?,width?} · remove_popup{popupId}. " +
1093
1093
  "props/style/theme/frame — JSON Merge Patch (null удаляет ключ, массивы заменяются целиком). Типы блоков: header, cover, text, image, gallery, buttons, features, form, video, html, spacer, footer, zero, messengers. " +
1094
- "Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank|mini-landing|shop-starter) — с чего начать пустой сайт (на сайте с черновиком игнорируется); shop-starter — многостраничный магазин (главная, каталог, о нас, доставка, контакты, политика, оферта, корзина; общие шапка и подвал); только init без ops — создать черновик из шаблона. " +
1095
- "Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] } }, required: ["siteId"] } },
1094
+ "Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank|mini-landing|shop-starter) — с чего начать пустой сайт (на сайте с черновиком игнорируется); shop-starter — многостраничный магазин (главная, каталог, о нас, доставка, контакты, политика, оферта, корзина; общие шапка и подвал); только init без ops — создать черновик из шаблона. reset:true (с init и revision) — пересобрать существующий черновик из шаблона init, ДЕСТРУКТИВНО (текущий черновик заменяется). " +
1095
+ "Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] }, reset: { type: "boolean", description: "true = пересобрать существующий черновик из шаблона init (ДЕСТРУКТИВНО: текущий черновик заменяется). Требует init и revision." } }, required: ["siteId"] } },
1096
1096
  { name: "site_publish", description: "Опубликовать черновик сайта (POST /api/bots/pages/{siteId}/publish): рендер в статику, адрес начинает отдавать новую версию. Ошибки проверки — HTTP 422 с путями. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" } }, required: ["siteId"] } },
1097
1097
  { name: "site_upload_asset", description: "Загрузить картинку/видео в сайт (POST /api/bots/pages/{siteId}/upload, папка assets). Передай path (локальный файл) ИЛИ url. Возвращает asset — строку вида assets/<имя> для полей image/logo/icon/style.bg.image.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, path: { type: "string" }, url: { type: "string" } }, required: ["siteId"] } },
1098
1098
  { name: "site_rollback", description: "Вернуть прошлую публикацию сайта (POST /api/bots/pages/{siteId}/publish/rollback): revision — номер из истории публикаций (site_get → versions[]). Черновик заменяется этой версией и сразу публикуется. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, revision: { type: "number" } }, required: ["siteId", "revision"] } },
@@ -1196,7 +1196,7 @@ const TOOLS = [
1196
1196
  { name: "booking_cancel", description: "Отменить бронь и освободить слот (POST /api/bots/booking/calendars/{calendarId}/bookings/{bookingId}/cancel). Повторная отмена — без ошибки. bookingId — из booking_list.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
1197
1197
  { name: "booking_confirm", description: "Подтвердить бронь, ждущую решения владельца (MANUAL confirmationMode) — POST .../bookings/{bookingId}/confirm. Клиенту уходит уведомление, если бронь пришла из чата бота. HTTP 404 — бронь не этого календаря, 409 — уже не ждёт решения (подтверждена/отменена/истёк pendingHoldHours).", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
1198
1198
  { name: "booking_reject", description: "Отклонить бронь, ждущую решения владельца (MANUAL confirmationMode) — POST .../bookings/{bookingId}/reject: освобождает слот, клиенту уходит «отклонено владельцем» (в отличие от booking_cancel — то работает на любой активной брони и ничего не присылает клиенту). HTTP 404/409 — как у booking_confirm.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
1199
- { name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
1199
+ { name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории. Ответ также содержит siteTemplates — стартовые шаблоны сайта (starter, blank, mini-landing, shop-starter) с описаниями: для site_create template / site_edit init.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
1200
1200
  // ---- Паритет платформы, часть 2: бот, база, агент, заявки, подписчики, каналы, ссылки, аналитика ----
1201
1201
  { name: "bot_rename", description: "Переименовать бота (PATCH /api/bots/{botId} {name}): отображаемое имя в кабинете, до 250 символов. Пустое name сбрасывает имя на @username. Имя в самом Telegram/MAX не меняется.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
1202
1202
  { name: "bot_change_token", description: "Сменить токен бота (POST /api/bots/{botId}/token {token}): сервер проверяет токен и перезапускает приём сообщений; сценарии, подписчики и ссылки остаются. Нужен, если токен перевыпущен в @BotFather (/revoke) или MAX. Неверный или чужой токен → ошибка с причиной. Владелец или ADMIN.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string", description: "Новый токен бота" } }, required: ["botId", "token"] } },
@@ -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}.`);
@@ -1565,16 +1569,23 @@ async function handleCall(params) {
1565
1569
  }
1566
1570
  return okResult(doc);
1567
1571
  }
1568
- case "site_schema": return okResult(await api("/api/bots/pages/schema"));
1572
+ case "site_schema": return okResult(await api(a.full === true ? "/api/bots/pages/schema?full=true" : "/api/bots/pages/schema"));
1569
1573
  case "site_edit": {
1570
1574
  const hasOps = Array.isArray(a.ops) && a.ops.length > 0;
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(", ")}.`);
1577
+ if (a.reset === true) {
1578
+ if (!a.init) throw new Error("reset — пересборка черновика из шаблона: нужен init (starter|blank|mini-landing|shop-starter).");
1579
+ if (typeof a.revision !== "number") throw new Error("reset удаляет текущий черновик — передай revision (site_get → revision).");
1580
+ }
1573
1581
  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 } }));
1582
+ const g = await invokeGuarded("site_edit", { siteId: a.siteId, ops, revision: a.revision, init: a.init, reset: a.reset === true ? true : undefined }, a);
1583
+ return g.pending || okResult(g.data);
1575
1584
  }
1576
1585
  case "site_publish": {
1577
- const r = await api(`/api/bots/pages/${a.siteId}/publish`, { method: "POST" });
1586
+ const g = await invokeGuarded("site_publish", { siteId: a.siteId }, a);
1587
+ if (g.pending) return g.pending;
1588
+ const r = g.data;
1578
1589
  return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
1579
1590
  }
1580
1591
  case "site_upload_asset": return okResult(await uploadSiteAsset(a.siteId, { filePath: a.path, url: a.url }));
@@ -1587,7 +1598,9 @@ async function handleCall(params) {
1587
1598
  }
1588
1599
  case "site_rollback": {
1589
1600
  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 } });
1601
+ const g = await invokeGuarded("site_rollback", { siteId: a.siteId, revision: a.revision }, a);
1602
+ if (g.pending) return g.pending;
1603
+ const r = g.data;
1591
1604
  return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
1592
1605
  }
1593
1606
  case "site_domains": {
@@ -1629,9 +1642,13 @@ async function handleCall(params) {
1629
1642
  const errs = validateAgentTools(a.patch.tools);
1630
1643
  if (errs.length) throw new Error("Инструменты агента не прошли проверку:\n- " + errs.join("\n- "));
1631
1644
  }
1632
- return okResult(await api(`/api/bots/agents/${a.agentId}`, { method: "PATCH", body: a.patch }));
1645
+ const g = await invokeGuarded("agent_update", { agentId: a.agentId, patch: a.patch }, a);
1646
+ return g.pending || okResult(g.data);
1647
+ }
1648
+ case "agent_publish": {
1649
+ const g = await invokeGuarded("agent_publish", { agentId: a.agentId }, a);
1650
+ return g.pending || okResult(g.data);
1633
1651
  }
1634
- case "agent_publish": return okResult(await api(`/api/bots/agents/${a.agentId}/publish`, { method: "POST" }));
1635
1652
  case "agent_health": return okResult(await api(`/api/bots/agents/${a.agentId}/health`));
1636
1653
  case "agent_test_chat": {
1637
1654
  if (!a.question || !String(a.question).trim()) throw new Error("Передай question.");
@@ -1655,7 +1672,10 @@ async function handleCall(params) {
1655
1672
  }
1656
1673
  case "kb_add_site": {
1657
1674
  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 } }));
1675
+ // schedule в реестре нет — такой вызов остаётся прямым REST
1676
+ if (a.schedule) return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
1677
+ const g = await invokeGuarded("kb_add_site", { kbId: a.kbId, url: a.url }, a);
1678
+ return g.pending || okResult(g.data);
1659
1679
  }
1660
1680
  case "kb_reindex": return okResult(await api(`/api/bots/kb/${a.kbId}/docs/${a.docId}/reindex`, { method: "POST", body: { headerRow: a.headerRow } }));
1661
1681
  case "analytics_ask": {
@@ -1710,7 +1730,8 @@ async function handleCall(params) {
1710
1730
  case "dialog_reply": {
1711
1731
  const text = String(a.text ?? "").trim();
1712
1732
  if (!text) throw new Error("Передай text — текст ответа.");
1713
- return okResult(await api(`/api/bots/${a.botId}/users/${chatIdArg(a.chatId)}/messages`, { method: "POST", body: { text } }));
1733
+ const g = await invokeGuarded("dialog_reply", { botId: a.botId, chatId: chatIdArg(a.chatId), text }, a);
1734
+ return g.pending || okResult(g.data);
1714
1735
  }
1715
1736
  // ---- Запись на слоты ----
1716
1737
  case "booking_calendar_list": return okResult(await api("/api/bots/booking/calendars"));
@@ -1775,7 +1796,8 @@ async function handleCall(params) {
1775
1796
  case "booking_cancel": {
1776
1797
  const id = uuidArg(a.calendarId, "calendarId");
1777
1798
  const bookingId = uuidArg(a.bookingId, "bookingId");
1778
- await api(`/api/bots/booking/calendars/${id}/bookings/${bookingId}/cancel`, { method: "POST" });
1799
+ const g = await invokeGuarded("booking_cancel", { calendarId: id, bookingId }, a);
1800
+ if (g.pending) return g.pending;
1779
1801
  return okResult(`✅ Бронь ${bookingId} отменена, слот свободен.`);
1780
1802
  }
1781
1803
  case "booking_confirm":
@@ -1870,8 +1892,8 @@ async function handleCall(params) {
1870
1892
  case "scenario_runs": return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/runs${pageQs(a)}`));
1871
1893
  case "bot_delete": {
1872
1894
  const botId = uuidArg(a.botId, "botId");
1873
- if (a.confirm !== true) throw new Error("Удаление бота необратимо. Спроси пользователя и повтори с confirm:true.");
1874
- await api(`/api/bots/${botId}`, { method: "DELETE" });
1895
+ const g = await invokeGuarded("bot_delete", { botId }, a);
1896
+ if (g.pending) return g.pending;
1875
1897
  return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
1876
1898
  }
1877
1899
  // ---- Паритет платформы, часть 2 ----
@@ -2156,10 +2178,15 @@ async function handleCall(params) {
2156
2178
  const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
2157
2179
  let bot;
2158
2180
  try {
2159
- bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
2181
+ if (inactive) bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
2182
+ else {
2183
+ const g = await invokeGuarded("bot_connect", { platform, token }, a);
2184
+ if (g.pending) return g.pending;
2185
+ bot = g.data;
2186
+ }
2160
2187
  } catch (e) {
2161
2188
  // 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
2162
- if (e.status === 400) throw new Error(`Бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
2189
+ if (e.status === 400) throw new Error(`Бот не подключён: ${bodyReason(e.data) || "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
2163
2190
  throw e;
2164
2191
  }
2165
2192
  if (a.name && bot?.id) {
@@ -2181,7 +2208,10 @@ async function handleCall(params) {
2181
2208
  case "vk_landing_delete":
2182
2209
  await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "DELETE" });
2183
2210
  return okResult({ deleted: true });
2184
- case "bot_stop": return okResult(await api(`/api/bots/${a.botId}/stop`, { method: "POST" }));
2211
+ case "bot_stop": {
2212
+ const g = await invokeGuarded("bot_stop", { botId: a.botId }, a);
2213
+ return g.pending || okResult(g.data);
2214
+ }
2185
2215
  case "bot_resume": return okResult(await api(`/api/bots/${a.botId}/resume`, { method: "POST" }));
2186
2216
  // ---- Подключения ----
2187
2217
  case "connect_integration": {
@@ -2341,7 +2371,9 @@ async function handleCall(params) {
2341
2371
  if (a.scheduledAt !== undefined) throw new Error(`scheduledAt ${body.scheduledAt} уже прошло — укажи будущее время или не передавай его (отправить сейчас).`);
2342
2372
  body.scheduledAt = null;
2343
2373
  }
2344
- const r = await api("/api/bots/broadcasts/direct", { method: "POST", body });
2374
+ const g = await invokeGuarded("broadcast_send_now", body, a);
2375
+ if (g.pending) return g.pending;
2376
+ const r = g.data;
2345
2377
  let draftDeleted = false;
2346
2378
  if (a.draftId) { try { await api(`/api/bots/broadcasts/drafts/${a.draftId}`, { method: "DELETE" }); draftDeleted = true; } catch { /* не критично */ } }
2347
2379
  // dedup/stagger → бэкенд создаёт BroadcastGroup вместо прямых TgBroadcast (другая форма ответа: id/perBot[]/status вместо broadcastIds/totalAudience).
@@ -2471,9 +2503,11 @@ async function handleCall(params) {
2471
2503
  }
2472
2504
  return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops/preview`, { method: "POST", body }));
2473
2505
  }
2474
- case "broadcast_cancel":
2475
- await api(`/api/bots/broadcasts/${a.broadcastId}/cancel`, { method: "POST" });
2506
+ case "broadcast_cancel": {
2507
+ const g = await invokeGuarded("broadcast_cancel", { broadcastId: a.broadcastId }, a);
2508
+ if (g.pending) return g.pending;
2476
2509
  return okResult(`⏹️ Рассылка ${a.broadcastId} отменяется (CANCELLING → CANCELLED).`);
2510
+ }
2477
2511
  case "broadcast_recurring": {
2478
2512
  const base = "/api/bots/broadcasts/recurring";
2479
2513
  if (a.action === "list") return okResult(await api(base));
@@ -2559,7 +2593,7 @@ async function handleCall(params) {
2559
2593
  const r = await api("/api/bots/pages/templates");
2560
2594
  let templates = Array.isArray(r?.templates) ? r.templates : [];
2561
2595
  if (a.category) templates = templates.filter((t) => t?.category === a.category);
2562
- return okResult({ categories: r?.categories ?? [], templates });
2596
+ return okResult({ categories: r?.categories ?? [], templates, siteTemplates: r?.siteTemplates ?? [] });
2563
2597
  }
2564
2598
  // ---- Публикации: цепочки ----
2565
2599
  case "chain_list": return okResult(await api("/api/bots/chains"));
@@ -2584,7 +2618,10 @@ async function handleCall(params) {
2584
2618
  await api(`/api/bots/chains/${chainId}`, { method: "DELETE" });
2585
2619
  return okResult(`🗑️ Цепочка ${chainId} удалена.`);
2586
2620
  }
2587
- case "chain_enable": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/enable`, { method: "POST" }));
2621
+ case "chain_enable": {
2622
+ const g = await invokeGuarded("chain_enable", { chainId: uuidArg(a.chainId, "chainId") }, a);
2623
+ return g.pending || okResult(g.data);
2624
+ }
2588
2625
  case "chain_pause": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/pause`, { method: "POST" }));
2589
2626
  case "chain_runs": {
2590
2627
  const chainId = uuidArg(a.chainId, "chainId");
@@ -2780,6 +2817,32 @@ async function callCatalog(params) {
2780
2817
  }
2781
2818
  }
2782
2819
 
2820
+ /**
2821
+ * Опасное действие через единый реестр: сервер сам требует подтверждение (409 + confirmToken) и пишет вызов в журнал.
2822
+ * { pending } — готовый MCP-результат «спроси пользователя» (с описанием изменений и токеном), { data } — выполнено.
2823
+ * Повтор: те же аргументы + confirmToken (параметр самого инструмента).
2824
+ */
2825
+ async function invokeGuarded(name, args, a) {
2826
+ const clean = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
2827
+ if (typeof a?.confirmToken === "string" && a.confirmToken) clean.confirmToken = a.confirmToken;
2828
+ try {
2829
+ return { data: await api("/api/agent-tools/invoke", { method: "POST", body: { name, args: clean } }) };
2830
+ } catch (e) {
2831
+ const text = e?.status === 409 ? confirmationText(e.data) : null;
2832
+ if (text) return { pending: okResult(text) };
2833
+ throw e;
2834
+ }
2835
+ }
2836
+ /** Обратимые правки: сервер обычно не требует подтверждения, токен нужен лишь при явном 409. */
2837
+ const SOFT_CONFIRM_TOOLS = new Set(["site_edit", "agent_update", "kb_add_site", "chain_enable"]);
2838
+ const GUARDED_TOOLS = new Set(["publish_graph", "site_publish", "site_rollback", "agent_publish", "booking_cancel", "bot_delete", "broadcast_cancel", "broadcast_send",
2839
+ "delete_graph", "agent_update", "chain_enable", "kb_add_site", "create_bot", "bot_stop", "dialog_reply", "site_edit"]);
2840
+ for (const t of TOOLS) {
2841
+ if (!GUARDED_TOOLS.has(t.name)) continue;
2842
+ const soft = SOFT_CONFIRM_TOOLS.has(t.name);
2843
+ t.inputSchema.properties = { ...(t.inputSchema.properties || {}), confirmToken: { type: "string", description: soft ? "Только если сервер вернул запрос подтверждения (409): токен одноразовый, при новом 409 используй новый токен и покажи пользователю новое описание изменений. Обычно не нужен." : "Токен подтверждения из ответа «Нужно подтверждение пользователя»: повтори вызов с теми же аргументами после явного согласия человека. Без него сервер ничего не выполнит." } };
2844
+ }
2845
+
2783
2846
  function notifyListChanged() { send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }); }
2784
2847
  /** Обновить каталог в фоне; изменился набор — сказать клиенту перечитать tools/list. */
2785
2848
  function refreshCatalog() {