zaytsv-bot-graph-mcp 0.1.0 → 0.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zaytsv-bot-graph",
3
3
  "displayName": "Zaytsv Bot Graph",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "MCP-сервер + скилл для сборки и публикации воронок Telegram-ботов в сервисе zaytsv /bots: из текстового описания → валидный граф → заливка и публикация по API.",
6
6
  "author": { "name": "zaytsv", "url": "https://zaytsv.ru" },
7
7
  "homepage": "https://zaytsv.ru/bots",
package/README.md CHANGED
@@ -53,12 +53,15 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
53
53
 
54
54
  1. Залогинься на https://zaytsv.ru → открой **`/bots/mcp-tokens`**.
55
55
  2. Создай токен → скопируй секрет `zmcp_...` (показывается один раз).
56
- 3. Передай его серверу через `ZAYTSV_MCP_TOKEN`:
57
- - как `env` в `.mcp.json` (Вариант B), **или**
58
- - переменной окружения (Вариант A, плагин): PowerShell `setx ZAYTSV_MCP_TOKEN "zmcp_..."`, bash `export ZAYTSV_MCP_TOKEN="zmcp_..."`.
56
+ 3. Передай токен любым способом:
57
+ - **просто пришли его агенту в чат** — он вызовет инструмент `set_token` и сохранит токен в `~/.zaytsv-bot-graph/token` (применяется сразу, без рестарта), **или**
58
+ - `env` в `.mcp.json` (Вариант B), **или**
59
+ - переменной окружения: PowerShell `setx ZAYTSV_MCP_TOKEN "zmcp_..."`, bash `export ZAYTSV_MCP_TOKEN="zmcp_..."`.
59
60
 
60
61
  Отозвать токен можно там же — доступ блокируется мгновенно.
61
62
 
63
+ > **Не знаешь, что делать?** Скажи агенту «настрой подключение» — он вызовет `setup`, объяснит шаги и попросит токен. Любой инструмент при отсутствии токена тоже вернёт пошаговую инструкцию.
64
+
62
65
  > Дев-окружение: `ZAYTSV_BASE_URL=http://localhost:8066`.
63
66
  > Fallback без токена: `ZAYTSV_SESSION_COOKIE` = значение куки `SESSION` из браузера.
64
67
 
@@ -76,6 +79,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
76
79
 
77
80
  | Tool | Назначение |
78
81
  |---|---|
82
+ | `setup` | статус авторизации + пошаговая инструкция подключения |
83
+ | `set_token` | сохранить присланный токен `zmcp_…` (без env/рестарта) |
79
84
  | `list_bots` | список ботов |
80
85
  | `list_graphs(botId)` | графы (сценарии) бота |
81
86
  | `get_graph(graphId)` | получить граф |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zaytsv-bot-graph-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server to build and publish Telegram bot funnels in the zaytsv /bots service. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": { "zaytsv-bot-graph-mcp": "src/index.mjs" },
@@ -20,7 +20,8 @@ description: Собрать воронку (сценарий) Telegram-бота
20
20
  - У каждого `SEND_MESSAGE` непустой `config.text` (и продублируй в `cards[0].text`).
21
21
  - id узлов и рёбер — валидные UUID, уникальные.
22
22
  - Есть хотя бы один корневой `TRIGGER_*`; все нелистовые узлы достижимы от триггера; синхронных циклов нет (цикл только через `ASK_QUESTION`/`DELAY`/`SCHEDULE`).
23
- - Кнопки-выборы разведены по хэндлам `btn_N`; условия — `yes`/`no`; вопрос — `valid`/`invalid`.
23
+ - Кнопки-выборы разведены по хэндлам `btn_N`; условия `CONDITION` — `yes`/`no`; вопрос — `valid`/`invalid`.
24
+ - `CONDITION` умеет: теги, переменные, UTM-метки, имя/email/телефон из профиля, @username, подписку на канал (`SUBSCRIBED`), дату/время/день недели. Полная таблица `kind`/`op` — в schema.md.
24
25
 
25
26
  4. **Проверь локально** перед заливкой:
