sgnl-mcp 0.1.0__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.
- sgnl_mcp-0.1.0/.gitignore +164 -0
- sgnl_mcp-0.1.0/PKG-INFO +173 -0
- sgnl_mcp-0.1.0/README.md +158 -0
- sgnl_mcp-0.1.0/pyproject.toml +47 -0
- sgnl_mcp-0.1.0/sgnl_mcp/__init__.py +0 -0
- sgnl_mcp-0.1.0/sgnl_mcp/audit.py +41 -0
- sgnl_mcp-0.1.0/sgnl_mcp/config.py +68 -0
- sgnl_mcp-0.1.0/sgnl_mcp/confirm.py +87 -0
- sgnl_mcp-0.1.0/sgnl_mcp/errors.py +72 -0
- sgnl_mcp-0.1.0/sgnl_mcp/guard.py +214 -0
- sgnl_mcp-0.1.0/sgnl_mcp/profiles.py +75 -0
- sgnl_mcp-0.1.0/sgnl_mcp/runtime.py +241 -0
- sgnl_mcp-0.1.0/sgnl_mcp/server.py +449 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/__init__.py +0 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/common.py +32 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/context.py +162 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/documents.py +262 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/issues.py +134 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/projects.py +34 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/reviews.py +162 -0
- sgnl_mcp-0.1.0/sgnl_mcp/services/transfers.py +192 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
.pypirc
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
*.xlsx
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
cover/
|
|
55
|
+
test.py
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
*.mo
|
|
59
|
+
*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
*.log
|
|
63
|
+
local_settings.py
|
|
64
|
+
db.sqlite3
|
|
65
|
+
db.sqlite3-journal
|
|
66
|
+
|
|
67
|
+
# Flask stuff:
|
|
68
|
+
instance/
|
|
69
|
+
.webassets-cache
|
|
70
|
+
|
|
71
|
+
# Scrapy stuff:
|
|
72
|
+
.scrapy
|
|
73
|
+
|
|
74
|
+
# Sphinx documentation
|
|
75
|
+
docs/_build/
|
|
76
|
+
|
|
77
|
+
# PyBuilder
|
|
78
|
+
.pybuilder/
|
|
79
|
+
target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
profile_default/
|
|
86
|
+
ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
90
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
91
|
+
# .python-version
|
|
92
|
+
|
|
93
|
+
# pipenv
|
|
94
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
95
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
96
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
97
|
+
# install all needed dependencies.
|
|
98
|
+
#Pipfile.lock
|
|
99
|
+
|
|
100
|
+
# poetry
|
|
101
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
102
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
103
|
+
# commonly ignored for libraries.
|
|
104
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
105
|
+
#poetry.lock
|
|
106
|
+
|
|
107
|
+
# pdm
|
|
108
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
109
|
+
#pdm.lock
|
|
110
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
111
|
+
# in version control.
|
|
112
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
113
|
+
.pdm.toml
|
|
114
|
+
|
|
115
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
+
__pypackages__/
|
|
117
|
+
|
|
118
|
+
# Celery stuff
|
|
119
|
+
celerybeat-schedule
|
|
120
|
+
celerybeat.pid
|
|
121
|
+
|
|
122
|
+
# SageMath parsed files
|
|
123
|
+
*.sage.py
|
|
124
|
+
|
|
125
|
+
# Environments
|
|
126
|
+
.env
|
|
127
|
+
.venv
|
|
128
|
+
env/
|
|
129
|
+
venv/
|
|
130
|
+
ENV/
|
|
131
|
+
env.bak/
|
|
132
|
+
venv.bak/
|
|
133
|
+
|
|
134
|
+
# Spyder project settings
|
|
135
|
+
.spyderproject
|
|
136
|
+
.spyproject
|
|
137
|
+
|
|
138
|
+
# Rope project settings
|
|
139
|
+
.ropeproject
|
|
140
|
+
|
|
141
|
+
# mkdocs documentation
|
|
142
|
+
/site
|
|
143
|
+
|
|
144
|
+
# mypy
|
|
145
|
+
.mypy_cache/
|
|
146
|
+
.dmypy.json
|
|
147
|
+
dmypy.json
|
|
148
|
+
|
|
149
|
+
# Pyre type checker
|
|
150
|
+
.pyre/
|
|
151
|
+
|
|
152
|
+
# pytype static type analyzer
|
|
153
|
+
.pytype/
|
|
154
|
+
|
|
155
|
+
# Cython debug symbols
|
|
156
|
+
cython_debug/
|
|
157
|
+
|
|
158
|
+
# PyCharm
|
|
159
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
160
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
161
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
162
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
163
|
+
.idea/
|
|
164
|
+
.vscode/
|
sgnl_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sgnl-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server giving an LLM agent controlled access to the SIGNAL Integration API
|
|
5
|
+
Project-URL: GitHub, https://github.com/gritsyuk/sgnl-api
|
|
6
|
+
Author-email: Igor Gritsyuk <gritsyuk.igor@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: mcp<3,>=2.3
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
11
|
+
Requires-Dist: pydantic>=2.7
|
|
12
|
+
Requires-Dist: sgnl-api>=0.4.0
|
|
13
|
+
Requires-Dist: tenacity>=8.2
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# sgnl-mcp — MCP-сервер для SIGNAL Integration API
|
|
17
|
+
|
|
18
|
+
MCP-сервер, через который LLM-агент (Claude и др.) работает с документацией в SIGNAL: смотрит проекты и папки, ищет и скачивает документы, загружает новые документы и версии, работает с замечаниями, передачами и рассмотрениями.
|
|
19
|
+
|
|
20
|
+
Агент остаётся под контролем: чтение без ограничений, запись — только после предпросмотра и согласия пользователя, отправка передач и запуск рассмотрений — только после подтверждения пользователем в диалоге. Удалять, перемещать и переименовывать документы и папки, а также менять права агент не может вообще.
|
|
21
|
+
|
|
22
|
+
Построен на [`sgnl-api`](../README.md) — обёртке над API из этого же репозитория.
|
|
23
|
+
|
|
24
|
+
## Как обеспечивается контроль
|
|
25
|
+
|
|
26
|
+
| Рубеж | Что делает |
|
|
27
|
+
|---|---|
|
|
28
|
+
| 1. Scopes токена | Сервер запрашивает у SIGNAL только права своего профиля. В профиле `read` в токене нет ни одного права на запись — SIGNAL сам откажет. Права `*:delete` не запрашиваются никогда. |
|
|
29
|
+
| 2. Профиль | Инструменты выше профиля не регистрируются: агент их не видит. |
|
|
30
|
+
| 3. Белый список запросов | Каждый HTTP-запрос проверяется до отправки: только разрешённые метод и путь, никаких `DELETE`, никакого поля `deleted` в запросах на запись, внешние хосты — только для загрузки файла. |
|
|
31
|
+
| 4. Подтверждение | Запись — в два вызова: предпросмотр с токеном, затем выполнение с тем же токеном. Отправка передачи и запуск рассмотрения — через диалог подтверждения у пользователя (MCP elicitation). |
|
|
32
|
+
| 5. Клиент и аудит | Инструменты помечены `readOnlyHint`: клиент спрашивает разрешение на запись. Все записи и отказы пишутся в JSONL-журнал без секретов. |
|
|
33
|
+
|
|
34
|
+
Нагрузка на SIGNAL ограничена: по умолчанию не больше 3 запросов в секунду и 4 параллельных на весь сервер, у каждого вызова инструмента свой бюджет запросов, справочники кэшируются на 5 минут.
|
|
35
|
+
|
|
36
|
+
## Установка
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uvx sgnl-mcp # запуск без установки
|
|
40
|
+
# или
|
|
41
|
+
pip install sgnl-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Нужен Python 3.11+.
|
|
45
|
+
|
|
46
|
+
## Настройка
|
|
47
|
+
|
|
48
|
+
Вся настройка — через переменные окружения (или файл `.env` в рабочем каталоге). Агент не может их изменить.
|
|
49
|
+
|
|
50
|
+
| Переменная | Обязательна | По умолчанию | Назначение |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `SGNL_CLIENT_ID` | да | — | Client ID интеграции SIGNAL |
|
|
53
|
+
| `SGNL_CLIENT_SECRET` | да | — | Client secret интеграции SIGNAL |
|
|
54
|
+
| `SGNL_MCP_PROFILE` | нет | `read` | `read`, `write` или `workflow` |
|
|
55
|
+
| `SGNL_MCP_ACTOR_USER_ID` | для `write`/`workflow` | — | ID пользователя SIGNAL, от имени которого делаются изменения (лучше служебная учётная запись «AI-агент») |
|
|
56
|
+
| `SGNL_MCP_UPLOAD_ROOT` | для `write`/`workflow` | — | Каталог, из которого разрешено загружать файлы. Файлы вне него, в том числе через символические ссылки, недоступны |
|
|
57
|
+
| `SGNL_MCP_ALLOWED_EXTENSIONS` | нет | `.pdf,.dwg,.dxf,…` | Разрешённые типы загружаемых файлов, через запятую |
|
|
58
|
+
| `SGNL_MCP_AUTO_CONFIRM` | нет | — | Write-инструменты, которые выполняются без предпросмотра (только для доверенных сценариев). Workflow сюда не входит |
|
|
59
|
+
| `SGNL_MCP_RATE_PER_SEC` | нет | `3` | Запросов в секунду (не больше 10) |
|
|
60
|
+
| `SGNL_MCP_BURST` | нет | `6` | Запас запросов для коротких всплесков |
|
|
61
|
+
| `SGNL_MCP_MAX_CONCURRENCY` | нет | `4` | Параллельных запросов к SIGNAL |
|
|
62
|
+
| `SGNL_MCP_TOOL_TIMEOUT` | нет | `30` | Таймаут вызова инструмента, с |
|
|
63
|
+
| `SGNL_MCP_UPLOAD_TIMEOUT` | нет | `600` | Таймаут загрузки файла, с |
|
|
64
|
+
| `SGNL_MCP_CONFIRMATION_TTL_SEC` | нет | `300` | Срок действия токена подтверждения |
|
|
65
|
+
| `SGNL_MCP_AUDIT_PATH` | нет | `~/.sgnl-mcp/audit.jsonl` | Журнал аудита |
|
|
66
|
+
| `SGNL_MCP_STATE_PATH` | нет | `~/.sgnl-mcp/state.sqlite` | Журнал выполненных операций (защита от повторов) |
|
|
67
|
+
|
|
68
|
+
При запуске сервер получает токен и проверяет, что интеграции выданы все права профиля, а для `write`/`workflow` — что пользователь `SGNL_MCP_ACTOR_USER_ID` есть в компании. Если что-то не так, сервер не запускается и пишет, чего не хватает.
|
|
69
|
+
|
|
70
|
+
### Права интеграции по профилям
|
|
71
|
+
|
|
72
|
+
| Профиль | Scopes, которые должны быть выданы интеграции в SIGNAL |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `read` | `project:list`, `project:read`, `docs:folder:read`, `docs:folder:list`, `docs:item:list`, `docs:item:read`, `docs:version:list`, `docs:docs_transfer:list`, `docs:docs_transfer:read`, `docs:issue:list`, `docs:review:list`, `docs:signing:list`, `docs:attribute:list` |
|
|
75
|
+
| `write` | всё из `read` + `company:user:list`, `docs:object:create`, `docs:version:create`, `docs:version:update`, `docs:item:create`, `docs:docs_transfer:create`, `docs:docs_transfer:update`, `docs:issue:update`, `docs:review:create`, `docs:review:update` |
|
|
76
|
+
| `workflow` | то же, что `write` |
|
|
77
|
+
|
|
78
|
+
Отправка передачи и запуск рассмотрения в SIGNAL используют те же права, что и их наполнение. Поэтому их отделяет сам MCP-сервер: в профиле `write` этих инструментов нет, а в `workflow` они всегда требуют подтверждения пользователя.
|
|
79
|
+
|
|
80
|
+
## Подключение к Claude
|
|
81
|
+
|
|
82
|
+
**Claude Desktop** — `claude_desktop_config.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"sgnl": {
|
|
88
|
+
"command": "uvx",
|
|
89
|
+
"args": ["sgnl-mcp"],
|
|
90
|
+
"env": {
|
|
91
|
+
"SGNL_CLIENT_ID": "…",
|
|
92
|
+
"SGNL_CLIENT_SECRET": "…",
|
|
93
|
+
"SGNL_MCP_PROFILE": "read"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Claude Code:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
claude mcp add sgnl -e SGNL_CLIENT_ID=… -e SGNL_CLIENT_SECRET=… -e SGNL_MCP_PROFILE=read -- uvx sgnl-mcp
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Для профиля `write` добавьте `SGNL_MCP_ACTOR_USER_ID` и `SGNL_MCP_UPLOAD_ROOT`.
|
|
107
|
+
|
|
108
|
+
### Разрешения в клиенте
|
|
109
|
+
- Инструменты чтения можно разрешить без вопросов.
|
|
110
|
+
- Инструменты записи и Workflow **не добавляйте в «разрешить всегда»**: клиент должен спрашивать перед каждым вызовом. Это один из рубежей контроля.
|
|
111
|
+
- Для профиля `workflow` клиент должен поддерживать MCP elicitation (диалог подтверждения). Без неё отправка передач и запуск рассмотрений завершаются ошибкой и ничего не делают.
|
|
112
|
+
|
|
113
|
+
## Инструменты
|
|
114
|
+
|
|
115
|
+
| Инструмент | Профиль | Что делает |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `list_projects` | read | Проекты, доступные интеграции |
|
|
118
|
+
| `get_project` | read | Данные проекта и его корневая папка |
|
|
119
|
+
| `get_project_folder_tree` | read | Дерево папок с правами пользователя |
|
|
120
|
+
| `search_documents` | read | Поиск документов по проекту или папке (имя, расширение, автор, даты) |
|
|
121
|
+
| `list_folder_documents` | read | Документы одной папки (без вложенных) |
|
|
122
|
+
| `list_document_versions` | read | История версий документа |
|
|
123
|
+
| `get_document_version` | read | Связь «документ — версия файла» по `item_version_id` |
|
|
124
|
+
| `get_document_download_url` | read | Временная ссылка на скачивание версии (сам файл не передаётся) |
|
|
125
|
+
| `get_document_context` | read | Всё о документе за один вызов: версии, атрибуты, замечания, рассмотрения, подписи; у каждого блока свой статус |
|
|
126
|
+
| `list_document_transfers` | read | Передачи проекта с фильтрами |
|
|
127
|
+
| `get_document_transfer` | read | Полные данные передачи |
|
|
128
|
+
| `list_transfer_documents` | read | Документы в передаче |
|
|
129
|
+
| `list_document_transfer_types` | read | Типы передач проекта |
|
|
130
|
+
| `list_issues` | read | Замечания с фильтрами |
|
|
131
|
+
| `list_reviews` | read | Рассмотрения проекта |
|
|
132
|
+
| `get_review_details` | read | Версии рассмотрения и документы в них |
|
|
133
|
+
| `get_document_review_status` | read | Статус рассмотрения версий документов |
|
|
134
|
+
| `list_review_types` | read | Типы рассмотрений проекта |
|
|
135
|
+
| `upload_document` | write | Новый документ из файла |
|
|
136
|
+
| `add_document_version` | write | Новая версия существующего документа |
|
|
137
|
+
| `update_document_version_comment` | write | Комментарий версии |
|
|
138
|
+
| `create_document_transfer` | write | Новая передача (черновик) |
|
|
139
|
+
| `add_documents_to_transfer` | write | Добавить версии документов в неотправленную передачу |
|
|
140
|
+
| `update_issues` | write | Изменить 1–25 замечаний |
|
|
141
|
+
| `create_review` | write | Новое рассмотрение (черновик) |
|
|
142
|
+
| `add_documents_to_review` | write | Добавить до 20 версий документов в рассмотрение |
|
|
143
|
+
| `prepare_send_document_transfer` | workflow | Проверить передачу перед отправкой и получить токен |
|
|
144
|
+
| `send_document_transfer` | workflow | Отправить передачу (подтверждение пользователем в диалоге) |
|
|
145
|
+
| `prepare_start_review` | workflow | Проверить рассмотрение перед запуском и получить токен |
|
|
146
|
+
| `start_review` | workflow | Запустить рассмотрение (подтверждение пользователем в диалоге) |
|
|
147
|
+
|
|
148
|
+
### Как проходит запись
|
|
149
|
+
|
|
150
|
+
1. Агент вызывает инструмент без `confirmation_token`. Сервер проверяет объекты и возвращает предпросмотр («было → станет», путь папки, состав передачи и т.п.) и токен. В SIGNAL ничего не меняется.
|
|
151
|
+
2. Агент показывает предпросмотр пользователю.
|
|
152
|
+
3. После согласия агент вызывает инструмент с теми же аргументами и токеном. Сервер перепроверяет объекты: если с момента предпросмотра что-то изменилось (другой файл, кто-то правил замечание), токен отклоняется.
|
|
153
|
+
|
|
154
|
+
Токен одноразовый, действует 5 минут и привязан к аргументам. Повторный вызов с уже использованным токеном возвращает сохранённый результат, а не выполняет операцию второй раз.
|
|
155
|
+
|
|
156
|
+
Если запрос на запись завершился без ясного ответа (таймаут, ошибка сервера), сервер **не повторяет его сам**, а возвращает `OUTCOME_UNKNOWN` с тем, что удалось выяснить. Для загрузки файла можно завершить операцию параметром `resume_version_id`, не загружая файл заново.
|
|
157
|
+
|
|
158
|
+
### Ограничения
|
|
159
|
+
- В API SIGNAL нет поиска передач по документу, поэтому блок `transfers` в `get_document_context` всегда `not_supported`.
|
|
160
|
+
- Создание замечаний, удаление, перемещение, переименование, изменение прав и пользователей в MCP-сервер намеренно не включены.
|
|
161
|
+
|
|
162
|
+
## Ответы и ошибки
|
|
163
|
+
|
|
164
|
+
Все инструменты возвращают `{"success": true, "data": …, "page": …, "warnings": […], "request_id": …}` или `{"success": false, "error": {"code", "message", "details"}, "request_id": …}`.
|
|
165
|
+
|
|
166
|
+
| Код | Значение |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `INVALID_REQUEST`, `NOT_FOUND`, `FORBIDDEN`, `AUTH_FAILED`, `QUOTA_EXCEEDED`, `RATE_LIMITED` | Ответ SIGNAL (400, 404, 403, 401, 450, 429) |
|
|
169
|
+
| `SERVER_ERROR`, `NETWORK_ERROR`, `TIMEOUT` | SIGNAL не ответил нормально; чтение можно повторить |
|
|
170
|
+
| `OUTCOME_UNKNOWN` | Запись могла пройти или не пройти; сервер её не повторял |
|
|
171
|
+
| `POLICY_DENIED` | Запрос запрещён политикой MCP-сервера |
|
|
172
|
+
| `CONFIRMATION_INVALID`, `CONFIRMATION_DECLINED` | Токен недействителен или пользователь не подтвердил |
|
|
173
|
+
| `INVALID_STATE`, `NOTHING_TO_DO`, `UPLOAD_FAILED`, `BUDGET_EXCEEDED` | Объект в неподходящем состоянии, изменять нечего, файл не передан, исчерпан бюджет запросов |
|
sgnl_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# sgnl-mcp — MCP-сервер для SIGNAL Integration API
|
|
2
|
+
|
|
3
|
+
MCP-сервер, через который LLM-агент (Claude и др.) работает с документацией в SIGNAL: смотрит проекты и папки, ищет и скачивает документы, загружает новые документы и версии, работает с замечаниями, передачами и рассмотрениями.
|
|
4
|
+
|
|
5
|
+
Агент остаётся под контролем: чтение без ограничений, запись — только после предпросмотра и согласия пользователя, отправка передач и запуск рассмотрений — только после подтверждения пользователем в диалоге. Удалять, перемещать и переименовывать документы и папки, а также менять права агент не может вообще.
|
|
6
|
+
|
|
7
|
+
Построен на [`sgnl-api`](../README.md) — обёртке над API из этого же репозитория.
|
|
8
|
+
|
|
9
|
+
## Как обеспечивается контроль
|
|
10
|
+
|
|
11
|
+
| Рубеж | Что делает |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 1. Scopes токена | Сервер запрашивает у SIGNAL только права своего профиля. В профиле `read` в токене нет ни одного права на запись — SIGNAL сам откажет. Права `*:delete` не запрашиваются никогда. |
|
|
14
|
+
| 2. Профиль | Инструменты выше профиля не регистрируются: агент их не видит. |
|
|
15
|
+
| 3. Белый список запросов | Каждый HTTP-запрос проверяется до отправки: только разрешённые метод и путь, никаких `DELETE`, никакого поля `deleted` в запросах на запись, внешние хосты — только для загрузки файла. |
|
|
16
|
+
| 4. Подтверждение | Запись — в два вызова: предпросмотр с токеном, затем выполнение с тем же токеном. Отправка передачи и запуск рассмотрения — через диалог подтверждения у пользователя (MCP elicitation). |
|
|
17
|
+
| 5. Клиент и аудит | Инструменты помечены `readOnlyHint`: клиент спрашивает разрешение на запись. Все записи и отказы пишутся в JSONL-журнал без секретов. |
|
|
18
|
+
|
|
19
|
+
Нагрузка на SIGNAL ограничена: по умолчанию не больше 3 запросов в секунду и 4 параллельных на весь сервер, у каждого вызова инструмента свой бюджет запросов, справочники кэшируются на 5 минут.
|
|
20
|
+
|
|
21
|
+
## Установка
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uvx sgnl-mcp # запуск без установки
|
|
25
|
+
# или
|
|
26
|
+
pip install sgnl-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Нужен Python 3.11+.
|
|
30
|
+
|
|
31
|
+
## Настройка
|
|
32
|
+
|
|
33
|
+
Вся настройка — через переменные окружения (или файл `.env` в рабочем каталоге). Агент не может их изменить.
|
|
34
|
+
|
|
35
|
+
| Переменная | Обязательна | По умолчанию | Назначение |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `SGNL_CLIENT_ID` | да | — | Client ID интеграции SIGNAL |
|
|
38
|
+
| `SGNL_CLIENT_SECRET` | да | — | Client secret интеграции SIGNAL |
|
|
39
|
+
| `SGNL_MCP_PROFILE` | нет | `read` | `read`, `write` или `workflow` |
|
|
40
|
+
| `SGNL_MCP_ACTOR_USER_ID` | для `write`/`workflow` | — | ID пользователя SIGNAL, от имени которого делаются изменения (лучше служебная учётная запись «AI-агент») |
|
|
41
|
+
| `SGNL_MCP_UPLOAD_ROOT` | для `write`/`workflow` | — | Каталог, из которого разрешено загружать файлы. Файлы вне него, в том числе через символические ссылки, недоступны |
|
|
42
|
+
| `SGNL_MCP_ALLOWED_EXTENSIONS` | нет | `.pdf,.dwg,.dxf,…` | Разрешённые типы загружаемых файлов, через запятую |
|
|
43
|
+
| `SGNL_MCP_AUTO_CONFIRM` | нет | — | Write-инструменты, которые выполняются без предпросмотра (только для доверенных сценариев). Workflow сюда не входит |
|
|
44
|
+
| `SGNL_MCP_RATE_PER_SEC` | нет | `3` | Запросов в секунду (не больше 10) |
|
|
45
|
+
| `SGNL_MCP_BURST` | нет | `6` | Запас запросов для коротких всплесков |
|
|
46
|
+
| `SGNL_MCP_MAX_CONCURRENCY` | нет | `4` | Параллельных запросов к SIGNAL |
|
|
47
|
+
| `SGNL_MCP_TOOL_TIMEOUT` | нет | `30` | Таймаут вызова инструмента, с |
|
|
48
|
+
| `SGNL_MCP_UPLOAD_TIMEOUT` | нет | `600` | Таймаут загрузки файла, с |
|
|
49
|
+
| `SGNL_MCP_CONFIRMATION_TTL_SEC` | нет | `300` | Срок действия токена подтверждения |
|
|
50
|
+
| `SGNL_MCP_AUDIT_PATH` | нет | `~/.sgnl-mcp/audit.jsonl` | Журнал аудита |
|
|
51
|
+
| `SGNL_MCP_STATE_PATH` | нет | `~/.sgnl-mcp/state.sqlite` | Журнал выполненных операций (защита от повторов) |
|
|
52
|
+
|
|
53
|
+
При запуске сервер получает токен и проверяет, что интеграции выданы все права профиля, а для `write`/`workflow` — что пользователь `SGNL_MCP_ACTOR_USER_ID` есть в компании. Если что-то не так, сервер не запускается и пишет, чего не хватает.
|
|
54
|
+
|
|
55
|
+
### Права интеграции по профилям
|
|
56
|
+
|
|
57
|
+
| Профиль | Scopes, которые должны быть выданы интеграции в SIGNAL |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `read` | `project:list`, `project:read`, `docs:folder:read`, `docs:folder:list`, `docs:item:list`, `docs:item:read`, `docs:version:list`, `docs:docs_transfer:list`, `docs:docs_transfer:read`, `docs:issue:list`, `docs:review:list`, `docs:signing:list`, `docs:attribute:list` |
|
|
60
|
+
| `write` | всё из `read` + `company:user:list`, `docs:object:create`, `docs:version:create`, `docs:version:update`, `docs:item:create`, `docs:docs_transfer:create`, `docs:docs_transfer:update`, `docs:issue:update`, `docs:review:create`, `docs:review:update` |
|
|
61
|
+
| `workflow` | то же, что `write` |
|
|
62
|
+
|
|
63
|
+
Отправка передачи и запуск рассмотрения в SIGNAL используют те же права, что и их наполнение. Поэтому их отделяет сам MCP-сервер: в профиле `write` этих инструментов нет, а в `workflow` они всегда требуют подтверждения пользователя.
|
|
64
|
+
|
|
65
|
+
## Подключение к Claude
|
|
66
|
+
|
|
67
|
+
**Claude Desktop** — `claude_desktop_config.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"sgnl": {
|
|
73
|
+
"command": "uvx",
|
|
74
|
+
"args": ["sgnl-mcp"],
|
|
75
|
+
"env": {
|
|
76
|
+
"SGNL_CLIENT_ID": "…",
|
|
77
|
+
"SGNL_CLIENT_SECRET": "…",
|
|
78
|
+
"SGNL_MCP_PROFILE": "read"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Claude Code:**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
claude mcp add sgnl -e SGNL_CLIENT_ID=… -e SGNL_CLIENT_SECRET=… -e SGNL_MCP_PROFILE=read -- uvx sgnl-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Для профиля `write` добавьте `SGNL_MCP_ACTOR_USER_ID` и `SGNL_MCP_UPLOAD_ROOT`.
|
|
92
|
+
|
|
93
|
+
### Разрешения в клиенте
|
|
94
|
+
- Инструменты чтения можно разрешить без вопросов.
|
|
95
|
+
- Инструменты записи и Workflow **не добавляйте в «разрешить всегда»**: клиент должен спрашивать перед каждым вызовом. Это один из рубежей контроля.
|
|
96
|
+
- Для профиля `workflow` клиент должен поддерживать MCP elicitation (диалог подтверждения). Без неё отправка передач и запуск рассмотрений завершаются ошибкой и ничего не делают.
|
|
97
|
+
|
|
98
|
+
## Инструменты
|
|
99
|
+
|
|
100
|
+
| Инструмент | Профиль | Что делает |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `list_projects` | read | Проекты, доступные интеграции |
|
|
103
|
+
| `get_project` | read | Данные проекта и его корневая папка |
|
|
104
|
+
| `get_project_folder_tree` | read | Дерево папок с правами пользователя |
|
|
105
|
+
| `search_documents` | read | Поиск документов по проекту или папке (имя, расширение, автор, даты) |
|
|
106
|
+
| `list_folder_documents` | read | Документы одной папки (без вложенных) |
|
|
107
|
+
| `list_document_versions` | read | История версий документа |
|
|
108
|
+
| `get_document_version` | read | Связь «документ — версия файла» по `item_version_id` |
|
|
109
|
+
| `get_document_download_url` | read | Временная ссылка на скачивание версии (сам файл не передаётся) |
|
|
110
|
+
| `get_document_context` | read | Всё о документе за один вызов: версии, атрибуты, замечания, рассмотрения, подписи; у каждого блока свой статус |
|
|
111
|
+
| `list_document_transfers` | read | Передачи проекта с фильтрами |
|
|
112
|
+
| `get_document_transfer` | read | Полные данные передачи |
|
|
113
|
+
| `list_transfer_documents` | read | Документы в передаче |
|
|
114
|
+
| `list_document_transfer_types` | read | Типы передач проекта |
|
|
115
|
+
| `list_issues` | read | Замечания с фильтрами |
|
|
116
|
+
| `list_reviews` | read | Рассмотрения проекта |
|
|
117
|
+
| `get_review_details` | read | Версии рассмотрения и документы в них |
|
|
118
|
+
| `get_document_review_status` | read | Статус рассмотрения версий документов |
|
|
119
|
+
| `list_review_types` | read | Типы рассмотрений проекта |
|
|
120
|
+
| `upload_document` | write | Новый документ из файла |
|
|
121
|
+
| `add_document_version` | write | Новая версия существующего документа |
|
|
122
|
+
| `update_document_version_comment` | write | Комментарий версии |
|
|
123
|
+
| `create_document_transfer` | write | Новая передача (черновик) |
|
|
124
|
+
| `add_documents_to_transfer` | write | Добавить версии документов в неотправленную передачу |
|
|
125
|
+
| `update_issues` | write | Изменить 1–25 замечаний |
|
|
126
|
+
| `create_review` | write | Новое рассмотрение (черновик) |
|
|
127
|
+
| `add_documents_to_review` | write | Добавить до 20 версий документов в рассмотрение |
|
|
128
|
+
| `prepare_send_document_transfer` | workflow | Проверить передачу перед отправкой и получить токен |
|
|
129
|
+
| `send_document_transfer` | workflow | Отправить передачу (подтверждение пользователем в диалоге) |
|
|
130
|
+
| `prepare_start_review` | workflow | Проверить рассмотрение перед запуском и получить токен |
|
|
131
|
+
| `start_review` | workflow | Запустить рассмотрение (подтверждение пользователем в диалоге) |
|
|
132
|
+
|
|
133
|
+
### Как проходит запись
|
|
134
|
+
|
|
135
|
+
1. Агент вызывает инструмент без `confirmation_token`. Сервер проверяет объекты и возвращает предпросмотр («было → станет», путь папки, состав передачи и т.п.) и токен. В SIGNAL ничего не меняется.
|
|
136
|
+
2. Агент показывает предпросмотр пользователю.
|
|
137
|
+
3. После согласия агент вызывает инструмент с теми же аргументами и токеном. Сервер перепроверяет объекты: если с момента предпросмотра что-то изменилось (другой файл, кто-то правил замечание), токен отклоняется.
|
|
138
|
+
|
|
139
|
+
Токен одноразовый, действует 5 минут и привязан к аргументам. Повторный вызов с уже использованным токеном возвращает сохранённый результат, а не выполняет операцию второй раз.
|
|
140
|
+
|
|
141
|
+
Если запрос на запись завершился без ясного ответа (таймаут, ошибка сервера), сервер **не повторяет его сам**, а возвращает `OUTCOME_UNKNOWN` с тем, что удалось выяснить. Для загрузки файла можно завершить операцию параметром `resume_version_id`, не загружая файл заново.
|
|
142
|
+
|
|
143
|
+
### Ограничения
|
|
144
|
+
- В API SIGNAL нет поиска передач по документу, поэтому блок `transfers` в `get_document_context` всегда `not_supported`.
|
|
145
|
+
- Создание замечаний, удаление, перемещение, переименование, изменение прав и пользователей в MCP-сервер намеренно не включены.
|
|
146
|
+
|
|
147
|
+
## Ответы и ошибки
|
|
148
|
+
|
|
149
|
+
Все инструменты возвращают `{"success": true, "data": …, "page": …, "warnings": […], "request_id": …}` или `{"success": false, "error": {"code", "message", "details"}, "request_id": …}`.
|
|
150
|
+
|
|
151
|
+
| Код | Значение |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `INVALID_REQUEST`, `NOT_FOUND`, `FORBIDDEN`, `AUTH_FAILED`, `QUOTA_EXCEEDED`, `RATE_LIMITED` | Ответ SIGNAL (400, 404, 403, 401, 450, 429) |
|
|
154
|
+
| `SERVER_ERROR`, `NETWORK_ERROR`, `TIMEOUT` | SIGNAL не ответил нормально; чтение можно повторить |
|
|
155
|
+
| `OUTCOME_UNKNOWN` | Запись могла пройти или не пройти; сервер её не повторял |
|
|
156
|
+
| `POLICY_DENIED` | Запрос запрещён политикой MCP-сервера |
|
|
157
|
+
| `CONFIRMATION_INVALID`, `CONFIRMATION_DECLINED` | Токен недействителен или пользователь не подтвердил |
|
|
158
|
+
| `INVALID_STATE`, `NOTHING_TO_DO`, `UPLOAD_FAILED`, `BUDGET_EXCEEDED` | Объект в неподходящем состоянии, изменять нечего, файл не передан, исчерпан бюджет запросов |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sgnl-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server giving an LLM agent controlled access to the SIGNAL Integration API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Igor Gritsyuk", email = "gritsyuk.igor@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"sgnl-api>=0.4.0",
|
|
17
|
+
"mcp>=2.3,<3",
|
|
18
|
+
"pydantic>=2.7",
|
|
19
|
+
"pydantic-settings>=2.3",
|
|
20
|
+
"tenacity>=8.2",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
sgnl-mcp = "sgnl_mcp.server:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
GitHub = "https://github.com/gritsyuk/sgnl-api"
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources]
|
|
30
|
+
sgnl-api = { workspace = true }
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-asyncio>=0.23",
|
|
36
|
+
"pyyaml>=6.0.3",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["sgnl_mcp"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["sgnl_mcp", "README.md", "pyproject.toml"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
asyncio_mode = "auto"
|
|
47
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Append-only JSONL audit log of Write and Workflow operations, with secrets removed."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
SECRET_KEYS = {
|
|
10
|
+
"client_secret", "clientsecret", "token", "authorization", "jwt",
|
|
11
|
+
"signed_url", "signedurl", "password", "confirmation_token",
|
|
12
|
+
}
|
|
13
|
+
# Pre-signed S3 query parameters: whoever has the URL can download or upload until it expires.
|
|
14
|
+
_SIGNED_QUERY = re.compile(r"(https?://[^\s\"?]+)\?[^\s\"]*(X-Amz-|Signature=|signature=)[^\s\"]*")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def redact(value: Any) -> Any:
|
|
18
|
+
if isinstance(value, dict):
|
|
19
|
+
return {k: ("[redacted]" if k.lower() in SECRET_KEYS else redact(v)) for k, v in value.items()}
|
|
20
|
+
if isinstance(value, list):
|
|
21
|
+
return [redact(v) for v in value]
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
return _SIGNED_QUERY.sub(r"\1?[redacted]", value)
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuditLog:
|
|
28
|
+
def __init__(self, path: Path, rotate_bytes: int = 10 * 1024 * 1024):
|
|
29
|
+
self.path = Path(path)
|
|
30
|
+
self.rotate_bytes = rotate_bytes
|
|
31
|
+
self._lock = threading.Lock()
|
|
32
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
def write(self, event: dict) -> None:
|
|
35
|
+
record = {"ts": datetime.now(timezone.utc).isoformat(), **redact(event)}
|
|
36
|
+
line = json.dumps(record, ensure_ascii=False, default=str)
|
|
37
|
+
with self._lock:
|
|
38
|
+
if self.path.exists() and self.path.stat().st_size > self.rotate_bytes:
|
|
39
|
+
self.path.replace(self.path.with_suffix(self.path.suffix + ".1"))
|
|
40
|
+
with self.path.open("a", encoding="utf-8") as f:
|
|
41
|
+
f.write(line + "\n")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, SecretStr, field_validator, model_validator
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
from .profiles import Profile
|
|
9
|
+
|
|
10
|
+
DEFAULT_EXTENSIONS = (
|
|
11
|
+
".pdf", ".dwg", ".dxf", ".dgn", ".rvt", ".ifc", ".nwd", ".nwc",
|
|
12
|
+
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".csv", ".xml",
|
|
13
|
+
".png", ".jpg", ".jpeg", ".tif", ".tiff", ".zip",
|
|
14
|
+
)
|
|
15
|
+
STATE_DIR = Path.home() / ".sgnl-mcp"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Settings(BaseSettings):
|
|
19
|
+
"""All configuration comes from the environment (or a .env file); none of it from tool arguments."""
|
|
20
|
+
|
|
21
|
+
model_config = SettingsConfigDict(env_prefix="SGNL_MCP_", env_file=".env", extra="ignore", populate_by_name=True)
|
|
22
|
+
|
|
23
|
+
client_id: str = Field(validation_alias="SGNL_CLIENT_ID")
|
|
24
|
+
client_secret: SecretStr = Field(validation_alias="SGNL_CLIENT_SECRET")
|
|
25
|
+
base_url: str = "https://api.sgnl.pro/public/v1"
|
|
26
|
+
|
|
27
|
+
profile: Profile = Profile.READ
|
|
28
|
+
# User id SIGNAL records as the author of every write. Never taken from the agent.
|
|
29
|
+
actor_user_id: Optional[UUID] = None
|
|
30
|
+
# Only files under this directory can be uploaded.
|
|
31
|
+
upload_root: Optional[Path] = None
|
|
32
|
+
allowed_extensions: tuple[str, ...] = DEFAULT_EXTENSIONS
|
|
33
|
+
# Write tools that run without the two-phase preview. Workflow tools can never be listed.
|
|
34
|
+
auto_confirm: tuple[str, ...] = ()
|
|
35
|
+
|
|
36
|
+
rate_per_sec: float = Field(3.0, gt=0, le=10)
|
|
37
|
+
burst: int = Field(6, ge=1, le=50)
|
|
38
|
+
max_concurrency: int = Field(4, ge=1, le=10)
|
|
39
|
+
http_timeout: float = 30.0
|
|
40
|
+
tool_timeout: float = 30.0
|
|
41
|
+
upload_timeout: float = 600.0
|
|
42
|
+
max_response_chars: int = 20_000
|
|
43
|
+
confirmation_ttl_sec: int = 300
|
|
44
|
+
cache_ttl_sec: int = 300
|
|
45
|
+
|
|
46
|
+
audit_path: Path = STATE_DIR / "audit.jsonl"
|
|
47
|
+
state_path: Path = STATE_DIR / "state.sqlite"
|
|
48
|
+
|
|
49
|
+
@field_validator("profile", mode="before")
|
|
50
|
+
@classmethod
|
|
51
|
+
def _parse_profile(cls, value):
|
|
52
|
+
return Profile.parse(value) if isinstance(value, str) else value
|
|
53
|
+
|
|
54
|
+
@field_validator("allowed_extensions", "auto_confirm", mode="before")
|
|
55
|
+
@classmethod
|
|
56
|
+
def _split_csv(cls, value):
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
return tuple(v.strip().lower() if v.strip().startswith(".") else v.strip() for v in value.split(",") if v.strip())
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
@model_validator(mode="after")
|
|
62
|
+
def _check_write_requirements(self):
|
|
63
|
+
if self.profile >= Profile.WRITE:
|
|
64
|
+
if self.actor_user_id is None:
|
|
65
|
+
raise ValueError("SGNL_MCP_ACTOR_USER_ID is required for the write and workflow profiles")
|
|
66
|
+
if self.upload_root is None:
|
|
67
|
+
raise ValueError("SGNL_MCP_UPLOAD_ROOT is required for the write and workflow profiles")
|
|
68
|
+
return self
|