mcp --http: SSE keepalive for long tool calls; public App Store search shares one 20-a-minute budget

This commit is contained in:
CRTkafa 2026-10-29 19:05:53 +07:00
parent c500866295
commit 9620768897
6 changed files with 65 additions and 4 deletions

View file

@ -190,7 +190,19 @@ export function lintListing(store: 'apple' | 'play', fields: Record<string, unkn
interface ItunesApp { trackId?: number; trackName?: string; sellerName?: string; averageUserRating?: number; userRatingCount?: number; primaryGenreName?: string }
// Apple allows about 20 search calls a minute per IP: one shared budget for the whole server, whoever asks
const SEARCH_PER_MIN = 20;
const searchTimes: number[] = [];
export function searchBudget(now = Date.now()): { ok: true } | { ok: false; retryAfter: number } {
while (searchTimes.length && searchTimes[0]! <= now - 60_000) searchTimes.shift();
if (searchTimes.length >= SEARCH_PER_MIN) return { ok: false, retryAfter: Math.ceil((searchTimes[0]! + 60_000 - now) / 1000) };
searchTimes.push(now);
return { ok: true };
}
export async function searchApps(term: string, country: string, limit: number, base: string, f: typeof fetch): Promise<{ results: Record<string, unknown>[] }> {
const b = searchBudget();
if (!b.ok) throw new Error(`the App Store search is busy (Apple allows ${SEARCH_PER_MIN} calls a minute for this server): try again in ${b.retryAfter} s`);
const url = `${base}${base.includes('?') ? '&' : '?'}${new URLSearchParams({ term, country: country.toLowerCase(), media: 'software', entity: 'software', limit: String(limit) })}`;
const r = await f(url, { signal: AbortSignal.timeout(10_000), headers: { accept: 'application/json' } });
if (!r.ok) throw new Error(`the App Store search answered ${r.status}`);