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

143 lines 5.8 kB
import express, { Router } from 'express'; import helmet from 'helmet'; import cors from 'cors'; import { HttpTransportManager } from '../transports/http.js'; import { createJwksResolver } from './jwks.js'; import { createAuthMiddleware } from './middleware/auth.js'; import { requestId } from './middleware/requestId.js'; import { errorHandler } from './middleware/errorHandler.js'; import { createOriginGuard } from './middleware/originGuard.js'; import { createHealthRouter } from './routes/health.js'; import { createWellKnownRouter } from './routes/wellKnown.js'; import { elsConfigSchemaHandler } from './routes/schema.js'; import { ALL_TOOL_NAMES } from '../tools/index.js'; import { getMetricsContentType, getMetricsText, } from '../observability/metrics.js'; export function createHttpApp(opts) { const { config, log } = opts; const app = express(); app.disable('x-powered-by'); app.set('trust proxy', true); // helmet — безопасные defaults. CSP в API-сервисе не нужен. app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, })); // CORS: allow известные origins + dev-friendly localhost-*. app.use(cors(buildCorsOptions(config.corsOrigins))); // request-id middleware с child logger. app.use(requestId({ logger: log })); // JSON body parser. MCP messages могут быть batch'ами; лимит 4 MB // (типичные tool responses ELS ≤ 1 MB). app.use(express.json({ limit: '4mb' })); // ─── /els router ────────────────────────────────────────────────────── const router = Router(); // Public — health (health-handlers с реальными probes: redis + ELS). router.use('/', createHealthRouter({ ...(opts.probeElsClient ? { elsClient: opts.probeElsClient } : {}), ...(opts.redis ? { redis: opts.redis } : {}), log, })); // Public — Prometheus metrics endpoint. if (config.metricsEnabled) { router.get('/metrics', async (_req, res) => { try { const body = await getMetricsText(); res.status(200); res.setHeader('content-type', getMetricsContentType()); res.send(body); } catch (err) { res.status(500).type('text/plain').send(`# metrics error: ${err.message}\n`); } }); } else { router.get('/metrics', (_req, res) => { res.status(404).json({ error: 'metrics_disabled' }); }); } // Public — well-known router.use('/.well-known', createWellKnownRouter({ config, toolNames: ALL_TOOL_NAMES, })); // Public — JSON Schema for els.config.json (auto-discovery). router.get('/schema/els.config.schema.json', elsConfigSchemaHandler()); // Protected — MCP endpoint const jwks = opts.jwksResolver ?? createJwksResolver(config); const manager = opts.transportManager ?? new HttpTransportManager({ config, log, ...(opts.redis ? { redis: opts.redis } : {}), ...(opts.middlewareDeps ? { middlewareDeps: opts.middlewareDeps } : {}), }); // originGuard — защита от browser-clients с unknown origin. const originGuard = createOriginGuard({ allowed: config.corsOrigins, log, }); const authMiddleware = createAuthMiddleware({ config, jwks, log, ...(opts.lkResolver !== undefined ? { lkResolver: opts.lkResolver } : {}), }); router.post('/mcp', originGuard, authMiddleware, async (req, res) => { await manager.handleRequest(req, res); }); // GET /mcp — long-lived SSE (server → client notifications). Hand off to SDK. router.get('/mcp', originGuard, authMiddleware, async (req, res) => { await manager.handleRequest(req, res); }); // DELETE /mcp — explicit session termination. router.delete('/mcp', originGuard, authMiddleware, async (req, res) => { await manager.handleRequest(req, res); }); app.use('/els', router); app.use(errorHandler(log)); return { app, manager, probeClient: opts.probeElsClient }; } function buildCorsOptions(allowList) { const exact = new Set(); // Точные origin'ы из конфига for (const o of allowList) { if (o === 'http://localhost' || o === 'http://127.0.0.1') continue; exact.add(o); } return { origin(origin, cb) { if (!origin) { // not-a-browser request (curl, server-to-server) — пропускаем без CORS-проверки cb(null, true); return; } if (exact.has(origin)) { cb(null, true); return; } // dev: любой порт на localhost / 127.0.0.1 if (allowList.includes('http://localhost') && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) { cb(null, true); return; } cb(new Error(`CORS: origin "${origin}" not allowed`)); }, methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Authorization', 'Content-Type', 'Accept', 'Mcp-Session-Id', 'Last-Event-ID', 'MCP-Protocol-Version', 'X-Request-Id', ], exposedHeaders: ['Mcp-Session-Id', 'X-Request-Id'], credentials: false, maxAge: 600, }; } //# sourceMappingURL=app.js.map