26
27
  ```bash
@@ -40,7 +41,7 @@ description: Собрать воронку (сценарий) Telegram-бота
40
41
 
41
42
  ## Важные ограничения
42
43
  - **Источник = текст** (этот режим). Если просят распознать с приватной Miro-доски — самый надёжный путь: CSV-экспорт из Miro; либо запуск залогиненного Chrome пользователя и съёмка экрана (headless WebGL-холст Miro не отдаёт). Это отдельный сценарий, не основной для этого скилла.
43
- - **Авторизация MCP** — персональный токен `ZAYTSV_MCP_TOKEN` (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Fallback — session-cookie. См. README MCP-сервера.
44
+ - **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Если инструмент вернул «нет токена» или ошибку доступа **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `ZAYTSV_MCP_TOKEN` и session-cookie.
44
45
  - Бэкенд читает плоские поля `config.text`/`config.photoUrl`; редактор берёт текст из первой карточки `type:"text"`. Поэтому **всегда заполняй и `text`, и `cards`**.
45
46
 
46
47
  ## Файлы скилла
@@ -46,7 +46,7 @@
46
46
  - `SEND_PHOTO` — `{ "photoUrl": "https://...", "text": "подпись (необязательно)" }`
47
47
 
48
48
  ### Логика / ветвление
49
- - `CONDITION` — выходы `yes` / `no`. `{ "match":"ALL"|"ANY", "conditions":[ { "kind":"...", "op":"...", "key":"...", "value":"..." } ] }`
49
+ - `CONDITION` — проверка условий, выходы `yes` / `no`. `{ "match":"ALL"|"ANY", "conditions":[ { "kind":"...", "op":"...", "key":"...", "value":"..." } ] }`. `match:"ALL"` — все условия истинны; `"ANY"` — хотя бы одно. Полный список `kind`/`op`/полей — в разделе [«Условия CONDITION»](#условия-condition).
50
50
  - `BRANCH` — `{ "cases":[ {"id":"c1","label":"...","expression":"var.x=='a'"} ], "hasDefault": false, "abTest": false }`. Выходы: `case_<id>` (+ `default`).
51
51
  - `ASK_QUESTION` — вопрос со сбором ответа. `{ "promptText":"...","saveTo":"name","validator":"ANY"|"PHONE"|"EMAIL"|"REGEX","regex":"...","retryText":"...","maxAttempts":3 }`. Выходы `valid` / `invalid`.
52
52
  - `END` — `{}` (конец ветки).
@@ -65,6 +65,28 @@
65
65
  - `CALL_WEBHOOK` — `{ "url":"https://...", "method":"POST", "bodyTemplate":"{...}", "timeoutMs":5000 }`. Выходы `ok` / `error`.
66
66
  - `AI_REPLY`, `PAYMENT_LINK`.
67
67
 
68
+ ## Условия CONDITION
69
+
70
+ Каждый элемент `conditions[]` — `{ "kind", "op", ...поля }`. Любая внутренняя ошибка условия = `false` (узел уходит в `no`).
71
+
72
+ | `kind` | `op` (допустимые) | Поля | Что проверяет |
73
+ |---|---|---|---|
74
+ | `TAG` | `HAS`, `NOT_HAS` | `value` — метка `[a-z0-9_-]{1,64}` | есть ли у пользователя тег |
75
+ | `VARIABLE` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY`, `GT`, `LT` | `key` — имя переменной `[a-z_][a-z0-9_]{0,63}`, `value` | значение переменной (`GT`/`LT` — числовое сравнение) |
76
+ | `UTM` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `key` ∈ `source`/`medium`/`campaign`/`content`/`term`, `value` | UTM-метку клика (`utm_<key>`), регистронезависимо |
77
+ | `NAME` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | имя из профиля, регистронезависимо |
78
+ | `EMAIL` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | email из профиля |
79
+ | `PHONE` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | телефон из профиля |
80
+ | `USERNAME` | `EQUALS`, `CONTAINS` | `value` | @username пользователя Telegram |
81
+ | `SUBSCRIBED` | `SUBSCRIBED`, `NOT_SUBSCRIBED` | `key` — **числовой** id канала/группы (напр. `-1001234567890`) | подписан ли пользователь на канал бота |
82
+ | `CURRENT_DATE` | `BEFORE`, `AFTER`, `EQUALS` | `value` — дата `YYYY-MM-DD` | сегодняшнюю дату (МСК) |
83
+ | `CURRENT_TIME` | `BETWEEN` | `value`, `value2` — время `HH:mm` | текущее время в интервале (через полночь — если `value`>`value2`) |
84
+ | `DAY_OF_WEEK` | `IN` | `days` — массив из `MON`,`TUE`,`WED`,`THU`,`FRI`,`SAT`,`SUN` | день недели (МСК) |
85
+
86
+ Для `NOT_EMPTY`/`EMPTY` поле `value` не нужно. `UTM` без `key` всегда `false`.
87
+
88
+ **`SUBSCRIBED`** работает только если бот **админ** в канале/группе и канал «привязан» (бот узнаёт о членстве через хук `my_chat_member` — добавь бота в канал админом). `key` должен парситься в число, иначе условие = `false`. Профильные поля (`NAME`/`EMAIL`/`PHONE`) и UTM заполняются по ходу воронки (`ASK_QUESTION`→`saveTo`, диплинк-клик с UTM).
89
+
68
90
  ## Выходные хэндлы (`sourceHandle`) — шпаргалка
