Skip to main content
Glama
DyakonovAlex

incident-mcp

by DyakonovAlex

incident-mcp

Инцидентный MCP-сервер для агента дежурного инженера (on-call). FastMCP (Python 3.12, uv), транспорт stdio, единый asyncpg-пул на процесс.

Сервер читает локальный инцидентный стенд из homework-stand/: Postgres с логами, деплоями и реестром инцидентов плюс payments-api под нагрузкой. В стенде есть намеренный дефект — деградация latency учебного эндпоинта; ищется он по данным через tools сервера, а не чтением исходников (см. «Разбор инцидента» ниже).

Состав

├── src/incident_mcp/      MCP-сервер
│   ├── app.py             инстанс FastMCP, lifespan (пул), инструкции агента
│   ├── db.py              доступ к БД: пул, fetch/execute, ToolError-обёртки
│   ├── schemas.py         схемы аргументов: enum-типы, парсер длительностей
│   ├── tools_read.py      read-tools (без побочных эффектов)
│   ├── tools_write.py     write-tools (меняют состояние в Postgres)
│   └── server.py          stdio entry point (в stdout только MCP-сообщения)
├── tests/                 unit + интеграционные (на живом стенде)
├── homework-stand/        стенд: docker compose (Postgres, payments-api, simulator)
└── memory/                проектная память агентов (brief, decisions, progress)

Обоснование состава tools

Каждый tool — один шаг разбора инцидента; универсального query(sql) нет намеренно, агент не видит SQL и не может обойти доменные ограничения.

tool

шаг разбора

почему отдельным tool'ом

incidents_search

найти открытый инцидент

входная точка сценария; фильтры severity/status/time_range

incident_get

карточка одного инцидента

сводка разбора по id; неизвестный id — ошибка со списком доступных

deploys_recent

сопоставить деградацию с релизом

рост latency сразу после деплоя — главный подозреваемый

logs_query

WARN/ERROR вокруг начала деградации

сообщения — пользовательские данные (в стенде есть prompt-injection строка; tool отдаёт её как данные)

metrics_latency

форма деградации latency

без него агент увидит только точку, а не кривую; возвращает готовый агрегат (корзины, avg, p95, hit-rate), а не сырые строки

runbook_get

типовые симптомы и шаги диагностики

читается перед началом разбора

service_catalog_get

кому эскалировать

команда, on-call, зависимости

incident_acknowledge

взять инцидент в работу

write-операция, отдельный tool, в description явно написано «WRITE-ОПЕРАЦИЯ»

incident_create_summary

зафиксировать выводы разбора

write-операция, отдельный tool

Описания tools важнее обычного: в OpenCode они попадают в один список со встроенными (bash, чтение файлов). Если из description не понятно, когда брать metrics_latency вместо bash/psql, агент возьмёт bash. Поэтому у каждого tool есть title и description, отвечающие на три вопроса: что делает, когда применять, какие ограничения и side effects. Аргументы валидируются inputSchema (enum-типы Literal, Field(ge=..., le=...), парсер длительностей 1m..7d); невалидный ввод возвращает структурированную ошибку isError с поправимым текстом, сервер не падает.

Быстрый старт

# 1. Стенд
cd homework-stand
cp .env.example .env
docker compose up -d --build
docker compose run --rm simulator   # ~5 минут: засев истории + живой трафик

# 2. MCP-сервер (из корня)
cp .env.example .env
uv sync
uv run incident-mcp                 # stdio-сервер

Проверка через MCP Inspector

Inspector (v2) работает в скриптовом CLI-режиме: цель (команда сервера) до --, опции после.

# lifecycle: initialize (DSN сервер возьмёт из .env сам)
npx @modelcontextprotocol/inspector --cli uv run incident-mcp -- \
  --method initialize --format json \
  --cwd "$(pwd)" -e FASTMCP_CHECK_FOR_UPDATES=off

