@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
JavaScript
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