agentfootprint 12.0.1 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/AGENTS.md +1 -1
  2. package/CAPABILITIES.md +3 -0
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/mcp/agentcoreRuntime/invoke.js +113 -7
  5. package/dist/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
  6. package/dist/adapters/mcp/agentcoreRuntime/session.js +21 -21
  7. package/dist/adapters/mcp/agentcoreRuntime/session.js.map +1 -1
  8. package/dist/adapters/mcp/agentcoreRuntime/transport.js +79 -5
  9. package/dist/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
  10. package/dist/core/Agent.js +6 -0
  11. package/dist/core/Agent.js.map +1 -1
  12. package/dist/core/agent/stages/toolCalls.js +4 -0
  13. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  14. package/dist/core/slots/buildToolsSlot.js +8 -0
  15. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  16. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.d.ts +23 -4
  17. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js +112 -6
  18. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
  19. package/dist/esm/adapters/mcp/agentcoreRuntime/session.d.ts +20 -6
  20. package/dist/esm/adapters/mcp/agentcoreRuntime/session.js +21 -21
  21. package/dist/esm/adapters/mcp/agentcoreRuntime/session.js.map +1 -1
  22. package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js +81 -7
  23. package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
  24. package/dist/esm/core/Agent.js +6 -0
  25. package/dist/esm/core/Agent.js.map +1 -1
  26. package/dist/esm/core/agent/stages/toolCalls.js +4 -0
  27. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  28. package/dist/esm/core/slots/buildToolsSlot.d.ts +12 -0
  29. package/dist/esm/core/slots/buildToolsSlot.js +8 -0
  30. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  31. package/dist/esm/events/payloads.d.ts +41 -0
  32. package/dist/esm/events/registry.d.ts +3 -1
  33. package/dist/esm/events/registry.js +2 -0
  34. package/dist/esm/events/registry.js.map +1 -1
  35. package/dist/esm/identity/types.d.ts +7 -3
  36. package/dist/esm/identity/types.js.map +1 -1
  37. package/dist/esm/index.d.ts +1 -1
  38. package/dist/esm/index.js +4 -1
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/lib/boundedJson.d.ts +14 -7
  41. package/dist/esm/lib/boundedJson.js +14 -7
  42. package/dist/esm/lib/boundedJson.js.map +1 -1
  43. package/dist/esm/mcp/callChecks.d.ts +160 -0
  44. package/dist/esm/mcp/callChecks.js +237 -0
  45. package/dist/esm/mcp/callChecks.js.map +1 -0
  46. package/dist/esm/mcp/callLifetime.js +5 -2
  47. package/dist/esm/mcp/callLifetime.js.map +1 -1
  48. package/dist/esm/mcp/clientPort.d.ts +78 -0
  49. package/dist/esm/mcp/clientPort.js +33 -0
  50. package/dist/esm/mcp/clientPort.js.map +1 -0
  51. package/dist/esm/mcp/gatewayTransport.js +13 -3
  52. package/dist/esm/mcp/gatewayTransport.js.map +1 -1
  53. package/dist/esm/mcp/index.d.ts +11 -1
  54. package/dist/esm/mcp/index.js +15 -0
  55. package/dist/esm/mcp/index.js.map +1 -1
  56. package/dist/esm/mcp/listingGenerations.d.ts +54 -0
  57. package/dist/esm/mcp/listingGenerations.js +75 -0
  58. package/dist/esm/mcp/listingGenerations.js.map +1 -0
  59. package/dist/esm/mcp/mcpClient.d.ts +18 -1
  60. package/dist/esm/mcp/mcpClient.js +302 -57
  61. package/dist/esm/mcp/mcpClient.js.map +1 -1
  62. package/dist/esm/mcp/mockMcpClient.js +13 -0
  63. package/dist/esm/mcp/mockMcpClient.js.map +1 -1
  64. package/dist/esm/mcp/pool/boundTool.d.ts +58 -0
  65. package/dist/esm/mcp/pool/boundTool.js +218 -0
  66. package/dist/esm/mcp/pool/boundTool.js.map +1 -0
  67. package/dist/esm/mcp/pool/clientSet.d.ts +73 -0
  68. package/dist/esm/mcp/pool/clientSet.js +152 -0
  69. package/dist/esm/mcp/pool/clientSet.js.map +1 -0
  70. package/dist/esm/mcp/pool/definitions.d.ts +103 -0
  71. package/dist/esm/mcp/pool/definitions.js +168 -0
  72. package/dist/esm/mcp/pool/definitions.js.map +1 -0
  73. package/dist/esm/mcp/pool/errors.d.ts +76 -0
  74. package/dist/esm/mcp/pool/errors.js +91 -0
  75. package/dist/esm/mcp/pool/errors.js.map +1 -0
  76. package/dist/esm/mcp/pool/identityKey.d.ts +57 -0
  77. package/dist/esm/mcp/pool/identityKey.js +94 -0
  78. package/dist/esm/mcp/pool/identityKey.js.map +1 -0
  79. package/dist/esm/mcp/pool/lease.d.ts +36 -0
  80. package/dist/esm/mcp/pool/lease.js +51 -0
  81. package/dist/esm/mcp/pool/lease.js.map +1 -0
  82. package/dist/esm/mcp/pool/listTools.d.ts +33 -0
  83. package/dist/esm/mcp/pool/listTools.js +146 -0
  84. package/dist/esm/mcp/pool/listTools.js.map +1 -0
  85. package/dist/esm/mcp/pool/mcpClientPool.d.ts +121 -0
  86. package/dist/esm/mcp/pool/mcpClientPool.js +151 -0
  87. package/dist/esm/mcp/pool/mcpClientPool.js.map +1 -0
  88. package/dist/esm/mcp/pool/options.d.ts +183 -0
  89. package/dist/esm/mcp/pool/options.js +119 -0
  90. package/dist/esm/mcp/pool/options.js.map +1 -0
  91. package/dist/esm/mcp/pool/plan.d.ts +77 -0
  92. package/dist/esm/mcp/pool/plan.js +134 -0
  93. package/dist/esm/mcp/pool/plan.js.map +1 -0
  94. package/dist/esm/mcp/pool/pool.d.ts +307 -0
  95. package/dist/esm/mcp/pool/pool.js +1071 -0
  96. package/dist/esm/mcp/pool/pool.js.map +1 -0
  97. package/dist/esm/mcp/pool/pooledClient.d.ts +93 -0
  98. package/dist/esm/mcp/pool/pooledClient.js +90 -0
  99. package/dist/esm/mcp/pool/pooledClient.js.map +1 -0
  100. package/dist/esm/mcp/pool/record.d.ts +80 -0
  101. package/dist/esm/mcp/pool/record.js +75 -0
  102. package/dist/esm/mcp/pool/record.js.map +1 -0
  103. package/dist/esm/mcp/pool/sessionEnded.d.ts +24 -0
  104. package/dist/esm/mcp/pool/sessionEnded.js +31 -0
  105. package/dist/esm/mcp/pool/sessionEnded.js.map +1 -0
  106. package/dist/esm/mcp/pool/work.d.ts +116 -0
  107. package/dist/esm/mcp/pool/work.js +213 -0
  108. package/dist/esm/mcp/pool/work.js.map +1 -0
  109. package/dist/esm/mcp/requestGuard.d.ts +70 -0
  110. package/dist/esm/mcp/requestGuard.js +98 -0
  111. package/dist/esm/mcp/requestGuard.js.map +1 -0
  112. package/dist/esm/mcp/requestLedger.d.ts +35 -1
  113. package/dist/esm/mcp/requestLedger.js +76 -3
  114. package/dist/esm/mcp/requestLedger.js.map +1 -1
  115. package/dist/esm/mcp/seriesEvidence.d.ts +10 -6
  116. package/dist/esm/mcp/seriesEvidence.js +16 -7
  117. package/dist/esm/mcp/seriesEvidence.js.map +1 -1
  118. package/dist/esm/mcp/terminate.d.ts +94 -0
  119. package/dist/esm/mcp/terminate.js +184 -0
  120. package/dist/esm/mcp/terminate.js.map +1 -0
  121. package/dist/esm/mcp/throttleRetry.js +71 -7
  122. package/dist/esm/mcp/throttleRetry.js.map +1 -1
  123. package/dist/esm/mcp/toolList.d.ts +90 -0
  124. package/dist/esm/mcp/toolList.js +123 -0
  125. package/dist/esm/mcp/toolList.js.map +1 -0
  126. package/dist/esm/mcp/types.d.ts +197 -3
  127. package/dist/esm/redaction/knownStrings.js +34 -0
  128. package/dist/esm/redaction/knownStrings.js.map +1 -1
  129. package/dist/esm/tool-providers/index.d.ts +2 -2
  130. package/dist/esm/tool-providers/index.js +15 -1
  131. package/dist/esm/tool-providers/index.js.map +1 -1
  132. package/dist/esm/tools/mcpClientUse.d.ts +59 -0
  133. package/dist/esm/tools/mcpClientUse.js +216 -0
  134. package/dist/esm/tools/mcpClientUse.js.map +1 -0
  135. package/dist/esm/tools/toolProvider.d.ts +16 -0
  136. package/dist/esm/tools/toolSessions.d.ts +41 -0
  137. package/dist/esm/tools/toolSessions.js +41 -1
  138. package/dist/esm/tools/toolSessions.js.map +1 -1
  139. package/dist/events/registry.js +2 -0
  140. package/dist/events/registry.js.map +1 -1
  141. package/dist/identity/types.js.map +1 -1
  142. package/dist/index.js +6 -3
  143. package/dist/index.js.map +1 -1
  144. package/dist/lib/boundedJson.js +14 -7
  145. package/dist/lib/boundedJson.js.map +1 -1
  146. package/dist/mcp/callChecks.js +247 -0
  147. package/dist/mcp/callChecks.js.map +1 -0
  148. package/dist/mcp/callLifetime.js +5 -2
  149. package/dist/mcp/callLifetime.js.map +1 -1
  150. package/dist/mcp/clientPort.js +38 -0
  151. package/dist/mcp/clientPort.js.map +1 -0
  152. package/dist/mcp/gatewayTransport.js +13 -3
  153. package/dist/mcp/gatewayTransport.js.map +1 -1
  154. package/dist/mcp/index.js +25 -1
  155. package/dist/mcp/index.js.map +1 -1
  156. package/dist/mcp/listingGenerations.js +80 -0
  157. package/dist/mcp/listingGenerations.js.map +1 -0
  158. package/dist/mcp/mcpClient.js +306 -59
  159. package/dist/mcp/mcpClient.js.map +1 -1
  160. package/dist/mcp/mockMcpClient.js +13 -0
  161. package/dist/mcp/mockMcpClient.js.map +1 -1
  162. package/dist/mcp/pool/boundTool.js +223 -0
  163. package/dist/mcp/pool/boundTool.js.map +1 -0
  164. package/dist/mcp/pool/clientSet.js +157 -0
  165. package/dist/mcp/pool/clientSet.js.map +1 -0
  166. package/dist/mcp/pool/definitions.js +174 -0
  167. package/dist/mcp/pool/definitions.js.map +1 -0
  168. package/dist/mcp/pool/errors.js +96 -0
  169. package/dist/mcp/pool/errors.js.map +1 -0
  170. package/dist/mcp/pool/identityKey.js +99 -0
  171. package/dist/mcp/pool/identityKey.js.map +1 -0
  172. package/dist/mcp/pool/lease.js +55 -0
  173. package/dist/mcp/pool/lease.js.map +1 -0
  174. package/dist/mcp/pool/listTools.js +150 -0
  175. package/dist/mcp/pool/listTools.js.map +1 -0
  176. package/dist/mcp/pool/mcpClientPool.js +155 -0
  177. package/dist/mcp/pool/mcpClientPool.js.map +1 -0
  178. package/dist/mcp/pool/options.js +123 -0
  179. package/dist/mcp/pool/options.js.map +1 -0
  180. package/dist/mcp/pool/plan.js +140 -0
  181. package/dist/mcp/pool/plan.js.map +1 -0
  182. package/dist/mcp/pool/pool.js +1075 -0
  183. package/dist/mcp/pool/pool.js.map +1 -0
  184. package/dist/mcp/pool/pooledClient.js +94 -0
  185. package/dist/mcp/pool/pooledClient.js.map +1 -0
  186. package/dist/mcp/pool/record.js +80 -0
  187. package/dist/mcp/pool/record.js.map +1 -0
  188. package/dist/mcp/pool/sessionEnded.js +35 -0
  189. package/dist/mcp/pool/sessionEnded.js.map +1 -0
  190. package/dist/mcp/pool/work.js +219 -0
  191. package/dist/mcp/pool/work.js.map +1 -0
  192. package/dist/mcp/requestGuard.js +106 -0
  193. package/dist/mcp/requestGuard.js.map +1 -0
  194. package/dist/mcp/requestLedger.js +76 -3
  195. package/dist/mcp/requestLedger.js.map +1 -1
  196. package/dist/mcp/seriesEvidence.js +16 -7
  197. package/dist/mcp/seriesEvidence.js.map +1 -1
  198. package/dist/mcp/terminate.js +193 -0
  199. package/dist/mcp/terminate.js.map +1 -0
  200. package/dist/mcp/throttleRetry.js +71 -7
  201. package/dist/mcp/throttleRetry.js.map +1 -1
  202. package/dist/mcp/toolList.js +128 -0
  203. package/dist/mcp/toolList.js.map +1 -0
  204. package/dist/redaction/knownStrings.js +34 -0
  205. package/dist/redaction/knownStrings.js.map +1 -1
  206. package/dist/tool-providers/index.js +20 -1
  207. package/dist/tool-providers/index.js.map +1 -1
  208. package/dist/tools/mcpClientUse.js +225 -0
  209. package/dist/tools/mcpClientUse.js.map +1 -0
  210. package/dist/tools/toolSessions.js +44 -1
  211. package/dist/tools/toolSessions.js.map +1 -1
  212. package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts +23 -4
  213. package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts.map +1 -1
  214. package/dist/types/adapters/mcp/agentcoreRuntime/session.d.ts +20 -6
  215. package/dist/types/adapters/mcp/agentcoreRuntime/session.d.ts.map +1 -1
  216. package/dist/types/adapters/mcp/agentcoreRuntime/transport.d.ts.map +1 -1
  217. package/dist/types/core/Agent.d.ts.map +1 -1
  218. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  219. package/dist/types/core/slots/buildToolsSlot.d.ts +12 -0
  220. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  221. package/dist/types/events/payloads.d.ts +41 -0
  222. package/dist/types/events/payloads.d.ts.map +1 -1
  223. package/dist/types/events/registry.d.ts +3 -1
  224. package/dist/types/events/registry.d.ts.map +1 -1
  225. package/dist/types/identity/types.d.ts +7 -3
  226. package/dist/types/identity/types.d.ts.map +1 -1
  227. package/dist/types/index.d.ts +1 -1
  228. package/dist/types/index.d.ts.map +1 -1
  229. package/dist/types/lib/boundedJson.d.ts +14 -7
  230. package/dist/types/lib/boundedJson.d.ts.map +1 -1
  231. package/dist/types/mcp/callChecks.d.ts +161 -0
  232. package/dist/types/mcp/callChecks.d.ts.map +1 -0
  233. package/dist/types/mcp/callLifetime.d.ts.map +1 -1
  234. package/dist/types/mcp/clientPort.d.ts +79 -0
  235. package/dist/types/mcp/clientPort.d.ts.map +1 -0
  236. package/dist/types/mcp/gatewayTransport.d.ts.map +1 -1
  237. package/dist/types/mcp/index.d.ts +11 -1
  238. package/dist/types/mcp/index.d.ts.map +1 -1
  239. package/dist/types/mcp/listingGenerations.d.ts +55 -0
  240. package/dist/types/mcp/listingGenerations.d.ts.map +1 -0
  241. package/dist/types/mcp/mcpClient.d.ts +18 -1
  242. package/dist/types/mcp/mcpClient.d.ts.map +1 -1
  243. package/dist/types/mcp/mockMcpClient.d.ts.map +1 -1
  244. package/dist/types/mcp/pool/boundTool.d.ts +59 -0
  245. package/dist/types/mcp/pool/boundTool.d.ts.map +1 -0
  246. package/dist/types/mcp/pool/clientSet.d.ts +74 -0
  247. package/dist/types/mcp/pool/clientSet.d.ts.map +1 -0
  248. package/dist/types/mcp/pool/definitions.d.ts +104 -0
  249. package/dist/types/mcp/pool/definitions.d.ts.map +1 -0
  250. package/dist/types/mcp/pool/errors.d.ts +77 -0
  251. package/dist/types/mcp/pool/errors.d.ts.map +1 -0
  252. package/dist/types/mcp/pool/identityKey.d.ts +58 -0
  253. package/dist/types/mcp/pool/identityKey.d.ts.map +1 -0
  254. package/dist/types/mcp/pool/lease.d.ts +37 -0
  255. package/dist/types/mcp/pool/lease.d.ts.map +1 -0
  256. package/dist/types/mcp/pool/listTools.d.ts +34 -0
  257. package/dist/types/mcp/pool/listTools.d.ts.map +1 -0
  258. package/dist/types/mcp/pool/mcpClientPool.d.ts +122 -0
  259. package/dist/types/mcp/pool/mcpClientPool.d.ts.map +1 -0
  260. package/dist/types/mcp/pool/options.d.ts +184 -0
  261. package/dist/types/mcp/pool/options.d.ts.map +1 -0
  262. package/dist/types/mcp/pool/plan.d.ts +78 -0
  263. package/dist/types/mcp/pool/plan.d.ts.map +1 -0
  264. package/dist/types/mcp/pool/pool.d.ts +308 -0
  265. package/dist/types/mcp/pool/pool.d.ts.map +1 -0
  266. package/dist/types/mcp/pool/pooledClient.d.ts +94 -0
  267. package/dist/types/mcp/pool/pooledClient.d.ts.map +1 -0
  268. package/dist/types/mcp/pool/record.d.ts +81 -0
  269. package/dist/types/mcp/pool/record.d.ts.map +1 -0
  270. package/dist/types/mcp/pool/sessionEnded.d.ts +25 -0
  271. package/dist/types/mcp/pool/sessionEnded.d.ts.map +1 -0
  272. package/dist/types/mcp/pool/work.d.ts +117 -0
  273. package/dist/types/mcp/pool/work.d.ts.map +1 -0
  274. package/dist/types/mcp/requestGuard.d.ts +71 -0
  275. package/dist/types/mcp/requestGuard.d.ts.map +1 -0
  276. package/dist/types/mcp/requestLedger.d.ts +35 -1
  277. package/dist/types/mcp/requestLedger.d.ts.map +1 -1
  278. package/dist/types/mcp/seriesEvidence.d.ts +10 -6
  279. package/dist/types/mcp/seriesEvidence.d.ts.map +1 -1
  280. package/dist/types/mcp/terminate.d.ts +95 -0
  281. package/dist/types/mcp/terminate.d.ts.map +1 -0
  282. package/dist/types/mcp/throttleRetry.d.ts.map +1 -1
  283. package/dist/types/mcp/toolList.d.ts +91 -0
  284. package/dist/types/mcp/toolList.d.ts.map +1 -0
  285. package/dist/types/mcp/types.d.ts +197 -3
  286. package/dist/types/mcp/types.d.ts.map +1 -1
  287. package/dist/types/redaction/knownStrings.d.ts.map +1 -1
  288. package/dist/types/tool-providers/index.d.ts +2 -2
  289. package/dist/types/tool-providers/index.d.ts.map +1 -1
  290. package/dist/types/tools/mcpClientUse.d.ts +60 -0
  291. package/dist/types/tools/mcpClientUse.d.ts.map +1 -0
  292. package/dist/types/tools/toolProvider.d.ts +16 -0
  293. package/dist/types/tools/toolProvider.d.ts.map +1 -1
  294. package/dist/types/tools/toolSessions.d.ts +41 -0
  295. package/dist/types/tools/toolSessions.d.ts.map +1 -1
  296. package/package.json +3 -3
