320 lines
16 KiB
Markdown
320 lines
16 KiB
Markdown
# MCP-интерфейс
|
||
|
||
Crank публикует REST-операции как MCP-инструменты через Streamable HTTP.
|
||
|
||
## Endpoint агента
|
||
|
||
Каждый агент имеет собственный MCP endpoint:
|
||
|
||
```text
|
||
/mcp/v1/{workspace_slug}/{agent_slug}
|
||
```
|
||
|
||
Для demo seed:
|
||
|
||
```text
|
||
/mcp/v1/default/currency-rates
|
||
```
|
||
|
||
Полный URL зависит от вашего домена:
|
||
|
||
```text
|
||
https://crank.example.com/mcp/v1/default/currency-rates
|
||
```
|
||
|
||
## Авторизация
|
||
|
||
MCP-клиент должен передавать API-ключ агента:
|
||
|
||
```http
|
||
Authorization: Bearer <agent_api_key>
|
||
```
|
||
|
||
Ключ выдается в разделе **API ключи**. Полное значение показывается только один раз при создании.
|
||
|
||
Для операций с подтверждением человеком нужен отдельный ключ подтверждения. Его тоже выдают в разделе **API ключи**, но в режиме **Подтверждения**. Такой ключ нельзя передавать LLM или MCP-клиенту. Он нужен только вашему внешнему интерфейсу, где пользователь нажимает «Подтвердить» или «Отклонить».
|
||
|
||
Ключи разных типов не взаимозаменяемы: `mcp_client` не принимается на approval
|
||
endpoints, а `approval` не принимается для `initialize`, `tools/list` и
|
||
`tools/call`. Revocation проверяется через PostgreSQL source of truth и начинает
|
||
действовать без restart, включая уже существующие MCP session. Для approval keys
|
||
может быть задан список `allowed_origins`; если HTTP `Origin` присутствует и не
|
||
совпадает с разрешённым origin, запрос отклоняется до выполнения side effect.
|
||
|
||
## Поддерживаемые методы
|
||
|
||
MCP methods:
|
||
|
||
- `initialize`;
|
||
- `notifications/initialized`;
|
||
- `ping`;
|
||
- `tools/list`;
|
||
- `tools/call`.
|
||
|
||
HTTP transport:
|
||
|
||
- `POST /mcp/v1/{workspace_slug}/{agent_slug}` - JSON-RPC запросы;
|
||
- `GET /mcp/v1/{workspace_slug}/{agent_slug}` - server-to-client stream;
|
||
- `DELETE /mcp/v1/{workspace_slug}/{agent_slug}` - закрытие сессии.
|
||
|
||
## Обязательные заголовки
|
||
|
||
Для `POST`:
|
||
|
||
```http
|
||
Authorization: Bearer <agent_api_key>
|
||
Content-Type: application/json
|
||
Accept: application/json, text/event-stream
|
||
```
|
||
|
||
Для `GET`:
|
||
|
||
```http
|
||
Authorization: Bearer <agent_api_key>
|
||
Accept: text/event-stream
|
||
MCP-Session-Id: <session_id>
|
||
```
|
||
|
||
После `initialize` сервер возвращает заголовок:
|
||
|
||
```http
|
||
MCP-Session-Id: <session_id>
|
||
```
|
||
|
||
Если клиент передает `MCP-Protocol-Version`, он должен совпадать с версией, согласованной при инициализации.
|
||
|
||
## Авторитетное evidence первого вызова
|
||
|
||
Getting Started засчитывает первый вызов только после полной публичной
|
||
последовательности `initialize` → `notifications/initialized` → `tools/list` →
|
||
успешный `tools/call` с тем же активным `mcp_client` key. Одного discovery,
|
||
failed call, success через другой key или external verifier credential
|
||
недостаточно. Invocation History сохраняет exact key ID вместе с Agent,
|
||
immutable Operation Version, tool, UTC timestamp, Request ID и Trace ID.
|
||
|
||
Admin UI может безопасно сослаться на соответствующую history row и correlation
|
||
IDs, но не показывает input arguments, payload, bearer value или upstream body.
|
||
Если key отозван/deleted, Agent/Operation archive либо binding/revision больше
|
||
не подтверждаются authoritative source of truth, onboarding возвращается в
|
||
actionable state; старый raw key не восстанавливается.
|
||
|
||
## Пример `initialize`
|
||
|
||
```bash
|
||
curl -i https://crank.example.com/mcp/v1/default/currency-rates \
|
||
-H 'Authorization: Bearer <agent_api_key>' \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json, text/event-stream' \
|
||
--data '{
|
||
"jsonrpc": "2.0",
|
||
"id": 1,
|
||
"method": "initialize",
|
||
"params": {
|
||
"protocolVersion": "2025-06-18",
|
||
"capabilities": {},
|
||
"clientInfo": {
|
||
"name": "curl",
|
||
"version": "1.0.0"
|
||
}
|
||
}
|
||
}'
|
||
```
|
||
|
||
Сохраните `MCP-Session-Id` из ответа.
|
||
|
||
## Пример `tools/list`
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/currency-rates \
|
||
-H 'Authorization: Bearer <agent_api_key>' \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json' \
|
||
-H 'MCP-Session-Id: <session_id>' \
|
||
--data '{
|
||
"jsonrpc": "2.0",
|
||
"id": 2,
|
||
"method": "tools/list",
|
||
"params": {}
|
||
}'
|
||
```
|
||
|
||
Ожидаемый результат для demo seed содержит инструмент:
|
||
|
||
```text
|
||
frankfurter_latest_rate
|
||
```
|
||
|
||
Если опубликованная версия агента использует подбор по запросу, `tools/list` содержит только:
|
||
|
||
```text
|
||
search_tools
|
||
call_tool
|
||
```
|
||
|
||
`search_tools` принимает текст задачи, необязательные идентификаторы разделов и предел результатов. Ответ содержит полные входные схемы найденных инструментов и `catalog_revision`.
|
||
|
||
`call_tool` принимает имя найденного инструмента, его аргументы и полученную `catalog_revision`. Если за время между поиском и вызовом опубликована новая версия агента, вызов отклоняется с кодом `agent_catalog_result_stale`: клиент должен повторить поиск.
|
||
|
||
`catalog_revision` берётся из immutable Published Agent catalog. Он не является user-controlled label и не заменяет Request/Trace ID; это bounded revision token для защиты пары `search_tools → call_tool` от stale results.
|
||
|
||
## Пример `tools/call`
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/currency-rates \
|
||
-H 'Authorization: Bearer <agent_api_key>' \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json' \
|
||
-H 'MCP-Session-Id: <session_id>' \
|
||
--data '{
|
||
"jsonrpc": "2.0",
|
||
"id": 3,
|
||
"method": "tools/call",
|
||
"params": {
|
||
"name": "frankfurter_latest_rate",
|
||
"arguments": {
|
||
"base": "USD",
|
||
"quote": "EUR"
|
||
}
|
||
}
|
||
}'
|
||
```
|
||
|
||
Crank выполнит REST-запрос:
|
||
|
||
```text
|
||
GET https://api.frankfurter.dev/v1/latest?base=USD&symbols=EUR
|
||
```
|
||
|
||
Ошибка `tools/call` сохраняет существующие JSON-RPC/`isError` semantics и
|
||
добавляет в structured content поля `error_code`, `stage`, `retryability`,
|
||
`outcome_certainty`, `request_id` и `trace_id`. Значение `manual_reconcile` вместе
|
||
с `outcome_unknown` означает, что автоматический повтор небезопасен. Raw upstream
|
||
body, URL, headers и внутренний текст ошибки не возвращаются.
|
||
|
||
## Операции с подтверждением человеком
|
||
|
||
Если в мастере операции включено **Подтверждение человеком**, первый `tools/call` не выполняет REST-запрос сразу. Вместо этого Crank создает ожидающий запрос на подтверждение и возвращает MCP-клиенту структурированный результат:
|
||
|
||
```json
|
||
{
|
||
"status": "approval_required",
|
||
"approval_id": "approval_...",
|
||
"approval_url": "/v1/default/sales/approvals/approval_...",
|
||
"approve": {
|
||
"method": "POST",
|
||
"url": "/v1/default/sales/approvals/approval_.../approve",
|
||
"body": { "approve": "yes" }
|
||
},
|
||
"deny": {
|
||
"method": "POST",
|
||
"url": "/v1/default/sales/approvals/approval_.../deny",
|
||
"body": { "approve": "no" }
|
||
}
|
||
}
|
||
```
|
||
|
||
`approval_url` в ответе является путем на MCP-сервере. Если вы публикуете MCP через префикс `/mcp`, внешний URL будет начинаться с `/mcp/v1/...`.
|
||
|
||
Approval identity считается по полному scope: workspace, Agent, immutable
|
||
Operation Version и canonical JSON аргументы. Служебные поля Crank, например
|
||
`_crank_confirmation_token`, не входят в fingerprint и не создают дубликаты.
|
||
Активный pending request с тем же scope возвращается повторно. В metadata и
|
||
`payload_preview` хранится только bounded safe summary: secret-like поля
|
||
редактируются, raw approval key/auth headers/control tokens не сохраняются и не
|
||
отдаются клиенту.
|
||
|
||
Внешний интерфейс подтверждения работает отдельным ключом подтверждения:
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/sales/approvals \
|
||
-H 'Authorization: Bearer <approval_api_key>'
|
||
```
|
||
|
||
Статус конкретного запроса:
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id> \
|
||
-H 'Authorization: Bearer <approval_api_key>'
|
||
```
|
||
|
||
Подтверждение:
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id>/approve \
|
||
-H 'Authorization: Bearer <approval_api_key>' \
|
||
-H 'Content-Type: application/json' \
|
||
--data '{ "approve": "yes", "note": "Пользователь подтвердил действие" }'
|
||
```
|
||
|
||
После подтверждения Crank выполняет исходный REST-запрос с тем payload, который был сохранен при первом `tools/call`. Если запрос прошел успешно, заявка получает статус `completed`, а результат сохраняется в `response_payload`. Если upstream вернул ошибку, заявка получает статус `failed`, а в `response_payload` сохраняется код и текст ошибки.
|
||
|
||
Перед обращением к upstream заявка атомарно переходит в статус `executing`. Если
|
||
`mcp-server` завершился во время выполнения, другой рабочий цикл повторно захватит
|
||
заявку после истечения аренды. Для изменяющих операций рекомендуется настроить
|
||
`execution_config.idempotency` и передавать поддерживаемый upstream заголовок
|
||
идемпотентности: универсальный HTTP-клиент не может гарантировать ровно одно внешнее
|
||
побочное действие при падении процесса между ответом upstream и записью результата.
|
||
|
||
Повторный `tools/call` с теми же агентом, операцией, версией и canonical
|
||
JSON-аргументами возвращает уже существующую активную заявку вместо создания
|
||
дубликата. Повторный approve/deny terminal заявки возвращает текущий status и не
|
||
запускает второй side effect.
|
||
|
||
Отклонение:
|
||
|
||
```bash
|
||
curl https://crank.example.com/mcp/v1/default/sales/approvals/<approval_id>/deny \
|
||
-H 'Authorization: Bearer <approval_api_key>' \
|
||
-H 'Content-Type: application/json' \
|
||
--data '{ "approve": "no", "note": "Пользователь отклонил действие" }'
|
||
```
|
||
|
||
Ключ MCP-клиента не подходит для этих endpoints. Ключ подтверждения, наоборот, не подходит для `initialize`, `tools/list` и `tools/call`.
|
||
|
||
Текущая реализация возвращает `approval_required` сразу и не держит исходный `tools/call` открытым до решения пользователя. Поэтому внешний интерфейс может восстановить результат через `GET /approvals/<approval_id>`. Долгое ожидание через Streamable HTTP/SSE запланировано отдельно.
|
||
|
||
## Как формируется каталог инструментов
|
||
|
||
MCP-клиент видит только опубликованные операции, которые привязаны к опубликованному агенту.
|
||
|
||
Способ выдачи, разделы и предел результатов входят в версионируемый контракт агента. Прямой режим не меняет обычный MCP-поток. Режим подбора скрывает исходные инструменты за совместимыми метаинструментами `search_tools` и `call_tool`; динамическое изменение `tools/list` и специальные расширения клиента не требуются.
|
||
|
||
Базовый поиск использует BM25 по имени, заголовку, описанию инструмента и описаниям его разделов. Фильтр по разделу применяется до ранжирования. По умолчанию возвращается не более восьми результатов, допустимый предел — от одного до двадцати.
|
||
|
||
Черновики операций не попадают в MCP-каталог. Если два пользователя работают в одном workspace, один может редактировать черновик, а второй публиковать агента. В опубликованный каталог попадут только опубликованные версии операций.
|
||
|
||
При каждом обновлении каталога Crank анализирует фактические определения `tools/list` и записывает в структурированный журнал:
|
||
|
||
- число инструментов;
|
||
- размер компактного JSON в байтах;
|
||
- оценочный объём контекста в токенах;
|
||
- размер крупнейшего инструмента;
|
||
- рекомендуемый предел и признак его превышения;
|
||
- число предупреждений качества каталога.
|
||
|
||
Оценка токенов равна округлённому вверх отношению размера UTF-8 к трём. Она нужна для стабильного сравнения ревизий каталога и не заменяет точный токенизатор конкретной модели.
|
||
|
||
## Обновление каталога
|
||
|
||
`mcp-server` периодически обновляет опубликованный каталог. Интервал задается:
|
||
|
||
```env
|
||
CRANK_MCP_REFRESH_MS=5000
|
||
```
|
||
|
||
После публикации операции или агента подождите один интервал обновления или перезапустите `mcp-server`.
|
||
|
||
## Ошибки
|
||
|
||
Частые причины ошибок:
|
||
|
||
- `401 Unauthorized` - отсутствует или неверен API-ключ агента.
|
||
- `403 Forbidden` - ключ не имеет нужного доступа.
|
||
- `404 Not Found` - workspace, агент или инструмент не опубликованы.
|
||
- `400 Bad Request` - неверный MCP-заголовок, session id или JSON-RPC payload.
|
||
- `429 Too Many Requests` - сработал rate limit.
|
||
|
||
Runtime-ошибки инструмента возвращаются как структурированный MCP tool error с кодом,
|
||
сообщением, `request_id` и отдельным `trace_id`. Транспортные ответы также содержат
|
||
`x-request-id` и `x-trace-id`; отклонённые входные значения в них не отражаются.
|