UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

133 lines (132 loc) • 5.86 kB
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { snapshotMcpEntries } from "./locked-config.js"; import { classifyMcpFailure, livenessOf } from "./mcp-failure.js"; import { MCP_ENV_MS_MAX, materializeMcpTools, mcpStartupTimeoutMs } from "./mcp.js"; import { findNamespacePrefixCollision } from "./protocol-naming.js"; import { MCP_NAMESPACE } from "./protocol-table.js"; import { mcpManifestEntries } from "./runner/prepare-wiring-manifest.js"; const DECLINE_EVERY_ELICITATION = async () => ({ action: "decline" }); function wholeInRange(name, value, min, max) { if (value === undefined) return undefined; if (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max) return value; const shown = typeof value === "number" ? String(value) : `a ${typeof value}`; const e = new Error(`probeMcpServers: \`${name}\` must be a whole number in [${min}, ${max}] — received ${shown}`); e.code = "config.mcp_probe_option"; throw e; } function abortCause(signal) { const reason = signal.reason; if (reason instanceof Error) return reason; const e = new Error("the MCP probe was aborted by its caller's signal"); e.name = "AbortError"; return e; } function whenAborted(signal, until) { if (signal.aborted) return Promise.resolve(); return new Promise((resolve) => { signal.addEventListener("abort", () => resolve(), { once: true, signal: until }); }); } function afterMs(ms, until) { return new Promise((resolve) => { const timer = setTimeout(resolve, ms); timer.unref(); until.addEventListener("abort", () => clearTimeout(timer), { once: true }); }); } async function raceDial(dial, ms, signal) { if (ms === undefined && signal === undefined) return { kind: "settled", handle: await dial }; const until = new AbortController(); const legs = [dial.then((handle) => ({ kind: "settled", handle }))]; if (ms !== undefined) legs.push(afterMs(ms, until.signal).then(() => ({ kind: "timeout", afterMs: ms }))); if (signal !== undefined) legs.push(whenAborted(signal, until.signal).then(() => ({ kind: "aborted", signal }))); try { return await Promise.race(legs); } finally { until.abort(); } } function stoppedWaitingStatus(entry, ms) { const condition = new McpError(ErrorCode.RequestTimeout, `the probe stopped waiting for this server's connect and initial tool listing after ${ms} ms`); const { kind, delivered } = classifyMcpFailure(condition, { phase: "connect", transport: entry.transport.kind }); const liveness = livenessOf(kind, Date.now()); return { name: entry.name, status: "failed", errorCode: kind, delivered, error: condition.message, ...(liveness !== undefined ? { liveness } : {}) }; } export async function probeMcpServers(specs, opts = {}) { const declared = snapshotMcpEntries(specs); const timeoutMs = wholeInRange("timeoutMs", opts.timeoutMs, 1, MCP_ENV_MS_MAX) ?? mcpStartupTimeoutMs(); const concurrency = wholeInRange("concurrency", opts.concurrency, 1, Number.MAX_SAFE_INTEGER) ?? 1; const onElicit = opts.onElicit ?? DECLINE_EVERY_ELICITATION; const signal = opts.signal; if (findNamespacePrefixCollision(MCP_NAMESPACE, declared.map((e) => e.name)) !== undefined) { await materializeMcpTools(declared, opts.principal, onElicit); } const statuses = []; const servers = new Array(declared.length); const teardowns = []; let fatal; const probeOne = async (i) => { const entry = declared[i]; const started = performance.now(); const elapsed = () => Math.round(performance.now() - started); const dial = materializeMcpTools([entry], opts.principal, onElicit); const verdict = await raceDial(dial, timeoutMs, signal); if (verdict.kind === "settled") { const found = verdict.handle.statuses.find((s) => s.name === entry.name); const status = found === undefined ? undefined : { ...found }; teardowns.push(verdict.handle.dispose()); if (status !== undefined) statuses.push(status); servers[i] = { name: entry.name, connectMs: elapsed(), tools: status?.toolNames ?? [], ...(status?.errorCode !== undefined ? { errorCode: status.errorCode } : {}) }; return; } teardowns.push(dial.then((h) => h.dispose(), () => undefined)); if (verdict.kind === "aborted") { fatal ??= { cause: abortCause(verdict.signal) }; return; } const status = stoppedWaitingStatus(entry, verdict.afterMs); statuses.push(status); servers[i] = { name: entry.name, connectMs: elapsed(), tools: [], errorCode: status.errorCode }; }; if (signal?.aborted === true) fatal = { cause: abortCause(signal) }; let next = 0; const lane = async () => { for (;;) { if (signal?.aborted === true) fatal ??= { cause: abortCause(signal) }; if (fatal !== undefined) return; const i = next++; if (i >= declared.length) return; try { await probeOne(i); } catch (e) { fatal ??= { cause: e }; return; } } }; try { await Promise.all(Array.from({ length: Math.min(concurrency, declared.length) }, lane)); } finally { await Promise.allSettled(teardowns); } if (fatal === undefined && signal?.aborted === true) fatal = { cause: abortCause(signal) }; if (fatal !== undefined) throw fatal.cause; return { entries: mcpManifestEntries(declared, statuses), servers }; }