UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

119 lines • 5.79 kB
/** * options — what `mcpClientPool` is built from, read and checked once. * * Pattern: one options type + one reader that refuses by name. * Role: The pool's budgets and clocks (`docs/design/2026-10-mcp-client-pool.md` * § 3.1). Every number is checked when the pool is built, never at the * first call. The pool takes the `mcpClient` options that shape one * connection and hands them to every client it opens; it never takes * `connection`, because it must build the transport itself — to end * the session, to hear the tools-changed notice, and to place the send * gate. */ import { replayDeclarationOf } from '../replay.js'; /** The defaults, named so a test and a docstring cannot drift from the code. */ export const MCP_POOL_DEFAULTS = Object.freeze({ maxClients: 64, maxClientsPerPrincipal: 8, maxEnding: 8, maxOperations: 256, initTimeoutMs: 30_000, idleMs: 600_000, maxLeaseMs: 900_000, definitionTtlMs: 600_000, }); /** At most this many shared definition entries are kept; the least recently used goes first. */ export const MAX_DEFINITION_ENTRIES = 256; /** A list longer than this is used by its own client and never shared. */ export const MAX_SHARED_TOOLS = 512; /** * Read `options` once. * * @throws TypeError naming the option, when one is missing, mistyped or out of * range, or when `connection` is given — here, when the pool is built. */ export function readPoolOptions(options) { if (options === null || typeof options !== 'object') { throw new TypeError('mcpClientPool: options must be an object with `name` and `transport`.'); } const { name } = options; if (typeof name !== 'string' || name.trim() === '') { throw new TypeError('mcpClientPool: `name` must be a non-empty string — the server name every tool and record carries.'); } const where = `mcpClientPool[${name}]`; if (options.connection !== undefined) { throw new TypeError(`${where}: \`connection\` is not taken. The pool builds every client's transport itself — ` + 'to end its session, to hear the tools-changed notice and to place the send gate. Pass ' + '`transport`, or a function of the identity that returns one.'); } const transport = transportOf(options.transport, where); const count = (key) => positiveInteger(options[key], MCP_POOL_DEFAULTS[key], key, where); const definitions = options.definitions ?? 'per-client'; if (definitions !== 'per-client' && typeof definitions?.share !== 'function') { throw new TypeError(`${where}: \`definitions\` must be 'per-client' or { share: (identity) => names | undefined }.`); } if (options.onEvent !== undefined && typeof options.onEvent !== 'function') { throw new TypeError(`${where}: \`onEvent\` must be a function.`); } if (options.now !== undefined && typeof options.now !== 'function') { throw new TypeError(`${where}: the clock option must be a function returning milliseconds.`); } // Refused by field now, as each client would refuse it at its first open. if (options.replay !== undefined) replayDeclarationOf(options.replay, where); return { name, transport, maxClients: count('maxClients'), maxClientsPerPrincipal: count('maxClientsPerPrincipal'), maxEnding: count('maxEnding'), maxOperations: count('maxOperations'), initTimeoutMs: count('initTimeoutMs'), idleMs: count('idleMs'), maxLeaseMs: count('maxLeaseMs'), definitionTtlMs: count('definitionTtlMs'), share: definitions === 'per-client' ? undefined : definitions.share, requirePrincipal: flag(options.requirePrincipal, true, 'requirePrincipal', where), requireTenant: flag(options.requireTenant, false, 'requireTenant', where), onEvent: options.onEvent, client: { ...(options.resultMode !== undefined && { resultMode: options.resultMode }), ...(options.clientInfo !== undefined && { clientInfo: options.clientInfo }), ...(options.retryOnThrottle !== undefined && { retryOnThrottle: options.retryOnThrottle }), ...(options.replay !== undefined && { replay: options.replay }), ...(options.sdk !== undefined && { sdk: options.sdk }), ...(options.jsonSchemaValidator !== undefined && { jsonSchemaValidator: options.jsonSchemaValidator, }), }, now: options.now ?? Date.now, open: options._open, }; } /** One transport for everyone, or a function of the identity — as a function either way. */ function transportOf(value, where) { if (typeof value === 'function') return value; if (value !== null && typeof value === 'object' && typeof value.transport === 'string') { return () => value; } throw new TypeError(`${where}: \`transport\` must be an MCP transport ({ transport: 'http' | 'gateway' | 'stdio', … }) ` + 'or a function of the identity that returns one.'); } /** A whole number ≥ 1, or the default when absent. */ function positiveInteger(value, fallback, key, where) { if (value === undefined) return fallback; if (typeof value === 'number' && Number.isInteger(value) && value >= 1) return value; throw new TypeError(`${where}: \`${key}\` must be a whole number ≥ 1.`); } /** A boolean, or the default when absent. */ function flag(value, fallback, key, where) { if (value === undefined) return fallback; if (typeof value === 'boolean') return value; throw new TypeError(`${where}: \`${key}\` must be true or false.`); } //# sourceMappingURL=options.js.map