@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-запуска.
59 lines • 1.99 kB
JavaScript
const DEFAULT_LIMIT = 10;
const DEFAULT_WINDOW_SEC = 3600;
const KEY_PREFIX = 'mcp:dcr:rl:';
export function createDcrRateLimit(opts = {}) {
const redis = opts.redis;
const limit = opts.limit ?? DEFAULT_LIMIT;
const windowSec = opts.windowSec ?? DEFAULT_WINDOW_SEC;
const log = opts.log;
return async function dcrRateLimit(req, res, next) {
const ip = pickIp(req);
if (!ip) {
// не определили IP — пропускаем
next();
return;
}
if (!redis || redis.unavailable) {
log?.warn?.({ ip }, 'dcrRateLimit: Redis unavailable, fail-open');
next();
return;
}
const key = `${KEY_PREFIX}${ip}`;
try {
const raw = redis.raw;
if (!raw) {
next();
return;
}
const current = await raw.incr(key);
if (current === 1) {
// первый INCR — выставляем TTL окна.
await raw.expire(key, windowSec);
}
if (current > limit) {
const ttl = await raw.ttl(key);
res.setHeader('Retry-After', String(Math.max(1, ttl)));
res.status(429).json({
error: 'rate_limited',
error_description: `DCR rate limit exceeded (${limit}/${windowSec}s)`,
});
return;
}
}
catch (err) {
// На ошибках Redis — fail-open.
log?.warn?.({ err: err.message, ip }, 'dcrRateLimit: Redis error, fail-open');
}
next();
};
}
function pickIp(req) {
const xff = req.headers['x-forwarded-for'];
if (typeof xff === 'string') {
const first = xff.split(',')[0]?.trim();
if (first)
return first;
}
return req.socket?.remoteAddress ?? '';
}
//# sourceMappingURL=dcrRateLimit.js.map