Metadata-Version: 2.5
Name: sgnl-mcp
Version: 0.2.0
Summary: MCP server giving an LLM agent controlled access to the SIGNAL Integration API
Project-URL: GitHub, https://github.com/gritsyuk/sgnl-api
Author-email: Igor Gritsyuk <gritsyuk.igor@gmail.com>
License-Expression: MIT
Requires-Python: >=3.11
Requires-Dist: mcp<3,>=2.3
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: sgnl-api>=0.5.0
Requires-Dist: tenacity>=8.2
Description-Content-Type: text/markdown

# sgnl-mcp — MCP-сервер для SIGNAL Integration API

MCP-сервер, через который LLM-агент (Claude и др.) работает с документацией в SIGNAL: смотрит проекты и папки, ищет и скачивает документы, загружает новые документы и версии, работает с замечаниями, передачами и рассмотрениями.

Агент остаётся под контролем: чтение без ограничений, запись — только после предпросмотра и согласия пользователя, отправка передач и запуск рассмотрений — только после подтверждения пользователем в диалоге. Удалять, перемещать и переименовывать документы и папки, а также менять права агент не может вообще.

Построен на [`sgnl-api`](../README.md) — обёртке над API из этого же репозитория.

## Как обеспечивается контроль

| Рубеж | Что делает |
|---|---|
| 1. Scopes токена | Сервер запрашивает у SIGNAL только права своего профиля. В профиле `read` в токене нет ни одного права на запись — SIGNAL сам откажет. Права `*:delete` не запрашиваются никогда. |
| 2. Профиль | Инструменты выше профиля не регистрируются: агент их не видит. |
| 3. Белый список запросов | Каждый HTTP-запрос проверяется до отправки: только разрешённые метод и путь, никаких `DELETE`, никакого поля `deleted` в запросах на запись, внешние хосты — только для загрузки файла. |
| 4. Подтверждение | Запись — в два вызова: предпросмотр с токеном, затем выполнение с тем же токеном. Отправка передачи и запуск рассмотрения — через диалог подтверждения у пользователя (MCP elicitation). |
| 5. Клиент и аудит | Инструменты помечены `readOnlyHint`: клиент спрашивает разрешение на запись. Все записи и отказы пишутся в JSONL-журнал без секретов. |

Нагрузка на SIGNAL ограничена: по умолчанию не больше 3 запросов в секунду и 4 параллельных на весь сервер, у каждого вызова инструмента свой бюджет запросов, справочники кэшируются на 5 минут.

## Установка

```bash
uvx sgnl-mcp          # запуск без установки
# или
pip install sgnl-mcp
```

Нужен Python 3.11+.

## Настройка

Вся настройка — через переменные окружения (или файл `.env` в рабочем каталоге). Агент не может их изменить.

| Переменная | Обязательна | По умолчанию | Назначение |
|---|---|---|---|
| `SGNL_CLIENT_ID` | да | — | Client ID интеграции SIGNAL |
| `SGNL_CLIENT_SECRET` | да | — | Client secret интеграции SIGNAL |
| `SGNL_MCP_PROFILE` | нет | `read` | `read`, `write` или `workflow` |
| `SGNL_MCP_ACTOR_USER_ID` | для `write`/`workflow` | — | ID пользователя SIGNAL, от имени которого делаются изменения (лучше служебная учётная запись «AI-агент») |
| `SGNL_MCP_UPLOAD_ROOT` | для `write`/`workflow` | — | Каталог, из которого разрешено загружать файлы. Файлы вне него, в том числе через символические ссылки, недоступны |
| `SGNL_MCP_ALLOWED_EXTENSIONS` | нет | `.pdf,.dwg,.dxf,…` | Разрешённые типы загружаемых файлов, через запятую |
| `SGNL_MCP_AUTO_CONFIRM` | нет | — | Write-инструменты, которые выполняются без предпросмотра (только для доверенных сценариев). Workflow сюда не входит |
| `SGNL_MCP_RATE_PER_SEC` | нет | `3` | Запросов в секунду (не больше 10) |
| `SGNL_MCP_BURST` | нет | `6` | Запас запросов для коротких всплесков |
| `SGNL_MCP_MAX_CONCURRENCY` | нет | `4` | Параллельных запросов к SIGNAL |
| `SGNL_MCP_TOOL_TIMEOUT` | нет | `30` | Таймаут вызова инструмента, с |
| `SGNL_MCP_UPLOAD_TIMEOUT` | нет | `600` | Таймаут загрузки файла, с |
| `SGNL_MCP_CONFIRMATION_TTL_SEC` | нет | `300` | Срок действия токена подтверждения |
| `SGNL_MCP_AUDIT_PATH` | нет | `~/.sgnl-mcp/audit.jsonl` | Журнал аудита |
| `SGNL_MCP_STATE_PATH` | нет | `~/.sgnl-mcp/state.sqlite` | Журнал выполненных операций (защита от повторов) |

