import { type McpFailureKind } from "./mcp-failure.js";
import type { McpServerSpec, OnElicit } from "./mcp-server-spec.js";
import type { WiringManifestMcpEntry } from "./wiring-manifest.js";
/** One server's probe reading, beside its manifest row. `connectMs` is wall time this probe measured
 *  for THIS server's dial — the spawn/handshake, the initial `tools/list` walk and the intake, ending
 *  at whichever verdict the server reached (connected, failed, or the clock below) — never a number the
 *  server reported. `tools` are the namespaced (`mcp__<server>__<tool>`) names that WOULD mount, i.e.
 *  exactly `McpServerStatus.toolNames` after the schema gate and the per-server allowlist, so
 *  `tools.length` equals the row's `toolCount`; a server that did not connect carries `[]`.
 *  `errorCode` is the row's failure class, repeated here so a caller reading only this array need not
 *  join back. */
export interface McpProbeServerReading {
    name: string;
    connectMs: number;
    tools: string[];
    errorCode?: McpFailureKind;
}
/**
 * What {@link probeMcpServers} answers.
 *
 * @contract mcp.probe.index_aligned — `entries` and `servers` always have the LENGTH of the `specs`
 * the caller passed and are ordered by it: `entries[i]` and `servers[i]` both describe `specs[i]`, for
 * every i, on every resolving call. A server that failed, that timed out, or that was never dialed
 * keeps its slot (the row carries its `status`/`errorCode`, the reading its measured `connectMs`), so a
 * consumer pairs its own declaration list with either array by index and never by name matching.
 */
export interface McpProbeResult {
    /** The rows, verbatim as a prepared leg would publish them — same projection, same failure
     *  vocabulary, same `liveness` snapshot, same neutralized and bounded remote text. */
    entries: WiringManifestMcpEntry[];
    /** The probe-only half, index-aligned with `entries` (see the contract above). */
    servers: McpProbeServerReading[];
}
/** The probe's knobs. Every one of them is optional; a value outside its range is REFUSED, never
 *  folded to the default (a bound the caller asked for and did not get is a bound nobody is keeping). */
