i2crm-mcp 0.1.2__tar.gz

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.
Files changed (33) hide show
  1. i2crm_mcp-0.1.2/.gitignore +29 -0
  2. i2crm_mcp-0.1.2/CHANGELOG.md +47 -0
  3. i2crm_mcp-0.1.2/LICENSE +21 -0
  4. i2crm_mcp-0.1.2/PKG-INFO +195 -0
  5. i2crm_mcp-0.1.2/README.md +174 -0
  6. i2crm_mcp-0.1.2/pyproject.toml +73 -0
  7. i2crm_mcp-0.1.2/scripts/check_no_deps.py +78 -0
  8. i2crm_mcp-0.1.2/scripts/check_version.py +66 -0
  9. i2crm_mcp-0.1.2/src/i2crm_mcp/__init__.py +12 -0
  10. i2crm_mcp-0.1.2/src/i2crm_mcp/cli.py +222 -0
  11. i2crm_mcp-0.1.2/src/i2crm_mcp/client.py +255 -0
  12. i2crm_mcp-0.1.2/src/i2crm_mcp/config.py +220 -0
  13. i2crm_mcp-0.1.2/src/i2crm_mcp/errors.py +183 -0
  14. i2crm_mcp-0.1.2/src/i2crm_mcp/listen.py +319 -0
  15. i2crm_mcp-0.1.2/src/i2crm_mcp/protocol.py +301 -0
  16. i2crm_mcp-0.1.2/src/i2crm_mcp/schema.py +84 -0
  17. i2crm_mcp-0.1.2/src/i2crm_mcp/server.py +194 -0
  18. i2crm_mcp-0.1.2/src/i2crm_mcp/spec.py +228 -0
  19. i2crm_mcp-0.1.2/src/i2crm_mcp/tools/__init__.py +40 -0
  20. i2crm_mcp-0.1.2/src/i2crm_mcp/tools/actions.py +416 -0
  21. i2crm_mcp-0.1.2/src/i2crm_mcp/tools/base.py +61 -0
  22. i2crm_mcp-0.1.2/src/i2crm_mcp/tools/knowledge.py +236 -0
  23. i2crm_mcp-0.1.2/tests/fake_api.py +215 -0
  24. i2crm_mcp-0.1.2/tests/test_actions.py +276 -0
  25. i2crm_mcp-0.1.2/tests/test_cli.py +193 -0
  26. i2crm_mcp-0.1.2/tests/test_client.py +254 -0
  27. i2crm_mcp-0.1.2/tests/test_config.py +186 -0
  28. i2crm_mcp-0.1.2/tests/test_errors.py +146 -0
  29. i2crm_mcp-0.1.2/tests/test_knowledge.py +223 -0
  30. i2crm_mcp-0.1.2/tests/test_listen.py +346 -0
  31. i2crm_mcp-0.1.2/tests/test_protocol.py +484 -0
  32. i2crm_mcp-0.1.2/tests/test_schema.py +116 -0
  33. i2crm_mcp-0.1.2/tests/test_spec.py +171 -0