69
91
  | Узел | Хэндлы |
70
92
  |---|---|
@@ -30,6 +30,7 @@
30
30
  - `TRIGGER_TEXT`: `matchMode` ∈ {ANY,EQUALS,CONTAINS,REGEX}; для не-ANY нужен `value`.
31
31
  - `ASK_QUESTION`: `promptText` обязателен; `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}`.
32
32
  - `BRANCH`: ≥1 case; выражения валидны (или `abTest:true`).
33
+ - `CONDITION`: `match` ∈ {ALL, ANY} (`CONDITION_BAD_MATCH`); ≥1 элемент в `conditions` (`CONDITION_EMPTY`). Для `kind:"TAG"` — `value` ∈ `[a-z0-9_-]{1,64}` (`CONDITION_BAD_TAG`); для `kind:"VARIABLE"` — `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`CONDITION_BAD_KEY`). Остальные `kind` (UTM/NAME/EMAIL/PHONE/USERNAME/SUBSCRIBED/CURRENT_*/DAY_OF_WEEK) бэкенд проверяет в рантайме — некорректный `kind`/`op` или нечисловой `key` у `SUBSCRIBED` молча дают `false` (выход `no`), публикацию не блокируют. Список `kind`/`op` — в schema.md.
33
34
  - `CALL_WEBHOOK`: `url` обязателен, http(s)://.
34
35
  - `DELAY`: `FIXED` с `durationSec>0`, либо `UNTIL` с `isoDate`+`time`.
35
36
  - Метки: `[a-z0-9_-]{1,64}`. Переменные: `[a-z_][a-z0-9_]{0,63}`.
@@ -19,6 +19,22 @@ const warns = [];
19
19
 
20
20
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21
21
  const VAR_RE = /^[a-z_][a-z0-9_]{0,63}$/;
22
+ const TAG_RE = /^[a-z0-9_-]{1,64}$/;
23
+
24
+ // kind -> допустимые op (для подсказок; бэкенд блокирует только TAG/VARIABLE)
25
+ const COND_OPS = {
26
+ TAG: ["HAS", "NOT_HAS"],
27
+ VARIABLE: ["EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY", "GT", "LT"],
28
+ UTM: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
29
+ NAME: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
30
+ EMAIL: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
31
+ PHONE: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
32
+ USERNAME: ["EQUALS", "CONTAINS"],
33
+ SUBSCRIBED: ["SUBSCRIBED", "NOT_SUBSCRIBED"],
34
+ CURRENT_DATE: ["BEFORE", "AFTER", "EQUALS"],
35
+ CURRENT_TIME: ["BETWEEN"],
36
+ DAY_OF_WEEK: ["IN"],
37
+ };
22
38
 
23
39
  // cardsToLegacy: текст = первая text-карточка с непустым text (или image.url -> photoUrl)
24
40
  function cardsToLegacy(cards) {
@@ -99,6 +115,26 @@ for (const n of nodes) {
99
115
  case "BRANCH":
100
116
  if (!Array.isArray(c.cases) || c.cases.length === 0) errors.push(`${who}: нужен хотя бы один case.`);
101
117
  break;
118
+ case "CONDITION": {
119
+ if (c.match !== "ALL" && c.match !== "ANY") errors.push(`CONDITION_BAD_MATCH: ${who} — match ∈ {ALL,ANY}.`);
120
+ if (!Array.isArray(c.conditions) || c.conditions.length === 0) {
121
+ errors.push(`CONDITION_EMPTY: ${who} — нужно хотя бы одно условие.`);
122
+ break;
123
+ }
124
+ c.conditions.forEach((cond, i) => {
125
+ if (!cond || typeof cond !== "object") { errors.push(`${who}: условие #${i + 1} — не объект.`); return; }
126
+ const kind = cond.kind;
127
+ // Бэкенд блокирует публикацию только для TAG.value и VARIABLE.key:
128
+ if (kind === "TAG" && !TAG_RE.test(String(cond.value || ""))) errors.push(`CONDITION_BAD_TAG: ${who} — TAG.value ∈ [a-z0-9_-]{1,64}.`);
129
+ if (kind === "VARIABLE" && !VAR_RE.test(String(cond.key || ""))) errors.push(`CONDITION_BAD_KEY: ${who} — VARIABLE.key ∈ [a-z_][a-z0-9_]{0,63}.`);
130
+ // Остальное — подсказки (рантайм бэкенда отдаст false, но публикацию не сорвёт):
131
+ if (!COND_OPS[kind]) warns.push(`${who}: неизвестный kind «${kind}» в условии #${i + 1} — рантайм даст false.`);
132
+ else if (cond.op && !COND_OPS[kind].includes(cond.op)) warns.push(`${who}: op «${cond.op}» не из {${COND_OPS[kind].join(",")}} для ${kind} — рантайм даст false.`);
133
+ if (kind === "SUBSCRIBED" && !/^-?\d+$/.test(String(cond.key || "").trim())) warns.push(`${who}: SUBSCRIBED.key должен быть числовым id канала — иначе false (и бот должен быть админом канала).`);
134
+ if (kind === "UTM" && !cond.key) warns.push(`${who}: UTM без key — рантайм даст false (ожидается source/medium/campaign/content/term).`);
135
+ });
136
+ break;
137
+ }
102
138
  }