Пользователю нужен только ключ (`SGNL_CLIENT_ID` и `SGNL_CLIENT_SECRET`), scope он не задаёт: сервер сам запрашивает права своего профиля.

Если в интеграции включены не все scope, сервер всё равно запускается, но без недостающих:
- операции, которым они нужны, отвечают ошибкой `SCOPE_MISSING` с готовым текстом для администратора, например: «У вашего ключа SIGNAL ограниченные права… Передайте администратору SIGNAL: „В интеграции (client_id …) включите scope: docs:issue:list“»;
- запрос в SIGNAL при этом не отправляется;
- инструмент `get_access_status` заранее показывает, чего не хватает;
- то же сообщение сервер пишет в свой журнал при запуске.

Сервер не запускается, только если ключ неверный или в интеграции нет ни одного scope профиля. Для `write` и `workflow` при запуске проверяется, что пользователь `SGNL_MCP_ACTOR_USER_ID` есть в компании. Если для этой проверки нет `company:user:list`, запись недоступна, а чтение работает.

### Права интеграции по профилям

Это раздел для администратора SIGNAL. Включите в интеграции эти scope, чтобы были доступны все инструменты профиля:

| Профиль | Scopes интеграции в SIGNAL |
|---|---|
| `read` | `project:list`, `project:read`, `docs:folder:read`, `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` |
| `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` |
| `workflow` | то же, что `write` |

Список составляется автоматически из запросов, которые разрешены профилю. Тест сверяет его со спецификацией SIGNAL, поэтому лишнего сервер не запросит. Обратите внимание: нужен `docs:issue:list`, а не `docs:issue:read`.

Отправка передачи и запуск рассмотрения в SIGNAL используют те же права, что и их наполнение. Поэтому их отделяет сам MCP-сервер: в профиле `write` этих инструментов нет, а в `workflow` они всегда требуют подтверждения пользователя.

## Подключение к Claude

**Claude Desktop** — `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "sgnl": {
      "command": "uvx",
      "args": ["sgnl-mcp"],
      "env": {
        "SGNL_CLIENT_ID": "…",
        "SGNL_CLIENT_SECRET": "…",
        "SGNL_MCP_PROFILE": "read"
      }
    }
  }
}
```

**Claude Code:**

```bash
claude mcp add sgnl -e SGNL_CLIENT_ID=… -e SGNL_CLIENT_SECRET=… -e SGNL_MCP_PROFILE=read -- uvx sgnl-mcp
```

Для профиля `write` добавьте `SGNL_MCP_ACTOR_USER_ID` и `SGNL_MCP_UPLOAD_ROOT`.

## Запуск в Docker

Готовый образ для `linux/amd64` и `linux/arm64` (в том числе Mac на Apple Silicon) публикуется в GitHub Container Registry:

```bash
docker pull ghcr.io/gritsyuk/sgnl-mcp:latest   # или конкретная версия, например :0.1.0
docker tag ghcr.io/gritsyuk/sgnl-mcp:latest sgnl-mcp
```

Для работы стоит закрепить версию (`:0.1.0`), а не `latest`: тогда образ не обновится без вашего ведома. Можно и собрать образ самостоятельно из корня репозитория (ему нужны оба пакета рабочего пространства):

```bash
docker build -f mcp/Dockerfile -t sgnl-mcp .
```

Сервер общается с клиентом через stdin/stdout, поэтому контейнер запускается с `-i`, а клиент сам стартует его на каждую сессию. Секреты удобно держать в env-файле, а не в конфигурации клиента. Например, `~/.sgnl-mcp.env`:

