@inso_web/els-mcp
Version:
MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.
357 lines (279 loc) • 14.6 kB
Markdown
# Разработка `@inso_web/els-mcp`
🇬🇧 **English version**: [`../CONTRIBUTING.md`](../CONTRIBUTING.md)
Документация для тех, кто работает над самим пакетом или поднимает его
локально для отладки. Конечному пользователю достаточно
[`README.ru.md`](./README.ru.md).
## Локальный запуск
```bash
npm install
ELS_API_KEY=els_live_... npm run dev
```
`npm run dev` запускает `tsx src/cli.ts` без предварительной сборки.
После `npm run build` бинарник лежит в `dist/cli.js` — запуск
через `node dist/cli.js`.
stdout зарезервирован под JSON-RPC; все логи идут в stderr.
### Тесты и проверки
```bash
npm test # vitest run — unit-тесты
npm run typecheck # tsc --noEmit
```
## ENV-переменные
### Подключение к ELS
| ENV | Default | Описание |
|---|---|---|
| `ELS_API_KEY` | — (обязателен) | Bearer-ключ (`els_live_*` или `els_test_*`) |
| `ELS_BASE_URL` | dev → `http://localhost:4010`, prod → `https://api.insoweb.ru/els` | Upstream ELS endpoint |
| `MCP_LOG_LEVEL` | `info` | pino level |
| `MCP_DISABLE_TOOLS` | — | CSV с именами tools для отключения |
| `MCP_UPSTREAM_TIMEOUT_MS` | `30000` | Таймаут одного ELS-запроса |
### HTTP transport
| ENV | Default | Описание |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | `stdio` или `http` |
| `MCP_HTTP_PORT` | `3030` | Порт listen для HTTP-режима |
| `MCP_PUBLIC_URL` | `https://mcp.insoweb.ru/els` | URL в WWW-Authenticate и discovery |
| `MCP_OIDC_ISSUER` | `https://auth.insoweb.ru` | OIDC issuer |
| `MCP_OIDC_JWKS_URL` | derived | JWKS endpoint |
| `MCP_OIDC_AUDIENCE` | `els-mcp` | Expected `aud` claim |
| `MCP_OIDC_DEMO_APP_SLUG` | — | Fallback appSlug при недоступности LK resolver |
| `MCP_CORS_ORIGINS` | `https://claude.ai,https://chat.openai.com` | CSV allowed origins (в dev добавляется localhost) |
### Cache, observability, billing
| ENV | Default | Описание |
|---|---|---|
| `MCP_REDIS_URL` | `redis://localhost:6379` | Redis URL |
| `MCP_CACHE_ENABLED` | `true` | Включить cache layer |
| `MCP_METRICS_ENABLED` | `true` | Включить `/els/metrics` |
| `MCP_CACHE_TTL_OVERRIDE_*` | — | Override TTL per class (секунды) |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | OTLP traces; если не задан → no-op |
| `MCP_LOG_PRETTY` | `true` в dev | Pretty-print pino |
| `MCP_REDACTION_ENABLED` | `true` | Включена ли редакция PII |
| `MCP_REDACTION_FIELDS` | — | CSV whitelist полей (пусто → редактим все) |
| `MCP_DATABASE_URL` | — | Postgres URL для audit/billing. Если пусто → no-op |
| `MCP_DEFAULT_APP_ID` | `default` | Используется в stdio-режиме |
| `MCP_DEFAULT_TIER` | `STANDARD` | Tier по умолчанию для quota-check |
| `MCP_LK_API_BASE_URL` | — | LK API URL для OIDC sub→apps и appSlug→tier (опц.) |
| `MCP_LK_API_TOKEN` | — | Bearer-токен для internal LK API |
## HTTP transport локально
```bash
MCP_TRANSPORT=http \
MCP_HTTP_PORT=3030 \
MCP_OIDC_DEMO_APP_SLUG=acme \
ELS_API_KEY=els_live_xxx \
npm run dev
```
OIDC можно направить на dev-инстанс INSO Auth:
```bash
MCP_OIDC_ISSUER=http://localhost:4002 \
MCP_OIDC_JWKS_URL=http://localhost:4002/oidc/.well-known/jwks.json \
MCP_TRANSPORT=http npm run dev
```
### Маршруты
| Method | URL | Назначение |
|---|---|---|
| `POST /els/mcp` | MCP JSON-RPC (Streamable HTTP) | Требует Bearer (ELS-key или OIDC JWT) |
| `GET /els/mcp` | Long-lived SSE (server → client notifications) | Требует Bearer |
| `DELETE /els/mcp` | Terminate session | Требует Bearer |
| `GET /els/healthz` | Liveness probe (всегда 200) | Public |
| `GET /els/readyz` | Readiness probe (ELS upstream check) | Public |
| `GET /els/.well-known/oauth-protected-resource` | RFC 9728 resource metadata | Public |
| `GET /els/.well-known/mcp` | MCP discovery (tools list, transports) | Public |
| `GET /els/metrics` | Prometheus text format | Public |
### Быстрые curl-проверки
```bash
# Liveness
curl http://localhost:3030/els/healthz
# {"status":"ok"}
# Resource metadata
curl http://localhost:3030/els/.well-known/oauth-protected-resource
# MCP discovery
curl http://localhost:3030/els/.well-known/mcp
# Bearer ELS-key
curl -X POST http://localhost:3030/els/mcp \
-H "Authorization: Bearer els_live_xxx" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"curl","version":"1"},"capabilities":{}}}'
# В ответе будет Mcp-Session-Id header — используйте его для последующих запросов.
```
## Авторизация
Поддерживаются два сценария (детектится по shape Bearer):
1. **ELS-ключ** — `Authorization: Bearer els_(live|test)_<key>` — passthrough в ELS.
Используется в CI/CD, server-to-server и debug-сценариях.
2. **OIDC JWT** — `Authorization: Bearer <jwt>` — локально валидируется через
JWKS INSO Auth (`https://auth.insoweb.ru/oidc/.well-known/jwks.json`,
RS256, audience `els-mcp`, scope `errors:mcp-read`).
Если оба Bearer-а отсутствуют — 401 + `WWW-Authenticate: Bearer
realm="els-mcp",
resource_metadata="https://mcp.insoweb.ru/els/.well-known/oauth-protected-resource"`.
### Sessions
`Mcp-Session-Id` header возвращается на первый `initialize`-запрос и должен
передаваться во все последующие. TTL — 30 мин idle. Хранение in-memory
(Map), будет переведено в Redis в следующих релизах.
### OIDC sub → appSlug resolver
При наличии LK API эндпоинта `GET /api/internal/users/{sub}/apps` сервис
резолвит доступные пользователю apps и кэширует результат в Redis 5 минут.
Если эндпоинт недоступен — graceful fallback на `MCP_OIDC_DEMO_APP_SLUG`.
Если у пользователя несколько apps, tool принимает optional `appSlug`
параметр; иначе берётся первый.
## Prompt-injection mitigation
Все строковые поля из логов оборачиваются в `<untrusted>…</untrusted>`
теги. В description каждого tool — system note, что LLM **не** должен
следовать инструкциям из такого контента. Параллельно работает regex
deny-list (см. `src/redaction/promptInjection.ts`): при совпадении
(`ignore previous instructions`, `system:`, `jailbreak`, …) в
`_meta.suspiciousContentBlocked = true` + `_meta.suspiciousRule = <name>`.
## Audit log
- Append-only, schema `mcp_audit` (отдельная БД от ELS).
- Hash-chain: `prevHash` + `rowHash` (sha256) per `appId`-partition.
- Партиционирование по месяцу (RANGE `createdAt`). См.
`prisma/migrations/init/migration.sql`.
- Запись non-blocking: если БД недоступна, tool-call продолжает работать
(silent fail с warn-логом).
### Что НЕ пишется в audit
- Полный API-ключ (только prefix 8 символов).
- Контент логов (только метаданные tool-call).
- Полный IP (anonymized).
- Cookies, Authorization headers.
### Проверка целостности hash-chain
```bash
# Integrity check audit log для app 'acme'
MCP_DATABASE_URL=postgres://... npm run audit:verify -- --app=acme
# С диапазоном дат
els-mcp verify-audit --app=acme --from=2026-05-01 --to=2026-05-17
```
Exit code `0` — цепочка целая; `1` — найден разрыв (с указанием
проблемной строки).
## Prisma setup
```bash
# Сгенерировать клиент (output → node_modules/.prisma/mcp)
npm run prisma:generate
# Применить миграцию (создаёт schemas + partitioned audit table)
psql $MCP_DATABASE_URL -f prisma/migrations/init/migration.sql
```
## Cache (Redis)
Lookup-aside кэш для read-heavy эндпоинтов. TTL — по классам (см.
`src/cache/policies.ts`):
| Class | TTL | Tool(s) |
|---|---|---|
| `log_details` | 1h | `get_log_details` |
| `top_messages` | 2m | `top_error_messages` |
| `histogram` | 1m | `error_histogram` |
| `heatmap` | 5m | `error_heatmap` |
| `traffic_long` | 5m | `traffic_stats` |
| `search_recent` | 15s | `search_logs` |
| `list_apps` | 30s | `list_apps` |
| `stats_breakdown` | 2m | `error_stats_breakdown` |
| `baseline` | 5m | `baseline_compare` |
| `version_timeline` | 5m | `version_regression` |
| `grouped_errors` | 2m | `grouped_errors` |
Все cache-keys обязательно tenant-prefixed:
`mcp:cache:{class}:{appSlug | k:keyPrefix}:{...}` — защита от
cross-tenant data leak.
**Graceful degradation**. Если Redis недоступен или
`MCP_CACHE_ENABLED=false` — все запросы прозрачно идут в ELS без ошибок.
Sub-25ms задержка коннекта/PING не блокирует старт процесса
(`lazyConnect: true`).
**Compression**. Значения > 10 KB автоматически gzip-сжимаются (префикс
`gz:`) и расшифровываются при чтении.
## Prometheus metrics
Endpoint: `GET /els/metrics`.
Ключевые метрики:
- `mcp_requests_total{tool,status,cached}`
- `mcp_request_duration_seconds{tool}` — histogram (buckets:
0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30)
- `mcp_errors_total{tool,code}`
- `mcp_cache_hits_total{tool_class}`, `mcp_cache_misses_total{tool_class}`,
`mcp_cache_hit_ratio{tool_class}`
- `mcp_els_upstream_errors_total{endpoint,status}`
- `mcp_sse_connections_active`
- `mcp_redaction_applied_total{field}`
- `mcp_billing_events_total{appSlug,tier}`
### Prometheus scrape config
```yaml
scrape_configs:
- job_name: els-mcp
scrape_interval: 15s
metrics_path: /els/metrics
static_configs:
- targets: ['mcp-1.internal:3030', 'mcp-2.internal:3030']
```
### Grafana dashboard (минимальный JSON)
```json
{
"title": "MCP — Overview",
"panels": [
{ "title": "RPS by tool",
"targets": [{ "expr": "sum by (tool) (rate(mcp_requests_total[1m]))" }] },
{ "title": "p95 latency",
"targets": [{ "expr": "histogram_quantile(0.95, sum by (tool, le) (rate(mcp_request_duration_seconds_bucket[5m])))" }] },
{ "title": "Cache hit ratio",
"targets": [{ "expr": "mcp_cache_hit_ratio" }] },
{ "title": "Upstream errors",
"targets": [{ "expr": "sum by (status) (rate(mcp_els_upstream_errors_total[5m]))" }] }
]
}
```
Полный SRE-dashboard + per-tool + per-tenant — `07-observability.md`.
## Логи (Loki shipper)
Логи pino → stderr (stdio-mode) или stdout (HTTP-mode) → Promtail → Loki.
```yaml
scrape_configs:
- job_name: els-mcp
static_configs:
- targets: [localhost]
labels:
job: els-mcp
service: els-mcp
__path__: /var/log/els-mcp/*.log
pipeline_stages:
- json:
expressions:
level: level
tool: tool
appSlug: appSlug
requestId: requestId
- labels:
level:
tool:
```
Чувствительные поля (`*.token`, `*.apiKey`, `Authorization` headers и
т. д.) автоматически замещаются на `<REDACTED>` в pino-логах
(см. `src/observability/logger.ts`).
## OpenTelemetry tracing
Опциональный — включается через `OTEL_EXPORTER_OTLP_ENDPOINT`. Если не
задан, SDK вообще не загружается (zero overhead).
Auto-instrumentation: HTTP, undici (ELS calls), ioredis, Express.
## Health endpoints
- `GET /els/healthz` — liveness (всегда 200 если процесс жив).
- `GET /els/readyz` — readiness: проверяет Redis ping + ELS upstream
reachability. Возвращает 503 если хотя бы одна зависимость не отвечает.
Готовые handler'ы — `src/http/routes/metrics.ts`.
## Публикация в npm
Релиз автоматизирован через GitLab CI:
1. Bump version в `package.json` (`0.3.x` → `0.3.(x+1)` для bug-fix,
`0.(x+1).0` для новых фич).
2. Commit изменений в `master`.
3. Создать tag формата `sdk/mcp/v<X.Y.Z>`:
```bash
git tag sdk/mcp/v0.3.1
git push origin master
git push origin sdk/mcp/v0.3.1
```
4. GitLab job `publish:mcp` (см. `.gitlab-ci.yml`) сработает по tag,
выполнит `npm version`, `npm run build`, `npm publish --access public`.
Требуется `NPM_TOKEN` в protected CI/CD-переменных GitLab.
## Limitations / TODO
- **DCR (Dynamic Client Registration).** Rate-limit middleware готов
(`src/http/middleware/dcrRateLimit.ts`), но `/oauth/register` endpoint
планируется в v2. Сейчас используется OIDC discovery без runtime
регистрации клиентов.
- **Mistral AI summary в `explain_error`.** Сейчас tool возвращает
контекст ошибки без AI-обёртки (`aiAvailable=false`); LLM-клиент сам
синтезирует объяснение из переданных данных. Планируется в следующих
релизах.
- **OIDC sub → apps resolver через LK API.** Эндпоинт
`GET /api/internal/users/{sub}/apps` ожидается на LK backend; до его
появления используется fallback на `MCP_OIDC_DEMO_APP_SLUG`.
- **Tier resolver через LK API.** Эндпоинт
`GET /api/internal/apps/{appSlug}/billing/tier` ожидается на LK backend;
до его появления используется `MCP_DEFAULT_TIER`.