@mcp-abap-adt/llm-agent-server-libs 20.7.1 → 20.9.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 (58) hide show
  1. package/dist/generated/version.d.ts +1 -1
  2. package/dist/generated/version.js +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/pipelines/controller.d.ts.map +1 -1
  8. package/dist/pipelines/controller.js +9 -1
  9. package/dist/pipelines/controller.js.map +1 -1
  10. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts +14 -1
  11. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts.map +1 -1
  12. package/dist/smart-agent/controller/controller-coordinator-handler.js +41 -6
  13. package/dist/smart-agent/controller/controller-coordinator-handler.js.map +1 -1
  14. package/dist/smart-agent/controller/types.d.ts +4 -0
  15. package/dist/smart-agent/controller/types.d.ts.map +1 -1
  16. package/dist/smart-agent/http/adapter-route-handler.d.ts +2 -2
  17. package/dist/smart-agent/http/adapter-route-handler.d.ts.map +1 -1
  18. package/dist/smart-agent/http/adapter-route-handler.js +12 -4
  19. package/dist/smart-agent/http/adapter-route-handler.js.map +1 -1
  20. package/dist/smart-agent/http/chat-route-handler.d.ts.map +1 -1
  21. package/dist/smart-agent/http/chat-route-handler.js +87 -79
  22. package/dist/smart-agent/http/chat-route-handler.js.map +1 -1
  23. package/dist/smart-agent/http/sse-heartbeat.d.ts +24 -0
  24. package/dist/smart-agent/http/sse-heartbeat.d.ts.map +1 -0
  25. package/dist/smart-agent/http/sse-heartbeat.js +62 -0
  26. package/dist/smart-agent/http/sse-heartbeat.js.map +1 -0
  27. package/dist/smart-agent/mcp/build-session-mcp-clients.d.ts +8 -6
  28. package/dist/smart-agent/mcp/build-session-mcp-clients.d.ts.map +1 -1
  29. package/dist/smart-agent/mcp/build-session-mcp-clients.js +20 -7
  30. package/dist/smart-agent/mcp/build-session-mcp-clients.js.map +1 -1
  31. package/dist/smart-agent/mcp/mcp-clients-with-descriptors.d.ts +16 -0
  32. package/dist/smart-agent/mcp/mcp-clients-with-descriptors.d.ts.map +1 -0
  33. package/dist/smart-agent/mcp/mcp-clients-with-descriptors.js +2 -0
  34. package/dist/smart-agent/mcp/mcp-clients-with-descriptors.js.map +1 -0
  35. package/dist/smart-agent/mcp/namespaced-bridge.d.ts +41 -0
  36. package/dist/smart-agent/mcp/namespaced-bridge.d.ts.map +1 -0
  37. package/dist/smart-agent/mcp/namespaced-bridge.js +90 -0
  38. package/dist/smart-agent/mcp/namespaced-bridge.js.map +1 -0
  39. package/dist/smart-agent/resolve-agent-embedder.d.ts +3 -3
  40. package/dist/smart-agent/resolve-agent-embedder.d.ts.map +1 -1
  41. package/dist/smart-agent/resolve-agent-embedder.js +25 -8
  42. package/dist/smart-agent/resolve-agent-embedder.js.map +1 -1
  43. package/dist/smart-agent/resolve-config-sections.d.ts.map +1 -1
  44. package/dist/smart-agent/resolve-config-sections.js +54 -3
  45. package/dist/smart-agent/resolve-config-sections.js.map +1 -1
  46. package/dist/smart-agent/session-lifecycle/index.d.ts +20 -5
  47. package/dist/smart-agent/session-lifecycle/index.d.ts.map +1 -1
  48. package/dist/smart-agent/session-lifecycle/index.js +20 -0
  49. package/dist/smart-agent/session-lifecycle/index.js.map +1 -1
  50. package/dist/smart-agent/smart-server.d.ts +155 -5
  51. package/dist/smart-agent/smart-server.d.ts.map +1 -1
  52. package/dist/smart-agent/smart-server.js +297 -17
  53. package/dist/smart-agent/smart-server.js.map +1 -1
  54. package/dist/smart-agent/tools-rag-handle.d.ts +4 -2
  55. package/dist/smart-agent/tools-rag-handle.d.ts.map +1 -1
  56. package/dist/smart-agent/tools-rag-handle.js +24 -13
  57. package/dist/smart-agent/tools-rag-handle.js.map +1 -1
  58. package/package.json +7 -7
@@ -6,7 +6,7 @@ import http from 'node:http';
6
6
  import { createRequire } from 'node:module';
