bitrix24-mcp-bridge 0.1.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/LICENSE +21 -0
- package/README.md +170 -0
- package/actions.example.json +33 -0
- package/dist/cli.js +25905 -0
- package/docs/api-notes.md +130 -0
- package/extension/dist/connector.js +121 -0
- package/extension/dist/connector.js.map +7 -0
- package/extension/dist/sessid-shim.js +24 -0
- package/extension/dist/sessid-shim.js.map +7 -0
- package/package.json +37 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Bitrix24 API notes (reverse-engineered)
|
|
2
|
+
|
|
3
|
+
Справочник по внутреннему API Bitrix24, добытый **живым реверсом** (у внутренних ajax-контроллеров
|
|
4
|
+
нет публичной документации). Предназначен для любого ИИ-агента или разработчика, который вызывает
|
|
5
|
+
эти методы через мост (`bitrix_call` / типизированные инструменты) или расширяет каталог.
|
|
6
|
+
|
|
7
|
+
Наблюдения сняты на портале `example.bitrix24.ru` (облако, мессенджер **im.v2**, задачи в режиме
|
|
8
|
+
**scrum-борда**). Имена полей и конвенции стандартны для облачного Bitrix24, но при переносе на другой
|
|
9
|
+
портал сверяйся реверсом (`docs/reconnaissance.md`).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Как вызывать
|
|
14
|
+
|
|
15
|
+
- **Типизированные инструменты** (`bitrix_tasks_list`, `bitrix_task_get`, `bitrix_projects_list`,
|
|
16
|
+
`bitrix_project_get`, `bitrix_chats_recent`, `bitrix_chat_messages`) — с разумными дефолтами.
|
|
17
|
+
Точная выборка — через опциональный аргумент **`params`** (нативный формат Bitrix), он мержится
|
|
18
|
+
**последним** и перекрывает дефолты. Подсказки по параметрам есть в описании каждого инструмента.
|
|
19
|
+
- **`bitrix_call { name, params }`** — универсальный вызов любого разрешённого имени из каталога
|
|
20
|
+
(`actions.json`). Тот же `params`-механизм.
|
|
21
|
+
- **`bitrix_help`** — этот же гайд, отдаётся через MCP (инструмент + resource `bitrix://api-notes`),
|
|
22
|
+
чтобы любой агент разобрался без доступа к репозиторию. Источник — **этот файл** (`docs/api-notes.md`).
|
|
23
|
+
- Всё **read-only**: мутирующие вызовы (`…add/update/delete/set/complete/…`) отвергаются при загрузке каталога.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Формат ответа (важно!)
|
|
28
|
+
|
|
29
|
+
Ajax-контроллеры (`/bitrix/services/main/ajax.php`) отвечают HTTP 200 с конвертом:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
{ "status": "success" | "error",
|
|
33
|
+
"data": { ... },
|
|
34
|
+
"errors": [] } // ПУСТОЙ массив при успехе! (в JS [] — truthy)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Успех определяется `status:"success"` и/или **пустым** `errors`. Непустой `errors` / `status:"error"` /
|
|
38
|
+
top-level `error` — реальная ошибка.
|
|
39
|
+
- REST-эндпоинты (`/rest/*.json`) отвечают иначе: `{ "result": …, "next", "total", "time" }` — без `errors`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. Транспорт и конвенции параметров (различаются по методу!)
|
|
44
|
+
|
|
45
|
+
Мост шлёт `application/x-www-form-urlencoded` со **свежим `sessid`** в теле; вложенность кодируется
|
|
46
|
+
PHP-стилем (`filter[STATUS]=2`, `select[0]=ID`). Объект в `params` рекурсивно разворачивается в такие ключи.
|
|
47
|
+
|
|
48
|
+
| Метод (action / endpoint) | Транспорт | Где `select`/`filter`/`order` | Пагинация |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `tasks.task.list` | ajax, форма | **верхний уровень**: `select[]`, `filter{}`, `order{}` | `start` (сдвиг, шаг 50) |
|
|
51
|
+
| `tasks.task.get` | ajax, форма | верхний уровень: `taskId`, `select[]` | — |
|
|
52
|
+
| `socialnetwork.api.workgroup.list` | ajax, форма | **верхний уровень**: `select[]`, `order{}`, `filter{}` | (nav — не проверено) |
|
|
53
|
+
| `socialnetwork.api.workgroup.get` | ajax, форма | **обёрнуто**: `params[groupId]`, `params[select][]` | — |
|
|
54
|
+
| `im.v2.Chat.Message.list` | ajax, форма | плоско: `chatId`, `limit`, `filter[lastId]` | `filter[lastId]` + `limit` |
|
|
55
|
+
| `im.recent.list` (`/rest/im.recent.list.json`) | rest, форма | плоско: `LIMIT`, `SKIP_OPENLINES`, `UNREAD_ONLY`, … | `LIMIT` |
|
|
56
|
+
|
|
57
|
+
⚠️ **`tasks.v2.*` (напр. `tasks.v2.Task.get`) шлёт JSON-тело** (`{"task":{"id":N}}`), а мост кодирует
|
|
58
|
+
форму — **не используй `tasks.v2.*`**, бери классические `tasks.task.*`. Мессенджер `im.v2.*` при этом
|
|
59
|
+
принимает обычную форму (это разные подсистемы, несмотря на общий префикс «v2»).
|
|
60
|
+
|
|
61
|
+
CSRF: контроллеры требуют `sessid` в теле — расширение подставляет свежий `BX.bitrix_sessid()`
|
|
62
|
+
автоматически. Ответ `invalid_csrf`/`invalid_authentication` = сессия протухла (перелогинься в браузере).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Триада: инструмент → action → параметры
|
|
67
|
+
|
|
68
|
+
| Инструмент | Каталог (`actions.json`) | action / endpoint | Ключевые params |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `bitrix_tasks_list` | `tasks.list` | `tasks.task.list` | `filter{RESPONSIBLE_ID,REAL_STATUS,GROUP_ID,…}`, `select[]`, `order{}`, `start` |
|
|
71
|
+
| `bitrix_task_get` | `task.get` | `tasks.task.get` | `taskId`, `select[]` |
|
|
72
|
+
| `bitrix_projects_list` | `projects.list` | `socialnetwork.api.workgroup.list` | `select[]`, `order{}`, `filter{}` |
|
|
73
|
+
| `bitrix_project_get` | `projects.get` | `socialnetwork.api.workgroup.get` | `params[groupId]`, `params[select][]` |
|
|
74
|
+
| `bitrix_chats_recent` | `chats.recent` | `/rest/im.recent.list.json` | `LIMIT`, `UNREAD_ONLY`, `SKIP_OPENLINES` |
|
|
75
|
+
| `bitrix_chat_messages` | `chat.messages` | `im.v2.Chat.Message.list` | `chatId`, `limit`, `filter[lastId]` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 5. Справочник полей
|
|
80
|
+
|
|
81
|
+
**Задача** (`tasks.task.*`): `ID`, `TITLE`, `DESCRIPTION`, `STATUS`, `REAL_STATUS`, `RESPONSIBLE_ID`,
|
|
82
|
+
`CREATED_BY`, `CREATED_DATE`, `CHANGED_DATE`, `DEADLINE`, `CLOSED_DATE`, `PRIORITY`, `GROUP_ID`,
|
|
83
|
+
`TAGS`, `TIME_ESTIMATE`, `UF_*`. Ответ также подкладывает объекты `group`, `responsible`, `creator`, `action`.
|
|
84
|
+
|
|
85
|
+
Статусы задачи (`STATUS`): `1` Новая · `2` Ждёт выполнения · `3` Выполняется · `4` Ждёт контроля ·
|
|
86
|
+
`5` Завершена · `6` Отложена · `7` Отклонена.
|
|
87
|
+
|
|
88
|
+
**Группа/проект — список** (`workgroup.list`, camelCase в ответе): `ID`, `NAME`, `DESCRIPTION`,
|
|
89
|
+
`NUMBER_OF_MEMBERS`, `OWNER_ID`, `DATE_CREATE`, `PROJECT` (Y/N), `TYPE`.
|
|
90
|
+
**Группа — карточка** (`workgroup.get`): плюс `OWNER_DATA`, `SUBJECT_DATA`, `MEMBERS[]`,
|
|
91
|
+
`MODERATOR_MEMBERS[]`, `CHAT_ID`, `DIALOG_ID`, `IMAGE_ID`, UF-поля.
|
|
92
|
+
|
|
93
|
+
**Недавний чат** (`im.recent.list`): `chat_id`, `title`, `type`, `message{id,text,date,author_id}`,
|
|
94
|
+
`last_id`, `unread`, `pinned`, `user{…}`, `chat{…}`.
|
|
95
|
+
**Сообщение** (`im.v2.Chat.Message.list`): `id`, `chatId`, `authorId`, `date`, `text`, `params`, `viewed`.
|
|
96
|
+
Ответ также несёт `users[]` (участники), `additionalMessages[]`, `hasPrevPage`/`hasNextPage`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 6. Примеры точных выборок
|
|
101
|
+
|
|
102
|
+
```jsonc
|
|
103
|
+
// открытые задачи ответственного 55, по дедлайну, вторая страница
|
|
104
|
+
bitrix_tasks_list { "params": { "filter": {"RESPONSIBLE_ID":55,"REAL_STATUS":2},
|
|
105
|
+
"order": {"DEADLINE":"asc"}, "start": 50 } }
|
|
106
|
+
|
|
107
|
+
// карточка задачи с расширенным набором полей
|
|
108
|
+
bitrix_task_get { "taskId": 4229, "params": { "select": ["ID","TITLE","DESCRIPTION","TAGS","TIME_ESTIMATE"] } }
|
|
109
|
+
|
|
110
|
+
// проекты, отсортированные по дате создания
|
|
111
|
+
bitrix_projects_list { "params": { "order": {"DATE_CREATE":"desc"} } }
|
|
112
|
+
|
|
113
|
+
// полная карточка группы
|
|
114
|
+
bitrix_project_get { "groupId": 15 }
|
|
115
|
+
|
|
116
|
+
// глубже в историю чата
|
|
117
|
+
bitrix_chat_messages { "chatId": 485, "limit": 50, "beforeId": 1936695 }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 7. Как расширить (новый домен: календарь, диск, CRM, …)
|
|
123
|
+
|
|
124
|
+
1. Сними реальные вызовы (`docs/reconnaissance.md` — авто-запись `bun run capture` или HAR).
|
|
125
|
+
2. Определи транспорт и форму параметров по разделу 3 (сверься с `sampleParams` из черновика).
|
|
126
|
+
3. Добавь запись в `actions.json` (только read-only). Проверь через `bitrix_call { name, params }`.
|
|
127
|
+
4. Если удобно — оберни в типизированный инструмент в `src/tools/register.ts` (с дефолтным `select`).
|
|
128
|
+
|
|
129
|
+
> `actions.json` — данные конкретного портала (gitignored). Этот файл (`api-notes.md`) — переносимые
|
|
130
|
+
> знания о самом API; держи его в актуальном состоянии при добавлении новых методов.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// extension/src/bridge-core.ts
|
|
4
|
+
function encodeForm(params) {
|
|
5
|
+
let out = new URLSearchParams(), add = (key, val) => {
|
|
6
|
+
if (val != null)
|
|
7
|
+
if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));
|
|
8
|
+
else if (typeof val == "object") for (let [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);
|
|
9
|
+
else out.set(key, String(val));
|
|
10
|
+
};
|
|
11
|
+
for (let [k, v] of Object.entries(params)) add(k, v);
|
|
12
|
+
return out.toString();
|
|
13
|
+
}
|
|
14
|
+
function buildRequest(origin, req, sessid) {
|
|
15
|
+
let q = req.action ? `?action=${encodeURIComponent(req.action)}` : "", url = `${origin}${req.endpoint}${q}`, body = encodeForm({ ...req.params, sessid });
|
|
16
|
+
return { url, body };
|
|
17
|
+
}
|
|
18
|
+
function interpret(json) {
|
|
19
|
+
let list = json && (Array.isArray(json.errors) ? json.errors : json.error ? [{ code: json.error }] : []), hasErrors = Array.isArray(list) && list.length > 0;
|
|
20
|
+
if (json && (json.status === "error" || hasErrors)) {
|
|
21
|
+
let first = list[0], code = first && (first.code || first.message) || json.error || "bitrix_error", description = json.error_description || first && first.message;
|
|
22
|
+
return { ok: !1, error: description && description !== code ? `${code}: ${description}` : code, data: json };
|
|
23
|
+
}
|
|
24
|
+
return { ok: !0, data: json };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// extension/src/bridge-protocol.ts
|
|
28
|
+
function parseConfig(raw) {
|
|
29
|
+
let doc;
|
|
30
|
+
try {
|
|
31
|
+
doc = JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("config.json is not valid JSON");
|
|
34
|
+
}
|
|
35
|
+
if (typeof doc != "object" || doc === null)
|
|
36
|
+
throw new Error("config.json must be an object like { token, port }");
|
|
37
|
+
let obj = doc, token = typeof obj.token == "string" ? obj.token.trim() : "";
|
|
38
|
+
if (!token) throw new Error("config.json is missing a non-empty `token`");
|
|
39
|
+
let port = Number(obj.port);
|
|
40
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
41
|
+
throw new Error("config.json is missing a numeric `port`");
|
|
42
|
+
return { token, port };
|
|
43
|
+
}
|
|
44
|
+
var BRIDGE_MSG_SOURCE = "bitrix24-mcp-bridge";
|
|
45
|
+
function buildSessidRequest(nonce) {
|
|
46
|
+
return { source: BRIDGE_MSG_SOURCE, kind: "sessid-request", nonce };
|
|
47
|
+
}
|
|
48
|
+
function isRecord(v) {
|
|
49
|
+
return typeof v == "object" && v !== null;
|
|
50
|
+
}
|
|
51
|
+
function parseSessidResponse(data, expectedNonce) {
|
|
52
|
+
return !isRecord(data) || data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-response" || typeof data.nonce != "string" || data.nonce !== expectedNonce || typeof data.sessid != "string" ? null : { source: BRIDGE_MSG_SOURCE, kind: "sessid-response", nonce: data.nonce, sessid: data.sessid };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// extension/src/connector.ts
|
|
56
|
+
var ORIGIN = location.origin, SESSID_TIMEOUT_MS = 2e3, RECONNECT_MS = 3e3, socket = null, nonceSeq = 0;
|
|
57
|
+
function freshSessid() {
|
|
58
|
+
let nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let onMsg = (ev) => {
|
|
61
|
+
if (ev.source !== window) return;
|
|
62
|
+
let res = parseSessidResponse(ev.data, nonce);
|
|
63
|
+
res && (cleanup(), resolve(res.sessid));
|
|
64
|
+
}, timer = setTimeout(() => {
|
|
65
|
+
cleanup(), resolve("");
|
|
66
|
+
}, SESSID_TIMEOUT_MS), cleanup = () => {
|
|
67
|
+
clearTimeout(timer), window.removeEventListener("message", onMsg);
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener("message", onMsg), window.postMessage(buildSessidRequest(nonce), ORIGIN);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function handleCall(req) {
|
|
73
|
+
let sessid = await freshSessid();
|
|
74
|
+
if (!sessid) return { ok: !1, error: "session context not ready \u2014 open a normal Bitrix24 portal tab" };
|
|
75
|
+
let { url, body } = buildRequest(ORIGIN, req, sessid), init = {
|
|
76
|
+
method: req.method,
|
|
77
|
+
credentials: "include",
|
|
78
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
79
|
+
}, finalUrl = url;
|
|
80
|
+
req.method === "POST" ? init.body = body : finalUrl += (url.includes("?") ? "&" : "?") + body;
|
|
81
|
+
let json = await (await fetch(finalUrl, init)).json();
|
|
82
|
+
return interpret(json);
|
|
83
|
+
}
|
|
84
|
+
function connect(config) {
|
|
85
|
+
let ws = new WebSocket(`ws://127.0.0.1:${config.port}`);
|
|
86
|
+
ws.addEventListener("open", () => {
|
|
87
|
+
ws.send(JSON.stringify({ type: "auth", token: config.token })), socket = ws;
|
|
88
|
+
}), ws.addEventListener("message", async (ev) => {
|
|
89
|
+
let req;
|
|
90
|
+
try {
|
|
91
|
+
req = JSON.parse(ev.data);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error("[bitrix-bridge] malformed WS frame, dropping:", e);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (req.type === "call")
|
|
97
|
+
try {
|
|
98
|
+
let r = await handleCall(req);
|
|
99
|
+
ws.send(JSON.stringify({ type: "result", id: req.id, ok: r.ok, data: r.data, error: r.error }));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
ws.send(JSON.stringify({ type: "result", id: req.id, ok: !1, error: String(e) }));
|
|
102
|
+
}
|
|
103
|
+
}), ws.addEventListener("close", () => {
|
|
104
|
+
socket === ws && (socket = null), setTimeout(() => connect(config), RECONNECT_MS);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function start() {
|
|
108
|
+
let config;
|
|
109
|
+
try {
|
|
110
|
+
let resp = await fetch(chrome.runtime.getURL("config.json"));
|
|
111
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
112
|
+
config = parseConfig(await resp.text());
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error("[bitrix-bridge] cannot load config.json:", e);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
connect(config);
|
|
118
|
+
}
|
|
119
|
+
start();
|
|
120
|
+
})();
|
|
121
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bridge-core.ts", "../src/bridge-protocol.ts", "../src/connector.ts"],
|
|
4
|
+
"sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested.\n\n// Wire types come from the shared single source; re-exported so downstream\n// extension modules keep importing them from here.\nimport type { CallRequest } from \"../../shared/wire.ts\";\nexport type { CallRequest, CapturedEntry } from \"../../shared/wire.ts\";\n\n// Extension-only: the shape handleCall returns to the bridge (not a wire message).\nexport interface InterpretResult {\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function encodeForm(params: Record<string, unknown>): string {\n const out = new URLSearchParams();\n const add = (key: string, val: unknown): void => {\n if (val === null || val === undefined) return;\n if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));\n else if (typeof val === \"object\") for (const [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);\n else out.set(key, String(val));\n };\n for (const [k, v] of Object.entries(params)) add(k, v);\n return out.toString();\n}\n\nexport function buildRequest(\n origin: string,\n req: CallRequest,\n sessid: string,\n): { url: string; body: string } {\n const q = req.action ? `?action=${encodeURIComponent(req.action)}` : \"\";\n const url = `${origin}${req.endpoint}${q}`;\n const body = encodeForm({ ...req.params, sessid });\n return { url, body };\n}\n\n// Bitrix wraps errors in HTTP 200: { status:\"error\", errors:[{code}] } or { error, error_description }.\n// NOTE: successful ajax responses carry an EMPTY `errors: []` array \u2014 a truthy value in JS \u2014 so we\n// must treat only a NON-EMPTY errors list (or status:\"error\"/a top-level error) as a real failure.\nexport function interpret(json: any): InterpretResult {\n const list = json && (Array.isArray(json.errors) ? json.errors : json.error ? [{ code: json.error }] : []);\n const hasErrors = Array.isArray(list) && list.length > 0;\n if (json && (json.status === \"error\" || hasErrors)) {\n const first = list[0];\n const code = (first && (first.code || first.message)) || json.error || \"bitrix_error\";\n const description = json.error_description || (first && first.message);\n const error = description && description !== code ? `${code}: ${description}` : code;\n return { ok: false, error, data: json };\n }\n return { ok: true, data: json };\n}\n", "// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response, and \u2014 in the\n// capture build \u2014 MAIN-observed capture entries forwarded to the connector).\n\nimport type { CapturedEntry } from \"./bridge-core.ts\";\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n\n// --- capture forwarding (capture build only) -------------------------------\n//\n// The capture hooks must observe the PAGE's fetch/XHR, so they run in the MAIN world.\n// The socket lives only in the ISOLATED connector, so MAIN forwards each observed entry\n// to the connector over postMessage; the connector relays it to the daemon.\n\nexport interface CaptureForwardMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"capture\";\n call: CapturedEntry;\n}\n\nexport function buildCaptureForward(call: CapturedEntry): CaptureForwardMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call };\n}\n\n/** For the ISOLATED connector: a capture entry forwarded by the MAIN script, or null. */\nexport function parseCaptureForward(data: unknown): CaptureForwardMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"capture\") return null;\n if (!isRecord(data.call)) return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call: data.call as unknown as CapturedEntry };\n}\n", "// ISOLATED-world connector. This is where the token and the WebSocket live \u2014 never in\n// the page's MAIN world, so the page cannot read the token without knowing the dynamic\n// (`use_dynamic_url`) resource URL for config.json, and cannot hijack the socket. It has\n// `chrome.runtime`, so it loads the per-user config from the packaged config.json at\n// runtime (nothing per-user is baked into this JS \u2014 the file is static). The actual\n// boundary is `use_dynamic_url: true` on config.json in the manifest: it makes the\n// resource URL unguessable from the page while `chrome.runtime.getURL()` here still\n// resolves it.\n//\n// Per daemon call it asks the MAIN shim for a FRESH sessid over postMessage (nonce-matched),\n// then reuses the pure bridge-core helpers to build the request, fetch it with the page's\n// cookies, and interpret the response.\n\nimport { buildRequest, interpret, type CallRequest, type InterpretResult } from \"./bridge-core.ts\";\nimport {\n parseConfig,\n buildSessidRequest,\n parseSessidResponse,\n parseCaptureForward,\n type BridgeConfig,\n} from \"./bridge-protocol.ts\";\n\n// Gates the capture relay listener; stripped from normal builds via esbuild `define`.\ndeclare const __BITRIX_CAPTURE__: boolean;\n\n// Minimal ambient for the one chrome API we use (no @types/chrome dependency).\ndeclare const chrome: { runtime: { getURL(path: string): string } };\n\nconst ORIGIN = location.origin;\nconst SESSID_TIMEOUT_MS = 2000;\nconst RECONNECT_MS = 3000;\n\nlet socket: WebSocket | null = null;\nlet nonceSeq = 0;\n\n/** Ask the MAIN shim for a fresh sessid; resolves \"\" if it doesn't answer in time. */\nfunction freshSessid(): Promise<string> {\n const nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;\n return new Promise((resolve) => {\n const onMsg = (ev: MessageEvent): void => {\n if (ev.source !== window) return;\n const res = parseSessidResponse(ev.data, nonce);\n if (!res) return;\n cleanup();\n resolve(res.sessid);\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve(\"\");\n }, SESSID_TIMEOUT_MS);\n const cleanup = (): void => {\n clearTimeout(timer);\n window.removeEventListener(\"message\", onMsg);\n };\n window.addEventListener(\"message\", onMsg);\n window.postMessage(buildSessidRequest(nonce), ORIGIN);\n });\n}\n\nasync function handleCall(req: CallRequest): Promise<InterpretResult> {\n const sessid = await freshSessid();\n if (!sessid) return { ok: false, error: \"session context not ready \u2014 open a normal Bitrix24 portal tab\" }; // G5\n const { url, body } = buildRequest(ORIGIN, req, sessid);\n const init: RequestInit = {\n method: req.method,\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n };\n let finalUrl = url;\n if (req.method === \"POST\") init.body = body;\n else finalUrl += (url.includes(\"?\") ? \"&\" : \"?\") + body;\n const resp = await fetch(finalUrl, init);\n const json = await resp.json();\n return interpret(json); // G3\n}\n\nfunction connect(config: BridgeConfig): void {\n const ws = new WebSocket(`ws://127.0.0.1:${config.port}`);\n ws.addEventListener(\"open\", () => {\n ws.send(JSON.stringify({ type: \"auth\", token: config.token })); // auth first (server closes non-auth)\n socket = ws;\n });\n ws.addEventListener(\"message\", async (ev: MessageEvent) => {\n let req: CallRequest;\n try {\n req = JSON.parse(ev.data) as CallRequest;\n } catch (e) {\n console.error(\"[bitrix-bridge] malformed WS frame, dropping:\", e);\n return;\n }\n if (req.type !== \"call\") return;\n try {\n const r = await handleCall(req);\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: r.ok, data: r.data, error: r.error }));\n } catch (e) {\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: false, error: String(e) }));\n }\n });\n ws.addEventListener(\"close\", () => {\n if (socket === ws) socket = null;\n setTimeout(() => connect(config), RECONNECT_MS); // auto-reconnect\n });\n}\n\n// Capture build only: relay MAIN-observed capture entries to the daemon.\nif (__BITRIX_CAPTURE__) {\n window.addEventListener(\"message\", (ev: MessageEvent) => {\n if (ev.source !== window) return;\n const fwd = parseCaptureForward(ev.data);\n if (!fwd) return;\n if (socket && socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: \"capture\", call: fwd.call }));\n }\n });\n}\n\nasync function start(): Promise<void> {\n let config: BridgeConfig;\n try {\n const resp = await fetch(chrome.runtime.getURL(\"config.json\"));\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n config = parseConfig(await resp.text());\n } catch (e) {\n // Nothing to fall back to \u2014 surface why the bridge won't start and stop.\n console.error(\"[bitrix-bridge] cannot load config.json:\", e);\n return;\n }\n connect(config);\n}\n\nstart();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// extension/src/bridge-protocol.ts
|
|
4
|
+
var BRIDGE_MSG_SOURCE = "bitrix24-mcp-bridge";
|
|
5
|
+
function buildSessidResponse(nonce, sessid) {
|
|
6
|
+
return { source: BRIDGE_MSG_SOURCE, kind: "sessid-response", nonce, sessid };
|
|
7
|
+
}
|
|
8
|
+
function isRecord(v) {
|
|
9
|
+
return typeof v == "object" && v !== null;
|
|
10
|
+
}
|
|
11
|
+
function parseSessidRequest(data) {
|
|
12
|
+
return !isRecord(data) || data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-request" || typeof data.nonce != "string" ? null : { source: BRIDGE_MSG_SOURCE, kind: "sessid-request", nonce: data.nonce };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// extension/src/sessid-shim.ts
|
|
16
|
+
window.addEventListener("message", (ev) => {
|
|
17
|
+
if (ev.source !== window) return;
|
|
18
|
+
let req = parseSessidRequest(ev.data);
|
|
19
|
+
if (!req) return;
|
|
20
|
+
let sessid = window.BX?.bitrix_sessid?.() || "";
|
|
21
|
+
window.postMessage(buildSessidResponse(req.nonce, sessid), location.origin);
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
//# sourceMappingURL=sessid-shim.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/bridge-protocol.ts", "../src/sessid-shim.ts"],
|
|
4
|
+
"sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response, and \u2014 in the\n// capture build \u2014 MAIN-observed capture entries forwarded to the connector).\n\nimport type { CapturedEntry } from \"./bridge-core.ts\";\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n\n// --- capture forwarding (capture build only) -------------------------------\n//\n// The capture hooks must observe the PAGE's fetch/XHR, so they run in the MAIN world.\n// The socket lives only in the ISOLATED connector, so MAIN forwards each observed entry\n// to the connector over postMessage; the connector relays it to the daemon.\n\nexport interface CaptureForwardMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"capture\";\n call: CapturedEntry;\n}\n\nexport function buildCaptureForward(call: CapturedEntry): CaptureForwardMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call };\n}\n\n/** For the ISOLATED connector: a capture entry forwarded by the MAIN script, or null. */\nexport function parseCaptureForward(data: unknown): CaptureForwardMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"capture\") return null;\n if (!isRecord(data.call)) return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call: data.call as unknown as CapturedEntry };\n}\n", "// MAIN-world shim. The ONLY reason it runs in the page's MAIN world is that\n// window.BX (and thus the rotating CSRF sessid) lives there. It holds NO token and\n// NO socket \u2014 it just answers the ISOLATED connector's sessid requests.\n//\n// In the capture build it additionally installs the page-traffic recorder (which must\n// run in MAIN to observe the page's own fetch/XHR) and forwards each observed entry to\n// the connector over postMessage; the connector owns the socket and relays to the daemon.\n\nimport { parseSessidRequest, buildSessidResponse, buildCaptureForward } from \"./bridge-protocol.ts\";\nimport { installCapture } from \"./capture.ts\";\n\n// Stripped from normal builds via esbuild `define` + dead-code elimination.\ndeclare const __BITRIX_CAPTURE__: boolean;\n\n// window.BX is provided by the Bitrix24 page itself (MAIN world).\ndeclare global {\n interface Window {\n BX?: { bitrix_sessid?: () => string };\n }\n}\n\nwindow.addEventListener(\"message\", (ev: MessageEvent) => {\n if (ev.source !== window) return; // same frame only \u2014 ignore other frames/windows\n const req = parseSessidRequest(ev.data);\n if (!req) return;\n const sessid = window.BX?.bitrix_sessid?.() || \"\";\n window.postMessage(buildSessidResponse(req.nonce, sessid), location.origin);\n});\n\nif (__BITRIX_CAPTURE__) {\n installCapture((call) => window.postMessage(buildCaptureForward(call), location.origin));\n}\n"],
|
|
5
|
+
"mappings": ";;;AAiDO,MAAM,oBAAoB;AAmB1B,WAAS,oBAAoB,OAAe,QAAmC;AACpF,WAAO,EAAE,QAAQ,mBAAmB,MAAM,mBAAmB,OAAO,OAAO;AAAA,EAC7E;AAEA,WAAS,SAAS,GAA0C;AAC1D,WAAO,OAAO,KAAM,YAAY,MAAM;AAAA,EACxC;AAGO,WAAS,mBAAmB,MAAwC;AAGzE,WAFI,CAAC,SAAS,IAAI,KACd,KAAK,WAAW,qBAAqB,KAAK,SAAS,oBACnD,OAAO,KAAK,SAAU,WAAiB,OACpC,EAAE,QAAQ,mBAAmB,MAAM,kBAAkB,OAAO,KAAK,MAAM;AAAA,EAChF;;;AC7DA,SAAO,iBAAiB,WAAW,CAAC,OAAqB;AACvD,QAAI,GAAG,WAAW,OAAQ;AAC1B,QAAM,MAAM,mBAAmB,GAAG,IAAI;AACtC,QAAI,CAAC,IAAK;AACV,QAAM,SAAS,OAAO,IAAI,gBAAgB,KAAK;AAC/C,WAAO,YAAY,oBAAoB,IAAI,OAAO,MAAM,GAAG,SAAS,MAAM;AAAA,EAC5E,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "bitrix24-mcp-bridge",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "MCP bridge server for Bitrix24 — exposes REST API access, tasks, projects and chat to MCP clients like Claude Code.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/DarWiM/bitrix24-mcp-bridge.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": { "bitrix24-bridge": "dist/cli.js" },
|
|
12
|
+
"files": ["dist", "extension/dist", "docs/api-notes.md", "actions.example.json"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "bun test",
|
|
15
|
+
"dev": "bun run src/index.ts",
|
|
16
|
+
"typecheck": "bunx tsc --noEmit && bunx tsc -p extension/tsconfig.json",
|
|
17
|
+
"build:dist": "node scripts/build-dist.mjs",
|
|
18
|
+
"build:ext": "bun run scripts/build-extension.ts",
|
|
19
|
+
"build:ext:capture": "BITRIX_CAPTURE=1 bun run scripts/build-extension.ts",
|
|
20
|
+
"build:ext:static": "node scripts/build-ext-static.mjs",
|
|
21
|
+
"prepare": "node scripts/build-dist.mjs && node scripts/build-ext-static.mjs",
|
|
22
|
+
"capture": "bun run src/capture-server.ts",
|
|
23
|
+
"catalog:draft": "bun run src/catalog/build-catalog.ts captured.har > actions.draft.json",
|
|
24
|
+
"catalog": "bun run src/catalog/build-catalog.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
|
+
"ws": "^8.18.0",
|
|
29
|
+
"zod": "^3.23.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "^1.3.14",
|
|
33
|
+
"@types/ws": "^8.18.1",
|
|
34
|
+
"esbuild": "^0.23.0",
|
|
35
|
+
"typescript": "^5.5.0"
|
|
36
|
+
}
|
|
37
|
+
}
|