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-запуска.

194 lines 7.48 kB
/** * LK API resolvers. * * Два резолвера дёргают LK backend и кэшируются в Redis (если доступен): * * 1. `resolveOidcSubApps(sub)` — `GET /api/internal/users/{sub}/apps` * возвращает список доступных пользователю appSlugs. * Кэш: `mcp:oidc:apps:{sub}` TTL 5 min. * * 2. `resolveAppTier(appSlug)` — `GET /api/internal/apps/{slug}/billing/tier` * возвращает один из `FREE`/`STANDARD`/`PREMIUM`/`UNLIMITED`. * Кэш: `mcp:tier:{appSlug}` TTL 30 sec. * * TODO (LK backend): оба эндпоинта **пока не реализованы** — резолверы * корректно отрабатывают 404/500/timeout, возвращая fallback значения. * Когда эндпоинты появятся, никакого изменения кода не потребуется. * * Все вызовы — best-effort: при недоступности LK сервис **не** падает, * а использует safety-net fallback (`MCP_OIDC_DEMO_APP_SLUG`, * `config.defaultTier`). */ const TIERS = new Set(['FREE', 'STANDARD', 'PREMIUM', 'UNLIMITED']); const APPS_CACHE_TTL_SEC = 300; // 5 min const TIER_CACHE_TTL_SEC = 30; const LK_REQUEST_TIMEOUT_MS = 2_000; export function createLkResolver(opts) { const fetchImpl = opts.fetchImpl ?? fetch; const log = opts.log; const baseUrl = opts.lkApiBaseUrl?.replace(/\/$/, '') ?? null; async function getCached(key) { if (!opts.redis) return null; try { return await opts.redis.get(key); } catch (err) { log?.debug?.({ err, key }, 'lkResolver: redis get failed (degraded)'); return null; } } async function setCached(key, value, ttlSec) { if (!opts.redis) return; try { await opts.redis.set(key, value, 'EX', ttlSec); } catch (err) { log?.debug?.({ err, key }, 'lkResolver: redis set failed (degraded)'); } } function authHeaders() { const headers = { Accept: 'application/json' }; if (opts.lkApiToken) headers.Authorization = `Bearer ${opts.lkApiToken}`; return headers; } async function doFetch(url) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), LK_REQUEST_TIMEOUT_MS); try { const res = await fetchImpl(url, { headers: authHeaders(), signal: ctrl.signal }); return res; } catch (err) { log?.debug?.({ err, url }, 'lkResolver: LK fetch failed (degraded)'); return null; } finally { clearTimeout(timer); } } return { async resolveOidcSubApps(sub) { const cacheKey = `mcp:oidc:apps:${sub}`; const cached = await getCached(cacheKey); if (cached) { try { const parsed = JSON.parse(cached); if (Array.isArray(parsed.apps) && parsed.apps.length > 0) { return { apps: parsed.apps, fromLk: parsed.fromLk === true }; } } catch { // Ignore — fall through to LK fetch. } } const fallback = { apps: opts.fallbackAppSlug ? [opts.fallbackAppSlug] : [], fromLk: false, }; if (!baseUrl) return fallback; const url = `${baseUrl}/api/internal/users/${encodeURIComponent(sub)}/apps`; const res = await doFetch(url); if (!res) return fallback; if (res.status === 404 || res.status >= 500) { log?.debug?.({ sub, status: res.status, url }, 'lkResolver: LK returned error status; using fallback'); return fallback; } if (res.status !== 200) { log?.debug?.({ sub, status: res.status }, 'lkResolver: unexpected LK status; using fallback'); return fallback; } let body = null; try { body = await res.json(); } catch (err) { log?.debug?.({ err, sub }, 'lkResolver: failed to parse LK response'); return fallback; } const apps = extractApps(body); if (apps.length === 0) { log?.debug?.({ sub }, 'lkResolver: LK returned empty app list; using fallback'); return fallback; } const result = { apps, fromLk: true }; await setCached(cacheKey, JSON.stringify(result), APPS_CACHE_TTL_SEC); return result; }, async resolveAppTier(appSlug) { const cacheKey = `mcp:tier:${appSlug}`; const cached = await getCached(cacheKey); if (cached) { try { const parsed = JSON.parse(cached); if (typeof parsed.tier === 'string' && TIERS.has(parsed.tier)) { return { tier: parsed.tier, fromLk: parsed.fromLk === true }; } } catch { // Ignore — fall through to LK fetch. } } const fallback = { tier: opts.fallbackTier, fromLk: false }; if (!baseUrl) return fallback; const url = `${baseUrl}/api/internal/apps/${encodeURIComponent(appSlug)}/billing/tier`; const res = await doFetch(url); if (!res) return fallback; if (res.status === 404 || res.status >= 500) { log?.debug?.({ appSlug, status: res.status, url }, 'lkResolver: LK tier endpoint returned error; using fallback'); return fallback; } if (res.status !== 200) return fallback; let body = null; try { body = await res.json(); } catch { return fallback; } const tier = extractTier(body); if (!tier) return fallback; const result = { tier, fromLk: true }; await setCached(cacheKey, JSON.stringify(result), TIER_CACHE_TTL_SEC); return result; }, }; } function extractApps(body) { if (!body || typeof body !== 'object') return []; const b = body; const raw = b.apps ?? b.appSlugs ?? b.data; if (!Array.isArray(raw)) return []; const apps = []; for (const item of raw) { if (typeof item === 'string' && item.length > 0) apps.push(item); else if (item && typeof item === 'object') { const slug = item.slug ?? item.appSlug; if (typeof slug === 'string' && slug.length > 0) apps.push(slug); } } return Array.from(new Set(apps)); } function extractTier(body) { if (!body || typeof body !== 'object') return null; const b = body; const raw = b.tier ?? b.name ?? b.plan; if (typeof raw !== 'string') return null; const upper = raw.toUpperCase(); return TIERS.has(upper) ? upper : null; } //# sourceMappingURL=lkResolver.js.map