@@ -0,0 +1,1075 @@
1
+ "use strict";
2
+ /**
3
+ * pool — the state behind `mcpClientPool`: who holds which client, what is
4
+ * charged, and what ends when.
5
+ *
6
+ * Pattern: one state machine, mutated only between awaits.
7
+ * Role: §§ 5–7 of the design. Every mutation of the sets and the budget runs
8
+ * synchronously — a check and the move it allows are never split by
9
+ * an await — so two callers cannot both take the last slot.
10
+ *
11
+ * - ADMISSION: every listing, call, acquire and ending is admitted against
12
+ * `maxOperations` before anything is sent. At each interaction the lazy
13
+ * sweep (revoked leases, idle ends, deferred ends) runs FIRST, so it
14
+ * reserves its endings before the new operation is admitted.
15
+ * - ONE OPEN PER KEY, from a plan resolved at that moment, under the pool's own
16
+ * signal and `initTimeoutMs`, never a caller's. A caller that gives up
17
+ * detaches alone; the operation that started the open keeps its charge until
18
+ * the open's work settles. Past the deadline every waiter is refused at once
19
+ * (`init-timeout`), and until the open's work settles the key cannot open
20
+ * again. A failure reaches every current waiter and is not cached.
21
+ * - THE HANDOFF: every operation waiting for an open holds a RESERVATION on
22
+ * the client until it takes its lease, so between the open settling and
23
+ * that lease the client is busy — nobody can evict, idle-end or end it. The
24
+ * client's state is checked again after every wait, and the lease taken in
25
+ * the same step: a lease is never handed out on a client that is ending.
26
+ * - MAKING ROOM: the principal's, then the pool's, least recently used IDLE
27
+ * client moves to the ending set — or `PoolFullError` at once.
28
+ * - ENDING reserves an ending slot and an operation slot FIRST; when either is
29
+ * full the client stays in the open set, counted, reported deferred, and is
30
+ * ended when slots free. Every end goes through `terminate()` — but a client
31
+ * whose handle this process lost: nothing can be sent for it, so its ending
32
+ * is its local close alone, charged the same way until it settles.
33
+ * - SETTLEMENT, never an abort, returns a slot (`work.ts`).
34
+ * - NOTHING UNCHECKED: on a pooled client every request carries a guard — its
35
+ * call's, or the client's open, listing or ending — and a request with none
36
+ * is refused before it is sent (`request-unmatched`), at the watched `send`
37
+ * and at the pool's own fetch alike.
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.Pool = void 0;
41
+ const abort_js_1 = require("../../lib/abort.js");
42
+ const toolSessions_js_1 = require("../../tools/toolSessions.js");
43
+ const clientPort_js_1 = require("../clientPort.js");
44
+ const mcpClient_js_1 = require("../mcpClient.js");
45
+ const requestGuard_js_1 = require("../requestGuard.js");
46
+ const clientSet_js_1 = require("./clientSet.js");
47
+ const definitions_js_1 = require("./definitions.js");
48
+ const errors_js_1 = require("./errors.js");
49
+ const encode_js_1 = require("../../memory/identity/encode.js");
50
+ const identityKey_js_1 = require("./identityKey.js");
51
+ const options_js_1 = require("./options.js");
52
+ const plan_js_1 = require("./plan.js");
53
+ const pooledClient_js_1 = require("./pooledClient.js");
54
+ const record_js_1 = require("./record.js");
55
+ const work_js_1 = require("./work.js");
56
+ class Pool {
57
+ settings;
58
+ clients = new clientSet_js_1.ClientSet();
59
+ budget;
60
+ shared = new definitions_js_1.SharedDefinitions();
61
+ reporter;
62
+ /** The pool's own signal: what every open, listing and call runs under; aborted at `close()`. */
63
+ lifetime = new AbortController();
64
+ leases = new Set();
65
+ settleWaiters = [];
66
+ /** Waiting for some client to become idle (`terminate`, `close`). */
67
+ idleWaiters = new Set();
68
+ closed = false;
69
+ unresolvedEndings = 0;
70
+ constructor(settings) {
71
+ this.settings = settings;
72
+ this.budget = new work_js_1.OperationBudget(settings.maxOperations);
73
+ this.reporter = new record_js_1.Reporter(settings.name, settings.onEvent);
74
+ this.budget.whenReleased((operation) => {
75
+ if (operation.settledLate) {
76
+ this.reporter.report({
77
+ kind: 'settled-late',
78
+ work: operation.phase,
79
+ durationMs: Math.max(0, this.now() - operation.admittedAt),
80
+ });
81
+ }
82
+ this.retryDeferred();
83
+ this.notifySettled();
84
+ });
85
+ }
86
+ now() {
87
+ return this.settings.now();
88
+ }
89
+ get signal() {
90
+ return this.lifetime.signal;
91
+ }
92
+ // ─── Admission ─────────────────────────────────────────────────────
93
+ /**
94
+ * The lazy sweep, run at the start of every interaction: leases past
95
+ * `maxLeaseMs` revoked, idle clients ended, deferred endings retried — all of
96
+ * it before a new operation is admitted.
97
+ */
98
+ interact() {
99
+ this.revokeExpiredLeases();
100
+ this.sweepIdle();
101
+ this.retryDeferred();
102
+ }
103
+ /**
104
+ * Admit one operation, or refuse at once: `closed`, or `PoolFullError`
105
+ * (`limit: 'operations'`) — before anything is sent.
106
+ */
107
+ admit(kind, phase, callerSignal) {
108
+ if (this.closed)
109
+ throw this.refused('closed');
110
+ const operation = this.budget.admit(kind, phase, [this.lifetime.signal, callerSignal], this.now());
111
+ if (operation === undefined)
112
+ throw this.full('operations');
113
+ return operation;
114
+ }
115
+ refused(reason, cause) {
116
+ return new errors_js_1.McpPoolRefusedError(reason, this.settings.name, cause === undefined ? {} : { cause });
117
+ }
118
+ full(limit) {
119
+ const max = limit === 'pool'
120
+ ? this.settings.maxClients
121
+ : limit === 'principal'
122
+ ? this.settings.maxClientsPerPrincipal
123
+ : limit === 'ending'
124
+ ? this.settings.maxEnding
125
+ : this.settings.maxOperations;
126
+ const busy = this.clients.openClients().filter((c) => c.busy).length;
127
+ return new errors_js_1.PoolFullError(this.settings.name, limit, max, busy, this.clients.endingCount, this.budget.count);
128
+ }
129
+ // ─── Clients and leases ────────────────────────────────────────────
130
+ /**
131
+ * A library lease on `key`'s client for `operation`, opening one when the key
132
+ * has none (inside the operation's slot). `plan` is the plan resolved for a
133
+ * new client; a key that already has one keeps its own. The lease is
134
+ * released when the operation SETTLES.
135
+ */
136
+ async leaseClient(key, operation, facts, plan) {
137
+ facts.keyHash = key.keyHash;
138
+ for (let round = 0; round < 3; round += 1) {
139
+ const current = this.clients.currentFor(key.key);
140
+ if (current === undefined) {
141
+ const opened = this.openFor(key, operation, facts, plan);
142
+ const leased = await this.leaseOnceOpen(opened, operation, facts);
143
+ if (leased !== undefined)
144
+ return leased;
145
+ continue;
146
+ }
147
+ if (current.abandoned)
148
+ throw this.refused('init-timeout');
149
+ if (current.state === 'open') {
150
+ if (facts.client === 'refused')
151
+ facts.client = 'reused';
152
+ return this.takeLease(current, operation, facts);
153
+ }
154
+ // Opening: join it, inside this operation's own slot.
155
+ facts.client = 'joined';
156
+ facts.clientId = current.clientId;
157
+ facts.generation = current.generation;
158
+ const leased = await this.leaseOnceOpen(current, operation, facts);
159
+ if (leased !== undefined)
160
+ return leased;
161
+ }
162
+ throw this.refused('init-failed');
163
+ }
164
+ /**
165
+ * Wait for `client`'s open inside `operation`, holding a reservation on it
166
+ * all the while — between the open settling and this lease the client is
167
+ * busy, so nobody can evict, idle-end or end it — then check it again and
168
+ * lease it in the same step, with no wait between. `undefined` when it no
169
+ * longer serves: the caller's next round finds the key's next client.
170
+ */
171
+ async leaseOnceOpen(client, operation, facts) {
172
+ client.reserved += 1;
173
+ try {
174
+ await this.waitOpen(client, operation);
175
+ }
176
+ catch (error) {
177
+ client.reserved -= 1;
178
+ this.becameIdle(client);
179
+ // The pool closed while it waited: that is the refusal, whatever the wait threw.
180
+ throw this.closed ? this.refused('closed') : error;
181
+ }
182
+ client.reserved -= 1;
183
+ if (!client.serving) {
184
+ this.becameIdle(client);
185
+ return undefined;
186
+ }
187
+ try {
188
+ return this.takeLease(client, operation, facts);
189
+ }
190
+ catch (error) {
191
+ this.becameIdle(client);
192
+ throw this.closed ? this.refused('closed') : error;
193
+ }
194
+ }
195
+ /** `client` may have just become idle: a deferred ending may run, and a waiter for idleness may go on. */
196
+ becameIdle(client) {
197
+ if (client.busy)
198
+ return;
199
+ this.retryDeferred();
200
+ this.notifyIdle();
201
+ }
202
+ takeLease(client, operation, facts) {
203
+ // The caller detached while it waited, or before it was admitted: nothing
204
+ // more is done for it. The cached path reaches here with no wait at all,
205
+ // so this is its only check.
206
+ operation.check();
207
+ client.leases += 1;
208
+ client.usedAt = this.now();
209
+ // Used again: an idle end or an eviction waiting for slots no longer applies.
210
+ if (client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
211
+ client.deferredEnd = undefined;
212
+ }
213
+ facts.clientId = client.clientId;
214
+ facts.generation = client.generation;
215
+ if (client.initMs !== undefined && (facts.client === 'opened' || facts.client === 'joined')) {
216
+ facts.initMs = client.initMs;
217
+ }
218
+ operation.onSettled(() => this.releaseLease(client));
219
+ return client;
220
+ }
221
+ /** One lease ended: the client may now be idle, and a deferred ending may run. */
222
+ releaseLease(client) {
223
+ client.leases -= 1;
224
+ client.usedAt = this.now();
225
+ if (!client.busy && client.deferredEnd !== undefined) {
226
+ this.endClient(client, client.deferredEnd);
227
+ }
228
+ this.notifyIdle();
229
+ }
230
+ notifyIdle() {
231
+ for (const check of [...this.idleWaiters])
232
+ check();
233
+ }
234
+ /** An app lease: a hold on a client for as long as the app keeps it, at most `maxLeaseMs`. */
235
+ holdLease(client) {
236
+ client.leases += 1;
237
+ client.usedAt = this.now();
238
+ if (client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
239
+ client.deferredEnd = undefined;
240
+ }
241
+ const lease = { client, takenAt: this.now(), revoked: false, released: false };
242
+ this.leases.add(lease);
243
+ return lease;
244
+ }
245
+ /** End an app lease — at most once, whether released or revoked. */
246
+ endLease(lease) {
247
+ if (!this.leases.delete(lease))
248
+ return;
249
+ this.releaseLease(lease.client);
250
+ }
251
+ revokeExpiredLeases() {
252
+ const now = this.now();
253
+ for (const lease of [...this.leases]) {
254
+ if (now - lease.takenAt < this.settings.maxLeaseMs)
255
+ continue;
256
+ lease.revoked = true;
257
+ this.endLease(lease);
258
+ this.reporter.report({
259
+ kind: 'lease-revoked',
260
+ clientId: lease.client.clientId,
261
+ keyHash: lease.client.key.keyHash,
262
+ durationMs: Math.max(0, now - lease.takenAt),
263
+ });
264
+ }
265
+ }
266
+ /** Every app lease is revoked: the pool is closing. */
267
+ revokeAllLeases() {
268
+ for (const lease of [...this.leases]) {
269
+ lease.revoked = true;
270
+ this.endLease(lease);
271
+ }
272
+ }
273
+ // ─── The open ──────────────────────────────────────────────────────
274
+ /**
275
+ * Open a client for `key`: make room, resolve its plan, admit it to the open
276
+ * set, and start the one open. The operation that started it tracks the open's
277
+ * settlement, so its charge stays until the open's work has settled.
278
+ */
279
+ openFor(key, operation, facts, resolved) {
280
+ this.makeRoom(key);
281
+ let plan;
282
+ try {
283
+ plan = resolved ?? (0, plan_js_1.resolvePlan)(this.settings, key);
284
+ }
285
+ catch (cause) {
286
+ throw this.refused('init-failed', cause);
287
+ }
288
+ const { generation, previous } = this.clients.nextIncarnation(key.key);
289
+ const client = new pooledClient_js_1.PooledClient(key, plan, generation, this.now());
290
+ this.clients.admit(client);
291
+ facts.client = 'opened';
292
+ facts.clientId = client.clientId;
293
+ facts.generation = generation;
294
+ if (previous !== undefined)
295
+ facts.previous = previous;
296
+ const work = this.startOpen(client);
297
+ operation.phase = 'open';
298
+ operation.track(work.whenSettled());
299
+ return client;
300
+ }
301
+ /** The one open of `client`, under the pool's own signal and `initTimeoutMs`. */
302
+ startOpen(client) {
303
+ const work = new work_js_1.Work('open', [this.lifetime.signal]);
304
+ const startedAt = this.now();
305
+ let resolveReady;
306
+ let rejectReady;
307
+ const ready = new Promise((resolve, reject) => {
308
+ resolveReady = resolve;
309
+ rejectReady = reject;
310
+ });
311
+ ready.catch(() => undefined);
312
+ client.opening = { ready, work, startedAt };
313
+ client.phase = work;
314
+ let timedOut = false;
315
+ let opened;
316
+ let halfOpen;
317
+ const timer = setTimeout(() => {
318
+ timedOut = true;
319
+ client.abandoned = true;
320
+ work.abort(initTimeout());
321
+ rejectReady(this.refused('init-timeout'));
322
+ }, this.settings.initTimeoutMs);
323
+ const seam = this.seamFor(client, work.signal);
324
+ let pending;
325
+ try {
326
+ pending = (this.settings.open ?? mcpClient_js_1.openPooledClient)(this.clientOptions(client), seam);
327
+ }
328
+ catch (error) {
329
+ pending = Promise.reject(error);
330
+ }
331
+ work.track(pending);
332
+ pending.then((result) => {
333
+ clearTimeout(timer);
334
+ opened = result;
335
+ client.client = result.client;
336
+ client.port = result.port;
337
+ if (!timedOut && !this.closed) {
338
+ client.state = 'open';
339
+ client.initMs = Math.max(0, this.now() - startedAt);
340
+ client.opening = undefined;
341
+ if (client.phase === work)
342
+ client.phase = undefined;
343
+ resolveReady();
344
+ this.notifyIdle();
345
+ }
346
+ else if (!timedOut) {
347
+ // The pool closed while it opened: nobody gets it; it is ended below.
348
+ timedOut = true;
349
+ rejectReady(this.refused('closed'));
350
+ }
351
+ work.finish();
352
+ }, (error) => {
353
+ clearTimeout(timer);
354
+ halfOpen = (0, clientPort_js_1.halfOpenOf)(error);
355
+ if (!timedOut) {
356
+ rejectReady(this.closed ? this.refused('closed') : this.refused('init-failed', error));
357
+ }
358
+ work.finish();
359
+ });
360
+ // Once the open's own work has settled: an abandoned open that opened a
361
+ // session is ended under the ending rule; a failed one frees its slot.
362
+ // Either way the client has stopped opening — a wait for idle clients
363
+ // (`terminate`, `close`) is told once its new state is in place.
364
+ work.onSettled(() => {
365
+ if (!timedOut && opened !== undefined) {
366
+ this.notifyIdle();
367
+ return;
368
+ }
369
+ client.opening = undefined;
370
+ if (client.phase === work)
371
+ client.phase = undefined;
372
+ // A failed open whose server opened a session all the same has a session
373
+ // to end: so does an abandoned open that succeeded.
374
+ const session = opened?.port ?? (halfOpen?.hasSession() === true ? halfOpen : undefined);
375
+ if (session === undefined) {
376
+ this.dropFailedOpen(client);
377
+ }
378
+ else {
379
+ if (opened === undefined)
380
+ client.port = endingOnly(session);
381
+ client.state = 'open';
382
+ client.closedToNew = true;
383
+ this.clients.retire(client, 'terminated');
384
+ this.endClient(client, 'abandoned-open');
385
+ }
386
+ this.notifyIdle();
387
+ });
388
+ return work;
389
+ }
390
+ dropFailedOpen(client) {
391
+ client.opening = undefined;
392
+ client.state = 'ended';
393
+ client.phase = undefined;
394
+ this.clients.drop(client);
395
+ this.retryDeferred();
396
+ this.notifySettled();
397
+ }
398
+ /** Wait for `client`'s open inside `operation`: the caller's signal detaches the caller only. */
399
+ async waitOpen(client, operation) {
400
+ const opening = client.opening;
401
+ if (opening === undefined) {
402
+ if (client.state === 'open')
403
+ return;
404
+ throw this.refused(client.abandoned ? 'init-timeout' : 'init-failed');
405
+ }
406
+ await (0, abort_js_1.untilAborted)(opening.ready, operation.signal);
407
+ }
408
+ /** What `mcpClient` is opened with: the connection-shaping options, the pool's signal. */
409
+ clientOptions(client) {
410
+ return {
411
+ name: this.settings.name,
412
+ transport: client.plan.transport,
413
+ signal: this.lifetime.signal,
414
+ ...this.settings.client,
415
+ };
416
+ }
417
+ /** What only a pool asks of the client it opens (`clientPort.ts`). */
418
+ seamFor(client, connectSignal) {
419
+ return {
420
+ connectSignal,
421
+ wrapFetch: (inner, claim) => this.pooledFetch(client, inner, claim),
422
+ guardFor: (message) => this.guardOf(client, message),
423
+ onToolsChanged: () => this.toolsChangedOn(client),
424
+ };
425
+ }
426
+ /**
427
+ * The guard for a message on `client` that carries none of its own: a tool
428
+ * call always brings its call's guard, so one without it is refused; any
429
+ * other request runs under the client's open, listing or ending — and with
430
+ * none under way, it is refused too. Nothing pooled is sent unchecked.
431
+ */
432
+ guardOf(client, message) {
433
+ if (isToolCall(message))
434
+ return this.unmatched;
435
+ return client.phase ?? this.unmatched;
436
+ }
437
+ /** The guard of a request nothing covers: it refuses, so the request is never sent. */
438
+ unmatched = {
439
+ check: () => {
440
+ throw this.refused('request-unmatched');
441
+ },
442
+ track: () => undefined,
443
+ };
444
+ /**
445
+ * The pool's own fetch, outermost on every pooled transport: a request is
446
+ * checked against its guard (the pooled call's, else the client's phase) and
447
+ * counted under it; the guard's signal is linked in; and the answer is read
448
+ * for the two pieces of evidence the pool classifies on — a 404 the MCP server
449
+ * itself sent to a request that carried its session id, and a handle this
450
+ * process lost. The client's own standalone stream (a GET), a notification and
451
+ * an answer to the server's own request are the client's, not an operation's:
452
+ * they pass untouched. Any other request with no guard is refused unsent.
453
+ */
454
+ pooledFetch(client, inner, claim) {
455
+ return async (input, init) => {
456
+ // The client's own stream, a notification, an answer to the server —
457
+ // never refused, never cut by an operation; a cancellation is counted
458
+ // under its call at `send`.
459
+ if ((init?.method ?? 'GET').toUpperCase() === 'GET' || passesUnguarded(init?.body)) {
460
+ return inner(input, init);
461
+ }
462
+ const guard = claim(init?.body) ?? client.phase;
463
+ if (guard === undefined)
464
+ throw this.refused('request-unmatched');
465
+ guard.check();
466
+ const carriesSession = new Headers(init?.headers).has('mcp-session-id');
467
+ const signal = (0, abort_js_1.eitherSignal)(init?.signal ?? undefined, guard.signal);
468
+ const sent = inner(input, {
469
+ ...(0, requestGuard_js_1.withGuard)(init, guard),
470
+ ...(signal !== undefined && { signal }),
471
+ });
472
+ guard.track(sent);
473
+ try {
474
+ const response = await sent;
475
+ if (response.status === 404 && carriesSession && guard instanceof work_js_1.Work) {
476
+ if (await isServerAnswer(response))
477
+ guard.note('server-404');
478
+ }
479
+ return response;
480
+ }
481
+ catch (error) {
482
+ if ((0, toolSessions_js_1.isLocalSessionHandleLost)(error) && guard instanceof work_js_1.Work)
483
+ guard.note('handle-lost');
484
+ throw error;
485
+ }
486
+ };
487
+ }
488
+ // ─── Making room, and ending ───────────────────────────────────────
489
+ /**
490
+ * Make room for a new client of `key`: the principal's least recently used
491
+ * idle client, then the pool's, moves to the ending set — or `PoolFullError`
492
+ * at once, naming the budget. There is no queue.
493
+ */
494
+ makeRoom(key) {
495
+ if (this.clients.principalCount(key.principal) >= this.settings.maxClientsPerPrincipal) {
496
+ const victim = this.clients.leastRecentlyUsedIdle((c) => c.key.principal === key.principal);
497
+ if (victim === undefined)
498
+ throw this.full('principal');
499
+ this.evict(victim);
500
+ }
501
+ if (this.clients.openCount >= this.settings.maxClients) {
502
+ const victim = this.clients.leastRecentlyUsedIdle(() => true);
503
+ if (victim === undefined)
504
+ throw this.full('pool');
505
+ this.evict(victim);
506
+ }
507
+ }
508
+ /**
509
+ * Move `victim` to the ending set to make room. When the slots cannot be
510
+ * reserved, the arrival is refused at once, and the victim stays in the open
511
+ * set, counted, reported deferred — ended once slots free.
512
+ */
513
+ evict(victim) {
514
+ const reserved = this.reserveEnding();
515
+ if (typeof reserved === 'string') {
516
+ this.defer(victim, 'evicted');
517
+ throw this.full(reserved);
518
+ }
519
+ // A victim already waiting to end for a stronger reason ends for that one.
520
+ const waiting = victim.deferredEnd;
521
+ const reason = waiting !== undefined && waiting !== 'idle' ? waiting : 'evicted';
522
+ this.startEnding(victim, reason, reserved);
523
+ }
524
+ /** An ending slot and an operation slot, both, or which one is full. */
525
+ reserveEnding() {
526
+ if (this.clients.endingCount >= this.settings.maxEnding)
527
+ return 'ending';
528
+ return this.budget.admit('ending', 'ending', [], this.now()) ?? 'operations';
529
+ }
530
+ /**
531
+ * End `client` for `reason` — when it is idle and both slots can be reserved.
532
+ * Otherwise it stays in the open set, still counted, and is reported deferred
533
+ * (once); it is ended when its last lease ends or slots free.
534
+ */
535
+ endClient(client, reason) {
536
+ if (client.state !== 'open' || client.busy || client.port === undefined) {
537
+ this.defer(client, reason);
538
+ return;
539
+ }
540
+ const reserved = this.reserveEnding();
541
+ if (typeof reserved === 'string') {
542
+ this.defer(client, reason);
543
+ return;
544
+ }
545
+ this.startEnding(client, reason, reserved);
546
+ }
547
+ defer(client, reason) {
548
+ const first = client.deferredEnd === undefined;
549
+ // A stronger reason replaces a weaker one: terminated, closed or expired
550
+ // outrank an idle end or an eviction.
551
+ if (first || client.deferredEnd === 'idle' || client.deferredEnd === 'evicted') {
552
+ client.deferredEnd = reason;
553
+ }
554
+ if (!first || client.state === 'ending' || client.state === 'ended')
555
+ return;
556
+ this.reporter.report({
557
+ kind: 'deferred',
558
+ clientId: client.clientId,
559
+ keyHash: client.key.keyHash,
560
+ reason,
561
+ durationMs: 0,
562
+ });
563
+ }
564
+ /**
565
+ * Move `client` into the ending set under `operation`, and end it through
566
+ * `terminate()` — or, for a lost handle, through its local close alone.
567
+ */
568
+ startEnding(client, reason, operation) {
569
+ client.deferredEnd = undefined;
570
+ client.state = 'ending';
571
+ this.clients.moveToEnding(client, (0, clientSet_js_1.previousOf)(reason));
572
+ client.phase = operation;
573
+ const ending = client.lost === 'handle-lost'
574
+ ? closeLocally(client)
575
+ : client.port.end();
576
+ client.endingOutcome = ending.outcome;
577
+ operation.track(ending.settled);
578
+ void ending.outcome.then((outcome) => {
579
+ this.noteEnded(client, reason, outcome);
580
+ operation.finish();
581
+ });
582
+ operation.onSettled(() => {
583
+ this.clients.ended(client);
584
+ client.state = 'ended';
585
+ client.phase = undefined;
586
+ // The ending slot is free only now: deferred endings may run, and a
587
+ // `settled()` waiting on the ending set may resolve.
588
+ this.retryDeferred();
589
+ this.notifySettled();
590
+ });
591
+ }
592
+ noteEnded(client, reason, outcome) {
593
+ if (!outcome.resolved)
594
+ this.unresolvedEndings += 1;
595
+ this.reporter.report({
596
+ kind: 'ended',
597
+ clientId: client.clientId,
598
+ keyHash: client.key.keyHash,
599
+ reason,
600
+ remote: outcome.remote,
601
+ resolved: outcome.resolved,
602
+ ...(outcome.status !== undefined && { status: outcome.status }),
603
+ ...(outcome.errorClass !== undefined && { errorClass: outcome.errorClass }),
604
+ durationMs: outcome.durationMs,
605
+ });
606
+ }
607
+ /**
608
+ * A client whose handle this process lost: nothing can be sent for it, so it
609
+ * leaves its key at once — the next use opens the key's next client — and is
610
+ * ended under the ending rule like any other: its ending is its LOCAL close,
611
+ * charged an ending slot and an operation slot until that close settles, its
612
+ * remote outcome recorded unresolved. Busy, or with no slots free, it waits —
613
+ * still counted in the open set, reported deferred.
614
+ */
615
+ lostHandle(client) {
616
+ if (client.lost !== undefined || client.state !== 'open')
617
+ return;
618
+ client.lost = 'handle-lost';
619
+ client.closedToNew = true;
620
+ this.clients.retire(client, 'handle-lost');
621
+ this.endClient(client, 'handle-lost');
622
+ }
623
+ /** A client whose session the server ended: no new lease; ended (through `terminate()`) when idle. */
624
+ expired(client) {
625
+ if (client.lost !== undefined)
626
+ return;
627
+ client.lost = 'expired';
628
+ client.closedToNew = true;
629
+ this.clients.retire(client, 'expired');
630
+ this.endClient(client, 'expired');
631
+ }
632
+ sweepIdle() {
633
+ const cutoff = this.now() - this.settings.idleMs;
634
+ for (const client of this.clients.openClients()) {
635
+ if (client.state !== 'open' || client.busy || client.deferredEnd !== undefined)
636
+ continue;
637
+ // Retired only once its ending starts (`startEnding`): while it waits
638
+ // for slots, its own conversation may still use it.
639
+ if (client.usedAt <= cutoff)
640
+ this.endClient(client, 'idle');
641
+ }
642
+ }
643
+ /** Retry every deferred ending whose client is now idle. */
644
+ retryDeferred() {
645
+ for (const client of this.clients.openClients()) {
646
+ if (client.deferredEnd === undefined || client.busy || client.state !== 'open')
647
+ continue;
648
+ this.endClient(client, client.deferredEnd);
649
+ }
650
+ }
651
+ // ─── Lists ─────────────────────────────────────────────────────────
652
+ /** Where `client`'s definitions are served from: its shared entry, or its own list. */
653
+ servedListOf(client) {
654
+ const { context, shared } = client.definitions;
655
+ if (shared) {
656
+ return { list: this.shared.entryFor(context, client.key.tenant).list, shared: true, context };
657
+ }
658
+ return { list: client.list, shared: false, context };
659
+ }
660
+ /**
661
+ * The definitions context `client` serves under NOW: the app's `share` asked
662
+ * again over the client's own configuration. A changed answer moves the
663
+ * client to the new context and stops serving its own list, so every tool
664
+ * bound under the old one is refused before it is sent.
665
+ *
666
+ * @throws `binding-changed`, `share`'s error as its cause, when the answer
667
+ * cannot be had — never the old context
668
+ */
669
+ refreshContext(client) {
670
+ if (this.settings.share === undefined)
671
+ return client.definitions;
672
+ let now;
673
+ try {
674
+ now = (0, plan_js_1.contextFor)(this.settings, client.key, client.plan.configHash);
675
+ }
676
+ catch (cause) {
677
+ throw this.refused('binding-changed', cause);
678
+ }
679
+ if (now.context !== client.definitions.context) {
680
+ client.definitions = now;
681
+ client.list.invalidate();
682
+ }
683
+ return now;
684
+ }
685
+ /** Does `context` still name the definitions context `client` serves under? False when it cannot be had. */
686
+ contextHolds(client, context) {
687
+ try {
688
+ return this.refreshContext(client).context === context;
689
+ }
690
+ catch {
691
+ return false;
692
+ }
693
+ }
694
+ /**
695
+ * The list `client` serves under NOW, listed again on it when it cannot be
696
+ * served as it stands — one listing at a time per list; a second caller joins
697
+ * it inside its own slot. Every attempt asks the app's names again
698
+ * (`refreshContext`), so a listing that a moved context discarded or refused
699
+ * at its send is listed once more under the context of now, never written
700
+ * into the one it began under. A listing raced by an invalidation is
701
+ * discarded too. Refused (`stale-tools`) when three attempts serve nothing.
702
+ * What it returns was checked against the context with no wait since.
703
+ */
704
+ async ensureListed(client, operation) {
705
+ for (let attempt = 0; attempt < 3; attempt += 1) {
706
+ const served = this.servableNow(client);
707
+ if (served.ready !== undefined)
708
+ return served.ready;
709
+ const { list } = served.wanted;
710
+ if (list.listing === undefined)
711
+ this.startListing(client, served.wanted, operation);
712
+ operation.phase = 'listing';
713
+ try {
714
+ await (0, abort_js_1.untilAborted)(list.listing, operation.signal);
715
+ }
716
+ catch (error) {
717
+ operation.check();
718
+ // Refused at its send because the context moved: asked again next round.
719
+ if (!isContextMoved(error))
720
+ throw error;
721
+ }
722
+ operation.check();
723
+ }
724
+ const served = this.servableNow(client);
725
+ if (served.ready !== undefined)
726
+ return served.ready;
727
+ throw this.refused('stale-tools');
728
+ }
729
+ /**
730
+ * What `client` can serve under the context of now — asked of the app's names
731
+ * here — or, when nothing can, the list it should list. Too long to share:
732
+ * the conversation's own client keeps that list, under the same context.
733
+ */
734
+ servableNow(client) {
735
+ this.refreshContext(client);
736
+ const wanted = this.servedListOf(client);
737
+ const now = this.now();
738
+ const ttl = this.settings.definitionTtlMs;
739
+ if (wanted.list.servable(now, ttl))
740
+ return { ready: wanted, wanted };
741
+ // A context change invalidates the client's own list, so a servable one
742
+ // was written under the context of now.
743
+ if (wanted.shared && client.list.servable(now, ttl)) {
744
+ return { ready: { list: client.list, shared: false, context: wanted.context }, wanted };
745
+ }
746
+ return { wanted };
747
+ }
748
+ /**
749
+ * One listing on `client` for `served`'s context. Its requests are sent only
750
+ * while that context holds — asked at every boundary they cross — and its
751
+ * answer is written only if the context still holds when it arrives and the
752
+ * list's generation held: an answer the app's names moved under belongs to
753
+ * no context, and is dropped.
754
+ */
755
+ startListing(client, served, operation) {
756
+ const { list } = served;
757
+ const began = list.generation;
758
+ const beganOwn = client.list.generation;
759
+ const work = new work_js_1.Work('listing', [this.lifetime.signal]);
760
+ work.guardWith(() => {
761
+ if (!this.contextHolds(client, served.context))
762
+ throw this.refused('binding-changed');
763
+ });
764
+ operation.track(work.whenSettled());
765
+ const port = client.port;
766
+ if (port === undefined)
767
+ throw this.refused('init-failed');
768
+ client.phase = work;
769
+ const pending = port.listing(work.signal);
770
+ work.track(pending);
771
+ list.listing = pending
772
+ .then((listing) => {
773
+ // Asked again before anything is kept: listed under one context,
774
+ // answered under another, it is no one's.
775
+ if (!this.contextHolds(client, served.context))
776
+ return;
777
+ const definitions = listing.listed
778
+ .map((listed) => (0, definitions_js_1.definitionOf)(listed))
779
+ .filter((d) => d !== undefined);
780
+ if (served.shared && definitions.length > options_js_1.MAX_SHARED_TOOLS) {
781
+ // Used by its own client, never shared.
782
+ client.list.write(beganOwn, definitions, listing.tools, this.now());
783
+ return;
784
+ }
785
+ list.write(began, definitions, served.shared ? undefined : listing.tools, this.now());
786
+ })
787
+ .catch((error) => {
788
+ this.classifyFailure(client, work, error);
789
+ throw error;
790
+ })
791
+ .finally(() => {
792
+ list.listing = undefined;
793
+ if (client.phase === work)
794
+ client.phase = undefined;
795
+ work.finish();
796
+ });
797
+ list.listing.catch(() => undefined);
798
+ }
799
+ /**
800
+ * A request failed: when the pool's fetch read the server's own 404 for the
801
+ * session, or this process lost the handle, the client leaves its key — the
802
+ * next use opens the next client under the SAME key — and the error says so.
803
+ */
804
+ classifyFailure(client, work, error) {
805
+ if ((0, toolSessions_js_1.isLocalSessionHandleLost)(error) || work.evidence === 'handle-lost') {
806
+ this.lostHandle(client);
807
+ return 'handle-lost';
808
+ }
809
+ if (work.evidence === 'server-404') {
810
+ this.expired(client);
811
+ return 'expired';
812
+ }
813
+ return undefined;
814
+ }
815
+ // ─── Invalidation ──────────────────────────────────────────────────
816
+ /** The server said a client's tool list changed: that client's list and its shared entry. */
817
+ toolsChangedOn(client) {
818
+ client.list.invalidate();
819
+ const { context, shared } = client.definitions;
820
+ if (shared)
821
+ this.shared.peek(context)?.list.invalidate();
822
+ }
823
+ /** The app's permission signal: every list in the scope. */
824
+ permissionsChanged(scope) {
825
+ const matches = (0, identityKey_js_1.scopeMatcher)({
826
+ ...(scope.tenant !== undefined && { tenant: scope.tenant }),
827
+ ...(scope.principal !== undefined && { principal: scope.principal }),
828
+ });
829
+ for (const client of this.clients.openClients()) {
830
+ if (matches(client.key))
831
+ client.list.invalidate();
832
+ }
833
+ // A shared entry is used across the tenant's principals: any of them changing invalidates it.
834
+ const tenant = (0, encode_js_1.encodeIdentityField)(scope.tenant);
835
+ this.shared.invalidate((entry) => entry.tenant === tenant);
836
+ }
837
+ /** The server changed: every list. */
838
+ toolsChanged() {
839
+ for (const client of this.clients.openClients())
840
+ client.list.invalidate();
841
+ this.shared.invalidate(() => true);
842
+ }
843
+ // ─── Terminate, close, settle ──────────────────────────────────────
844
+ /**
845
+ * Stop new leases on every client `scope` names, at once; wait for their
846
+ * in-flight work up to `waitMs`; then end each one whose slots can be
847
+ * reserved. The rest are reported deferred and ended when they can be.
848
+ */
849
+ async terminate(matches, reason, waitMs) {
850
+ const named = this.clients.openClients().filter((c) => matches(c.key));
851
+ for (const client of named) {
852
+ client.closedToNew = true;
853
+ this.clients.retire(client, (0, clientSet_js_1.previousOf)(reason) ?? 'terminated');
854
+ }
855
+ await this.waitIdle(named, waitMs);
856
+ return this.endNow(named, reason);
857
+ }
858
+ /** End each of `clients` whose slots can be reserved; the outcomes of those that started. */
859
+ endNow(clients, reason) {
860
+ const outcomes = [];
861
+ for (const client of clients) {
862
+ if (client.state !== 'open')
863
+ continue;
864
+ this.endClient(client, reason);
865
+ if (client.state === 'ending' && client.endingOutcome !== undefined) {
866
+ outcomes.push(client.endingOutcome);
867
+ }
868
+ }
869
+ return Promise.all(outcomes);
870
+ }
871
+ /** Wait until every client in `clients` is idle, or `waitMs` passed. */
872
+ waitIdle(clients, waitMs) {
873
+ if (!clients.some((c) => c.busy))
874
+ return Promise.resolve();
875
+ return new Promise((resolve) => {
876
+ const done = () => {
877
+ clearTimeout(timer);
878
+ this.idleWaiters.delete(check);
879
+ resolve();
880
+ };
881
+ const check = () => {
882
+ if (!clients.some((c) => c.busy))
883
+ done();
884
+ };
885
+ const timer = setTimeout(done, waitMs);
886
+ timer.unref?.();
887
+ this.idleWaiters.add(check);
888
+ });
889
+ }
890
+ /**
891
+ * Close: refuse new work, revoke app leases, abort in-flight work through the
892
+ * pool's own signal, then end every client as slots can be reserved — at most
893
+ * `maxEnding` at a time — in rounds, each bounded by `waitMs` (and each
894
+ * ending's answer by its own deadline). What could not be ended yet is
895
+ * reported deferred and ended when slots free; work still unsettled stays
896
+ * charged until it settles.
897
+ */
898
+ async closeAll(waitMs) {
899
+ this.shutDown();
900
+ const all = this.clients.openClients();
901
+ for (const client of all) {
902
+ client.closedToNew = true;
903
+ this.clients.retire(client, 'terminated');
904
+ }
905
+ await this.waitIdle(all, waitMs);
906
+ const outcomes = [];
907
+ for (;;) {
908
+ const started = await this.endNow(all, 'closed');
909
+ if (started.length === 0)
910
+ break;
911
+ outcomes.push(...started);
912
+ if (!all.some((c) => c.state === 'open'))
913
+ break;
914
+ await this.waitSlots(waitMs);
915
+ }
916
+ for (const client of all) {
917
+ if (client.state === 'open' || client.state === 'opening')
918
+ this.defer(client, 'closed');
919
+ }
920
+ return outcomes;
921
+ }
922
+ /** Wait until an ending slot and an operation slot are free, or `waitMs` passed. */
923
+ waitSlots(waitMs) {
924
+ const free = () => this.clients.endingCount < this.settings.maxEnding && this.budget.hasRoom;
925
+ if (free())
926
+ return Promise.resolve();
927
+ return new Promise((resolve) => {
928
+ const done = () => {
929
+ clearTimeout(timer);
930
+ unsubscribe();
931
+ resolve();
932
+ };
933
+ const timer = setTimeout(done, waitMs);
934
+ timer.unref?.();
935
+ const unsubscribe = this.budget.whenReleased(() => {
936
+ if (free())
937
+ done();
938
+ });
939
+ });
940
+ }
941
+ /** Refuse new work, revoke app leases, abort in-flight work through the pool's signal. */
942
+ shutDown() {
943
+ this.closed = true;
944
+ this.revokeAllLeases();
945
+ this.lifetime.abort(poolClosed());
946
+ }
947
+ /** Resolve once every charged operation and every ending has settled. */
948
+ settled() {
949
+ if (this.quiet())
950
+ return Promise.resolve();
951
+ return new Promise((resolve) => this.settleWaiters.push(resolve));
952
+ }
953
+ quiet() {
954
+ return (this.budget.count === 0 &&
955
+ this.clients.endingCount === 0 &&
956
+ !this.clients.openClients().some((c) => c.state === 'opening'));
957
+ }
958
+ notifySettled() {
959
+ if (!this.quiet())
960
+ return;
961
+ for (const resolve of this.settleWaiters.splice(0))
962
+ resolve();
963
+ }
964
+ stats() {
965
+ const open = this.clients.openClients();
966
+ const busy = open.filter((c) => c.busy).length;
967
+ return {
968
+ open: open.length,
969
+ busy,
970
+ idle: open.length - busy,
971
+ opening: open.filter((c) => c.state === 'opening').length,
972
+ ending: this.clients.endingCount,
973
+ operations: this.budget.count,
974
+ entries: this.shared.size,
975
+ unsettled: this.budget.unsettled(),
976
+ unresolvedEndings: this.unresolvedEndings,
977
+ };
978
+ }
979
+ /** The bound shape of every definition in `served`, for a client of `key`. */
980
+ definitionsOf(served) {
981
+ return served.list.all();
982
+ }
983
+ /** The tools a list's client-own entries carry, by name (per-client lists only). */
984
+ ownTool(served, name) {
985
+ return served.shared ? undefined : served.list.tool(name);
986
+ }
987
+ }
988
+ exports.Pool = Pool;
989
+ /** A port that can only end: what a half-opened session leaves the pool. */
990
+ function endingOnly(halfOpen) {
991
+ return {
992
+ listing: () => Promise.reject(new Error('This client never opened.')),
993
+ bind: () => {
994
+ throw new Error('This client never opened.');
995
+ },
996
+ end: () => halfOpen.end(),
997
+ };
998
+ }
999
+ /** The reason an open is aborted at its deadline — a fixed sentence. */
1000
+ function initTimeout() {
1001
+ return Object.assign(new Error('The pooled client did not open within its deadline.'), {
1002
+ name: 'TimeoutError',
1003
+ });
1004
+ }
1005
+ /** The reason in-flight work is aborted when the pool closes — a fixed sentence. */
1006
+ function poolClosed() {
1007
+ return Object.assign(new Error('The MCP client pool was closed.'), { name: 'AbortError' });
1008
+ }
1009
+ /**
1010
+ * A body the pool's fetch passes with no guard: a JSON-RPC notification (a
1011
+ * method, no id) or an answer to the server's own request (an id, a result or
1012
+ * an error, no method). Never refused, never counted here. Anything else — a
1013
+ * request, a batch, a body that is not JSON-RPC — needs a guard.
1014
+ */
1015
+ function passesUnguarded(body) {
1016
+ if (typeof body !== 'string')
1017
+ return false;
1018
+ let message;
1019
+ try {
1020
+ message = JSON.parse(body);
1021
+ }
1022
+ catch {
1023
+ return false;
1024
+ }
1025
+ if (message === null || typeof message !== 'object' || Array.isArray(message))
1026
+ return false;
1027
+ const fields = message;
1028
+ if (typeof fields.method === 'string')
1029
+ return fields.id === undefined;
1030
+ return fields.method === undefined && ('result' in fields || 'error' in fields);
1031
+ }
1032
+ /** A listing refused at its send because the app's names moved: listed again under the new context. */
1033
+ function isContextMoved(error) {
1034
+ return error instanceof errors_js_1.McpPoolRefusedError && error.reason === 'binding-changed';
1035
+ }
1036
+ /** A `tools/call` request: a pooled one always brings its own call's guard. */
1037
+ function isToolCall(message) {
1038
+ return (message !== null &&
1039
+ typeof message === 'object' &&
1040
+ message.method === 'tools/call');
1041
+ }
1042
+ /**
1043
+ * A lost handle's ending: nothing can be sent for it, so the remote outcome is
1044
+ * known at once — `handle-lost`, unresolved — and the work is the local close
1045
+ * alone, which the ending stays charged for until it settles.
1046
+ */
1047
+ function closeLocally(client) {
1048
+ const target = client.client;
1049
+ const settled = target === undefined
1050
+ ? Promise.resolve()
1051
+ : Promise.resolve()
1052
+ .then(() => target.close())
1053
+ .then(() => undefined, () => undefined);
1054
+ return {
1055
+ outcome: Promise.resolve({ remote: 'handle-lost', resolved: false, durationMs: 0 }),
1056
+ settled,
1057
+ };
1058
+ }
1059
+ /**
1060
+ * Is a 404 the MCP server's own answer — a JSON-RPC error response — rather than
1061
+ * an intermediary's (a platform's "not found" for a missing endpoint, a proxy's
1062
+ * page)? Read from a copy of the body, which the SDK reads too.
1063
+ */
1064
+ async function isServerAnswer(response) {
1065
+ try {
1066
+ const text = await response.clone().text();
1067
+ const parsed = JSON.parse(text);
1068
+ const error = parsed?.error;
1069
+ return parsed?.jsonrpc === '2.0' && typeof error?.code === 'number';
1070
+ }
1071
+ catch {
1072
+ return false;
1073
+ }
1074
+ }
1075
+ //# sourceMappingURL=pool.js.map