103
139
  }
104
140
 
package/src/index.mjs CHANGED
@@ -1,39 +1,69 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * zaytsv-bot-graph-mcp — MCP-сервер для сборки и публикации графов Telegram-ботов
4
- * через API сервиса zaytsv /bots. Без внешних зависимостей (голый JSON-RPC по stdio),
5
- * поэтому работает сразу, без npm install — в Claude Code, Cursor, Windsurf и т.п.
3
+ * zaytsv-bot-graph-mcp — MCP-сервер для сборки и публикации воронок Telegram-ботов
4
+ * через API сервиса zaytsv /bots. Без внешних зависимостей (голый JSON-RPC по stdio).
6
5
  *
7
- * Авторизация: персональный токен (PAT) Bearer; fallback — session-cookie.
6
+ * Авторизация порядке приоритета):
7
+ * 1) env ZAYTSV_MCP_TOKEN — персональный токен "zmcp_..."
8
+ * 2) файл ~/.zaytsv-bot-graph/token (заполняется инструментом set_token)
9
+ * 3) session-cookie (ZAYTSV_SESSION_COOKIE / ZAYTSV_COOKIE) — fallback
10
+ *
11
+ * Если токена нет — инструменты не падают с сухой ошибкой, а возвращают пошаговую
12
+ * инструкцию; есть инструменты `setup` (статус + как подключить) и `set_token`
13
+ * (пользователь присылает токен в чат — агент сохраняет его в конфиг, без рестарта).
8
14
  *
9
15
  * ENV:
10
- * ZAYTSV_MCP_TOKEN персональный токен "zmcp_..." (создаётся в вебе: /bots/mcp-tokens)
11
- * ZAYTSV_BASE_URL база API. По умолчанию https://zaytsv.ru (дев: http://localhost:8066)
12
- * ZAYTSV_SESSION_COOKIE (fallback) значение куки SESSION из браузера
13
- * ZAYTSV_COOKIE (fallback) полная строка Cookie
16
+ * ZAYTSV_MCP_TOKEN, ZAYTSV_BASE_URL, ZAYTSV_SESSION_COOKIE, ZAYTSV_COOKIE
14
17
  */
15
18
 
16
19
  import { createInterface } from "node:readline";
