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

@ -37,6 +37,9 @@ import { runCommand } from './lib/runner';
export const DEFAULT_PORT = 7787;
export const PUBLIC_PORT = 7788;
const MAX_BODY = 4 * 1024 * 1024;
/** SSE comment line interval while a tool call runs (proxies cut silent responses). */
export let SSE_KEEPALIVE_MS = 20_000;
export const setKeepaliveMs = (ms: number): void => { SSE_KEEPALIVE_MS = ms; };
/** Public mode: one JSON-RPC message of a read-only tool never needs more. */
export const PUBLIC_MAX_BODY = 64 * 1024;
export const PUBLIC_DEFAULT_RATE = 60;
@ -213,10 +216,22 @@ export function startHttp(o: HttpOptions): Promise<{ close: () => void; port: nu
let msg: unknown;
try { msg = JSON.parse(body); } catch { return send(res, 400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); }
if (Array.isArray(msg) || typeof msg !== 'object' || msg === null) return send(res, 400, { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'One JSON-RPC message per request' } });
try {
const r = pub ? await handle(msg as never, noRun, pub.surface) : await answer(msg as Record<string, unknown>, o);
return r ? send(res, 200, r) : send(res, 202);
} catch (e) { return send(res, 500, { jsonrpc: '2.0', id: (msg as { id?: unknown }).id ?? null, error: { code: -32603, message: String((e as Error).message) } }); }
const work = (async () => {
try { return pub ? await handle(msg as never, noRun, pub.surface) : await answer(msg as Record<string, unknown>, o); }
catch (e) { return { jsonrpc: '2.0', id: (msg as { id?: unknown }).id ?? null, error: { code: -32603, message: String((e as Error).message) } }; }
})();
// A tool call may run for minutes; a proxy (Cloudflare: 100 s) cuts a response that sends nothing. A client that
// accepts SSE gets the answer as one SSE event with a comment line every 20 s until then.
if ((msg as { method?: unknown }).method === 'tools/call' && /text\/event-stream/.test(String(req.headers.accept ?? ''))) {
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store', 'x-accel-buffering': 'no' });
res.write(': started\n\n');
const beat = setInterval(() => res.write(': keepalive\n\n'), SSE_KEEPALIVE_MS);
const r = await work.finally(() => clearInterval(beat));
res.end(`event: message\ndata: ${JSON.stringify(r)}\n\n`);
return;
}
const r = await work;
return r ? send(res, 200, r) : send(res, 202);
});
// a tool call can run for many minutes (check on a big project, uploads): Node's 300 s request timeout would cut it
server.requestTimeout = 0;