export interface McpProbeOptions {
    /** The end-user identity a per-task principal header carries, exactly as `TaskSpec.principal` does
     *  on a real leg — so a server that applies per-user rules answers the probe as it would answer the
     *  task. */
    principal?: string;
    /** An OVERALL ceiling on the VERDICT for each server, in whole milliseconds within `[1, MCP_ENV_MS_MAX]` (the
     *  ceiling every env-supplied MCP bound keeps, because a larger delay is silently rewritten to 1 ms). The
     *  caller's own choice, and it lives BESIDE the per-phase startup bounds the dial runs under — whichever
     *  arrives first decides. When it elapses, THAT server's row reads `failed` with `errorCode: "timeout"` and
     *  the walk moves on; nothing throws. It does NOT end the dial — nothing here can — so the transport stays
     *  open until the dial lands on its own; see the residue note on {@link probeMcpServers}.
     *
     *  Default: none. The probe has no clock of its own: it dials through the entrance a task does, so each
     *  startup phase (the handshake, then the initial listing) carries that server's own bound from the one
     *  reader ({@link import("./mcp.js").mcpStartupTimeoutMs}: its `startupTimeoutMs`, else env `MCP_TIMEOUT`, else
     *  the SDK's 60 000 ms) — exactly what a task allows it. */
    timeoutMs?: number;
    /** Abort the whole probe. The call rejects with the signal's own reason (a bare `abort()` gives an
     *  `AbortError`) AFTER every server it had opened is closed — see the residue contract on
     *  {@link probeMcpServers}. An already-aborted signal dials nothing. */
    signal?: AbortSignal;
    /** The elicitation seat, for a caller that has a human behind it. Default: decline everything. */
    onElicit?: OnElicit;
    /** Abort reading, stated once: the call rejects with the signal's own reason when that reason is an
     *  Error (a bare `abort()` supplies a DOMException named `AbortError`) and with this module's own
     *  `AbortError` otherwise — a face that throws a string is a face no `catch` can read. A condition
     *  already held when the abort lands wins: the FIRST thing that ended the walk is what it rejects
     *  with, so a materialization's refusal is never overwritten by the abort that followed it. */
    /** How many servers this walk PURSUES A VERDICT for at once, ≥ 1. Default 1 — one at a time, in
     *  declaration order. Raising it trades what isolation there is for wall time; the rows and the index
     *  alignment are unaffected.
     *
     *  Even at 1, `connectMs` is not a contention-free measurement: a settled dial's CLOSE is started and
     *  not waited for, so the next dial begins while the previous transport is still shutting down.
     *
     *  IT IS NOT A CEILING ON OPEN TRANSPORTS, and the difference is the whole point of `timeoutMs`: a
     *  dial the clock abandoned keeps running (nothing here can cancel it) while the walk moves on, so
     *  with a short `timeoutMs` against an unresponsive fleet the transports actually in flight can reach
     *  one per DECLARATION, not `concurrency`. A caller probing a large fleet through a tight clock is
     *  choosing that: the alternative — holding the slot until the abandoned dial lands — makes one stuck
     *  server the walk's pace, which is the thing the clock exists to prevent. */
    concurrency?: number;
}
/**
 * Connect to each declared MCP server, list its tools and close it again — no task, no run.
 *
 * The rows in `entries` are minted by the SAME projection a prepared leg uses, from the SAME
 * materialization, so they are the rows that leg would publish: `connected` with the mounted tool
 * count, `failed` with the connect-failure record as closed words (`errorCode` / `delivered` /
 * `httpStatus`) beside the neutralized, bounded remote text, `skipped` for a declaration this probe
 * produced no attempt record for. No word is added to the failure vocabulary
 * ({@link import("./mcp-failure.js").MCP_FAILURE_KINDS}) and none is spelled twice.
 *
 * NOTHING IS REGISTERED AND NOTHING IS MOUNTED: no task registry entry, no session, no checkpoint, no
 * store write, no `TaskEvent`. The materialized tools are dropped unread and never reach a harness, and
 * the result carries tool NAMES, so a caller holds nothing it could invoke.
 *
 * FAILURE IS PER SERVER, REFUSAL IS NOT. A server that cannot be dialed, that answers a status, or that
 * outlives `timeoutMs` takes its verdict in its own row and the walk continues. What DOES reject is the
 * declaration set the materialization itself refuses — two names that mount under one namespace prefix
 * are refused before any transport is touched, by that same function, so the probe answers a colliding
 * configuration exactly as a task would rather than showing two healthy-looking rows for a pair the
 * engine would never mount.
 *
 * @contract mcp.probe.zero_residue_bound — this call does not settle while a handle it opened is
 * undisposed: every dial's handle is disposed, a dial abandoned by the clock is disposed WHEN IT LANDS,
 * and all of them are awaited before the call resolves OR rejects. What that promises is that no
 * connection is left owned by nobody — NOT that every child process has already exited at the instant of
 * return, and NOT that the call itself is bounded. What bounds it, stated part by part rather than
 * summarised into a number that would be wrong:
 *  · a dial has two REQUEST-BUDGET phases, each carrying the startup-phase bound ({@link
 *    import("./mcp.js").mcpStartupTimeoutMs}: the server's own `startupTimeoutMs`, else env `MCP_TIMEOUT`, else the SDK's
 *    60 000 ms default): the handshake, one
 *    request; and the initial tool listing, a PAGINATED WALK whose whole budget is that same number
 *    (however many pages it takes — and no request at all when the peer advertises no tools). Two
 *    budgets, so twice that, not once;
 *  · the transport's own close ladder then reaps a stdio child that does not exit when its input closes:
 *    at most 2 000 ms to SIGTERM, 2 000 ms more to SIGKILL;
 *  · and ONE part is bounded by nothing here: a transport SEND carries no request deadline of its own
 *    (the post-handshake `notifications/initialized`, an HTTP body read), so a peer that answers the
 *    handshake and then stalls that send holds the dial — and therefore this call — open. The bound
 *    belongs to the dial, which owns no cancellation; see `docs/KNOWN-LIMITS.md`.
 * So, PER DIAL and against a peer that either answers or fails its requests: that dial's residue is gone
 * within `max(timeoutMs, the handshake budget + the listing budget) + 4 000 ms`, and a server that exits
 * on its closed input (every well-behaved one) is gone at return. THE CALL is the walk over the
 * declarations, so its own duration is those per-dial times summed over `ceil(n / concurrency)` rounds —
 * this is not a whole-call number, and a caller budgeting a health check must do that arithmetic itself.
 * Against a peer that stalls a send, neither the dial nor this call has a deadline at all.
 */
export declare function probeMcpServers(specs: readonly McpServerSpec[], opts?: McpProbeOptions): Promise<McpProbeResult>;