```bash
SGNL_CLIENT_ID=…
SGNL_CLIENT_SECRET=…
SGNL_MCP_PROFILE=read
```

**Claude Desktop** — `claude_desktop_config.json` (пути — абсолютные):

```json
{
  "mcpServers": {
    "sgnl": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "--init",
        "--read-only", "--tmpfs", "/tmp",
        "--cap-drop", "ALL", "--security-opt", "no-new-privileges",
        "--env-file", "/Users/me/.sgnl-mcp.env",
        "-v", "sgnl-mcp-data:/data",
        "sgnl-mcp"
      ]
    }
  }
}
```

**Claude Code:**

```bash
claude mcp add sgnl -- docker run -i --rm --init --read-only --tmpfs /tmp --cap-drop ALL \
  --security-opt no-new-privileges --env-file ~/.sgnl-mcp.env -v sgnl-mcp-data:/data sgnl-mcp
```

**Windows.** В конфигурации Claude Desktop (`%APPDATA%\Claude\claude_desktop_config.json`) обратные слеши в путях удваиваются: `"--env-file", "C:\\Users\\Admin\\.sgnl-mcp.env"`, а папка для загрузки подключается как `"-v", "C:\\SIGNAL\\outgoing:/uploads:ro"`. В командной строке `cmd` вместо `/dev/null` используется `NUL`. Проверить, что образ запускается:

```cmd
docker run -i --rm --init ghcr.io/gritsyuk/sgnl-mcp:0.1.0 < NUL
```

Без учётных данных сервер сразу завершится с ошибкой проверки настроек (не заданы `SGNL_CLIENT_ID` / `SGNL_CLIENT_SECRET`) — значит, образ рабочий.

Что внутри образа:

- Процесс работает от непривилегированного пользователя (uid 10001), зависимости зафиксированы `uv.lock`.
- `/data` — том для журнала аудита (`audit.jsonl`) и журнала выполненных операций (`state.sqlite`). Именованный том `sgnl-mcp-data` сохраняет их между запусками; без него защита от повторов и аудит теряются при каждом перезапуске.
- `/uploads` — каталог загрузки (`SGNL_MCP_UPLOAD_ROOT` уже указывает на него). Для профилей `write`/`workflow` смонтируйте туда папку с файлами **только на чтение**: `-v /path/to/outgoing:/uploads:ro`. Тогда агент физически не может ни изменить файлы на компьютере, ни прочитать что-то за пределами этой папки.
- Файл `.env` в образ не попадает (`.dockerignore` пропускает только исходники пакетов).

Флаги `--read-only`, `--cap-drop ALL` и `no-new-privileges` необязательны, но сервер с ними работает, и это ещё один рубеж: контейнер не может ничего записать, кроме `/data` и `/tmp`.

### Разрешения в клиенте
- Инструменты чтения можно разрешить без вопросов.
- Инструменты записи и Workflow **не добавляйте в «разрешить всегда»**: клиент должен спрашивать перед каждым вызовом. Это один из рубежей контроля.
- Для профиля `workflow` клиент должен поддерживать MCP elicitation (диалог подтверждения). Без неё отправка передач и запуск рассмотрений завершаются ошибкой и ничего не делают.

## Инструменты