20
+ import os from "node:os";
21
+ import fs from "node:fs";
22
+ import path from "node:path";
17
23
 
18
- const VERSION = "0.1.0";
24
+ const VERSION = "0.3.0";
19
25
  const BASE = (process.env.ZAYTSV_BASE_URL || "https://zaytsv.ru").replace(/\/+$/, "");
20
- const TOKEN = (process.env.ZAYTSV_MCP_TOKEN || "").trim();
21
- const COOKIE =
22
- process.env.ZAYTSV_COOKIE ||
23
- (process.env.ZAYTSV_SESSION_COOKIE ? `SESSION=${process.env.ZAYTSV_SESSION_COOKIE}` : "");
26
+ const CONFIG_DIR = path.join(os.homedir(), ".zaytsv-bot-graph");
27
+ const TOKEN_FILE = path.join(CONFIG_DIR, "token");
28
+ const TOKENS_PAGE = `${BASE}/bots/mcp-tokens`;
29
+
30
+ function readFileToken() {
31
+ try { return fs.readFileSync(TOKEN_FILE, "utf8").trim(); } catch { return ""; }
32
+ }
33
+ function getToken() {
34
+ return (process.env.ZAYTSV_MCP_TOKEN || "").trim() || readFileToken();
35
+ }
36
+ function getCookie() {
37
+ return process.env.ZAYTSV_COOKIE ||
38
+ (process.env.ZAYTSV_SESSION_COOKIE ? `SESSION=${process.env.ZAYTSV_SESSION_COOKIE}` : "");
39
+ }
40
+ function saveToken(token) {
41
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
42
+ fs.writeFileSync(TOKEN_FILE, token.trim() + "\n", { mode: 0o600 });
43
+ try { fs.chmodSync(TOKEN_FILE, 0o600); } catch { /* windows */ }
44
+ }
45
+ function isAuthed() { return !!(getToken() || getCookie()); }
46
+
47
+ const NO_AUTH_HELP =
48
+ "Нет доступа к zaytsv /bots — не настроена авторизация.\n\n" +
49
+ `Как подключить (помоги пользователю по шагам):\n` +
50
+ `1) Открой ${TOKENS_PAGE} и войди в аккаунт.\n` +
51
+ `2) Нажми «Создать токен», скопируй секрет (вид zmcp_...). Он показывается один раз.\n` +
52
+ `3) Пришли токен сюда — и я вызову инструмент set_token, чтобы сохранить его. (Или задай env ZAYTSV_MCP_TOKEN.)\n\n` +
53
+ "Подсказка ассистенту: попроси пользователя выполнить шаги 1–2 и прислать токен, затем вызови set_token с этим значением.";
24
54
 
25
55
  function authHeaders() {
26
56
  const h = { "Content-Type": "application/json" };
27
- if (TOKEN) h.Authorization = `Bearer ${TOKEN}`;
28
- else if (COOKIE) h.Cookie = COOKIE;
57
+ const token = getToken();
58
+ const cookie = getCookie();
59
+ if (token) h.Authorization = `Bearer ${token}`;
60
+ else if (cookie) h.Cookie = cookie;
29
61
  return h;
30
62
  }
31
63
 
