agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
123 lines • 6.03 kB
JavaScript
;
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.readPoolOptions = exports.MAX_SHARED_TOOLS = exports.MAX_DEFINITION_ENTRIES = exports.MCP_POOL_DEFAULTS = void 0;
const replay_js_1 = require("../replay.js");
/** The defaults, named so a test and a docstring cannot drift from the code. */
exports.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. */
exports.MAX_DEFINITION_ENTRIES = 256;
/** A list longer than this is used by its own client and never shared. */
exports.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.
*/
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], exports.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)
(0, replay_js_1.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,
};
}
exports.readPoolOptions = readPoolOptions;
/** 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