@@ -0,0 +1,29 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+
5
+ # Локальные заметки и приватные инструкции агентам: адреса стендов, токены, ТЗ.
6
+ # Строки оставлены, чтобы копия, положенная в репозиторий «на минутку», не уехала
7
+ # в коммит — а из коммита в публичный снимок релиза.
8
+ CLAUDE.local.md
9
+ *.local.md
10
+ NOTES.md
11
+
12
+ # Артефакты сборки и вывод CI-задания install.
13
+ dist/
14
+ build/
15
+ *.egg-info/
16
+ before.txt
17
+ after.txt
18
+ where.txt
19
+
20
+ # Настройка сервера, если её случайно создали в рабочей копии: там пользовательский
21
+ # токен i2crm.
22
+ .i2crm-mcp.json
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+
28
+ # macOS
29
+ .DS_Store
@@ -0,0 +1,47 @@
1
+ # Изменения
2
+
3
+ Формат: SemVer. Minor — новые инструменты, patch — исправления, major — изменение
4
+ ответа или аргументов существующего инструмента так, что ломается рабочий промпт агента.
5
+
6
+ ## 0.1.2 - 2026-09-10
7
+
8
+ ### Изменено
9
+
10
+ - Публикация в PyPI: `pip install i2crm-mcp` без адреса реестра.
11
+ - Метаданные пакета: ссылки на сайт и AI-документацию, классификаторы Python 3.12 и 3.13.
12
+ - README: убраны ссылки, которые вне репозитория ведут в 404, и абзац про закрытый реестр.
13
+
14
+ ## 0.1.1 - 2026-09-10
15
+
16
+ ### Добавлено
17
+
18
+ - Лицензия
19
+
20
+ ## 0.1.0 — 2026-09-03
21
+
22
+ ### Добавлено
23
+
24
+ - Транспорт MCP на стандартной библиотеке: JSON-RPC 2.0 по строкам, методы `initialize`,
25
+ `tools/list`, `tools/call`, `ping`; согласование версии протокола.
26
+ - Разбор конверта публичного API: успех — `error: false`, отказы разбираются по
27
+ `data.validation` и `data.status` в типизированные исключения.
28
+ - Настройки: `I2CRM_BASE_URL` и `I2CRM_TOKEN` либо профиль пользователя по соглашению ОС;
29
+ команды `i2crm-mcp setup`, `where`, `version`.
30
+ - Клиент API: подстановка пользовательского токена и ключа канала, разбор конверта,
31
+ повтор только чтений — потерянный ответ на отправку не значит, что сообщение не ушло.
32
+ - Загрузка клиентской спеки с хоста и кеш на 10 минут; поиск пути в том виде, в каком его
33
+ присылает агент, и раскрытие `$ref` с защитой от циклов.
34
+ - Инструменты знания: `i2crm_playbook`, `i2crm_endpoints`, `i2crm_endpoint_get`,
35
+ `i2crm_schema_get`. Ответ, не влезающий в потолок, сворачивает вложенные схемы до имён
36
+ и говорит об этом, а не урезается молча.
37
+ - Команда `i2crm-mcp check`: спека читается без токена, поэтому «не тот адрес» и «не тот
38
+ токен» различимы. Ключи каналов в выводе — маской.
39
+ - Инструменты действий: `i2crm_diagnose`, `i2crm_sources`, `i2crm_targets`,
40
+ `i2crm_target_create`, `i2crm_target_update`, `i2crm_target_validate`,
41
+ `i2crm_reply_sources`, `i2crm_send`. Ключ исходящего канала подставляется по номеру
42
+ канала, а при единственном канале — молча; разрушающих операций нет намеренно.
43
+ - Разрежение запросов: у API своего лимита нет, поэтому паузу держит сам пакет, и
44
+ отправки разрежены сильнее чтений.
45
+ - Команда `i2crm-mcp listen`: локальный приёмник вебхуков — печатает входящие сообщения,
46
+ правки и статусы доставки и называет канал по подписи вебхука. Отвечает JSON, а не
47
+ пустым 200: тело ответа разбирается, и на не-JSON доставка повторяется.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ИП Слесарев Евгений Владимирович
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.5
2
+ Name: i2crm-mcp
3
+ Version: 0.1.2
4
+ Summary: MCP-сервер для публичного API i2crm: отправка и приём сообщений в мессенджерах из ИИ-агента
5
+ Project-URL: Homepage, https://i2crm.ru
6
+ Project-URL: Documentation, https://app.i2crm.ru/api_v1/for-ai
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Python: >=3.11
14
+ Provides-Extra: dev
15
+ Requires-Dist: build>=1.0; extra == 'dev'
16
+ Requires-Dist: ruff==0.16.3; extra == 'dev'
17
+ Requires-Dist: twine>=5.0; extra == 'dev'
18
+ Provides-Extra: lint
19
+ Requires-Dist: ruff==0.16.3; extra == 'lint'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # i2crm-mcp
23
+
24
+ MCP-сервер для публичного API i2crm: отправка и приём сообщений в мессенджерах из
25
+ ИИ-агента — Claude Code, Cursor, VS Code и других совместимых. Разработчик ставит пакет,
26
+ задаёт адрес и токен, подключает сервер к агенту — и собирает интеграцию, спрашивая
27
+ контракт словами, а не вычитывая спеку целиком.
28
+
29
+ | | |
30
+ |---|---|
31
+ | Версия | 0.1.2 |
32
+ | Требуется | Python 3.11+ |
33
+ | Зависимости | **нет ни одной**, только стандартная библиотека |
34
+ | Транспорт | stdio, сервер работает локально у разработчика |
35
+ | Инструментов | 12 — контракт API и действия в аккаунте |
36
+
37
+ ---
38
+
39
+ ## Состояние
40
+
41
+ Путь интеграции проходится целиком: агент спрашивает порядок шагов и контракт, смотрит
42
+ каналы, заводит исходящий канал с callback-URL, отправляет текстовое сообщение — и
43
+ `i2crm-mcp listen` показывает, что пришло обратно.
44
+
45
+ ---
46
+
47
+ ## Установка
48
+
49
+ Пакет лежит в реестре пакетов проекта. Своё окружение — чтобы путь к интерпретатору был
50
+ известен: он понадобится агенту.
51
+
52
+ ```bash
53
+ python -m venv ~/.venvs/i2crm-mcp
54
+ source ~/.venvs/i2crm-mcp/bin/activate # Windows: %USERPROFILE%\.venvs\i2crm-mcp\Scripts\activate
55
+ pip install i2crm-mcp
56
+ ```
57
+
58
+ `--index-url` подменяет PyPI целиком, и это безопасно ровно потому, что зависимостей у
59
+ пакета нет: резолвить в чужом индексе нечего.
60
+
61
+ Дальше — адрес и токен i2crm:
62
+
63
+ ```bash
64
+ i2crm-mcp setup # спросит адрес и токен, сохранит в профиль пользователя
65
+ i2crm-mcp check # проверит связь: спека читается, токен принят, каналы видны
66
+ i2crm-mcp where # покажет, откуда взяты настройки, и маску токена
67
+ i2crm-mcp listen # примет вебхуки и напечатает входящие и статусы доставки
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Подключение к агенту
73
+
74
+ Пример для Claude Code; у Cursor, Windsurf и VS Code свои файлы конфигурации, но поле
75
+ команды в них то же:
76
+
77
+ ```bash
78
+ claude mcp add --scope user i2crm -- ~/.venvs/i2crm-mcp/bin/python -m i2crm_mcp.server
79
+ ```
80
+
81
+ Интерпретатор указывается полным путём — тем, куда поставлен пакет. Агент запускает сервер
82
+ не из вашего шелла: ни `PATH`, ни активированное окружение до него не доходят, и короткое
83
+ `python` найдёт не то.
84
+
85
+ Настройки сервер берёт из профиля, созданного командой `setup`. Без профиля адрес и токен
86
+ задаются блоком `env` в конфиге агента — `I2CRM_BASE_URL` и `I2CRM_TOKEN`.
87
+
88
+ ---
89
+
90
+ ## Инструменты
91
+
92
+ **Знание о контракте.** Токен не нужен: спека публична. Нужен только адрес — контракт
93
+ берётся у той установки i2crm, к которой подключён разработчик, и не копируется в пакет.
94
+
95
+ | Инструмент | Назначение |
96
+ |---|---|
97
+ | `i2crm_playbook` | Порядок интеграции: что делает человек, что агент, чем проверяется |
98
+ | `i2crm_endpoints` | Вся поверхность API за пару килобайт: метод, путь, нужный токен, назначение |
99
+ | `i2crm_endpoint_get` | Контракт одного метода: параметры, тело с раскрытыми схемами, ответы, примеры |
100
+ | `i2crm_schema_get` | Схема по имени — по ней пишется тело запроса и приёмник вебхуков |
101
+
102
+ **Действия в аккаунте.** Токенов у API два, но агент держит только номер канала: ключ
103
+ исходящего канала инструменты подставляют сами, а при единственном канале берут его молча.
104
+
105
+ | Инструмент | Назначение |
106
+ |---|---|
107
+ | `i2crm_diagnose` | Проверка подключения: адрес, токен маской, спека, оба списка каналов |
108
+ | `i2crm_sources` | Входящие каналы: какие мессенджеры подключены и от какого аккаунта отвечать |
109
+ | `i2crm_targets` | Исходящие каналы: номер, тип, активность, ключ |
110
+ | `i2crm_target_create` | Создать исходящий канал с callback-URL и получить его ключ |
111
+ | `i2crm_target_update` | Переставить callback-URL, переименовать, включить или выключить |
112
+ | `i2crm_target_validate` | Канал жив и ключ принимается |
113
+ | `i2crm_reply_sources` | Через что этот канал может отвечать: `domain`, `type`, `source`, шаблоны |
114
+ | `i2crm_send` | Отправить текстовое сообщение клиенту |
115
+
116
+ Спросите агента обычными словами:
117
+
118
+ > проверь подключение к i2crm и покажи, через какие каналы можно ответить
119
+
120
+ Ответ, который не влезает в потолок, не урезается молча: вложенные схемы сворачиваются до
121
+ имён, и агенту сказано, чем их раскрыть.
122
+
123
+ ---
124
+
125
+ ## Приём вебхуков
126
+
127
+ Отправку видно по ответу инструмента, а дошло ли сообщение — только по вебхуку. Принять
128
+ его локально можно, не написав обработчика:
129
+
130
+ ```bash
131
+ i2crm-mcp listen --port 3000 # --raw печатает payload целиком, --once ждёт одно событие
132
+ ```
133
+
134
+ Приёмник слушает только этот компьютер, поэтому наружу его выводит туннель:
135
+
136
+ ```bash
137
+ cloudflared tunnel --url http://localhost:3000
138
+ ```
139
+
140
+ Полученный адрес ставится каналу с путём — `https://ваш-туннель/i2crm` — иначе i2crm его
141
+ не примет: требуется https и непустой путь. Дальше в терминале видно входящие сообщения,
142
+ правки и статусы доставки, а по подписи вебхука названо, какому каналу он адресован.
143
+
144
+ Два требования i2crm, которые приёмник закрывает сам и о которых стоит знать, когда
145
+ обработчик пишется в своём коде: **ответ обязан быть JSON**, а не просто 200, — тело
146
+ ответа разбирается, и на не-JSON доставка считается неудачной и повторяется; правка ранее
147
+ отправленного сообщения приходит методом `PATCH` на тот же адрес.
148
+
149
+ Приёмник отладочный: он никого не проверяет, ничего не хранит и не годится на роль
150
+ рабочего обработчика.
151
+
152
+ ---
153
+
154
+ ## Границы
155
+
156
+ - **Разрушающих операций нет вообще** — ни удаления канала, ни удаления сообщений. Агент
157
+ не может снести рабочий канал, даже если его попросить.
158
+ - **Подключение мессенджеров не автоматизируется**: QR и вход по коду интерактивны, это
159
+ делает человек в личном кабинете. Инструменты только показывают состояние.
160
+ - **Переписка не читается**: клиентская спека скрывает эту часть API, набор инструментов
161
+ повторяет её границу, а не расширяет.
162
+ - **Запросы разрежаются на нашей стороне**: своего лимита у API нет, и агент в цикле —
163
+ единственное, что может выжечь аккаунт отправками.
164
+ - **Токен не светится**: в профиле с правами только владельцу, в выводе — маской, в
165
+ stdout — ничего кроме протокола. Ключи каналов инструменты агенту показывают: без них
166
+ он не напишет код, который ходит в API сам. В терминале (`check`) они маской.
167
+
168
+ ---
169
+
170
+ ## Настройки
171
+
172
+ Адрес и токен берутся из двух мест, окружение перебивает профиль:
173
+
174
+ | Что | Переменная | Профиль |
175
+ |---|---|---|
176
+ | Адрес i2crm | `I2CRM_BASE_URL` | пишется командой `setup` |
177
+ | Токен пользователя | `I2CRM_TOKEN` | там же |
178
+
179
+ Профиль лежит по соглашению ОС — `%APPDATA%\i2crm-mcp\config.json` на Windows,
180
+ `~/.config/i2crm-mcp/config.json` на macOS и Linux — и читается только владельцем.
181
+ Переопределяется переменной `I2CRM_MCP_CONFIG`.
182
+
183
+ Адреса по умолчанию нет намеренно: каждая установка i2crm отвечает на своём хосте, и
184
+ угаданный адрес — это токен, отправленный туда, куда сегодня резолвится это имя. Адрес и
185
+ токен показаны в личном кабинете, на странице настройки API.
186
+
187
+ ---
188
+
189
+ ## Разработка
190
+
191
+ Свой протокол MCP на стандартной библиотеке (`protocol.py`), 249 тестов без обращений к
192
+ сети, ruff и сборка пакета — на каждый коммит.
193
+
194
+ Разработка ведётся в приватном репозитории i2crm; порядок правки и выпуска описан там же,
195
+ в `CONTRIBUTING.md`.
@@ -0,0 +1,174 @@
1
+ # i2crm-mcp
2
+
3
+ MCP-сервер для публичного API i2crm: отправка и приём сообщений в мессенджерах из
4
+ ИИ-агента — Claude Code, Cursor, VS Code и других совместимых. Разработчик ставит пакет,
5
+ задаёт адрес и токен, подключает сервер к агенту — и собирает интеграцию, спрашивая
6
+ контракт словами, а не вычитывая спеку целиком.
7
+
8
+ | | |
9
+ |---|---|
10
+ | Версия | 0.1.2 |
11
+ | Требуется | Python 3.11+ |
12
+ | Зависимости | **нет ни одной**, только стандартная библиотека |
13
+ | Транспорт | stdio, сервер работает локально у разработчика |
14
+ | Инструментов | 12 — контракт API и действия в аккаунте |
15
+
16
+ ---
17
+
18
+ ## Состояние
19
+
20
+ Путь интеграции проходится целиком: агент спрашивает порядок шагов и контракт, смотрит
21
+ каналы, заводит исходящий канал с callback-URL, отправляет текстовое сообщение — и
22
+ `i2crm-mcp listen` показывает, что пришло обратно.
23
+
24
+ ---
25
+
26
+ ## Установка
27
+
28
+ Пакет лежит в реестре пакетов проекта. Своё окружение — чтобы путь к интерпретатору был
29
+ известен: он понадобится агенту.
30
+
31
+ ```bash
32
+ python -m venv ~/.venvs/i2crm-mcp
33
+ source ~/.venvs/i2crm-mcp/bin/activate # Windows: %USERPROFILE%\.venvs\i2crm-mcp\Scripts\activate
34
+ pip install i2crm-mcp
35
+ ```
36
+
37
+ `--index-url` подменяет PyPI целиком, и это безопасно ровно потому, что зависимостей у
38
+ пакета нет: резолвить в чужом индексе нечего.
39
+
40
+ Дальше — адрес и токен i2crm:
41
+
42
+ ```bash
43
+ i2crm-mcp setup # спросит адрес и токен, сохранит в профиль пользователя
44
+ i2crm-mcp check # проверит связь: спека читается, токен принят, каналы видны
45
+ i2crm-mcp where # покажет, откуда взяты настройки, и маску токена
46
+ i2crm-mcp listen # примет вебхуки и напечатает входящие и статусы доставки
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Подключение к агенту
52
+
53
+ Пример для Claude Code; у Cursor, Windsurf и VS Code свои файлы конфигурации, но поле
54
+ команды в них то же:
55
+
56
+ ```bash
57
+ claude mcp add --scope user i2crm -- ~/.venvs/i2crm-mcp/bin/python -m i2crm_mcp.server
58
+ ```
59
+
60
+ Интерпретатор указывается полным путём — тем, куда поставлен пакет. Агент запускает сервер
61
+ не из вашего шелла: ни `PATH`, ни активированное окружение до него не доходят, и короткое
62
+ `python` найдёт не то.
63
+
64
+ Настройки сервер берёт из профиля, созданного командой `setup`. Без профиля адрес и токен
65
+ задаются блоком `env` в конфиге агента — `I2CRM_BASE_URL` и `I2CRM_TOKEN`.
66
+
67
+ ---
68
+
69
+ ## Инструменты
70
+
71
+ **Знание о контракте.** Токен не нужен: спека публична. Нужен только адрес — контракт
72
+ берётся у той установки i2crm, к которой подключён разработчик, и не копируется в пакет.
73
+
74
+ | Инструмент | Назначение |
75
+ |---|---|
76
+ | `i2crm_playbook` | Порядок интеграции: что делает человек, что агент, чем проверяется |
77
+ | `i2crm_endpoints` | Вся поверхность API за пару килобайт: метод, путь, нужный токен, назначение |
78
+ | `i2crm_endpoint_get` | Контракт одного метода: параметры, тело с раскрытыми схемами, ответы, примеры |
79
+ | `i2crm_schema_get` | Схема по имени — по ней пишется тело запроса и приёмник вебхуков |
80
+
81
+ **Действия в аккаунте.** Токенов у API два, но агент держит только номер канала: ключ
82
+ исходящего канала инструменты подставляют сами, а при единственном канале берут его молча.
83
+
84
+ | Инструмент | Назначение |
85
+ |---|---|
86
+ | `i2crm_diagnose` | Проверка подключения: адрес, токен маской, спека, оба списка каналов |
87
+ | `i2crm_sources` | Входящие каналы: какие мессенджеры подключены и от какого аккаунта отвечать |
88
+ | `i2crm_targets` | Исходящие каналы: номер, тип, активность, ключ |
89
+ | `i2crm_target_create` | Создать исходящий канал с callback-URL и получить его ключ |
90
+ | `i2crm_target_update` | Переставить callback-URL, переименовать, включить или выключить |
91
+ | `i2crm_target_validate` | Канал жив и ключ принимается |
92
+ | `i2crm_reply_sources` | Через что этот канал может отвечать: `domain`, `type`, `source`, шаблоны |
93
+ | `i2crm_send` | Отправить текстовое сообщение клиенту |
94
+
95
+ Спросите агента обычными словами:
96
+
97
+ > проверь подключение к i2crm и покажи, через какие каналы можно ответить
98
+
99
+ Ответ, который не влезает в потолок, не урезается молча: вложенные схемы сворачиваются до
100
+ имён, и агенту сказано, чем их раскрыть.
101
+
102
+ ---
103
+
104
+ ## Приём вебхуков
105
+
106
+ Отправку видно по ответу инструмента, а дошло ли сообщение — только по вебхуку. Принять
107
+ его локально можно, не написав обработчика:
108
+
109
+ ```bash
110
+ i2crm-mcp listen --port 3000 # --raw печатает payload целиком, --once ждёт одно событие
111
+ ```
112
+
113
+ Приёмник слушает только этот компьютер, поэтому наружу его выводит туннель:
114
+
115
+ ```bash
116
+ cloudflared tunnel --url http://localhost:3000
117
+ ```
118
+
119
+ Полученный адрес ставится каналу с путём — `https://ваш-туннель/i2crm` — иначе i2crm его
120
+ не примет: требуется https и непустой путь. Дальше в терминале видно входящие сообщения,
121
+ правки и статусы доставки, а по подписи вебхука названо, какому каналу он адресован.
122
+
123
+ Два требования i2crm, которые приёмник закрывает сам и о которых стоит знать, когда
124
+ обработчик пишется в своём коде: **ответ обязан быть JSON**, а не просто 200, — тело
125
+ ответа разбирается, и на не-JSON доставка считается неудачной и повторяется; правка ранее
126
+ отправленного сообщения приходит методом `PATCH` на тот же адрес.
127
+
128
+ Приёмник отладочный: он никого не проверяет, ничего не хранит и не годится на роль
129
+ рабочего обработчика.
130
+
131
+ ---
132
+
133
+ ## Границы
134
+
135
+ - **Разрушающих операций нет вообще** — ни удаления канала, ни удаления сообщений. Агент
136
+ не может снести рабочий канал, даже если его попросить.
137
+ - **Подключение мессенджеров не автоматизируется**: QR и вход по коду интерактивны, это
138
+ делает человек в личном кабинете. Инструменты только показывают состояние.
139
+ - **Переписка не читается**: клиентская спека скрывает эту часть API, набор инструментов
140
+ повторяет её границу, а не расширяет.
141
+ - **Запросы разрежаются на нашей стороне**: своего лимита у API нет, и агент в цикле —
142
+ единственное, что может выжечь аккаунт отправками.
143
+ - **Токен не светится**: в профиле с правами только владельцу, в выводе — маской, в
144
+ stdout — ничего кроме протокола. Ключи каналов инструменты агенту показывают: без них
145
+ он не напишет код, который ходит в API сам. В терминале (`check`) они маской.
146
+
147
+ ---
148
+
149
+ ## Настройки
150
+
151
+ Адрес и токен берутся из двух мест, окружение перебивает профиль:
152
+
153
+ | Что | Переменная | Профиль |
154
+ |---|---|---|
155
+ | Адрес i2crm | `I2CRM_BASE_URL` | пишется командой `setup` |
156
+ | Токен пользователя | `I2CRM_TOKEN` | там же |
157
+
158
+ Профиль лежит по соглашению ОС — `%APPDATA%\i2crm-mcp\config.json` на Windows,
159
+ `~/.config/i2crm-mcp/config.json` на macOS и Linux — и читается только владельцем.
160
+ Переопределяется переменной `I2CRM_MCP_CONFIG`.
161
+
162
+ Адреса по умолчанию нет намеренно: каждая установка i2crm отвечает на своём хосте, и
163
+ угаданный адрес — это токен, отправленный туда, куда сегодня резолвится это имя. Адрес и
164
+ токен показаны в личном кабинете, на странице настройки API.
165
+
166
+ ---
167
+
168
+ ## Разработка
169
+
170
+ Свой протокол MCP на стандартной библиотеке (`protocol.py`), 249 тестов без обращений к
171
+ сети, ruff и сборка пакета — на каждый коммит.
172
+
173
+ Разработка ведётся в приватном репозитории i2crm; порядок правки и выпуска описан там же,
174
+ в `CONTRIBUTING.md`.
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ # Имя в PyPI — `i2crm-mcp`: его набирает клиент в `uvx` и `pip install`. Репозиторий
7
+ # называется `i2crm-mcp-py`, потому что суффикс различает реализации, а не продукты.
8
+ name = "i2crm-mcp"
9
+ # Версия берётся из src/i2crm_mcp/__init__.py — иначе пакет, тег и ответ сервера в
10
+ # рукопожатии разъезжаются.
11
+ dynamic = ["version"]
12
+ description = "MCP-сервер для публичного API i2crm: отправка и приём сообщений в мессенджерах из ИИ-агента"
13
+ readme = "README.md"
14
+ requires-python = ">=3.11"
15
+ license = "MIT"
16
+ license-files = ["LICENSE"]
17
+
18
+ # Зависимостей нет ни одной, только стандартная библиотека: пакет ставит внешний
19
+ # разработчик одной командой, и каждая движущаяся часть в ней — вопрос в поддержку.
20
+ # Протокол stdio реализован в `i2crm_mcp/protocol.py`.
21
+ dependencies = []
22
+
23
+ classifiers = [
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ ]
29
+
30
+ # Единственные ссылки, которые видит человек на странице пакета: репозиторий приватный,
31
+ # и вести на него значит вести на форму входа. Адреса ниже открываются без авторизации.
32
+ [project.urls]
33
+ Homepage = "https://i2crm.ru"
34
+ Documentation = "https://app.i2crm.ru/api_v1/for-ai"
35
+
36
+ [project.scripts]
37
+ i2crm-mcp = "i2crm_mcp.cli:main"
38
+
39
+ # Инструменты разработчика — через extras: `pip install .` по-прежнему не тянет ничего,
40
+ # и это проверяет CI. Версия ruff записана здесь одним местом, оттуда же её ставит CI.
41
+ #
42
+ # Таблица идёт после всех простых ключей `[project]`: в TOML она забирает себе все
43
+ # последующие, и `classifiers` после неё ломает сборку метаданных.
44
+ [project.optional-dependencies]
45
+ lint = ["ruff==0.16.3"]
46
+ dev = ["i2crm-mcp[lint]", "build>=1.0", "twine>=5.0"]
47
+
48
+ [tool.hatch.version]
49
+ path = "src/i2crm_mcp/__init__.py"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/i2crm_mcp"]
53
+
54
+ # Белый список: это дерево видит внешний мир — и как пакет в PyPI, и как публичный
55
+ # снимок релиза. Внутренние документы (`CONTRIBUTING.md`, `.gitlab-ci.yml`) в него не
56
+ # входят намеренно.
57
+ [tool.hatch.build.targets.sdist]
58
+ include = [
59
+ "src",
60
+ "tests",
61
+ "scripts",
62
+ "docs",
63
+ "README.md",
64
+ "CHANGELOG.md",
65
+ "pyproject.toml",
66
+ ]
67
+
68
+ [tool.ruff]
69
+ line-length = 110
70
+ target-version = "py311"
71
+
72
+ [tool.ruff.lint]
73
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,78 @@
1
+ """Проверяет, что установка пакета не тянет ничего кроме него самого.
2
+
3
+ Две проверки, каждая ловит своё: объявленные зависимости (`Requires-Dist` пуст) и
4
+ фактические (разница `pip list` до и после — только наш пакет; так видно зависимость,
5
+ пришедшую через сборку или зашитую в колесо).
6
+
7
+ Белого списка «ожидаемых» пакетов нет: проверка, краснеющая на чужом пакете из базового
8
+ образа, учит игнорировать красный пайплайн.
9
+
10
+ Запуск:
11
+ pip list --format=freeze > before.txt
12
+ pip install .
13
+ pip list --format=freeze > after.txt
14
+ python scripts/check_no_deps.py before.txt after.txt
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from importlib.metadata import PackageNotFoundError, requires
21
+ from pathlib import Path
22
+
23
+ PACKAGE = "i2crm-mcp"
24
+
25
+
26
+ def normalized(name: str) -> str:
27
+ """Для pip `i2crm_mcp` и `i2crm-mcp` — один пакет, для `!=` — нет."""
28
+ return name.strip().lower().replace("_", "-")
29
+
30
+
31
+ def installed_names(path: Path) -> set[str]:
32
+ lines = path.read_text(encoding="utf-8").splitlines()
33
+ return {normalized(line.split("==")[0]) for line in lines if line.strip()}
34
+
35
+
36
+ def check_declared() -> list[str]:
37
+ """Никаких `Requires-Dist`, кроме условных по extra.
38
+
39
+ Обещание — «установка сервера не тянет ничего», а не «в метаданных ничего не
40
+ упомянуто»: инструменты разработчика из extras обычный `pip install .` не ставит.
41
+ """
42
+ try:
43
+ every = requires(PACKAGE) or []
44
+ except PackageNotFoundError:
45
+ sys.exit(f"Пакет {PACKAGE} не установлен — проверять нечего.")
46
+
47
+ unconditional = [r for r in every if "extra ==" not in r]
48
+ optional = len(every) - len(unconditional)
49
+
50
+ if unconditional:
51
+ return [f"пакет объявляет зависимости: {', '.join(unconditional)}"]
52
+ print(f"объявленных зависимостей нет ({optional} только для extras — они не в счёт)")
53
+ return []
54
+
55
+
56
+ def check_actual(before: Path, after: Path) -> list[str]:
57
+ added = installed_names(after) - installed_names(before)
58
+ extra = sorted(added - {normalized(PACKAGE)})
59
+ if extra:
60
+ return [f"установка притащила лишнее: {', '.join(extra)}"]
61
+ print(f"установка добавила ровно один пакет: {normalized(PACKAGE)}")
62
+ return []
63
+
64
+
65
+ def main(argv: list[str]) -> int:
66
+ problems = check_declared()
67
+ if len(argv) > 2:
68
+ problems += check_actual(Path(argv[1]), Path(argv[2]))
69
+ else:
70
+ print("списки пакетов до/после не переданы — проверена только декларация")
71
+
72
+ if problems:
73
+ sys.exit("Зависимостей быть не должно.\n " + "\n ".join(problems))
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main(sys.argv))