UNPKG

@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
# Разработка `@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 subapps и appSlugtier (опц.) | | `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 (serverclient 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`.