# tools/list — 9 tools с title/description/inputSchema
npx @modelcontextprotocol/inspector --cli uv run incident-mcp -- \
  --method tools/list --format json \
  --cwd "$(pwd)" -e FASTMCP_CHECK_FOR_UPDATES=off

# вызов tool
npx @modelcontextprotocol/inspector --cli uv run incident-mcp -- \
  --method tools/call --tool-name metrics_latency --format json \
  --tool-args-json '{"endpoint":"/api/v1/orders/{order_id}/price","time_range":"1h","bucket":"1m"}' \
  --cwd "$(pwd)" -e FASTMCP_CHECK_FOR_UPDATES=off

# невалидные аргументы -> isError:true, сервер жив
npx @modelcontextprotocol/inspector --cli uv run incident-mcp -- \
  --method tools/call --tool-name metrics_latency --format json \
  --tool-args-json '{"endpoint":"/api/v1/orders/{order_id}/price","bucket":"xyz"}' \
  --cwd "$(pwd)" -e FASTMCP_CHECK_FOR_UPDATES=off

Прогон всех 9 tools и четырёх невалидных вызовов (полный лог — docs/inspector-checks.txt) дал: initialize → serverInfo incident-mcp 3.4.7, protocolVersion 2025-11-25; tools/list → 9 tools; каждый tool вернул данные; невалидные аргументы → {"isError":true} с текстом («Invalid time range 'xyz'...», «Incident 'INC-999' not found. Known incident ids: ...», pydantic-ошибка limit: Input should be greater than or equal to 1).

Подключение к OpenCode

opencode.json (ключевая часть):

{
  "mcp": {
    "incident-mcp": {
      "type": "local",
      "command": ["uv", "run", "incident-mcp"],
      "cwd": "/абсолютный/путь/к/incident-mcp",
      "enabled": true,
      "timeout": 30000,
      "environment": {
        "FASTMCP_CHECK_FOR_UPDATES": "off"
      }
    }
  }
}

Нюансы, проверенные на практике:

  • DSN в конфиге не дублируется: STAND_DATABASE_URL берётся только из .env. Сервер ищет .env по абсолютному пути от файла модуля (src/incident_mcp/server.py → корень проекта), поэтому загрузка не зависит от cwd, с которого opencode запускает процесс. Переменная из окружения имеет приоритет (load_dotenv не перезаписывает существующие).

  • cwd — абсолютный: относительный opencode резолвит от корня workspace и процесс не находит проект.

  • FASTMCP_CHECK_FOR_UPDATES — строка "off": в FastMCP 3.x поле check_for_updates это Literal["stable","prerelease","off"], значение false роняет процесс pydantic-ошибкой при импорте, до MCP-handshake.

  • После подключения tools сервера видны агенту с префиксом incident-mcp_* (incidents_search, metrics_latency, ...).

Проверка кода

uv run pytest                     # 31 тест; интеграционные скипаются без стенда (порт 5433)
uv run ruff format src tests      # 9 files left unchanged
uv run ruff check src tests       # All checks passed!
uv run mypy src                   # Success: no issues found in 7 source files

Лог реального диалога (ReAct-сценарий)

Сценарий пройден агентом (OpenCode + этот MCP-сервер) на живом стенде, прогон симулятора 2026-08-12 18:32–18:37. Цепочка вызовов:

  1. incidents_search(service="payments", status="open", time_range="1h")INC-001, severity high, открыт 18:31:41, «Рост времени ответа /api/v1/orders/{order_id}/price».

  2. runbook_get(service="payments") → план: кривая latency, контрастный эндпоинт, деплои, логи кэша.

  3. metrics_latency(endpoint="/api/v1/orders/{order_id}/price", time_range="1h", bucket="1m") → форма деградации: до 18:32 фоновый трафик avg 3–8 мс при hit-rate ~100%; с 18:32 монотонный рост — avg 39.2 → 80.0 → 139.2 → 198.5 → 265.1 → 336.9 мс, p95 51.5 → 457.5 мс, cache_hit_pct = 0.0 во всех корзинах.

  4. metrics_latency(endpoint="/api/v1/catalog/items", ...) → здоровый эндпоинт ровен: avg 0.4–0.7 мс. Проблема локальна для расчёта цены.

  5. deploys_recent(service="payments") → v1.5.0 «refactor: unified response cache» (i.petrov), задеплоен 18:29:41 — за 2 минуты до открытия инцидента и за 3 минуты до начала деградации.

  6. logs_query(service="payments-api", time_range="1h", level="WARN") → «response cache grew to 5000...9495 entries, hit_rate=0.0»: кэш наполняется, но попаданий ноль. level="ERROR" → пусто.