| Инструмент | Профиль | Что делает |
|---|---|---|
| `get_access_status` | read | Что доступно ключу: профиль, недостающие scope и готовый текст для администратора SIGNAL (без запросов к SIGNAL) |
| `list_projects` | read | Проекты, доступные интеграции |
| `get_project` | read | Данные проекта и его корневая папка |
| `get_project_folder_tree` | read | Дерево папок с правами пользователя |
| `search_documents` | read | Поиск документов по проекту или папке (имя, расширение, автор, даты) |
| `list_folder_documents` | read | Документы одной папки (без вложенных) |
| `list_document_versions` | read | История версий документа |
| `get_document_version` | read | Связь «документ — версия файла» по `item_version_id` |
| `get_document_download_url` | read | Временная ссылка на скачивание версии (сам файл не передаётся) |
| `get_document_context` | read | Всё о документе за один вызов: версии, атрибуты, замечания, рассмотрения, подписи; у каждого блока свой статус |
| `list_document_transfers` | read | Передачи проекта с фильтрами |
| `get_document_transfer` | read | Полные данные передачи |
| `list_transfer_documents` | read | Документы в передаче |
| `list_document_transfer_types` | read | Типы передач проекта |
| `list_issues` | read | Замечания с фильтрами |
| `list_reviews` | read | Рассмотрения проекта |
| `get_review_details` | read | Версии рассмотрения и документы в них |
| `get_document_review_status` | read | Статус рассмотрения версий документов |
| `list_review_types` | read | Типы рассмотрений проекта |
| `upload_document` | write | Новый документ из файла |
| `add_document_version` | write | Новая версия существующего документа |
| `update_document_version_comment` | write | Комментарий версии |
| `create_document_transfer` | write | Новая передача (черновик) |
| `add_documents_to_transfer` | write | Добавить версии документов в неотправленную передачу |
| `update_issues` | write | Изменить 1–25 замечаний |
| `create_review` | write | Новое рассмотрение (черновик) |
| `add_documents_to_review` | write | Добавить до 20 версий документов в рассмотрение |
| `prepare_send_document_transfer` | workflow | Проверить передачу перед отправкой и получить токен |
| `send_document_transfer` | workflow | Отправить передачу (подтверждение пользователем в диалоге) |
| `prepare_start_review` | workflow | Проверить рассмотрение перед запуском и получить токен |
| `start_review` | workflow | Запустить рассмотрение (подтверждение пользователем в диалоге) |

### Как проходит запись

1. Агент вызывает инструмент без `confirmation_token`. Сервер проверяет объекты и возвращает предпросмотр («было → станет», путь папки, состав передачи и т.п.) и токен. В SIGNAL ничего не меняется.
2. Агент показывает предпросмотр пользователю.
3. После согласия агент вызывает инструмент с теми же аргументами и токеном. Сервер перепроверяет объекты: если с момента предпросмотра что-то изменилось (другой файл, кто-то правил замечание), токен отклоняется.

Токен одноразовый, действует 5 минут и привязан к аргументам. Повторный вызов с уже использованным токеном возвращает сохранённый результат, а не выполняет операцию второй раз.

Если запрос на запись завершился без ясного ответа (таймаут, ошибка сервера), сервер **не повторяет его сам**, а возвращает `OUTCOME_UNKNOWN` с тем, что удалось выяснить. Для загрузки файла можно завершить операцию параметром `resume_version_id`, не загружая файл заново.

### Ограничения
- В API SIGNAL нет поиска передач по документу, поэтому блок `transfers` в `get_document_context` всегда `not_supported`.
- Создание замечаний, удаление, перемещение, переименование, изменение прав и пользователей в MCP-сервер намеренно не включены.

## Ответы и ошибки

Все инструменты возвращают `{"success": true, "data": …, "page": …, "warnings": […], "request_id": …}` или `{"success": false, "error": {"code", "message", "details"}, "request_id": …}`.

| Код | Значение |
|---|---|
| `INVALID_REQUEST`, `NOT_FOUND`, `FORBIDDEN`, `AUTH_FAILED`, `QUOTA_EXCEEDED`, `RATE_LIMITED` | Ответ SIGNAL (400, 404, 403, 401, 450, 429) |
| `SERVER_ERROR`, `NETWORK_ERROR`, `TIMEOUT` | SIGNAL не ответил нормально; чтение можно повторить |
| `OUTCOME_UNKNOWN` | Запись могла пройти или не пройти; сервер её не повторял |
| `SCOPE_MISSING` | В интеграции ключа не включён нужный scope или у компании нет лицензии. В `message` — текст для администратора SIGNAL, в `details.missing_scopes` — список scope |
| `POLICY_DENIED` | Запрос запрещён политикой MCP-сервера |
| `CONFIRMATION_INVALID`, `CONFIRMATION_DECLINED` | Токен недействителен или пользователь не подтвердил |
| `INVALID_STATE`, `NOTHING_TO_DO`, `UPLOAD_FAILED`, `BUDGET_EXCEEDED` | Объект в неподходящем состоянии, изменять нечего, файл не передан, исчерпан бюджет запросов |
