@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.
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pipelines/controller.d.ts.map +1 -1
- package/dist/pipelines/controller.js +9 -1
- package/dist/pipelines/controller.js.map +1 -1
- package/dist/smart-agent/controller/controller-coordinator-handler.d.ts +14 -1
- package/dist/smart-agent/controller/controller-coordinator-handler.d.ts.map +1 -1
- package/dist/smart-agent/controller/controller-coordinator-handler.js +41 -6
- package/dist/smart-agent/controller/controller-coordinator-handler.js.map +1 -1
- package/dist/smart-agent/controller/types.d.ts +4 -0
- package/dist/smart-agent/controller/types.d.ts.map +1 -1
- package/dist/smart-agent/http/adapter-route-handler.d.ts +2 -2
- package/dist/smart-agent/http/adapter-route-handler.d.ts.map +1 -1
- package/dist/smart-agent/http/adapter-route-handler.js +12 -4
- package/dist/smart-agent/http/adapter-route-handler.js.map +1 -1
- package/dist/smart-agent/http/chat-route-handler.d.ts.map +1 -1
- package/dist/smart-agent/http/chat-route-handler.js +87 -79
- package/dist/smart-agent/http/chat-route-handler.js.map +1 -1
- package/dist/smart-agent/http/sse-heartbeat.d.ts +24 -0
- package/dist/smart-agent/http/sse-heartbeat.d.ts.map +1 -0
- package/dist/smart-agent/http/sse-heartbeat.js +62 -0
- package/dist/smart-agent/http/sse-heartbeat.js.map +1 -0
- package/dist/smart-agent/mcp/build-session-mcp-clients.d.ts +8 -6
- package/dist/smart-agent/mcp/build-session-mcp-clients.d.ts.map +1 -1
- package/dist/smart-agent/mcp/build-session-mcp-clients.js +20 -7
- package/dist/smart-agent/mcp/build-session-mcp-clients.js.map +1 -1
- package/dist/smart-agent/mcp/mcp-clients-with-descriptors.d.ts +16 -0
- package/dist/smart-agent/mcp/mcp-clients-with-descriptors.d.ts.map +1 -0
- package/dist/smart-agent/mcp/mcp-clients-with-descriptors.js +2 -0
- package/dist/smart-agent/mcp/mcp-clients-with-descriptors.js.map +1 -0
- package/dist/smart-agent/mcp/namespaced-bridge.d.ts +41 -0
- package/dist/smart-agent/mcp/namespaced-bridge.d.ts.map +1 -0
- package/dist/smart-agent/mcp/namespaced-bridge.js +90 -0
- package/dist/smart-agent/mcp/namespaced-bridge.js.map +1 -0
- package/dist/smart-agent/resolve-agent-embedder.d.ts +3 -3
- package/dist/smart-agent/resolve-agent-embedder.d.ts.map +1 -1
- package/dist/smart-agent/resolve-agent-embedder.js +25 -8
- package/dist/smart-agent/resolve-agent-embedder.js.map +1 -1
- package/dist/smart-agent/resolve-config-sections.d.ts.map +1 -1
- package/dist/smart-agent/resolve-config-sections.js +54 -3
- package/dist/smart-agent/resolve-config-sections.js.map +1 -1
- package/dist/smart-agent/session-lifecycle/index.d.ts +20 -5
- package/dist/smart-agent/session-lifecycle/index.d.ts.map +1 -1
- package/dist/smart-agent/session-lifecycle/index.js +20 -0
- package/dist/smart-agent/session-lifecycle/index.js.map +1 -1
- package/dist/smart-agent/smart-server.d.ts +155 -5
- package/dist/smart-agent/smart-server.d.ts.map +1 -1
- package/dist/smart-agent/smart-server.js +297 -17
- package/dist/smart-agent/smart-server.js.map +1 -1
- package/dist/smart-agent/tools-rag-handle.d.ts +4 -2
- package/dist/smart-agent/tools-rag-handle.d.ts.map +1 -1
- package/dist/smart-agent/tools-rag-handle.js +24 -13
- package/dist/smart-agent/tools-rag-handle.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
247
|
-
* `connectMcp`). In that case MCP is provisioned
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|