From abb513ef7295be5580b496f510bbff1bf20554a8 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 21 Oct 2026 23:47:57 +0000 Subject: [PATCH] fix(mcp): carry x-ratelimit headers onto MCP responses The MCP handler returns its own Response, which drops c.header() set by the meter, so MCP callers never saw their remaining allowance. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/lib/api-meter.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/web/src/lib/api-meter.js b/apps/web/src/lib/api-meter.js index a72faa5..7963507 100644 --- a/apps/web/src/lib/api-meter.js +++ b/apps/web/src/lib/api-meter.js @@ -40,10 +40,19 @@ export async function apiMeter(c, next) { ? config.api.premiumPerHour : config.api.anonPerHour; const used = await q.bumpApiUsage(bucket); - c.header('x-ratelimit-limit', String(limit)); - c.header('x-ratelimit-remaining', String(Math.max(0, limit - used))); - c.header('x-plan', paid ? 'crawl-pass' : plan); - if (used <= limit) return next(); + const headers = { + 'x-ratelimit-limit': String(limit), + 'x-ratelimit-remaining': String(Math.max(0, limit - used)), + 'x-plan': paid ? 'crawl-pass' : plan, + }; + for (const [k, v] of Object.entries(headers)) c.header(k, v); + if (used <= limit) { + await next(); + // The MCP handler answers with its own Response, which drops c.header(). + for (const [k, v] of Object.entries(headers)) + if (!c.res.headers.has(k)) c.res.headers.set(k, v); + return; + } if (viaKey || paid) { c.header('retry-after', String(HOUR));