32
- async function api(path, { method = "GET", body } = {}) {
33
- if (!TOKEN && !COOKIE) {
34
- throw new Error("Не задана авторизация: установи ZAYTSV_MCP_TOKEN (рекомендуется) или ZAYTSV_SESSION_COOKIE в env.");
35
- }
36
- const res = await fetch(`${BASE}${path}`, {
64
+ async function api(path_, { method = "GET", body } = {}) {
65
+ if (!isAuthed()) throw new Error(NO_AUTH_HELP);
66
+ const res = await fetch(`${BASE}${path_}`, {
37
67
  method,
38
68
  headers: authHeaders(),
39
69
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -42,8 +72,12 @@ async function api(path, { method = "GET", body } = {}) {
42
72
  let data = null;
43
73
  try { data = text ? JSON.parse(text) : null; } catch { data = text; }
44
74
  if (!res.ok) {
75
+ if (res.status === 401 || res.status === 403) {
76
+ throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден, отозван или истёк.\n` +
77
+ `Создай новый на ${TOKENS_PAGE} и пришли мне — я сохраню через set_token.`);
78
+ }
45
79
  const msg = typeof data === "string" ? data : JSON.stringify(data);
46
- throw new Error(`${method} ${path} → HTTP ${res.status}. ${(msg || "").slice(0, 600)}`);
80
+ throw new Error(`${method} ${path_} → HTTP ${res.status}. ${(msg || "").slice(0, 600)}`);
47
81
  }
48
82
  return data;
49
83
  }
@@ -60,6 +94,8 @@ function extractGraph(g) {
60
94
  }
61
95
 
62
96
  const TOOLS = [
97
+ { name: "setup", description: "Показать статус авторизации и пошаговую инструкцию подключения. Вызывай первым, если пользователь не знает, что делать, или при ошибке доступа.", inputSchema: { type: "object", properties: {} } },
98
+ { name: "set_token", description: "Сохранить персональный токен (zmcp_...), который пользователь создал на /bots/mcp-tokens. Применяется сразу, без рестарта.", inputSchema: { type: "object", properties: { token: { type: "string", description: "Секрет токена, начинается с zmcp_" } }, required: ["token"] } },
63
99
  { name: "list_bots", description: "Список ботов пользователя (id, имя, статус).", inputSchema: { type: "object", properties: {} } },
64
100
  { name: "list_graphs", description: "Список графов (сценариев) бота.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
65
101
  { name: "get_graph", description: "Получить граф целиком по graphId.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
@@ -73,6 +109,25 @@ const TOOLS = [
73
109
  async function handleCall(params) {
74
110
  const a = (params && params.arguments) || {};
75
111
  switch (params && params.name) {
112
+ case "setup": {
113
+ if (isAuthed()) {
114
+ const via = getToken() ? "персональный токен" : "session-cookie";
115
+ return okResult(`✅ Авторизация настроена (${via}). База API: ${BASE}.\n` +
116
+ `Можно собирать и публиковать ботов: list_bots, create_graph, import_funnel и др.`);
117
+ }
118
+ return okResult(NO_AUTH_HELP);
119
+ }
120
+ case "set_token": {
121
+ const t = (a.token || "").trim();
122
+ if (!t) throw new Error("Передай token — секрет вида zmcp_..., который ты создал на " + TOKENS_PAGE);
123
+ saveToken(t);
124
+ const warn = t.startsWith("zmcp_") ? "" : "\n⚠️ Обычно токен начинается с «zmcp_» — проверь, что скопирован весь секрет.";
125
+ // лёгкая проверка валидности
126
+ let check = "";
127
+ try { const bots = await api("/api/tg/bots"); check = `\nПроверка: доступно ботов — ${Array.isArray(bots) ? bots.length : "?"}.`; }
128
+ catch (e) { check = `\n⚠️ Токен сохранён, но проверка не прошла: ${(e.message || "").split("\n")[0]}`; }
129
+ return okResult(`✅ Токен сохранён (${TOKEN_FILE}). Применяется сразу.${warn}${check}`);
130
+ }
76
131
  case "list_bots": return okResult(await api("/api/tg/bots"));
77
132
  case "list_graphs": return okResult(await api(`/api/tg/bots/${a.botId}/graphs`));
78
133
  case "get_graph": return okResult(await api(`/api/tg/graphs/${a.graphId}`));
@@ -138,7 +193,6 @@ rl.on("line", async (line) => {
138
193
  } else if (method === "ping") {
139
194
  send({ jsonrpc: "2.0", id, result: {} });
140
195
  } else if (id !== undefined && id !== null) {
141
- // неизвестный метод с id — корректный JSON-RPC error; нотификации игнорируем
142
196
  send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
143
197
  }
144
198
  } catch (e) {
@@ -146,4 +200,4 @@ rl.on("line", async (line) => {
146
200
  }
147
201
  });
148
202
 
149
- process.stderr.write(`[zaytsv-bot-graph] MCP ${VERSION}. BASE=${BASE}. Авторизация: ${TOKEN ? "токен (Bearer)" : COOKIE ? "cookie" : "НЕ задана"}.\n`);
203
+ process.stderr.write(`[zaytsv-bot-graph] MCP ${VERSION}. BASE=${BASE}. Авторизация: ${getToken() ? "токен" : getCookie() ? "cookie" : "не задана (вызови setup)"}.\n`);