Гипотеза (со ссылками на данные): деплой v1.5.0 сломал кэш ответов — записи создаются, но поиск никогда не находит их (hit_rate=0.0 при растущем числе записей); значит, ключ записи не совпадает с ключом поиска, скорее всего в ключ попадает поле, уникальное для каждого запроса. Каждый запрос идёт по медленному пути расчёта цены, и под нагрузкой latency монотонно растёт.

Далее write-tools: incident_acknowledge("INC-001"){"status":"acknowledged","changed":true}; incident_create_summary("INC-001", ...) → сводка сохранена в incidents.summary.

Подтверждение по исходникам (после гипотезы): api/cache.py, build_key включал request_id — уникальный для каждого запроса, поэтому каждый lookup был промахом. Фикс: request_id из ключа убран (ключ = эндпоинт + параметры).

Разбор инцидента INC-001

  • Симптом: монотонный рост latency /api/v1/orders/{order_id}/price под нагрузкой 20 rps при ровном /api/v1/catalog/items.

  • Триггер: деплой payments v1.5.0 «refactor: unified response cache» (18:29:41), деградация с 18:32.

  • Корень: в ключе кэша ответов был request_id — кэш наполнялся, но не отдавал ни одного ответа (hit_rate 0.0), каждый запрос шёл по медленному пути (агрегация по журналу price_events).

  • Фикс: request_id исключён из build_key (homework-stand/api/cache.py, main.py), образ api пересобран (docker compose up -d --build api — заодно сбрасывает in-memory кэш для честного сравнения).

До/после: два реальных прогона

Оба прогона — симулятор стенда: 20 rps × 300 c = 6000 запросов, замер — metrics_latency (1m-корзины) и /internal/cache-stats.

До фикса (сломанный кэш, прогон 18:32–18:37):

минута

req

avg_ms

p95_ms

hit%

18:32

280

39.2

51.5

0.0

18:33

959

80.0

113.8

0.0

18:34

959

139.2

178.6

0.0

18:35

951

198.5

243.7

0.0

18:36

966

265.1

323.7

0.0

18:37

685

336.9

457.5

0.0

После фикса (прогон 18:41–18:46):

минута

req

avg_ms

p95_ms

hit%

18:41

342

20.8

54.4

39.2

18:42

960

13.4

54.8

71.0

18:43

960

10.6

56.8

81.7

18:44

959

20.0

73.5

71.0

18:45

962

25.8

88.9

69.1

18:46

617

39.3

105.4

59.6

Итог:

показатель

до

после

изменение

cache hit-rate

0.0% все минуты

39–82% (69% за прогон; 3314 hits / 1486 misses)

поднялся с 0%

avg latency, пик

336.9 мс

39.3 мс (минимум 10.6)

в 8.6 раза ниже

p95 latency, пик

457.5 мс

105.4 мс

в 4.3 раза ниже

Небольшой подъём avg в конце прогона «после» — рост медленного пути вместе с журналом price_events (357 тыс. строк к концу прогона) на фоне TTL-промахов; кэш при этом продолжает работать (hit-rate > 0).

Подробности про стенд — в homework-stand/README.md.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DyakonovAlex/incident-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server