mcp --http: SSE keepalive for long tool calls; public App Store search shares one 20-a-minute budget
This commit is contained in:
parent
c500866295
commit
9620768897
6 changed files with 65 additions and 4 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue