arc-1 0.9.27 → 1.0.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 (292) hide show
  1. package/README.md +28 -11
  2. package/dist/adt/abapgit.d.ts +19 -6
  3. package/dist/adt/abapgit.d.ts.map +1 -1
  4. package/dist/adt/abapgit.js +141 -36
  5. package/dist/adt/abapgit.js.map +1 -1
  6. package/dist/adt/client.d.ts +21 -8
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +47 -7
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/config.d.ts +15 -0
  11. package/dist/adt/config.d.ts.map +1 -1
  12. package/dist/adt/config.js.map +1 -1
  13. package/dist/adt/crud.d.ts +7 -2
  14. package/dist/adt/crud.d.ts.map +1 -1
  15. package/dist/adt/crud.js +12 -4
  16. package/dist/adt/crud.js.map +1 -1
  17. package/dist/adt/ddic-xml.d.ts +27 -17
  18. package/dist/adt/ddic-xml.d.ts.map +1 -1
  19. package/dist/adt/ddic-xml.js +45 -34
  20. package/dist/adt/ddic-xml.js.map +1 -1
  21. package/dist/adt/devtools.d.ts +13 -0
  22. package/dist/adt/devtools.d.ts.map +1 -1
  23. package/dist/adt/devtools.js +120 -50
  24. package/dist/adt/devtools.js.map +1 -1
  25. package/dist/adt/errors.d.ts +1 -1
  26. package/dist/adt/errors.d.ts.map +1 -1
  27. package/dist/adt/errors.js +18 -0
  28. package/dist/adt/errors.js.map +1 -1
  29. package/dist/adt/http.d.ts +17 -1
  30. package/dist/adt/http.d.ts.map +1 -1
  31. package/dist/adt/http.js +102 -19
  32. package/dist/adt/http.js.map +1 -1
  33. package/dist/adt/server-driven.d.ts +154 -34
  34. package/dist/adt/server-driven.d.ts.map +1 -1
  35. package/dist/adt/server-driven.js +178 -53
  36. package/dist/adt/server-driven.js.map +1 -1
  37. package/dist/adt/transport.d.ts +6 -6
  38. package/dist/adt/transport.d.ts.map +1 -1
  39. package/dist/adt/transport.js +23 -34
  40. package/dist/adt/transport.js.map +1 -1
  41. package/dist/adt/types.d.ts +47 -6
  42. package/dist/adt/types.d.ts.map +1 -1
  43. package/dist/adt/xml-parser.d.ts +52 -12
  44. package/dist/adt/xml-parser.d.ts.map +1 -1
  45. package/dist/adt/xml-parser.js +120 -10
  46. package/dist/adt/xml-parser.js.map +1 -1
  47. package/dist/authz/policy.d.ts +8 -0
  48. package/dist/authz/policy.d.ts.map +1 -1
  49. package/dist/authz/policy.js +23 -0
  50. package/dist/authz/policy.js.map +1 -1
  51. package/dist/cache/cache.d.ts +2 -34
  52. package/dist/cache/cache.d.ts.map +1 -1
  53. package/dist/cache/cache.js +2 -3
  54. package/dist/cache/cache.js.map +1 -1
  55. package/dist/cache/caching-layer.d.ts +3 -19
  56. package/dist/cache/caching-layer.d.ts.map +1 -1
  57. package/dist/cache/caching-layer.js +2 -26
  58. package/dist/cache/caching-layer.js.map +1 -1
  59. package/dist/cache/memory.d.ts +1 -12
  60. package/dist/cache/memory.d.ts.map +1 -1
  61. package/dist/cache/memory.js +0 -69
  62. package/dist/cache/memory.js.map +1 -1
  63. package/dist/cache/sqlite.d.ts +2 -9
  64. package/dist/cache/sqlite.d.ts.map +1 -1
  65. package/dist/cache/sqlite.js +8 -84
  66. package/dist/cache/sqlite.js.map +1 -1
  67. package/dist/cli.js +6 -3
  68. package/dist/cli.js.map +1 -1
  69. package/dist/context/unit-surgery.d.ts +28 -0
  70. package/dist/context/unit-surgery.d.ts.map +1 -0
  71. package/dist/context/unit-surgery.js +150 -0
  72. package/dist/context/unit-surgery.js.map +1 -0
  73. package/dist/handlers/activate.d.ts.map +1 -1
  74. package/dist/handlers/activate.js +44 -6
  75. package/dist/handlers/activate.js.map +1 -1
  76. package/dist/handlers/context.d.ts +1 -1
  77. package/dist/handlers/context.d.ts.map +1 -1
  78. package/dist/handlers/context.js +98 -28
  79. package/dist/handlers/context.js.map +1 -1
  80. package/dist/handlers/diagnose.d.ts.map +1 -1
  81. package/dist/handlers/diagnose.js +55 -31
  82. package/dist/handlers/diagnose.js.map +1 -1
  83. package/dist/handlers/dispatch.d.ts +12 -1
  84. package/dist/handlers/dispatch.d.ts.map +1 -1
  85. package/dist/handlers/dispatch.js +160 -25
  86. package/dist/handlers/dispatch.js.map +1 -1
  87. package/dist/handlers/feature-cache.d.ts +27 -14
  88. package/dist/handlers/feature-cache.d.ts.map +1 -1
  89. package/dist/handlers/feature-cache.js +63 -25
  90. package/dist/handlers/feature-cache.js.map +1 -1
  91. package/dist/handlers/function-processing.d.ts +61 -0
  92. package/dist/handlers/function-processing.d.ts.map +1 -0
  93. package/dist/handlers/function-processing.js +45 -0
  94. package/dist/handlers/function-processing.js.map +1 -0
  95. package/dist/handlers/git.d.ts.map +1 -1
  96. package/dist/handlers/git.js +44 -19
  97. package/dist/handlers/git.js.map +1 -1
  98. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  99. package/dist/handlers/hyperfocused.js +2 -3
  100. package/dist/handlers/hyperfocused.js.map +1 -1
  101. package/dist/handlers/lint.d.ts.map +1 -1
  102. package/dist/handlers/lint.js +7 -7
  103. package/dist/handlers/lint.js.map +1 -1
  104. package/dist/handlers/manage.d.ts.map +1 -1
  105. package/dist/handlers/manage.js +42 -25
  106. package/dist/handlers/manage.js.map +1 -1
  107. package/dist/handlers/navigate.d.ts.map +1 -1
  108. package/dist/handlers/navigate.js +27 -90
  109. package/dist/handlers/navigate.js.map +1 -1
  110. package/dist/handlers/object-types.d.ts +2 -0
  111. package/dist/handlers/object-types.d.ts.map +1 -1
  112. package/dist/handlers/object-types.js +10 -0
  113. package/dist/handlers/object-types.js.map +1 -1
  114. package/dist/handlers/query.d.ts.map +1 -1
  115. package/dist/handlers/query.js +2 -2
  116. package/dist/handlers/query.js.map +1 -1
  117. package/dist/handlers/read.d.ts.map +1 -1
  118. package/dist/handlers/read.js +64 -41
  119. package/dist/handlers/read.js.map +1 -1
  120. package/dist/handlers/schemas.d.ts +64 -16
  121. package/dist/handlers/schemas.d.ts.map +1 -1
  122. package/dist/handlers/schemas.js +123 -32
  123. package/dist/handlers/schemas.js.map +1 -1
  124. package/dist/handlers/search.d.ts.map +1 -1
  125. package/dist/handlers/search.js +8 -7
  126. package/dist/handlers/search.js.map +1 -1
  127. package/dist/handlers/shared.d.ts +13 -0
  128. package/dist/handlers/shared.d.ts.map +1 -1
  129. package/dist/handlers/shared.js +15 -0
  130. package/dist/handlers/shared.js.map +1 -1
  131. package/dist/handlers/tool-descriptions.d.ts +4 -0
  132. package/dist/handlers/tool-descriptions.d.ts.map +1 -0
  133. package/dist/handlers/tool-descriptions.js +52 -0
  134. package/dist/handlers/tool-descriptions.js.map +1 -0
  135. package/dist/handlers/tool-registry.d.ts +18 -4
  136. package/dist/handlers/tool-registry.d.ts.map +1 -1
  137. package/dist/handlers/tool-registry.js +18 -0
  138. package/dist/handlers/tool-registry.js.map +1 -1
  139. package/dist/handlers/tools.d.ts.map +1 -1
  140. package/dist/handlers/tools.js +113 -90
  141. package/dist/handlers/tools.js.map +1 -1
  142. package/dist/handlers/transport.d.ts +1 -1
  143. package/dist/handlers/transport.d.ts.map +1 -1
  144. package/dist/handlers/transport.js +37 -10
  145. package/dist/handlers/transport.js.map +1 -1
  146. package/dist/handlers/where-used.d.ts +25 -0
  147. package/dist/handlers/where-used.d.ts.map +1 -0
  148. package/dist/handlers/where-used.js +94 -0
  149. package/dist/handlers/where-used.js.map +1 -0
  150. package/dist/handlers/write/class-surgery.d.ts.map +1 -1
  151. package/dist/handlers/write/class-surgery.js +11 -12
  152. package/dist/handlers/write/class-surgery.js.map +1 -1
  153. package/dist/handlers/write/create.d.ts.map +1 -1
  154. package/dist/handlers/write/create.js +194 -84
  155. package/dist/handlers/write/create.js.map +1 -1
  156. package/dist/handlers/write/rap.d.ts.map +1 -1
  157. package/dist/handlers/write/rap.js +9 -9
  158. package/dist/handlers/write/rap.js.map +1 -1
  159. package/dist/handlers/write/unit-surgery.d.ts +5 -0
  160. package/dist/handlers/write/unit-surgery.d.ts.map +1 -0
  161. package/dist/handlers/write/unit-surgery.js +47 -0
  162. package/dist/handlers/write/unit-surgery.js.map +1 -0
  163. package/dist/handlers/write/update-delete.js +8 -8
  164. package/dist/handlers/write/update-delete.js.map +1 -1
  165. package/dist/handlers/write-helpers.d.ts +7 -4
  166. package/dist/handlers/write-helpers.d.ts.map +1 -1
  167. package/dist/handlers/write-helpers.js +118 -63
  168. package/dist/handlers/write-helpers.js.map +1 -1
  169. package/dist/handlers/write.d.ts.map +1 -1
  170. package/dist/handlers/write.js +35 -13
  171. package/dist/handlers/write.js.map +1 -1
  172. package/dist/lint/lint.d.ts.map +1 -1
  173. package/dist/lint/lint.js +4 -2
  174. package/dist/lint/lint.js.map +1 -1
  175. package/dist/probe/catalog.js +1 -1
  176. package/dist/probe/catalog.js.map +1 -1
  177. package/dist/public/ui/app.js +1 -14
  178. package/dist/server/audit.d.ts +27 -15
  179. package/dist/server/audit.d.ts.map +1 -1
  180. package/dist/server/audit.js.map +1 -1
  181. package/dist/server/auth-rate-limit.d.ts +18 -9
  182. package/dist/server/auth-rate-limit.d.ts.map +1 -1
  183. package/dist/server/auth-rate-limit.js +31 -6
  184. package/dist/server/auth-rate-limit.js.map +1 -1
  185. package/dist/server/auth-summary.d.ts +4 -0
  186. package/dist/server/auth-summary.d.ts.map +1 -0
  187. package/dist/server/auth-summary.js +67 -0
  188. package/dist/server/auth-summary.js.map +1 -0
  189. package/dist/server/config.d.ts.map +1 -1
  190. package/dist/server/config.js +106 -3
  191. package/dist/server/config.js.map +1 -1
  192. package/dist/server/context.d.ts +12 -0
  193. package/dist/server/context.d.ts.map +1 -1
  194. package/dist/server/context.js.map +1 -1
  195. package/dist/server/destination-discovery.d.ts +36 -0
  196. package/dist/server/destination-discovery.d.ts.map +1 -0
  197. package/dist/server/destination-discovery.js +130 -0
  198. package/dist/server/destination-discovery.js.map +1 -0
  199. package/dist/server/destination-registry.d.ts +111 -0
  200. package/dist/server/destination-registry.d.ts.map +1 -0
  201. package/dist/server/destination-registry.js +449 -0
  202. package/dist/server/destination-registry.js.map +1 -0
  203. package/dist/server/http.d.ts +43 -1
  204. package/dist/server/http.d.ts.map +1 -1
  205. package/dist/server/http.js +283 -73
  206. package/dist/server/http.js.map +1 -1
  207. package/dist/server/logger.d.ts.map +1 -1
  208. package/dist/server/logger.js +58 -13
  209. package/dist/server/logger.js.map +1 -1
  210. package/dist/server/mcp-rate-limit.d.ts +6 -4
  211. package/dist/server/mcp-rate-limit.d.ts.map +1 -1
  212. package/dist/server/mcp-rate-limit.js +6 -4
  213. package/dist/server/mcp-rate-limit.js.map +1 -1
  214. package/dist/server/multi-target-basic-auth.d.ts +49 -0
  215. package/dist/server/multi-target-basic-auth.d.ts.map +1 -0
  216. package/dist/server/multi-target-basic-auth.js +246 -0
  217. package/dist/server/multi-target-basic-auth.js.map +1 -0
  218. package/dist/server/multi-target-catalog.d.ts +33 -0
  219. package/dist/server/multi-target-catalog.d.ts.map +1 -0
  220. package/dist/server/multi-target-catalog.js +130 -0
  221. package/dist/server/multi-target-catalog.js.map +1 -0
  222. package/dist/server/multi-target-destination-config.d.ts +12 -0
  223. package/dist/server/multi-target-destination-config.d.ts.map +1 -0
  224. package/dist/server/multi-target-destination-config.js +37 -0
  225. package/dist/server/multi-target-destination-config.js.map +1 -0
  226. package/dist/server/multi-target-destination-runtime.d.ts +20 -0
  227. package/dist/server/multi-target-destination-runtime.d.ts.map +1 -0
  228. package/dist/server/multi-target-destination-runtime.js +191 -0
  229. package/dist/server/multi-target-destination-runtime.js.map +1 -0
  230. package/dist/server/multi-target-feature-state.d.ts +6 -0
  231. package/dist/server/multi-target-feature-state.d.ts.map +1 -0
  232. package/dist/server/multi-target-feature-state.js +41 -0
  233. package/dist/server/multi-target-feature-state.js.map +1 -0
  234. package/dist/server/multi-target-identity.d.ts +10 -0
  235. package/dist/server/multi-target-identity.d.ts.map +1 -0
  236. package/dist/server/multi-target-identity.js +18 -0
  237. package/dist/server/multi-target-identity.js.map +1 -0
  238. package/dist/server/multi-target-runtime.d.ts +27 -0
  239. package/dist/server/multi-target-runtime.d.ts.map +1 -0
  240. package/dist/server/multi-target-runtime.js +110 -0
  241. package/dist/server/multi-target-runtime.js.map +1 -0
  242. package/dist/server/multi-target-server.d.ts +53 -0
  243. package/dist/server/multi-target-server.d.ts.map +1 -0
  244. package/dist/server/multi-target-server.js +434 -0
  245. package/dist/server/multi-target-server.js.map +1 -0
  246. package/dist/server/multi-target-shared-auth-state.d.ts +82 -0
  247. package/dist/server/multi-target-shared-auth-state.d.ts.map +1 -0
  248. package/dist/server/multi-target-shared-auth-state.js +226 -0
  249. package/dist/server/multi-target-shared-auth-state.js.map +1 -0
  250. package/dist/server/multi-target-tools.d.ts +20 -0
  251. package/dist/server/multi-target-tools.d.ts.map +1 -0
  252. package/dist/server/multi-target-tools.js +191 -0
  253. package/dist/server/multi-target-tools.js.map +1 -0
  254. package/dist/server/oauth-session-recovery.d.ts +22 -0
  255. package/dist/server/oauth-session-recovery.d.ts.map +1 -0
  256. package/dist/server/oauth-session-recovery.js +69 -0
  257. package/dist/server/oauth-session-recovery.js.map +1 -0
  258. package/dist/server/server.d.ts +39 -27
  259. package/dist/server/server.d.ts.map +1 -1
  260. package/dist/server/server.js +433 -300
  261. package/dist/server/server.js.map +1 -1
  262. package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
  263. package/dist/server/sinks/btp-auditlog.js +83 -11
  264. package/dist/server/sinks/btp-auditlog.js.map +1 -1
  265. package/dist/server/tool-auth.d.ts +10 -0
  266. package/dist/server/tool-auth.d.ts.map +1 -0
  267. package/dist/server/tool-auth.js +75 -0
  268. package/dist/server/tool-auth.js.map +1 -0
  269. package/dist/server/trace-context.d.ts +51 -0
  270. package/dist/server/trace-context.d.ts.map +1 -0
  271. package/dist/server/trace-context.js +100 -0
  272. package/dist/server/trace-context.js.map +1 -0
  273. package/dist/server/types.d.ts +27 -6
  274. package/dist/server/types.d.ts.map +1 -1
  275. package/dist/server/types.js +5 -2
  276. package/dist/server/types.js.map +1 -1
  277. package/dist/server/ui-state.d.ts +0 -2
  278. package/dist/server/ui-state.d.ts.map +1 -1
  279. package/dist/server/ui-state.js +0 -4
  280. package/dist/server/ui-state.js.map +1 -1
  281. package/dist/server/ui.d.ts.map +1 -1
  282. package/dist/server/ui.js +0 -7
  283. package/dist/server/ui.js.map +1 -1
  284. package/package.json +16 -13
  285. package/dist/cache/warmup.d.ts +0 -41
  286. package/dist/cache/warmup.d.ts.map +0 -1
  287. package/dist/cache/warmup.js +0 -337
  288. package/dist/cache/warmup.js.map +0 -1
  289. package/dist/server/elicit.d.ts +0 -43
  290. package/dist/server/elicit.d.ts.map +0 -1
  291. package/dist/server/elicit.js +0 -183
  292. package/dist/server/elicit.js.map +0 -1
@@ -16,22 +16,34 @@ import { AdtApiError } from '../adt/errors.js';
16
16
  import { shouldWarnPreStatefulRelease } from '../adt/release.js';
17
17
  import { deriveUserSafety, deriveUserSafetyFromProfile } from '../adt/safety.js';
18
18
  import { Semaphore } from '../adt/semaphore.js';
19
- import { getActionPolicy, hasRequiredScope } from '../authz/policy.js';
19
+ import { hasRequiredScope } from '../authz/policy.js';
20
20
  import { CachingLayer } from '../cache/caching-layer.js';
21
21
  import { MemoryCache } from '../cache/memory.js';
22
22
  import { getToolRegistry, handleToolCall } from '../handlers/dispatch.js';
23
23
  import { getCachedDiscovery, getCachedFeatures, setCachedDiscovery, setCachedFeatures, } from '../handlers/feature-cache.js';
24
24
  import { getToolDefinitions } from '../handlers/tools.js';
25
+ import { logAuthSummary } from './auth-summary.js';
25
26
  import { API_KEY_PROFILES } from './config.js';
27
+ import { generateRequestId } from './context.js';
26
28
  import { isActionDenied } from './deny-actions.js';
29
+ import { canonicalDestinationUrl, opaqueDestinationValue } from './destination-discovery.js';
30
+ import { DestinationRegistry, duplicateSingleTargetIds, sharedBasicSingleTargetConflicts, targetConnectionFingerprint, } from './destination-registry.js';
27
31
  import { authLibLogger, initLogger, logger } from './logger.js';
28
32
  import { createMcpRateLimiter } from './mcp-rate-limit.js';
33
+ import { handleSharedBasicCall } from './multi-target-basic-auth.js';
34
+ import { RuntimeDestinationLevelError, resolveRuntimeSubaccountPpDestination, } from './multi-target-destination-runtime.js';
35
+ import { ensureMultiTargetFeatureProbe, hasAuthorizationLimitedFeatureEvidence } from './multi-target-feature-state.js';
36
+ import { buildAggregateToolSurfaceConfig, buildMultiTargetConfig, TargetConfigChangedError, validateTargetDrift, } from './multi-target-runtime.js';
37
+ import { buildMultiTargetServerInstructions, prepareMultiTargetCall, structuredToolError, } from './multi-target-server.js';
38
+ import { MultiTargetSharedAuthState } from './multi-target-shared-auth-state.js';
39
+ import { injectTargetSchema, multiTargetToolDefinitions, sapTargetsDefinition } from './multi-target-tools.js';
29
40
  import { loadPlugins } from './plugin-loader.js';
30
41
  import { FileSink } from './sinks/file.js';
42
+ import { filterToolsByAuthScope } from './tool-auth.js';
31
43
  import { startLocalUiServer } from './ui.js';
32
44
  import { UiLogBufferSink } from './ui-log-buffer.js';
33
45
  /** ARC-1 version */
34
- export const VERSION = '0.9.27'; // x-release-please-version
46
+ export const VERSION = '1.0.0'; // x-release-please-version
35
47
  // Soft warning for an unusually large served tools/list. It is re-sent on every conversation (a
36
48
  // recurring token + latency cost), and some MCP clients cap tool-list size. CI's
37
49
  // check-tool-schema-budget guards the built-in surface, but plugin (Custom_*) tools are added at
@@ -94,149 +106,32 @@ export function getToolDefinitionOptions(config, client) {
94
106
  export function getConfiguredToolDefinitions(config, textSearchAvailable, resolvedFeatures, client) {
95
107
  return getToolDefinitions(config, textSearchAvailable, resolvedFeatures, getToolDefinitionOptions(config, client));
96
108
  }
97
- /**
98
- * Prune a tool's action OR type enum (or both) based on the user's scopes and
99
- * the server's denyActions list. Uses ACTION_POLICY as the single source of truth.
100
- *
101
- * - For action-bearing tools (SAPWrite, SAPManage, SAPLint, SAPTransport, SAPGit, ...):
102
- * filter the `action` enum to entries the user can actually invoke.
103
- * - For SAPRead (which uses `type` not `action`): filter the `type` enum. The key one is
104
- * TABLE_CONTENTS, which requires the `data` scope — a read-scoped user sees SAPRead
105
- * without TABLE_CONTENTS in the type enum.
106
- */
107
- function pruneToolByPolicy(tool, scopes, denyActions) {
108
- const schema = tool.inputSchema;
109
- const properties = schema.properties ?? {};
110
- // Determine which enum this tool uses: SAPRead uses `type`; others use `action`.
111
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
112
- const enumDef = properties[enumField] ?? {};
113
- const enumValues = Array.isArray(enumDef.enum) ? enumDef.enum.map(String) : null;
114
- if (!enumValues)
115
- return tool; // no pruning needed
116
- const filtered = enumValues.filter((value) => {
117
- const policy = getActionPolicy(tool.name, value);
118
- if (!policy)
119
- return true; // unknown action/type — let it through and fail at runtime
120
- if (!hasRequiredScope(scopes, policy.scope))
121
- return false;
122
- if (isActionDenied(tool.name, value, denyActions))
123
- return false;
124
- return true;
125
- });
126
- return {
127
- ...tool,
128
- inputSchema: {
129
- ...schema,
130
- properties: {
131
- ...properties,
132
- [enumField]: {
133
- ...enumDef,
134
- enum: filtered,
135
- },
136
- },
137
- },
138
- };
139
- }
140
- function hasNonEmptyActionOrTypeEnum(tool) {
141
- const schema = tool.inputSchema;
142
- const properties = schema.properties ?? {};
143
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
144
- const enumDef = properties[enumField] ?? {};
145
- if (!Array.isArray(enumDef.enum))
146
- return true;
147
- return enumDef.enum.length > 0;
109
+ export { logAuthSummary } from './auth-summary.js';
110
+ export { filterToolsByAuthScope } from './tool-auth.js';
111
+ /** True only when bare /mcp can actually dispatch through resolved shared destination credentials. */
112
+ export function canUseSharedSingleTargetCredentials(config, username, password) {
113
+ const nonJwtSharedCallAllowed = (config.apiKeys?.length ?? 0) > 0 && !(config.ppStrictExplicit && config.ppStrict);
114
+ return !!(username && password) && (!config.ppEnabled || nonJwtSharedCallAllowed);
148
115
  }
149
116
  /**
150
- * Filter tools by user scope + server deny list.
117
+ * Describe the final bare-/mcp SAP connection for multi-target overlap checks.
151
118
  *
152
- * Tools are included when the user has the tool-level scope, OR when any action/type
153
- * in the tool has a scope the user satisfies (in which case the enum is pruned to
154
- * just those action/types). After pruning, tools with empty action/type enums are
155
- * removed entirely.
119
+ * This runs after service-key/destination resolution so legacy SAP_URL credentials
120
+ * and resolved BTP destinations follow the same physical-connection comparison.
156
121
  */
157
- export function filterToolsByAuthScope(tools, scopes, denyActions = []) {
158
- return tools
159
- .filter((tool) => {
160
- // Tool-level visibility: if the whole tool is tool-level-denied, hide it.
161
- if (isActionDenied(tool.name, undefined, denyActions))
162
- return false;
163
- // Must have scope for at least the tool-level default — otherwise no chance any action succeeds.
164
- const toolPolicy = getActionPolicy(tool.name);
165
- if (toolPolicy && !hasRequiredScope(scopes, toolPolicy.scope)) {
166
- // Still allow if any specific action has a scope the user HAS (e.g., SAPManage default='write'
167
- // but flp_list_* have scope='read' — a read user should see SAPManage with pruned actions).
168
- const schema = tool.inputSchema;
169
- const properties = schema.properties ?? {};
170
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
171
- const enumDef = properties[enumField] ?? {};
172
- const enumValues = Array.isArray(enumDef.enum) ? enumDef.enum.map(String) : null;
173
- if (!enumValues)
174
- return false;
175
- const anyAllowed = enumValues.some((v) => {
176
- const p = getActionPolicy(tool.name, v);
177
- return p && hasRequiredScope(scopes, p.scope) && !isActionDenied(tool.name, v, denyActions);
178
- });
179
- if (!anyAllowed)
180
- return false;
181
- }
182
- return true;
183
- })
184
- .map((tool) => pruneToolByPolicy(tool, scopes, denyActions))
185
- .filter(hasNonEmptyActionOrTypeEnum);
186
- }
187
- export function logAuthSummary(config) {
188
- const mcpMethods = [];
189
- const hasApiKeys = !!config.apiKeys?.length;
190
- if (hasApiKeys)
191
- mcpMethods.push('api-keys');
192
- if (config.oidcIssuer && config.oidcAudience)
193
- mcpMethods.push('oidc');
194
- if (config.xsuaaAuth)
195
- mcpMethods.push('xsuaa');
196
- if (mcpMethods.length === 0)
197
- mcpMethods.push('none');
198
- const hasCookie = !!(config.cookieFile || config.cookieString);
199
- const hasBearer = !!(config.btpServiceKey || config.btpServiceKeyFile);
200
- const hasDestination = !!process.env.SAP_BTP_DESTINATION;
201
- const hasBasic = !!(config.username && config.password);
202
- let sapMethod = 'none';
203
- if (config.ppEnabled) {
204
- if (hasDestination)
205
- sapMethod = 'destination+pp';
206
- else if (hasCookie)
207
- sapMethod = 'cookie+pp';
208
- else
209
- sapMethod = 'pp';
210
- }
211
- else if (hasBearer) {
212
- sapMethod = 'bearer';
213
- }
214
- else if (hasDestination) {
215
- sapMethod = 'destination';
216
- }
217
- else if (hasBasic && hasCookie) {
218
- sapMethod = 'basic+cookie';
219
- }
220
- else if (hasCookie) {
221
- sapMethod = 'cookie';
222
- }
223
- else if (hasBasic) {
224
- sapMethod = 'basic';
225
- }
226
- const strictPpOnly = config.ppEnabled && config.ppStrictExplicit && config.ppStrict;
227
- const mixedSapIdentity = config.ppEnabled && hasApiKeys && !strictPpOnly;
228
- const scope = mixedSapIdentity ? 'mixed: JWT per-user, API keys shared' : config.ppEnabled ? 'per-user' : 'shared';
229
- const samlSuffix = config.disableSaml2 ? ' disable-saml=on' : '';
230
- logger.info(`auth: MCP=[${mcpMethods.join(',')}] SAP=${sapMethod} (${scope})${samlSuffix}`);
231
- if (mixedSapIdentity) {
232
- logger.warn('auth topology: PP and API-key calls use different SAP identities. Mixed mode is supported. ' +
233
- 'Separate instances are recommended for clearer SAP identity and audit boundaries; set SAP_PP_STRICT=true ' +
234
- 'on the PP instance when using that topology.');
235
- }
236
- else if (strictPpOnly && hasApiKeys) {
237
- logger.warn('auth topology: ARC1_API_KEYS is configured but SAP_PP_STRICT=true rejects API-key MCP tool calls. ' +
238
- 'Set SAP_PP_STRICT=false for supported mixed operation, or remove/move the keys for a strict PP topology.');
239
- }
122
+ export function resolveSingleTargetOverlapState(config, proxy, hasBearerTokenProvider) {
123
+ const canonicalUrl = canonicalDestinationUrl(config.url);
124
+ const connectionFingerprint = canonicalUrl
125
+ ? targetConnectionFingerprint({
126
+ urlFingerprint: opaqueDestinationValue(canonicalUrl),
127
+ client: config.client,
128
+ cloudConnectorLocationIdFingerprint: proxy?.locationId ? opaqueDestinationValue(proxy.locationId) : undefined,
129
+ })
130
+ : undefined;
131
+ return Object.freeze({
132
+ usesSharedBasic: !hasBearerTokenProvider && canUseSharedSingleTargetCredentials(config, config.username, config.password),
133
+ ...(connectionFingerprint ? { connectionFingerprint } : {}),
134
+ });
240
135
  }
241
136
  /** Build the base ADT client config (without per-user auth) */
242
137
  // When perUser=true, strips shared credentials (username/password/cookies)
@@ -312,21 +207,45 @@ export function selectPerUserProxy(destination, btpProxy) {
312
207
  * The Cloud Connector uses this header to generate an X.509 cert
313
208
  * mapped to the SAP user via CERTRULE.
314
209
  */
315
- async function createPerUserClient(config, btpConfig, btpProxy, userJwt, adtSemaphore) {
316
- const { lookupDestinationWithUserToken } = await import('@arc-mcp/xsuaa-auth/btp');
317
- // Use SAP_BTP_PP_DESTINATION if set, otherwise fall back to SAP_BTP_DESTINATION.
318
- // This enables a dual-destination approach:
319
- // - SAP_BTP_DESTINATION = BasicAuth destination (shared client, startup resolution)
320
- // - SAP_BTP_PP_DESTINATION = PrincipalPropagation destination (per-user, runtime)
321
- const destName = process.env.SAP_BTP_PP_DESTINATION ?? process.env.SAP_BTP_DESTINATION;
210
+ /** Historical single-target dual-destination resolution. */
211
+ export function resolvePpDestinationName(config) {
212
+ if (config.destinationName) {
213
+ return config.destinationName;
214
+ }
215
+ return process.env.SAP_BTP_PP_DESTINATION || process.env.SAP_BTP_DESTINATION;
216
+ }
217
+ async function createPerUserClient(config, btpConfig, btpProxy, userJwt, adtSemaphore, multiTarget) {
218
+ const { createConnectivityProxy, lookupDestinationWithUserToken } = await import('@arc-mcp/xsuaa-auth/btp');
219
+ const destName = resolvePpDestinationName(config);
322
220
  if (!destName) {
323
221
  throw new Error('SAP_BTP_PP_DESTINATION or SAP_BTP_DESTINATION is required for principal propagation');
324
222
  }
325
- const { destination, authTokens } = await lookupDestinationWithUserToken(btpConfig, destName, userJwt, authLibLogger);
326
- const effectiveProxy = selectPerUserProxy(destination, btpProxy);
223
+ let destination;
224
+ let authTokens;
225
+ try {
226
+ ({ destination, authTokens } = multiTarget
227
+ ? await resolveRuntimeSubaccountPpDestination(btpConfig, destName, userJwt)
228
+ : await lookupDestinationWithUserToken(btpConfig, destName, userJwt, authLibLogger));
229
+ }
230
+ catch (error) {
231
+ if (multiTarget && error instanceof RuntimeDestinationLevelError) {
232
+ throw new TargetConfigChangedError(multiTarget.target.target, error.message);
233
+ }
234
+ throw error;
235
+ }
236
+ let resolvedUrl = destination.URL;
237
+ if (multiTarget) {
238
+ const drift = validateTargetDrift(destination, multiTarget.target, multiTarget.instanceConfig);
239
+ if (!drift.ok)
240
+ throw new TargetConfigChangedError(multiTarget.target.target, drift.message);
241
+ resolvedUrl = drift.url;
242
+ }
243
+ const effectiveProxy = multiTarget
244
+ ? (createConnectivityProxy(btpConfig, destination.CloudConnectorLocationId, authLibLogger) ?? undefined)
245
+ : selectPerUserProxy(destination, btpProxy);
327
246
  const adtConfig = buildAdtConfig(config, effectiveProxy, undefined, { perUser: true }, adtSemaphore);
328
247
  // Override URL from destination (in case it differs from startup-resolved URL)
329
- adtConfig.baseUrl = destination.URL;
248
+ adtConfig.baseUrl = resolvedUrl;
330
249
  // Set per-user auth for principal propagation.
331
250
  // Option 1 (Recommended): jwt-bearer exchanged token → Proxy-Authorization
332
251
  // Option 2 (Backward compat): SAML assertion → SAP-Connectivity-Authentication
@@ -401,64 +320,76 @@ export function applyPerUserAuthTokens(adtConfig, authTokens, displayUsername, d
401
320
  * Returns a promise that resolves once probe results are stored in cachedFeatures.
402
321
  * In PP mode (when btpConfig is available for per-user client creation), auth failures
403
322
  * (401/403) on textSearch are treated as "unknown" so the tool schema doesn't hide
404
- * source_code from users who might have authorization. Without btpConfig, PP cannot
405
- * create per-user clients, so shared-client auth failures are definitive.
323
+ * source_code from users who might have authorization. A shared Basic target is the narrow
324
+ * exception: every caller uses the same reviewed SAP identity, so authorization-limited
325
+ * evidence is definitive for that credential generation and may be cached.
406
326
  */
327
+ async function probeClientFeatures(config, client, btpConfig, cacheAuthorizationLimitedEvidence = false) {
328
+ const { defaultFeatureConfig } = await import('../adt/config.js');
329
+ const { probeFeatures } = await import('../adt/features.js');
330
+ const fc = defaultFeatureConfig();
331
+ fc.hana = config.featureHana;
332
+ fc.abapGit = config.featureAbapGit;
333
+ fc.gcts = config.featureGcts;
334
+ fc.rap = config.featureRap;
335
+ fc.amdp = config.featureAmdp;
336
+ fc.ui5 = config.featureUi5;
337
+ fc.transport = config.featureTransport;
338
+ fc.ui5repo = config.featureUi5Repo;
339
+ fc.flp = config.featureFlp;
340
+ const features = await probeFeatures(client.http, fc, config.systemType);
341
+ if (!cacheAuthorizationLimitedEvidence &&
342
+ config.ppEnabled &&
343
+ btpConfig &&
344
+ features.textSearch &&
345
+ !features.textSearch.available) {
346
+ const reason = features.textSearch.reason ?? '';
347
+ if (reason.includes('authorization') || reason.includes('401') || reason.includes('403')) {
348
+ features.textSearch = undefined;
349
+ }
350
+ }
351
+ if (config.targetId && !cacheAuthorizationLimitedEvidence && hasAuthorizationLimitedFeatureEvidence(features)) {
352
+ throw new Error('Multi-target feature evidence remains unknown because one or more probes were authorization-limited.');
353
+ }
354
+ // Log authorization probe results
355
+ if (features.authProbe) {
356
+ const ap = features.authProbe;
357
+ if (ap.searchAccess) {
358
+ logger.info('Authorization probe: object search access is available');
359
+ }
360
+ else {
361
+ logger.warn(`Authorization probe: object search access denied — ${ap.searchReason ?? 'unknown reason'}`);
362
+ }
363
+ if (ap.transportAccess) {
364
+ logger.info('Authorization probe: transport access is available');
365
+ }
366
+ else {
367
+ logger.info(`Authorization probe: transport access is not available — ${ap.transportReason ?? 'unknown reason'}`);
368
+ }
369
+ }
370
+ const featureKey = config.targetId ?? config.destinationName;
371
+ setCachedFeatures(features, featureKey);
372
+ // Proactive warning: on SAP_BASIS < 7.51 the ADT REST handler does not honor the
373
+ // stateful-session header over HTTP, so object writes fail with 423 "invalid lock
374
+ // handle" until the abapfs_extensions enhancement is installed. Warn at startup —
375
+ // before the first cryptic 423 — but only when writes are enabled (issue #293).
376
+ if (shouldWarnPreStatefulRelease(config.allowWrites, features.abapRelease)) {
377
+ logger.warn(`SAP_BASIS ${features.abapRelease} is below 7.51 and does not natively honor stateful ADT ` +
378
+ 'HTTP sessions — object writes will fail with 423 "invalid lock handle" UNLESS the ' +
379
+ 'abapfs_extensions enhancement is installed on the SAP system ' +
380
+ '(https://github.com/marcellourbani/abapfs_extensions). If writes already work, this is ' +
381
+ 'installed and you can ignore this. See docs/sap-trial-setup.md (423 troubleshooting).');
382
+ }
383
+ setCachedDiscovery(features.discoveryMap ?? new Map(), featureKey);
384
+ }
407
385
  export function runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore) {
408
386
  const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
409
387
  return (async () => {
410
388
  try {
411
- const { defaultFeatureConfig } = await import('../adt/config.js');
412
- const { probeFeatures } = await import('../adt/features.js');
413
- const fc = defaultFeatureConfig();
414
- fc.hana = config.featureHana;
415
- fc.abapGit = config.featureAbapGit;
416
- fc.gcts = config.featureGcts;
417
- fc.rap = config.featureRap;
418
- fc.amdp = config.featureAmdp;
419
- fc.ui5 = config.featureUi5;
420
- fc.transport = config.featureTransport;
421
- fc.ui5repo = config.featureUi5Repo;
422
- fc.flp = config.featureFlp;
423
- const features = await probeFeatures(client.http, fc, config.systemType);
424
- if (config.ppEnabled && btpConfig && features.textSearch && !features.textSearch.available) {
425
- const reason = features.textSearch.reason ?? '';
426
- if (reason.includes('authorization') || reason.includes('401') || reason.includes('403')) {
427
- features.textSearch = undefined;
428
- }
429
- }
430
- // Log authorization probe results
431
- if (features.authProbe) {
432
- const ap = features.authProbe;
433
- if (ap.searchAccess) {
434
- logger.info('Authorization probe: object search access is available');
435
- }
436
- else {
437
- logger.warn(`Authorization probe: object search access denied — ${ap.searchReason ?? 'unknown reason'}`);
438
- }
439
- if (ap.transportAccess) {
440
- logger.info('Authorization probe: transport access is available');
441
- }
442
- else {
443
- logger.info(`Authorization probe: transport access is not available — ${ap.transportReason ?? 'unknown reason'}`);
444
- }
445
- }
446
- setCachedFeatures(features);
447
- // Proactive warning: on SAP_BASIS < 7.51 the ADT REST handler does not honor the
448
- // stateful-session header over HTTP, so object writes fail with 423 "invalid lock
449
- // handle" until the abapfs_extensions enhancement is installed. Warn at startup —
450
- // before the first cryptic 423 — but only when writes are enabled (issue #293).
451
- if (shouldWarnPreStatefulRelease(config.allowWrites, features.abapRelease)) {
452
- logger.warn(`SAP_BASIS ${features.abapRelease} is below 7.51 and does not natively honor stateful ADT ` +
453
- 'HTTP sessions — object writes will fail with 423 "invalid lock handle" UNLESS the ' +
454
- 'abapfs_extensions enhancement is installed on the SAP system ' +
455
- '(https://github.com/marcellourbani/abapfs_extensions). If writes already work, this is ' +
456
- 'installed and you can ignore this. See docs/sap-trial-setup.md (423 troubleshooting).');
457
- }
458
- setCachedDiscovery(features.discoveryMap ?? new Map());
389
+ await probeClientFeatures(config, client, btpConfig);
459
390
  }
460
391
  catch {
461
- setCachedDiscovery(new Map());
392
+ setCachedDiscovery(new Map(), config.targetId ?? config.destinationName);
462
393
  // Probe failed (e.g., SAP system unreachable) — continue with default tool set
463
394
  }
464
395
  })();
@@ -556,23 +487,40 @@ export function formatStartupAuthPreflightToolError(preflight) {
556
487
  `Preflight endpoint: ${preflight.endpoint}\n` +
557
488
  `Checked at: ${preflight.checkedAt}`);
558
489
  }
559
- /**
560
- * Create the MCP server with registered tool handlers.
561
- * @param config Server configuration
562
- * @param btpProxy Optional BTP connectivity proxy config (resolved at startup)
563
- * @param btpConfig Optional BTP service config (for per-user destination lookup)
564
- * @param bearerTokenProvider Optional OAuth bearer token provider (BTP ABAP Environment)
565
- * @param cachingLayer Optional object cache layer
566
- * @param startupProbePromise Promise from runStartupProbe() — ListTools waits on this
567
- * @param startupAuthPreflightPromise Promise from runStartupAuthPreflight() — CallTool blocks on auth failure in shared mode
568
- */
569
- export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter) {
570
- const server = new Server({ name: 'arc-1', version: VERSION }, { capabilities: { tools: {} } });
490
+ /** Sent in the MCP initialize response. Clients that defer tool loading (Claude Code enables tool
491
+ * search by default) use this to decide whether to look for ARC-1's tools at all, so it names the
492
+ * domain first. Keep under 2 KB — Claude Code truncates server instructions silently. */
493
+ const SERVER_INSTRUCTIONS = [
494
+ 'ARC-1 gives this SAP ABAP system a read/write interface over SAP ADT: ABAP source (classes,',
495
+ 'programs, function modules, includes), CDS/RAP artifacts (DDLS, BDEF, SRVD, SRVB), DDIC objects',
496
+ '(tables, domains, data elements), transports, abapGit/gCTS, ATC and ABAP Unit, SQL/table data,',
497
+ 'and syntax/activation. Reach for it for any question about ABAP objects, CDS views, transport',
498
+ 'requests, or dumps/traces on this system.',
499
+ '',
500
+ 'Token-cheap paths, in order — a bare full read is the expensive last resort:',
501
+ '- Understanding an object: SAPContext(action="deps") returns compressed contracts, not source',
502
+ ' (measured 14-264x smaller than SAPRead on the same class).',
503
+ '- One method: SAPRead(type="CLAS", method="name"). Survey signatures: method="*".',
504
+ '- Finding a string: SAPRead(grep="pattern") instead of reading the whole object.',
505
+ '- Blast radius of a CDS change: SAPContext(action="impact").',
506
+ 'Where-used, usages, impact and transport lists are paged: they report a complete "total"/summary',
507
+ 'alongside a capped page — trust that count, not the page length. Other list actions are unpaged.',
508
+ '',
509
+ 'One SAP system per instance: there is no system/destination selector, by design.',
510
+ ].join('\n');
511
+ export function createServer(config, options = {}) {
512
+ const { btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter, multiTarget, } = options;
513
+ const server = new Server({ name: config.serverName, version: VERSION }, {
514
+ capabilities: { tools: { listChanged: true } },
515
+ instructions: multiTarget ? buildMultiTargetServerInstructions(multiTarget) : SERVER_INSTRUCTIONS,
516
+ });
571
517
  const apiKeyProvenanceVerifier = createConfiguredApiKeyVerifier(config);
572
518
  // Create default ADT client (shared, uses startup-time credentials or OAuth bearer).
573
519
  // Passes the shared server-wide semaphore so per-user PP clients (created at request
574
520
  // time) share the same Layer 3 concurrency cap.
575
- const defaultClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
521
+ const defaultClient = multiTarget
522
+ ? undefined
523
+ : new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
576
524
  // Cookie-auth preflight propagation: when startup preflight returned a non-blocking
577
525
  // 401 in SAP_COOKIE_FILE mode, the throwaway preflight client marked itself stale —
578
526
  // but the long-lived defaultClient was constructed independently with cookies read at
@@ -584,13 +532,15 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
584
532
  let schemaNullableAutoClientInfoLogged = false;
585
533
  // Register tool listing — filtered by user's scopes when auth is active
586
534
  server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
587
- // Wait for the startup probe (if provided), but with a timeout so a slow/unreachable
588
- // SAP system doesn't stall the MCP connection setup. If the probe doesn't finish in
589
- // time, fall back to the default tool set (textSearch unknown = show source_code).
590
- if (startupProbePromise) {
591
- await Promise.race([startupProbePromise, new Promise((resolve) => setTimeout(resolve, 10_000))]);
592
- }
593
- const features = getCachedFeatures();
535
+ // Never wait for the startup probe here. tools/list is protocol handshake, not SAP work:
536
+ // clients cancel it on their own schedule (Cline at ~5s) and a probe against a real system
537
+ // can outlast that, which used to leave the client with zero tools. Answer from whatever is
538
+ // cached now — unknown features yield a SUPERSET of the probed surface, never a subset, so
539
+ // nothing is ever missing — and let tools/list_changed deliver the narrowed list below.
540
+ const featureKey = config.targetId ?? config.destinationName;
541
+ // Multi-target schemas are immutable process contracts. User-backed feature probes may
542
+ // improve runtime errors, but must never rewrite another user's tools/list response.
543
+ const features = multiTarget ? undefined : getCachedFeatures(featureKey);
594
544
  const clientVersion = server.getClientVersion();
595
545
  if (config.schemaNullableOptionals === 'auto' && !schemaNullableAutoClientInfoLogged) {
596
546
  schemaNullableAutoClientInfoLogged = true;
@@ -600,6 +550,20 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
600
550
  });
601
551
  }
602
552
  let tools = getConfiguredToolDefinitions(config, features?.textSearch?.available, features, clientVersion);
553
+ if (multiTarget) {
554
+ tools =
555
+ !multiTarget.registry.available ||
556
+ (multiTarget.mode === 'aggregate' && multiTarget.registry.targets.length === 0)
557
+ ? []
558
+ : multiTargetToolDefinitions(tools, config);
559
+ if (multiTarget.mode === 'aggregate') {
560
+ if (tools.length > 0)
561
+ tools = tools.map((tool) => injectTargetSchema(tool, multiTarget.registry.targets));
562
+ const isAdmin = extra.authInfo ? hasRequiredScope(extra.authInfo.scopes, 'admin') : false;
563
+ if (multiTarget.registry.targets.length > 1 || isAdmin)
564
+ tools.push(sapTargetsDefinition());
565
+ }
566
+ }
603
567
  // When authenticated, only show tools the user has scopes for
604
568
  if (extra.authInfo) {
605
569
  tools = filterToolsByAuthScope(tools, extra.authInfo.scopes, config.denyActions);
@@ -607,7 +571,7 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
607
571
  // FEAT-61: append plugin (Custom_*) tools, gated identically to built-ins (deny-list + scope +
608
572
  // `availableOn` system-type visibility). Hyperfocused mode is out of scope for plugins (spec §10),
609
573
  // so its single `SAP` tool is the only surface there.
610
- if (config.toolMode !== 'hyperfocused') {
574
+ if (!multiTarget && config.toolMode !== 'hyperfocused') {
611
575
  const systemType = features?.systemType;
612
576
  for (const entry of getToolRegistry().list()) {
613
577
  if (entry.source !== 'plugin' || !entry.listing)
@@ -632,9 +596,32 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
632
596
  });
633
597
  // Register tool call handler — passes authInfo for scope enforcement + audit logging
634
598
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
599
+ const requestId = generateRequestId();
635
600
  const toolName = request.params.name;
636
- const args = (request.params.arguments ?? {});
637
- if (startupAuthPreflightPromise) {
601
+ const rawArgs = (request.params.arguments ?? {});
602
+ let args = rawArgs;
603
+ let activeConfig = config;
604
+ let selectedTarget;
605
+ let multiTargetMcpRateLimitConsumed = false;
606
+ let multiError = (code, message, details = {}) => structuredToolError(code, message, details);
607
+ if (multiTarget) {
608
+ const prepared = await prepareMultiTargetCall({
609
+ options: multiTarget,
610
+ toolName,
611
+ rawArgs,
612
+ requestId,
613
+ authInfo: extra.authInfo,
614
+ mcpRateLimiter,
615
+ });
616
+ if (prepared.handled)
617
+ return prepared.result;
618
+ args = prepared.args;
619
+ activeConfig = prepared.activeConfig;
620
+ selectedTarget = prepared.selectedTarget;
621
+ multiError = prepared.error;
622
+ multiTargetMcpRateLimitConsumed = prepared.mcpRateLimitConsumed;
623
+ }
624
+ if (startupAuthPreflightPromise && !multiTarget) {
638
625
  const startupAuth = await startupAuthPreflightPromise;
639
626
  if (startupAuth.blocking) {
640
627
  return {
@@ -651,7 +638,7 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
651
638
  // stale so its first call goes straight to the lazy reload path instead of repeating
652
639
  // the failure. Fires once per process; subsequent calls early-return.
653
640
  if (!preflightStalePropagated && startupAuth.status === 'inconclusive' && startupAuth.statusCode === 401) {
654
- defaultClient.http.markCookiesStale();
641
+ defaultClient?.http.markCookiesStale();
655
642
  preflightStalePropagated = true;
656
643
  }
657
644
  }
@@ -664,34 +651,90 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
664
651
  const apiKeyProfile = await configuredApiKeyProfile(apiKeyProvenanceVerifier, token);
665
652
  const isApiKey = apiKeyProfile !== undefined;
666
653
  const isJwt = !isApiKey && typeof token === 'string' && token.split('.').length === 3;
667
- if (config.ppEnabled && isJwt) {
654
+ const dispatchWithClient = async (resolvedClient, perUserClient, postDispatchResult) => {
655
+ if (multiTarget && btpConfig) {
656
+ try {
657
+ const targetKey = activeConfig.targetId;
658
+ const cacheAuthorizationLimitedEvidence = selectedTarget?.authentication === 'BasicAuthentication';
659
+ await ensureMultiTargetFeatureProbe(targetKey, !!targetKey && !!getCachedFeatures(targetKey), () => probeClientFeatures(activeConfig, resolvedClient, btpConfig, cacheAuthorizationLimitedEvidence), () => setCachedFeatures(undefined, targetKey));
660
+ }
661
+ catch {
662
+ // Feature evidence stays unknown. The requested operation still gets one direct attempt.
663
+ }
664
+ }
665
+ const featureKey = activeConfig.targetId ?? activeConfig.destinationName;
666
+ resolvedClient.http.setDiscoveryMap(getCachedDiscovery(featureKey));
667
+ let effectiveClient = resolvedClient;
668
+ if (apiKeyProfile) {
669
+ const profile = API_KEY_PROFILES[apiKeyProfile];
670
+ if (profile) {
671
+ const effectiveSafety = deriveUserSafetyFromProfile(resolvedClient.safety, profile.safety);
672
+ effectiveClient = resolvedClient.withSafety(effectiveSafety);
673
+ }
674
+ }
675
+ else if (extra.authInfo?.scopes) {
676
+ const effectiveSafety = deriveUserSafety(resolvedClient.safety, extra.authInfo.scopes);
677
+ effectiveClient = resolvedClient.withSafety(effectiveSafety);
678
+ }
679
+ effectiveClient.http.setDiscoveryMap(getCachedDiscovery(featureKey));
680
+ const result = await handleToolCall(effectiveClient, activeConfig, toolName, args, extra.authInfo, server, multiTarget ? undefined : cachingLayer, perUserClient, multiTargetMcpRateLimitConsumed ? undefined : mcpRateLimiter, requestId, postDispatchResult);
681
+ return { ...result };
682
+ };
683
+ if (selectedTarget?.authentication === 'BasicAuthentication') {
684
+ return handleSharedBasicCall({
685
+ isJwt,
686
+ btpConfig,
687
+ sharedAuthState: multiTarget?.sharedAuthState,
688
+ instanceConfig: multiTarget?.instanceConfig ?? activeConfig,
689
+ target: selectedTarget,
690
+ requestId,
691
+ user: (extra.authInfo?.extra?.userName ?? extra.authInfo?.clientId),
692
+ clientId: extra.authInfo?.clientId,
693
+ toolName,
694
+ multiError,
695
+ buildClientConfig: (proxy) => buildAdtConfig(activeConfig, proxy, undefined, undefined, adtSemaphore),
696
+ dispatch: (basicClient, postDispatchResult) => dispatchWithClient(basicClient, false, postDispatchResult),
697
+ });
698
+ }
699
+ if (activeConfig.ppEnabled && isJwt) {
668
700
  const ppUser = (extra.authInfo?.extra?.userName ?? extra.authInfo?.clientId);
669
- const ppDest = process.env.SAP_BTP_PP_DESTINATION ?? process.env.SAP_BTP_DESTINATION ?? '';
701
+ const ppDest = resolvePpDestinationName(activeConfig) ?? '';
670
702
  if (!btpConfig) {
671
703
  const errMsg = 'BTP runtime configuration is unavailable for principal propagation';
672
704
  logger.emitAudit({
673
705
  timestamp: new Date().toISOString(),
674
706
  level: 'error',
675
707
  event: 'auth_pp_created',
708
+ requestId,
676
709
  user: ppUser,
677
- destination: ppDest,
710
+ destination: activeConfig.targetId ? undefined : ppDest,
711
+ target: activeConfig.targetId,
712
+ identity: activeConfig.targetId ? 'per-user' : undefined,
678
713
  success: false,
679
714
  errorMessage: errMsg,
680
715
  });
716
+ if (multiTarget) {
717
+ return multiError('PP_SETUP_FAILED', `Principal propagation for ${selectedTarget?.target ?? 'the selected target'} cannot start because BTP runtime configuration is unavailable. Fix the service bindings, then try again now.`, { retryable: true }, 'pp_exchange_failed');
718
+ }
681
719
  return {
682
720
  content: [{ type: 'text', text: `Principal propagation failed: ${errMsg}` }],
683
721
  isError: true,
684
722
  };
685
723
  }
686
724
  try {
687
- client = await createPerUserClient(config, btpConfig, btpProxy, token, adtSemaphore);
725
+ client = await createPerUserClient(activeConfig, btpConfig, btpProxy, token, adtSemaphore, selectedTarget
726
+ ? { target: selectedTarget, instanceConfig: multiTarget?.instanceConfig ?? config }
727
+ : undefined);
688
728
  isPerUserClient = true;
689
729
  logger.emitAudit({
690
730
  timestamp: new Date().toISOString(),
691
731
  level: 'info',
692
732
  event: 'auth_pp_created',
733
+ requestId,
693
734
  user: ppUser,
694
- destination: ppDest,
735
+ destination: activeConfig.targetId ? undefined : ppDest,
736
+ target: activeConfig.targetId,
737
+ identity: activeConfig.targetId ? 'per-user' : undefined,
695
738
  success: true,
696
739
  });
697
740
  }
@@ -701,26 +744,33 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
701
744
  timestamp: new Date().toISOString(),
702
745
  level: 'error',
703
746
  event: 'auth_pp_created',
747
+ requestId,
704
748
  user: ppUser,
705
- destination: ppDest,
749
+ destination: activeConfig.targetId ? undefined : ppDest,
750
+ target: activeConfig.targetId,
751
+ identity: activeConfig.targetId ? 'per-user' : undefined,
706
752
  success: false,
707
753
  errorMessage: errMsg,
708
754
  });
709
755
  // A JWT-authenticated request must never change SAP identity after a PP error.
710
756
  // Non-JWT API-key requests still use the shared client through the branch below.
757
+ if (multiTarget) {
758
+ const changed = err instanceof TargetConfigChangedError;
759
+ return multiError(changed ? 'TARGET_CONFIG_CHANGED' : 'PP_SETUP_FAILED', changed
760
+ ? err.message
761
+ : `Principal propagation for ${selectedTarget?.target ?? 'the selected target'} failed. Check the user mapping and destination/Cloud Connector setup, then ask the client to try again now.`, { retryable: !changed }, changed ? undefined : 'pp_exchange_failed');
762
+ }
711
763
  return {
712
- content: [
713
- {
714
- type: 'text',
715
- text: `Principal propagation failed: ${errMsg}`,
716
- },
717
- ],
764
+ content: [{ type: 'text', text: `Principal propagation failed: ${errMsg}` }],
718
765
  isError: true,
719
766
  };
720
767
  }
721
768
  }
722
- else if (config.ppStrictExplicit && config.ppStrict && config.ppEnabled && !isJwt) {
769
+ else if (activeConfig.ppStrictExplicit && activeConfig.ppStrict && activeConfig.ppEnabled && !isJwt) {
723
770
  // Strict mode with non-JWT token (e.g., API key) — reject
771
+ if (multiTarget) {
772
+ return multiError('PP_SETUP_FAILED', 'Multi-target routes require an XSUAA JWT for strict principal propagation. Sign in with XSUAA and try again.', { retryable: true }, 'pp_exchange_failed');
773
+ }
724
774
  return {
725
775
  content: [
726
776
  {
@@ -731,29 +781,28 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
731
781
  isError: true,
732
782
  };
733
783
  }
734
- // Inject startup discovery MIME map (shared for default and per-user clients).
735
- client.http.setDiscoveryMap(getCachedDiscovery());
736
- // Per-request safety: merge server ceiling with per-user policy.
737
- // - API-key path: authenticated configured key — intersect server with the key profile's partial SafetyConfig.
738
- // - XSUAA/OIDC path: derive from scopes only (server ceiling, scopes can only tighten).
739
- // API-key intersection is stricter — profile can narrow allowedPackages / feature flags
740
- // that scopes alone cannot (scopes don't encode allowedPackages, etc.).
741
- let effectiveClient = client;
742
- if (apiKeyProfile) {
743
- const profile = API_KEY_PROFILES[apiKeyProfile];
744
- if (profile) {
745
- const effectiveSafety = deriveUserSafetyFromProfile(client.safety, profile.safety);
746
- effectiveClient = client.withSafety(effectiveSafety);
747
- }
748
- }
749
- else if (extra.authInfo?.scopes) {
750
- const effectiveSafety = deriveUserSafety(client.safety, extra.authInfo.scopes);
751
- effectiveClient = client.withSafety(effectiveSafety);
784
+ if (!client) {
785
+ return multiTarget
786
+ ? multiError('PP_SETUP_FAILED', 'A per-user SAP client could not be created. Fix principal propagation, then try again now.', { retryable: true }, 'pp_exchange_failed')
787
+ : structuredToolError('PP_SETUP_FAILED', 'A per-user SAP client could not be created.', { retryable: true });
752
788
  }
753
- effectiveClient.http.setDiscoveryMap(getCachedDiscovery());
754
- const result = await handleToolCall(effectiveClient, config, toolName, args, extra.authInfo, server, cachingLayer, isPerUserClient, mcpRateLimiter);
755
- return { ...result };
789
+ return dispatchWithClient(client, isPerUserClient);
756
790
  });
791
+ // Discovery finished after we already answered tools/list with the unprobed superset — tell the
792
+ // client so it can re-fetch and drop what this system does not actually have. stdio only: the
793
+ // HTTP transport builds a fresh Server per request (see serveMcpRequest), so no instance outlives
794
+ // a request to deliver this, and none needs to — by then the probe is cached and the first
795
+ // tools/list of every request is already the narrowed list.
796
+ if (startupProbePromise && !multiTarget && config.transport === 'stdio') {
797
+ startupProbePromise
798
+ .then(() => server.sendToolListChanged())
799
+ .catch((err) => {
800
+ // Best-effort: a client that never connected (or disconnected) must not crash startup.
801
+ logger.debug('Skipped tools/list_changed notification after startup probe', {
802
+ error: err instanceof Error ? err.message : String(err),
803
+ });
804
+ });
805
+ }
757
806
  return server;
758
807
  }
759
808
  /**
@@ -851,7 +900,10 @@ export async function createAndStartServer(config, sources) {
851
900
  });
852
901
  // Pre-flight: warn clearly when no SAP connection is configured so users know
853
902
  // why all feature probes will fail (rather than seeing cryptic network errors).
854
- const hasBtpConnection = !!(config.btpServiceKey || config.btpServiceKeyFile || process.env.SAP_BTP_DESTINATION);
903
+ const hasBtpConnection = !!(config.btpServiceKey ||
904
+ config.btpServiceKeyFile ||
905
+ process.env.SAP_BTP_DESTINATION ||
906
+ config.multiTargetEndpoints);
855
907
  if (!config.url && !hasBtpConnection) {
856
908
  logger.warn('SAP_URL is not configured — no SAP system connection available. ' +
857
909
  'Copy .env.example to .env and set SAP_URL, SAP_USER, SAP_PASSWORD (or configure SAP_BTP_DESTINATION / SAP_BTP_SERVICE_KEY_FILE).');
@@ -882,7 +934,7 @@ export async function createAndStartServer(config, sources) {
882
934
  callbackPort: config.btpOAuthCallbackPort || 'auto',
883
935
  });
884
936
  }
885
- // Resolve BTP Destination if configured (overrides SAP_URL/USER/PASSWORD)
937
+ // Resolve the optional single-target destination. It remains independent from discovered targets.
886
938
  let btpProxy;
887
939
  let btpConfig;
888
940
  const btpDestination = process.env.SAP_BTP_DESTINATION;
@@ -904,12 +956,65 @@ export async function createAndStartServer(config, sources) {
904
956
  }
905
957
  logger.info('BTP destination resolved', {
906
958
  destination: btpDestination,
907
- url: resolved.url,
908
- user: resolved.username,
959
+ hasUrl: !!resolved.url,
960
+ hasSharedCredentials: !!(resolved.username && resolved.password),
909
961
  hasProxy: !!btpProxy,
910
962
  ppEnabled: config.ppEnabled,
911
963
  });
912
964
  }
965
+ const singleTargetOverlap = resolveSingleTargetOverlapState(config, btpProxy, !!bearerTokenProvider);
966
+ const singleTargetConnectionFingerprint = singleTargetOverlap.connectionFingerprint;
967
+ const singleTargetUsesSharedBasic = singleTargetOverlap.usesSharedBasic;
968
+ // Destination-discovered multi-target snapshot. Discovery failure degrades only the new routes.
969
+ let registry;
970
+ if (config.multiTargetEndpoints) {
971
+ const { parseVCAPServices } = await import('@arc-mcp/xsuaa-auth/btp');
972
+ btpConfig ??= parseVCAPServices() ?? undefined;
973
+ if (!btpConfig?.destinationUrl ||
974
+ !btpConfig.destinationClientId ||
975
+ !btpConfig.destinationSecret ||
976
+ !btpConfig.connectivityProxyHost ||
977
+ !btpConfig.connectivityClientId) {
978
+ throw new Error('ARC1_MULTI_TARGET_ENDPOINTS=true requires Destination and Connectivity service bindings in VCAP_SERVICES.');
979
+ }
980
+ try {
981
+ const { discoverDestinations } = await import('./destination-discovery.js');
982
+ registry = DestinationRegistry.fromDiscovery(await discoverDestinations(btpConfig), config);
983
+ }
984
+ catch (error) {
985
+ const message = error instanceof Error ? error.message : 'Destination discovery failed.';
986
+ registry = DestinationRegistry.unavailable({ code: 'REGISTRY_DISCOVERY_ERROR', message });
987
+ logger.error('Multi-target destination discovery failed', { error: message });
988
+ }
989
+ logger.info('Multi-target registry loaded', {
990
+ available: registry.available,
991
+ targets: registry.targets.length,
992
+ quarantined: registry.diagnostics.filter((entry) => entry.status === 'quarantined').length,
993
+ disabled: registry.diagnostics.filter((entry) => entry.status === 'disabled').length,
994
+ revision: registry.revision,
995
+ failure: registry.failure?.code,
996
+ });
997
+ const duplicateSingleTargets = duplicateSingleTargetIds(registry, [btpDestination, process.env.SAP_BTP_PP_DESTINATION], singleTargetConnectionFingerprint);
998
+ const duplicateSharedBasicTargets = sharedBasicSingleTargetConflicts(registry,
999
+ // Only SAP_BTP_DESTINATION can supply the bare /mcp shared credentials.
1000
+ // SAP_BTP_PP_DESTINATION belongs exclusively to the JWT PP path and may
1001
+ // legitimately name a discovered target without creating a shared-identity overlap.
1002
+ btpDestination, singleTargetConnectionFingerprint, singleTargetUsesSharedBasic);
1003
+ if (duplicateSharedBasicTargets.length > 0) {
1004
+ throw new Error(`A shared Basic SAP connection cannot be exposed through both /mcp and multi-target routes in v1 (${duplicateSharedBasicTargets.join(', ')}). Remove one exposure and restart ARC-1.`);
1005
+ }
1006
+ if (duplicateSingleTargets.length > 0) {
1007
+ logger.warn('The single-target /mcp connection is also exposed by discovered multi-target routes. Both remain active, but their policies can differ; verify that this duplicate exposure is intentional.', { targets: duplicateSingleTargets });
1008
+ }
1009
+ if (config.multiTargetAllowBasicAuth) {
1010
+ logger.warn('Experimental multi-target Basic authentication is enabled: Basic targets use a shared SAP identity, ' +
1011
+ 'are visible to all read-scoped users, remain mutation-free, and require exactly one CF app instance.');
1012
+ }
1013
+ }
1014
+ // One process-wide guard shared by every fresh pinned and aggregate MCP Server.
1015
+ // HTTP creates a Server per request, so constructing this inside createServer()
1016
+ // would defeat lockout protection across requests and endpoint styles.
1017
+ const sharedAuthState = config.multiTargetEndpoints ? new MultiTargetSharedAuthState() : undefined;
913
1018
  // ─── Layer 3: shared SAP-bound Semaphore (server-wide cap) ────────
914
1019
  // One Semaphore for the whole process. Threaded into the shared startup client AND
915
1020
  // every per-user PP client built at request time, so ARC1_MAX_CONCURRENT is a true
@@ -934,44 +1039,54 @@ export async function createAndStartServer(config, sources) {
934
1039
  mode: config.cacheMode,
935
1040
  sources: stats.sourceCount,
936
1041
  depGraphs: stats.contractCount,
937
- edges: stats.edgeCount,
938
1042
  });
939
1043
  }
940
- // Run warmup if configured (before starting transport so it completes before serving)
941
- if (config.cacheWarmup && cachingLayer && config.url) {
942
- try {
943
- const { runWarmup } = await import('../cache/warmup.js');
944
- const warmupClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
945
- const result = await runWarmup(warmupClient, cachingLayer, config.cacheWarmupPackages || undefined, config.systemType);
946
- logger.info('Cache warmup completed', {
947
- objects: result.totalObjects,
948
- fetched: result.fetched,
949
- skipped: result.skipped,
950
- failed: result.failed,
951
- edges: result.edgesCreated,
952
- durationMs: result.durationMs,
953
- });
954
- }
955
- catch (err) {
956
- logger.warn('Cache warmup failed — continuing without warm cache', {
957
- error: err instanceof Error ? err.message : String(err),
958
- });
959
- }
960
- }
961
1044
  // Run feature probe once at startup — shared across all requests (stdio and HTTP).
962
1045
  // First run startup auth preflight in shared mode. If it blocks (401/403), skip feature probe
963
1046
  // to avoid firing many failing requests with invalid technical credentials.
964
- const startupAuthPreflightPromise = runStartupAuthPreflight(config, btpProxy, bearerTokenProvider, adtSemaphore);
965
- const startupProbePromise = (async () => {
966
- const authPreflight = await startupAuthPreflightPromise;
967
- if (authPreflight.blocking) {
968
- setCachedFeatures(undefined);
969
- setCachedDiscovery(new Map());
970
- return;
971
- }
972
- await runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore);
973
- })();
974
- const server = createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter);
1047
+ const hasSingleTarget = !!(config.url || btpDestination || bearerTokenProvider);
1048
+ const shouldStartSingleTarget = !config.multiTargetEndpoints || hasSingleTarget;
1049
+ const startupAuthPreflightPromise = shouldStartSingleTarget
1050
+ ? runStartupAuthPreflight(config, btpProxy, bearerTokenProvider, adtSemaphore)
1051
+ : Promise.resolve({
1052
+ status: 'skipped',
1053
+ blocking: false,
1054
+ endpoint: STARTUP_AUTH_ENDPOINT,
1055
+ checkedAt: new Date().toISOString(),
1056
+ reason: 'No single-target /mcp connection is configured.',
1057
+ });
1058
+ const startupProbePromise = shouldStartSingleTarget
1059
+ ? (async () => {
1060
+ const authPreflight = await startupAuthPreflightPromise;
1061
+ if (authPreflight.blocking) {
1062
+ setCachedFeatures(undefined);
1063
+ setCachedDiscovery(new Map());
1064
+ return;
1065
+ }
1066
+ await runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore);
1067
+ })()
1068
+ : Promise.resolve();
1069
+ const buildDefaultServer = () => createServer(config, {
1070
+ btpProxy,
1071
+ btpConfig,
1072
+ bearerTokenProvider,
1073
+ cachingLayer,
1074
+ startupProbePromise,
1075
+ startupAuthPreflightPromise,
1076
+ adtSemaphore,
1077
+ mcpRateLimiter,
1078
+ });
1079
+ const aggregateConfig = registry ? buildAggregateToolSurfaceConfig(config, registry.targets) : undefined;
1080
+ const buildAggregateServer = registry && aggregateConfig && btpConfig
1081
+ ? () => createServer(aggregateConfig, {
1082
+ btpConfig,
1083
+ adtSemaphore,
1084
+ mcpRateLimiter,
1085
+ multiTarget: { mode: 'aggregate', registry, instanceConfig: config, sharedAuthState },
1086
+ })
1087
+ : undefined;
1088
+ const serveSingleTargetEndpoint = shouldStartSingleTarget;
1089
+ const server = serveSingleTargetEndpoint ? buildDefaultServer() : (buildAggregateServer?.() ?? buildDefaultServer());
975
1090
  const uiDeps = config.uiMode !== 'off'
976
1091
  ? {
977
1092
  config,
@@ -980,7 +1095,7 @@ export async function createAndStartServer(config, sources) {
980
1095
  startedAt,
981
1096
  cachingLayer,
982
1097
  logBuffer: uiLogBuffer,
983
- getFeatures: getCachedFeatures,
1098
+ getFeatures: () => getCachedFeatures(),
984
1099
  }
985
1100
  : undefined;
986
1101
  // Shutdown hook for SQLite cache cleanup (guard against double-close from multiple signals).
@@ -993,7 +1108,7 @@ export async function createAndStartServer(config, sources) {
993
1108
  return;
994
1109
  cacheClosed = true;
995
1110
  try {
996
- cachingLayer.cache.close();
1111
+ cachingLayer?.cache.close();
997
1112
  }
998
1113
  catch {
999
1114
  // Ignore close errors during shutdown
@@ -1055,10 +1170,28 @@ export async function createAndStartServer(config, sources) {
1055
1170
  logger.error('Failed to load XSUAA credentials — XSUAA auth will not work', {
1056
1171
  error: err instanceof Error ? err.message : String(err),
1057
1172
  });
1173
+ if (config.multiTargetEndpoints) {
1174
+ throw new Error('ARC1_MULTI_TARGET_ENDPOINTS=true requires a valid bound XSUAA service.');
1175
+ }
1058
1176
  }
1059
1177
  }
1060
1178
  const { startHttpServer } = await import('./http.js');
1061
- await startHttpServer(() => createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter), config, xsuaaCredentials, config.uiMode === 'web' ? uiDeps : undefined);
1179
+ const multiTargets = registry && btpConfig && buildAggregateServer
1180
+ ? {
1181
+ registry,
1182
+ aggregateFactory: buildAggregateServer,
1183
+ createPinnedServer: (target) => {
1184
+ const targetConfig = buildMultiTargetConfig(config, target);
1185
+ return createServer(targetConfig, {
1186
+ btpConfig,
1187
+ adtSemaphore,
1188
+ mcpRateLimiter,
1189
+ multiTarget: { mode: 'pinned', registry, instanceConfig: config, target, sharedAuthState },
1190
+ });
1191
+ },
1192
+ }
1193
+ : undefined;
1194
+ await startHttpServer(serveSingleTargetEndpoint ? buildDefaultServer : undefined, config, xsuaaCredentials, config.uiMode === 'web' ? uiDeps : undefined, multiTargets);
1062
1195
  }
1063
1196
  return server;
1064
1197
  }