UNPKG

agentfootprint

Version:

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

155 lines • 7.05 kB
"use strict"; /** * mcpClientPool — a client per conversation, for one MCP server. * * const orders = mcpClientPool({ * name: 'orders', * transport: () => * agentCoreRuntimeMcpTransport({ agentRuntimeArn, region, session: 'reuse', client: aws }), * }); * const agent = Agent.create({ provider, model }).toolProvider(orders.toolProvider()).build(); * await agent.run({ message, identity: { tenant, principal, conversationId } }); * * Pattern: Facade over one state machine (`pool.ts`), its accounting (`work.ts`), * its definitions (`definitions.ts`) and its bound tools (`boundTool.ts`). * Role: `docs/design/2026-10-mcp-client-pool.md`. A `'reuse'` session costs a * cold start once per conversation instead of once per call — but only * if each conversation has a client of its own, opened once, bounded, * never closed while in use, ended remotely and said to be. This is * the one place that does all of it: * * - one client per authenticated conversation, keyed by the library's * identity encoding of the identity the CALLER named; * - one open per key, under the pool's own signal and `initTimeoutMs`; * - three budgets — open clients, endings, outstanding operations — each * charged until the work under it has SETTLED, and `PoolFullError` at once * when one is full: there is no queue; * - tool definitions per client incarnation by default, shared only under the * app's declared contract, every tool re-checked after each wait and at the * send gate; * - every ending through `terminate()`, with the remote outcome reported, and * `close()` still only local. * * What it does not do: repeat a call, share anything across processes, bound a * remote session it could not end, or make a server enforce per-user * permissions. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.mcpClientPool = void 0; const mcpClientUse_js_1 = require("../../tools/mcpClientUse.js"); const toolSessions_js_1 = require("../../tools/toolSessions.js"); const boundTool_js_1 = require("./boundTool.js"); const identityKey_js_1 = require("./identityKey.js"); const lease_js_1 = require("./lease.js"); const listTools_js_1 = require("./listTools.js"); const options_js_1 = require("./options.js"); const pool_js_1 = require("./pool.js"); const record_js_1 = require("./record.js"); /** * Build a pool for one MCP server. * * @throws TypeError when an option is missing, mistyped or out of range, or when * `connection` is given — here, when the pool is built */ function mcpClientPool(options) { const settings = (0, options_js_1.readPoolOptions)(options); const pool = new pool_js_1.Pool(settings); const rules = { requirePrincipal: settings.requirePrincipal, requireTenant: settings.requireTenant, }; let closing; const provider = { id: `mcp-pool:${settings.name}`, list: async (ctx) => { const facts = new record_js_1.UseFacts(settings.name, 'list'); try { const key = (0, identityKey_js_1.poolKeyOf)(ctx.callerIdentity, rules); if (typeof key === 'string') throw pool.refused(key); return await (0, listTools_js_1.listTools)(pool, key, ctx.signal, facts); } catch (error) { (0, boundTool_js_1.noteRefusal)(facts, error); throw error; } finally { // Filed against the context the tools slot passed; the slot emits it. (0, mcpClientUse_js_1.fileListUse)(ctx, facts.toUse()); } }, }; return { name: settings.name, toolProvider: () => provider, async acquire(identity, acquireOptions) { const key = (0, identityKey_js_1.poolKeyOf)(identity, rules); if (typeof key === 'string') throw pool.refused(key); pool.interact(); const operation = pool.admit('acquire', 'open', acquireOptions?.signal); try { const facts = new record_js_1.UseFacts(settings.name, 'list'); for (let round = 0; round < 3; round += 1) { const client = await pool.leaseClient(key, operation, facts); // After that wait, before anything is held: the pool may have closed, // the caller may have gone, the client may have been named by a // terminate. Refused, the operation's own lease ends with it below. if (pool.closed) throw pool.refused('closed'); operation.check(); if (client.serving) return (0, lease_js_1.leaseHandle)(pool, key, pool.holdLease(client)); } throw pool.refused('init-failed'); } finally { operation.finish(); } }, async terminate(scope) { assertScope(scope, settings.name); pool.interact(); return pool.terminate((0, identityKey_js_1.scopeMatcher)(scope), 'terminated', toolSessions_js_1.TOOL_TEARDOWN_TIMEOUT_MS); }, close() { closing ??= pool.closeAll(toolSessions_js_1.TOOL_TEARDOWN_TIMEOUT_MS); return closing; }, settled: () => pool.settled(), permissionsChanged(scope) { assertFields(scope, settings.name, 'permissionsChanged'); pool.permissionsChanged(scope); }, toolsChanged: () => pool.toolsChanged(), sweep: () => pool.interact(), stats: () => pool.stats(), }; } exports.mcpClientPool = mcpClientPool; /** * A `terminate` scope names a principal or a conversation: ending every * conversation of a whole tenant is `close()`'s job, and a mistyped scope must * not do it. */ function assertScope(scope, name) { assertFields(scope, name, 'terminate'); const named = (value) => value !== undefined && value !== ''; if (!named(scope.principal) && !named(scope.conversationId)) { throw new TypeError(`mcpClientPool[${name}].terminate: the scope must name a principal or a conversation ` + '({ tenant, principal } or { tenant, principal, conversationId }).'); } } /** Every field present is a string. Never quoted: a scope is an identity. */ function assertFields(scope, name, door) { if (scope === null || typeof scope !== 'object') { throw new TypeError(`mcpClientPool[${name}].${door}: the scope must be an object.`); } for (const field of ['tenant', 'principal', 'conversationId']) { const value = scope[field]; if (value !== undefined && typeof value !== 'string') { throw new TypeError(`mcpClientPool[${name}].${door}: \`${field}\` must be a string when present.`); } } } //# sourceMappingURL=mcpClientPool.js.map