7
7
  import { resolve as pathResolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
- import { isReadinessReporter, } from '@mcp-abap-adt/llm-agent';
9
+ import { buildNamespacedTools, defaultToolNamespace, isReadinessReporter, } from '@mcp-abap-adt/llm-agent';
10
10
  import { ClaudeSkillManager, CodexSkillManager, FileSystemPluginLoader, FileSystemSkillManager, getDefaultPluginDirs, HealthChecker, InMemoryKnowledgeBackend, KnowledgeRag, makeLlm, mergePluginExports, SessionRequestLogger, SmartAgentBuilder, SmartAgentSubAgent, WindowContextStrategy, } from '@mcp-abap-adt/llm-agent-libs';
11
11
  import { DefaultMcpFailureClassifier, MCPClientWrapper, McpClientAdapter, } from '@mcp-abap-adt/llm-agent-mcp';
12
12
  import { makeRag, prefetchEmbedderFactories, resolveEmbedder, } from '@mcp-abap-adt/llm-agent-rag';
@@ -56,6 +56,7 @@ import { StepperPipelinePlugin } from '../pipelines/stepper.js';
56
56
  import { normalizeLlmConfig, resolveLlmConfig, resolveLlmConfigStrict, resolveToolSelectionStrategy, } from './config.js';
57
57
  import { makeKnowledgeBackend } from './knowledge/make-knowledge-backend.js';
58
58
  import { buildSessionMcpClients, serverOwnsMcpConnection, shouldIsolateMcpPerSession, } from './mcp/build-session-mcp-clients.js';
59
+ import { buildNamespacedMcpBridge, rebindProvenanceToClients, } from './mcp/namespaced-bridge.js';
59
60
  import { makePgPool, makePgReadPool } from './pg-pool.js';
60
61
  import { InMemorySessionMetaStore } from './session-meta-store.js';
61
62
  import { buildSkillHostFromConfig, initSkillHost, } from './skill-plugins-host-factory.js';
@@ -91,7 +92,9 @@ export { buildSessionLifecycle, handleDeleteSession, handleListSessions, handleR
91
92
  * Exported for testability — tests can call this with a fake IMcpClient list.
92
93
  */
93
94
  /**
94
- * Connect MCP clients from a YAML `mcp:` config block (single or array).
95
+ * Connect MCP clients from a YAML `mcp:` config block (single or array),
96
+ * pairing each connected client with a stable per-slot descriptor
97
+ * (`{ slotIndex: i, label: cfg[i].name }`, #244).
95
98
  *
96
99
  * Mirrors the builder's connection logic (builder.ts ~lines 897-920) so the
97
100
  * Stepper path gets the same clients that the builder would have connected
@@ -101,12 +104,14 @@ export { buildSessionLifecycle, handleDeleteSession, handleListSessions, handleR
101
104
  * `pipeline.mcp` or `this.cfg.mcp`). Accepts the union so callers can pass
102
105
  * either directly without pre-normalising.
103
106
  */
104
- export async function connectMcpClientsFromConfig(mcpCfg) {
107
+ export async function connectMcpClientsWithDescriptorsFromConfig(mcpCfg) {
105
108
  if (!mcpCfg)
106
- return [];
109
+ return { clients: [], clientDescriptors: [], configuredSlotCount: 0 };
107
110
  const list = Array.isArray(mcpCfg) ? mcpCfg : [mcpCfg];
108
111
  const connected = [];
109
- for (const cfg of list) {
112
+ const clientDescriptors = [];
113
+ for (let i = 0; i < list.length; i++) {
114
+ const cfg = list[i];
110
115
  let wrapper;
111
116
  if (cfg.type === 'stdio') {
112
117
  wrapper = new MCPClientWrapper({
@@ -128,8 +133,22 @@ export async function connectMcpClientsFromConfig(mcpCfg) {
128
133
  }
129
134
  await wrapper.connect();
130
135
  connected.push(new McpClientAdapter(wrapper));
136
+ clientDescriptors.push({ slotIndex: i, label: cfg.name });
131
137
  }
132
- return connected;
138
+ return {
139
+ clients: connected,
140
+ clientDescriptors,
141
+ configuredSlotCount: list.length,
142
+ };
143
+ }
144
+ /**
145
+ * Compat wrapper preserving the original bare-array export: delegates to
146
+ * `connectMcpClientsWithDescriptorsFromConfig` and returns only `.clients`.
147
+ * Kept so existing callers/tests that depend on `Promise<IMcpClient[]>` are
148
+ * unaffected by the #244 descriptor-producing seam.
149
+ */
150
+ export async function connectMcpClientsFromConfig(mcpCfg) {
151
+ return (await connectMcpClientsWithDescriptorsFromConfig(mcpCfg)).clients;
133
152
  }
134
153
  export function buildMcpBridge(clients, classifier = new DefaultMcpFailureClassifier()) {
135
154
  return async (name, args, signal) => {
@@ -243,14 +262,23 @@ export class SmartServer {
243
262
  */
244
263
  _stepperMcpClients;
245
264
  /**
246
- * True when the consumer injected an MCP seam (`BuildAgentDeps.mcpClients` or
247
- * `connectMcp`). In that case MCP is provisioned ONLY through the seam (the
265
+ * True when the consumer injected an MCP seam (`BuildAgentDeps.mcpClients`,
266
+ * `connectMcp`, or `connectMcpWithDescriptors`). In that case MCP is provisioned
267
+ * ONLY through the seam (the
248
268
  * embeddable path must never force a real connect / builder self-connect). When
249
269
  * false (default), the YAML `mcp:` path keeps the builder-owned connect so the
250
270
  * builder VECTORIZES the tools into `toolsRag` (the ToolSelect ranking contract;
251
271
  * see mcp-yaml-vectorization.test.ts).
252
272
  */
253
273
  _mcpSeamInjected;
274
+ /**
275
+ * True when the consumer injected a BARE `connectMcp` (as opposed to it
276
+ * being defaulted to `connectMcpClientsFromConfig` in the constructor).
277
+ * Lets the provisioning precedence distinguish "consumer gave us a
278
+ * connector with no descriptors" (array-index descriptors synthesized)
279
+ * from "nothing was injected, use the descriptor-producing default" (#244).
280
+ */
281
+ _connectMcpInjected;
254
282
  /**
255
283
  * The MCP clients the pipeline `callMcp` bridge dispatches over — resolved
256
284
  * UNCONDITIONALLY in `start()` as DI/plugin clients (`mcpClients`) ?? the
@@ -258,6 +286,39 @@ export class SmartServer {
258
286
  * stepper) gets a working `ctx.callMcp` without opening a second connection.
259
287
  */
260
288
  _sharedMcpClients;
289
+ /**
290
+ * Per-slot descriptors paired with `_sharedMcpClients` (#244), captured
291
+ * from whichever provisioning path won (injected `connectMcpWithDescriptors`
292
+ * > injected bare `connectMcp` (array-index descriptors) > the descriptor-
293
+ * producing default). Consumed by Tasks 6/7 (namespacing + toolsChanged).
294
+ */
295
+ _sharedMcpClientDescriptors;
296
+ /**
297
+ * Total configured `mcp[]` slots (independent of how many actually
298
+ * connected), captured alongside `_sharedMcpClientDescriptors` (#244).
299
+ */
300
+ _configuredSlotCount;
301
+ /**
302
+ * The ONE authoritative namespaced tool catalog snapshot (#244 Task 6),
303
+ * fed to the tools-RAG handle (`makeToolsRagHandle`'s `namespaced` param).
304
+ * Populated from either source:
305
+ * - yaml-builder path: harvested from `agentHandle.namespacedTools` (the
306
+ * startup builder computed it while auto-connecting `cfg.mcp`).
307
+ * - seam / consumer-builder path: built ONCE by
308
+ * `resolveAuthoritativeSnapshot()` over `_sharedMcpClients` +
309
+ * `_sharedMcpClientDescriptors` (the builder never connected itself, so
310
+ * its handle carries no snapshot).
311
+ * Undefined only when neither source produced one (e.g. no MCP clients).
312
+ */
313
+ _namespacedTools;
314
+ /**
315
+ * Provenance for `_namespacedTools` — exposed (namespaced) name → its
316
+ * originating slot + original tool name. Doubles as the memoization guard
317
+ * for `resolveAuthoritativeSnapshot()`: once set (from either source above)
318
+ * a later call is a no-op. See `_namespacedTools` field doc for the two
319
+ * sources.
320
+ */
321
+ _toolProvenance;
261
322
  /**
262
323
  * The ONE shared knowledge backend for the Stepper path (set during build).
263
324
  * Held so DELETE /v1/sessions/:id can evict a session's entries from it —
@@ -296,6 +357,15 @@ export class SmartServer {
296
357
  _runExecutionControl;
297
358
  _auxiliaryMcpTools;
298
359
  _waitStrategy;
360
+ /**
361
+ * Tool-namespacing strategy (#244) — DI'd via `BuildAgentDeps.toolNamespace`,
362
+ * default `defaultToolNamespace`. Threaded onto the startup builder
363
+ * (`buildBaseBuilder` → `.withToolNamespace`) so its own namespaced snapshot
364
+ * (yaml-builder path) honors it, and reused verbatim by
365
+ * `resolveAuthoritativeSnapshot()`'s fallback build (seam path) so both
366
+ * snapshot sources agree on the same naming rule.
367
+ */
368
+ _toolNamespace;
299
369
  /**
300
370
  * Defaulted construction deps (the BuildAgentDeps DI seam). Required members
301
371
  * always resolve to the real implementation when not injected; `skillHost`
@@ -305,7 +375,10 @@ export class SmartServer {
305
375
  constructor(config, deps = {}) {
306
376
  this.cfg = config;
307
377
  this._mcpSeamInjected =
308
- deps.mcpClients !== undefined || deps.connectMcp !== undefined;
378
+ deps.mcpClients !== undefined ||
379
+ deps.connectMcp !== undefined ||
380
+ deps.connectMcpWithDescriptors !== undefined;
381
+ this._connectMcpInjected = deps.connectMcp !== undefined;
309
382
  this._mcpFailureClassifier =
310
383
  deps.mcpFailureClassifier ?? new DefaultMcpFailureClassifier();
311
384
  // The DI seam carries the CONSUMER-injected factory ONLY (undefined when not
@@ -321,6 +394,7 @@ export class SmartServer {
321
394
  this._runExecutionControl = deps.runExecutionControl;
322
395
  this._auxiliaryMcpTools = deps.auxiliaryMcpTools;
323
396
  this._waitStrategy = deps.waitStrategy;
397
+ this._toolNamespace = deps.toolNamespace ?? defaultToolNamespace;
324
398
  this._deps = {
325
399
  makeLlm: deps.makeLlm ?? ((cfg) => this._makeLlmDefault(cfg)),
326
400
  resolveEmbedder: deps.resolveEmbedder ?? resolveEmbedder,
@@ -330,6 +404,9 @@ export class SmartServer {
330
404
  ...(deps.skillHost ? { skillHost: deps.skillHost } : {}),
331
405
  ...(deps.embedder ? { embedder: deps.embedder } : {}),
332
406
  ...(deps.mcpClients ? { mcpClients: deps.mcpClients } : {}),
407
+ ...(deps.connectMcpWithDescriptors
408
+ ? { connectMcpWithDescriptors: deps.connectMcpWithDescriptors }
409
+ : {}),
333
410
  };
334
411
  }
335
412
  async start() {
@@ -509,7 +586,7 @@ export class SmartServer {
509
586
  });
510
587
  // Resolve the embedder ONCE so the same instance feeds both makeRag and the
511
588
  // subagent context-builder's toolSource (#137). See resolve-agent-embedder.
512
- const resolvedEmbedder = await resolveAgentEmbedder(this.cfg.rag, this._deps.embedder ?? this.cfg.embedder, mergedEmbedderFactories);
589
+ const resolvedEmbedder = await resolveAgentEmbedder(this.cfg.rag, this._deps.embedder ?? this.cfg.embedder, mergedEmbedderFactories, this._fileLogger);
513
590
  // Hold the resolved embedder so buildServerCtx can thread it onto every
514
591
  // pipeline context (the controller pipeline needs it for target-state).
515
592
  this._resolvedEmbedder = resolvedEmbedder;
@@ -657,8 +734,12 @@ export class SmartServer {
657
734
  // Injected seam + YAML `mcp:` → the seam is the SINGLE provisioning point
658
735
  // (the embeddable path must never force a real connect). Stash on
659
736
  // `_stepperMcpClients` so the idempotent guard inside
660
- // buildSharedPipelineInfra does not connect a second time.
661
- this._stepperMcpClients = await this._deps.connectMcp(this.cfg.mcp);
737
+ // buildSharedPipelineInfra does not connect a second time. Precedence
738
+ // among seams (connectMcpWithDescriptors > bare connectMcp > default) —
739
+ // and recording `_sharedMcpClientDescriptors`/`_configuredSlotCount` — is
740
+ // handled by `_resolveMcpWithDescriptors` (#244).
741
+ const resolvedMcp = await this._resolveMcpWithDescriptors(this.cfg.mcp);
742
+ this._stepperMcpClients = resolvedMcp.clients;
662
743
  mcpClients = this._stepperMcpClients;
663
744
  }
664
745
  else {
@@ -727,6 +808,28 @@ export class SmartServer {
727
808
  const agentHandle = await builder.build();
728
809
  const { agent: smartAgent, chat, streamChat, close: closeAgent, circuitBreakers, ragStores, modelProvider, } = agentHandle;
729
810
  const { ragRegistry: globalRagRegistry, mcpClients: globalMcpClients } = agentHandle;
811
+ // ---- Authoritative namespaced snapshot — yaml-builder path (#244) --------
812
+ // The startup builder computes `namespacedTools`/`toolProvenance` (+ the
813
+ // descriptors backing them) ONLY when it owns the connection itself (the
814
+ // `yamlBuilderConnect` path — no ready clients, no injected seam); on every
815
+ // other path the handle's fields stay undefined (the builder skipped its
816
+ // own connect via `withMcpClients`) and `resolveAuthoritativeSnapshot()`
817
+ // builds the fallback later instead. Guard on presence so an undefined
818
+ // handle field never clobbers a fallback snapshot built earlier in
819
+ // `buildSharedPipelineInfra` (the seam / consumer-builder path runs BEFORE
820
+ // `builder.build()`).
821
+ if (agentHandle.namespacedTools !== undefined) {
822
+ this._namespacedTools = agentHandle.namespacedTools;
823
+ }
824
+ if (agentHandle.toolProvenance !== undefined) {
825
+ this._toolProvenance = agentHandle.toolProvenance;
826
+ }
827
+ if (agentHandle.mcpClientDescriptors !== undefined) {
828
+ this._sharedMcpClientDescriptors = agentHandle.mcpClientDescriptors;
829
+ }
830
+ if (agentHandle.configuredSlotCount !== undefined) {
831
+ this._configuredSlotCount = agentHandle.configuredSlotCount;
832
+ }
730
833
  // ---- YAML-builder-connect MCP harvest (single-connect + vectorization) ----
731
834
  // Only on the `yamlBuilderConnect` path (YAML `mcp:`, no ready clients, no
732
835
  // injected seam) did the startup builder OWN the connection AND vectorize the
@@ -777,6 +880,13 @@ export class SmartServer {
777
880
  maxSessions: sessionCfg.maxSessions ?? 1000,
778
881
  cookieName: sessionCfg.cookieName ?? 'sid',
779
882
  mcpClients: globalMcpClients,
883
+ // Shared/global-set provenance (#244) — forwarded so the lifecycle's
884
+ // isolation-OFF branch (no `buildPerSessionMcpClients`, or
885
+ // `mcpSharedClient: true`) still pairs `SessionAgentParts.mcpClients`
886
+ // with the ORIGINAL slotIndex-keyed descriptors, even when the shared
887
+ // set is a FILTERED subset (e.g. LazyConnectionStrategy dropped a slot).
888
+ mcpClientDescriptors: this._sharedMcpClientDescriptors,
889
+ configuredSlotCount: this._configuredSlotCount,
780
890
  // Per-session MCP isolation (#213): only for the YAML `mcp:` path (the one
781
891
  // the server itself connects). Ready-client sources (deps/cfg/plugin) are
782
892
  // consumer/plugin-owned and stay shared. `agent.mcpSharedClient: true`
@@ -945,12 +1055,18 @@ export class SmartServer {
945
1055
  * path).
946
1056
  * Mirrors EXACTLY what the session lifecycle passes to `buildSessionAgent`:
947
1057
  * the global mcpClients + the global ragRegistry + the global tools store,
948
- * with a fresh per-(embedded-)session request logger.
1058
+ * with a fresh per-(embedded-)session request logger. Also threads
1059
+ * `_sharedMcpClientDescriptors`/`_configuredSlotCount` (#244) — the shared
1060
+ * set's per-slot provenance, captured by whichever descriptor-producing
1061
+ * seam won — so the embedded path carries the same descriptor pairing the
1062
+ * per-session lifecycle now does.
949
1063
  */
950
1064
  _embeddedSessionParts(mcpClients, ragRegistry) {
951
1065
  return {
952
1066
  sessionId: 'embedded',
953
1067
  mcpClients: mcpClients ?? this._sharedMcpClients ?? [],
1068
+ mcpClientDescriptors: this._sharedMcpClientDescriptors,
1069
+ configuredSlotCount: this._configuredSlotCount,
954
1070
  toolsRag: this._toolsRag,
955
1071
  ragRegistry,
956
1072
  logger: new SessionRequestLogger(),
@@ -1206,8 +1322,29 @@ export class SmartServer {
1206
1322
  await seedSessionKnowledge(kr, seeds, new Date().toISOString());
1207
1323
  return kr;
1208
1324
  }
1325
+ /**
1326
+ * Memoized namespaced bridge over the GLOBAL `_sharedMcpClients` (#244
1327
+ * Task 8). Built once, lazily, from the authoritative snapshot's provenance
1328
+ * rebound onto `_sharedMcpClients` + `_sharedMcpClientDescriptors` — the
1329
+ * dispatch TARGET stays the server-wide shared clients (never retargeted to
1330
+ * a session's clients here); only `buildServerCtx`'s per-session
1331
+ * `ctx.toolClientMap` uses the session-scoped clients.
1332
+ */
1333
+ _namespacedCallMcpBridge;
1209
1334
  /** callMcp bridge over the shared connected MCP clients (empty when none). */
1210
1335
  callMcp(name, args, signal) {
1336
+ // Namespaced routing (#244 Task 8): when the authoritative snapshot has
1337
+ // provenance (collision path took effect), dispatch namespaced exposed
1338
+ // names to their owning client via the shared bridge — memoized so the
1339
+ // rebind only runs once. No provenance (no MCP / no collisions) falls
1340
+ // back to today's plain `buildMcpBridge` scan, unchanged.
1341
+ if (this._toolProvenance) {
1342
+ if (!this._namespacedCallMcpBridge) {
1343
+ const toolClientMap = rebindProvenanceToClients(this._toolProvenance, this._sharedMcpClients ?? [], this._sharedMcpClientDescriptors);
1344
+ this._namespacedCallMcpBridge = buildNamespacedMcpBridge(toolClientMap, this._mcpFailureClassifier);
1345
+ }
1346
+ return this._namespacedCallMcpBridge(name, args, signal);
1347
+ }
1211
1348
  return buildMcpBridge(this._sharedMcpClients ?? [], this._mcpFailureClassifier)(name, args, signal);
1212
1349
  }
1213
1350
  _mintStepperId() {
@@ -1232,6 +1369,49 @@ export class SmartServer {
1232
1369
  * fallback otherwise). Undefined toolsRag/embedder still yields a usable
1233
1370
  * catalog-backed handle.
1234
1371
  */
1372
+ /**
1373
+ * Resolve MCP clients paired with per-slot descriptors (#244) for the YAML
1374
+ * `mcp:` path, honoring provisioning precedence:
1375
+ * 1. An injected `connectMcpWithDescriptors` seam wins outright — it
1376
+ * already reports its own descriptors/configuredSlotCount.
1377
+ * 2. Else an injected bare `connectMcp` wins next — its clients get
1378
+ * synthesized array-index descriptors (a bare connector cannot report
1379
+ * labels), `configuredSlotCount` = the connected count.
1380
+ * 3. Else (neither seam injected) fall back to the descriptor-producing
1381
+ * default, `connectMcpClientsWithDescriptorsFromConfig`, which DOES
1382
+ * read `cfg.name` labels.
1383
+ * Used at both seam call sites so `_sharedMcpClientDescriptors` /
1384
+ * `_configuredSlotCount` are populated consistently regardless of which
1385
+ * path provisioned the clients (Tasks 6/7 consume these fields).
1386
+ */
1387
+ async _resolveMcpWithDescriptors(mcpCfg) {
1388
+ let resolved;
1389
+ if (this._deps.connectMcpWithDescriptors) {
1390
+ resolved = await this._deps.connectMcpWithDescriptors(mcpCfg);
1391
+ }
1392
+ else if (this._connectMcpInjected) {
1393
+ const clients = await this._deps.connectMcp(mcpCfg);
1394
+ resolved = {
1395
+ clients,
1396
+ clientDescriptors: clients.map((_, slotIndex) => ({ slotIndex })),
1397
+ configuredSlotCount: clients.length,
1398
+ };
1399
+ }
1400
+ else {
1401
+ resolved = await connectMcpClientsWithDescriptorsFromConfig(mcpCfg);
1402
+ }
1403
+ // Recorded as a side effect (not just returned) so `_sharedMcpClientDescriptors`
1404
+ // / `_configuredSlotCount` are populated at BOTH seam call sites through this
1405
+ // single funnel — kept in sync with `_sharedMcpClients` for Tasks 6/7.
1406
+ this._sharedMcpClientDescriptors = resolved.clientDescriptors;
1407
+ this._configuredSlotCount = resolved.configuredSlotCount;
1408
+ this.cfg.log?.({
1409
+ event: 'mcp_descriptors_resolved',
1410
+ configuredSlotCount: this._configuredSlotCount,
1411
+ clientDescriptorCount: this._sharedMcpClientDescriptors?.length,
1412
+ });
1413
+ return resolved;
1414
+ }
1235
1415
  async buildSharedPipelineInfra(input) {
1236
1416
  const { toolsRag, resolvedEmbedder, mcpClients } = input;
1237
1417
  // Record the resolved embedder BEFORE building the knowledge backend so the
@@ -1241,9 +1421,13 @@ export class SmartServer {
1241
1421
  this.buildKnowledgeBackend();
1242
1422
  // MCP clients for the callMcp bridge. DI/plugin clients win; otherwise
1243
1423
  // connect the YAML `mcp:` block ONCE (connect is not safe to invoke twice
1244
- // on the same wrapper — guard via the cache field).
1424
+ // on the same wrapper — guard via the cache field). Seam precedence
1425
+ // (connectMcpWithDescriptors > bare connectMcp > default) — and recording
1426
+ // `_sharedMcpClientDescriptors`/`_configuredSlotCount` — is handled by
1427
+ // `_resolveMcpWithDescriptors` (#244).
1245
1428
  if (!mcpClients && !this._stepperMcpClients) {
1246
- this._stepperMcpClients = await this._deps.connectMcp(this.cfg.mcp);
1429
+ const resolvedMcp = await this._resolveMcpWithDescriptors(this.cfg.mcp);
1430
+ this._stepperMcpClients = resolvedMcp.clients;
1247
1431
  }
1248
1432
  this._sharedMcpClients = mcpClients ?? this._stepperMcpClients ?? [];
1249
1433
  await this.buildToolsRagHandle({ toolsRag, resolvedEmbedder });
@@ -1263,6 +1447,77 @@ export class SmartServer {
1263
1447
  embedder: this._resolvedEmbedder,
1264
1448
  });
1265
1449
  }
1450
+ /**
1451
+ * Resolve the ONE authoritative namespaced tool snapshot (#244 Task 6) —
1452
+ * `this._namespacedTools` / `this._toolProvenance` — from whichever source
1453
+ * applies:
1454
+ * - yaml-builder path: the startup builder already computed it, harvested
1455
+ * onto these SAME fields right after `builder.build()` (see the
1456
+ * `agentHandle.namespacedTools`/`toolProvenance` destructure above). This
1457
+ * method is then a no-op (memoized on `_toolProvenance`).
1458
+ * - seam / consumer-builder path (ready clients, or an injected MCP seam):
1459
+ * the handle carries no snapshot (the builder skipped its own connect via
1460
+ * `withMcpClients`), so build ONE here via `buildNamespacedTools` over
1461
+ * `_sharedMcpClients` + `_sharedMcpClientDescriptors` + the server's
1462
+ * `_toolNamespace` — the SAME strategy instance threaded onto the
1463
+ * startup builder, so both snapshot sources agree on the naming rule.
1464
+ *
1465
+ * Preserves the ORIGINAL client index on a partial `listTools()` failure:
1466
+ * the per-client input is built index-preservingly via `settled.flatMap`
1467
+ * aligned by position (mirroring the builder's own snapshot build), NEVER
1468
+ * `filter().map()` — a middle-client failure must not shift later slots'
1469
+ * `slotIndex`.
1470
+ */
1471
+ async resolveAuthoritativeSnapshot() {
1472
+ // Memoized: a handle-carried snapshot (yaml path) or a prior fallback
1473
+ // build (seam path) both set `_toolProvenance` — either way, done.
1474
+ if (this._toolProvenance)
1475
+ return;
1476
+ const clients = this._sharedMcpClients ?? [];
1477
+ if (clients.length === 0)
1478
+ return;
1479
+ const descs = this._sharedMcpClientDescriptors ??
1480
+ clients.map((_, i) => ({ slotIndex: i }));
1481
+ const settled = await Promise.all(clients.map(async (client) => {
1482
+ try {
1483
+ const result = await client.listTools();
1484
+ return result.ok
1485
+ ? { ok: true, value: result.value }
1486
+ : { ok: false };
1487
+ }
1488
+ catch {
1489
+ return { ok: false };
1490
+ }
1491
+ }));
1492
+ const perClient = settled.flatMap((entry, i) => {
1493
+ if (!entry.ok)
1494
+ return [];
1495
+ return [
1496
+ {
1497
+ slotIndex: descs[i]?.slotIndex ?? i,
1498
+ label: descs[i]?.label,
1499
+ client: clients[i],
1500
+ tools: entry.value,
1501
+ },
1502
+ ];
1503
+ });
1504
+ const built = buildNamespacedTools(perClient, this._toolNamespace);
1505
+ this._namespacedTools = built.tools;
1506
+ this._toolProvenance = built.provenance;
1507
+ // Spec §4: a partial listTools() failure must be LOGGED (aligned with
1508
+ // vectorizeMcpTools's `clientFailures` reporting), never a silent drop —
1509
+ // this snapshot is the ONLY source when there is no writable tools RAG,
1510
+ // in which case vectorizeMcpTools never runs and never logs either.
1511
+ const clientFailures = settled.filter((entry) => !entry.ok).length;
1512
+ if (clientFailures > 0) {
1513
+ this.cfg.log?.({
1514
+ event: 'authoritative_snapshot_client_failures',
1515
+ message: `resolveAuthoritativeSnapshot: ${clientFailures} client(s) failed to list tools`,
1516
+ clientFailures,
1517
+ clientCount: clients.length,
1518
+ });
1519
+ }
1520
+ }
1266
1521
  /**
1267
1522
  * Build `_toolsRagHandle` — a real IToolsRagHandle over the tools RAG store +
1268
1523
  * MCP catalog, dispatching over the ALREADY-RESOLVED `this._sharedMcpClients`.
@@ -1272,10 +1527,19 @@ export class SmartServer {
1272
1527
  * connection there, and `_sharedMcpClients` is harvested from its handle). For
1273
1528
  * the DI/plugin path it still runs early via `buildSharedPipelineInfra`.
1274
1529
  * Requires `this._sharedMcpClients` to be set by the caller.
1530
+ *
1531
+ * Also resolves the authoritative namespaced snapshot (#244 Task 6) via
1532
+ * `resolveAuthoritativeSnapshot()` — a no-op when the yaml-builder path
1533
+ * already harvested one from the handle — and passes it into
1534
+ * `makeToolsRagHandle` so the catalog is keyed by the EXPOSED (namespaced)
1535
+ * name.
1275
1536
  */
1276
1537
  async buildToolsRagHandle(input) {
1277
1538
  const { toolsRag, resolvedEmbedder } = input;
1278
- this._toolsRagHandle = await makeToolsRagHandle(this._sharedMcpClients ?? [], toolsRag, resolvedEmbedder, this.cfg.log);
1539
+ await this.resolveAuthoritativeSnapshot();
1540
+ this._toolsRagHandle = await makeToolsRagHandle(this._sharedMcpClients ?? [], toolsRag, resolvedEmbedder, this.cfg.log, this._namespacedTools
1541
+ ? { namespacedTools: this._namespacedTools }
1542
+ : undefined);
1279
1543
  }
1280
1544
  /**
1281
1545
  * Build the per-session pipeline instance from the registry. Selects the
@@ -1357,6 +1621,17 @@ export class SmartServer {
1357
1621
  // (buildKnowledgeBackend); guard idempotently so the ctx field is always
1358
1622
  // populated even if buildServerCtx is ever reached before start() finishes.
1359
1623
  this.buildKnowledgeBackend();
1624
+ // Per-session namespaced tool-client map (#244 Task 8): rebind the
1625
+ // authoritative snapshot's provenance (`_toolProvenance`, resolved once at
1626
+ // startup via `resolveAuthoritativeSnapshot()`) onto THIS session's own
1627
+ // MCP clients, pairing by `slotIndex` from `scope.parts.mcpClientDescriptors`
1628
+ // — never by array index (a filtered/reordered per-session client set would
1629
+ // otherwise rebind to the wrong client). No provenance (no MCP / no
1630
+ // collisions) leaves `toolClientMap` undefined so the pipeline's own
1631
+ // fallback bridge (`buildMcpBridge(ctx.mcpClients, …)`) applies unchanged.
1632
+ const toolClientMap = this._toolProvenance
1633
+ ? rebindProvenanceToClients(this._toolProvenance, scope.parts.mcpClients, scope.parts.mcpClientDescriptors)
1634
+ : undefined;
1360
1635
  return createServerPipelineContext({
1361
1636
  resolveLlm: (role) => this.resolveRoleLlm(role),
1362
1637
  knowledgeRagFor: (sid) => this.knowledgeRagFor(sid),
@@ -1396,6 +1671,7 @@ export class SmartServer {
1396
1671
  ragRegistry: scope.parts.ragRegistry,
1397
1672
  callMcp: (n, a, s) => this.callMcp(n, a, s),
1398
1673
  mcpClients: scope.parts.mcpClients,
1674
+ ...(toolClientMap ? { toolClientMap } : {}),
1399
1675
  mcpFailureClassifier: this._mcpFailureClassifier,
1400
1676
  ...(this._toolLoopContextStrategyFactory
1401
1677
  ? {
@@ -1544,6 +1820,10 @@ export class SmartServer {
1544
1820
  }
1545
1821
  // Thread the instance-level MCP failure classifier (DI/programmatic only).
1546
1822
  builder = builder.withMcpFailureClassifier(this._mcpFailureClassifier);
1823
+ // Thread the tool-namespacing strategy (#244) so the startup builder's OWN
1824
+ // namespaced snapshot (yaml-builder-connect path) honors the same rule as
1825
+ // `resolveAuthoritativeSnapshot()`'s server-side fallback build below.
1826
+ builder = builder.withToolNamespace(this._toolNamespace);
1547
1827
  // Tool-loop context strategy for the NON-controller pipelines (default / flat /
1548
1828
  // linear / dag / direct SmartAgent). Honor a consumer-injected factory; else
1549
1829
  // default to a bounded RAG-less Window (a strict improvement over Legacy's
@@ -1769,7 +2049,7 @@ export class SmartServer {
1769
2049
  return;
1770
2050
  }
1771
2051
  await rc.server._withSession(rc.req, rc.res, async (graph, sessionId, traceId) => {
1772
- await handleAdapterRequest(rc.req, rc.res, graph.agent ?? rc.smartAgent, anthropicAdapter, { sessionId, traceId, graph });
2052
+ await handleAdapterRequest(rc.req, rc.res, graph.agent ?? rc.smartAgent, anthropicAdapter, { sessionId, traceId, graph }, this.cfg.agent?.heartbeatIntervalMs);
1773
2053
  });
1774
2054
  },
1775
2055
  });