arc-1 0.9.25 → 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 (332) hide show
  1. package/README.md +29 -12
  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/authorization-trace.d.ts +23 -0
  7. package/dist/adt/authorization-trace.d.ts.map +1 -0
  8. package/dist/adt/authorization-trace.js +196 -0
  9. package/dist/adt/authorization-trace.js.map +1 -0
  10. package/dist/adt/client.d.ts +21 -8
  11. package/dist/adt/client.d.ts.map +1 -1
  12. package/dist/adt/client.js +47 -7
  13. package/dist/adt/client.js.map +1 -1
  14. package/dist/adt/config.d.ts +15 -0
  15. package/dist/adt/config.d.ts.map +1 -1
  16. package/dist/adt/config.js.map +1 -1
  17. package/dist/adt/crud.d.ts +7 -2
  18. package/dist/adt/crud.d.ts.map +1 -1
  19. package/dist/adt/crud.js +12 -4
  20. package/dist/adt/crud.js.map +1 -1
  21. package/dist/adt/ddic-xml.d.ts +27 -17
  22. package/dist/adt/ddic-xml.d.ts.map +1 -1
  23. package/dist/adt/ddic-xml.js +45 -34
  24. package/dist/adt/ddic-xml.js.map +1 -1
  25. package/dist/adt/devtools.d.ts +13 -0
  26. package/dist/adt/devtools.d.ts.map +1 -1
  27. package/dist/adt/devtools.js +120 -50
  28. package/dist/adt/devtools.js.map +1 -1
  29. package/dist/adt/diagnostics.d.ts +1 -1
  30. package/dist/adt/diagnostics.d.ts.map +1 -1
  31. package/dist/adt/diagnostics.js +1 -0
  32. package/dist/adt/diagnostics.js.map +1 -1
  33. package/dist/adt/errors.d.ts +8 -8
  34. package/dist/adt/errors.d.ts.map +1 -1
  35. package/dist/adt/errors.js +30 -11
  36. package/dist/adt/errors.js.map +1 -1
  37. package/dist/adt/http.d.ts +17 -1
  38. package/dist/adt/http.d.ts.map +1 -1
  39. package/dist/adt/http.js +102 -19
  40. package/dist/adt/http.js.map +1 -1
  41. package/dist/adt/oauth.d.ts +2 -2
  42. package/dist/adt/oauth.d.ts.map +1 -1
  43. package/dist/adt/oauth.js +5 -3
  44. package/dist/adt/oauth.js.map +1 -1
  45. package/dist/adt/package-hierarchy.d.ts.map +1 -1
  46. package/dist/adt/package-hierarchy.js.map +1 -1
  47. package/dist/adt/rap-generate.js.map +1 -1
  48. package/dist/adt/rap-handlers.js.map +1 -1
  49. package/dist/adt/rap-preflight.js.map +1 -1
  50. package/dist/adt/safety.d.ts +13 -13
  51. package/dist/adt/safety.d.ts.map +1 -1
  52. package/dist/adt/semaphore.d.ts.map +1 -1
  53. package/dist/adt/server-driven.d.ts +154 -34
  54. package/dist/adt/server-driven.d.ts.map +1 -1
  55. package/dist/adt/server-driven.js +178 -53
  56. package/dist/adt/server-driven.js.map +1 -1
  57. package/dist/adt/structure-hierarchy.js.map +1 -1
  58. package/dist/adt/transport.d.ts +6 -6
  59. package/dist/adt/transport.d.ts.map +1 -1
  60. package/dist/adt/transport.js +23 -34
  61. package/dist/adt/transport.js.map +1 -1
  62. package/dist/adt/types.d.ts +94 -6
  63. package/dist/adt/types.d.ts.map +1 -1
  64. package/dist/adt/version-diff.js.map +1 -1
  65. package/dist/adt/xml-parser.d.ts +52 -12
  66. package/dist/adt/xml-parser.d.ts.map +1 -1
  67. package/dist/adt/xml-parser.js +120 -10
  68. package/dist/adt/xml-parser.js.map +1 -1
  69. package/dist/aff/validator.js.map +1 -1
  70. package/dist/authz/policy.d.ts +8 -0
  71. package/dist/authz/policy.d.ts.map +1 -1
  72. package/dist/authz/policy.js +24 -0
  73. package/dist/authz/policy.js.map +1 -1
  74. package/dist/cache/cache.d.ts +4 -36
  75. package/dist/cache/cache.d.ts.map +1 -1
  76. package/dist/cache/cache.js +4 -5
  77. package/dist/cache/cache.js.map +1 -1
  78. package/dist/cache/caching-layer.d.ts +6 -22
  79. package/dist/cache/caching-layer.d.ts.map +1 -1
  80. package/dist/cache/caching-layer.js +5 -29
  81. package/dist/cache/caching-layer.js.map +1 -1
  82. package/dist/cache/inactive-list-cache.d.ts.map +1 -1
  83. package/dist/cache/memory.d.ts +2 -13
  84. package/dist/cache/memory.d.ts.map +1 -1
  85. package/dist/cache/memory.js +1 -70
  86. package/dist/cache/memory.js.map +1 -1
  87. package/dist/cache/sqlite.d.ts +3 -10
  88. package/dist/cache/sqlite.d.ts.map +1 -1
  89. package/dist/cache/sqlite.js +9 -85
  90. package/dist/cache/sqlite.js.map +1 -1
  91. package/dist/cli.js +6 -3
  92. package/dist/cli.js.map +1 -1
  93. package/dist/context/compressor.js.map +1 -1
  94. package/dist/context/grep.js.map +1 -1
  95. package/dist/context/unit-surgery.d.ts +28 -0
  96. package/dist/context/unit-surgery.d.ts.map +1 -0
  97. package/dist/context/unit-surgery.js +150 -0
  98. package/dist/context/unit-surgery.js.map +1 -0
  99. package/dist/extract-sap-cookies.js.map +1 -1
  100. package/dist/handlers/activate.d.ts.map +1 -1
  101. package/dist/handlers/activate.js +44 -6
  102. package/dist/handlers/activate.js.map +1 -1
  103. package/dist/handlers/context.d.ts +1 -1
  104. package/dist/handlers/context.d.ts.map +1 -1
  105. package/dist/handlers/context.js +98 -28
  106. package/dist/handlers/context.js.map +1 -1
  107. package/dist/handlers/diagnose.d.ts.map +1 -1
  108. package/dist/handlers/diagnose.js +74 -31
  109. package/dist/handlers/diagnose.js.map +1 -1
  110. package/dist/handlers/dispatch.d.ts +12 -1
  111. package/dist/handlers/dispatch.d.ts.map +1 -1
  112. package/dist/handlers/dispatch.js +160 -25
  113. package/dist/handlers/dispatch.js.map +1 -1
  114. package/dist/handlers/feature-cache.d.ts +27 -14
  115. package/dist/handlers/feature-cache.d.ts.map +1 -1
  116. package/dist/handlers/feature-cache.js +63 -25
  117. package/dist/handlers/feature-cache.js.map +1 -1
  118. package/dist/handlers/function-processing.d.ts +61 -0
  119. package/dist/handlers/function-processing.d.ts.map +1 -0
  120. package/dist/handlers/function-processing.js +45 -0
  121. package/dist/handlers/function-processing.js.map +1 -0
  122. package/dist/handlers/git.d.ts.map +1 -1
  123. package/dist/handlers/git.js +44 -19
  124. package/dist/handlers/git.js.map +1 -1
  125. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  126. package/dist/handlers/hyperfocused.js +2 -3
  127. package/dist/handlers/hyperfocused.js.map +1 -1
  128. package/dist/handlers/lint.d.ts.map +1 -1
  129. package/dist/handlers/lint.js +7 -7
  130. package/dist/handlers/lint.js.map +1 -1
  131. package/dist/handlers/manage.d.ts.map +1 -1
  132. package/dist/handlers/manage.js +47 -28
  133. package/dist/handlers/manage.js.map +1 -1
  134. package/dist/handlers/navigate.d.ts.map +1 -1
  135. package/dist/handlers/navigate.js +27 -90
  136. package/dist/handlers/navigate.js.map +1 -1
  137. package/dist/handlers/object-types.d.ts +2 -0
  138. package/dist/handlers/object-types.d.ts.map +1 -1
  139. package/dist/handlers/object-types.js +10 -0
  140. package/dist/handlers/object-types.js.map +1 -1
  141. package/dist/handlers/query-errors.d.ts +11 -0
  142. package/dist/handlers/query-errors.d.ts.map +1 -0
  143. package/dist/handlers/query-errors.js +216 -0
  144. package/dist/handlers/query-errors.js.map +1 -0
  145. package/dist/handlers/query.d.ts.map +1 -1
  146. package/dist/handlers/query.js +62 -67
  147. package/dist/handlers/query.js.map +1 -1
  148. package/dist/handlers/read.d.ts.map +1 -1
  149. package/dist/handlers/read.js +64 -41
  150. package/dist/handlers/read.js.map +1 -1
  151. package/dist/handlers/schemas.d.ts +285 -234
  152. package/dist/handlers/schemas.d.ts.map +1 -1
  153. package/dist/handlers/schemas.js +126 -32
  154. package/dist/handlers/schemas.js.map +1 -1
  155. package/dist/handlers/search.d.ts.map +1 -1
  156. package/dist/handlers/search.js +8 -7
  157. package/dist/handlers/search.js.map +1 -1
  158. package/dist/handlers/shared.d.ts +13 -0
  159. package/dist/handlers/shared.d.ts.map +1 -1
  160. package/dist/handlers/shared.js +15 -0
  161. package/dist/handlers/shared.js.map +1 -1
  162. package/dist/handlers/tool-descriptions.d.ts +4 -0
  163. package/dist/handlers/tool-descriptions.d.ts.map +1 -0
  164. package/dist/handlers/tool-descriptions.js +52 -0
  165. package/dist/handlers/tool-descriptions.js.map +1 -0
  166. package/dist/handlers/tool-registry.d.ts +20 -6
  167. package/dist/handlers/tool-registry.d.ts.map +1 -1
  168. package/dist/handlers/tool-registry.js +18 -0
  169. package/dist/handlers/tool-registry.js.map +1 -1
  170. package/dist/handlers/tools.d.ts.map +1 -1
  171. package/dist/handlers/tools.js +124 -92
  172. package/dist/handlers/tools.js.map +1 -1
  173. package/dist/handlers/transport.d.ts +1 -1
  174. package/dist/handlers/transport.d.ts.map +1 -1
  175. package/dist/handlers/transport.js +37 -10
  176. package/dist/handlers/transport.js.map +1 -1
  177. package/dist/handlers/where-used.d.ts +25 -0
  178. package/dist/handlers/where-used.d.ts.map +1 -0
  179. package/dist/handlers/where-used.js +94 -0
  180. package/dist/handlers/where-used.js.map +1 -0
  181. package/dist/handlers/write/class-surgery.d.ts.map +1 -1
  182. package/dist/handlers/write/class-surgery.js +11 -12
  183. package/dist/handlers/write/class-surgery.js.map +1 -1
  184. package/dist/handlers/write/create.d.ts.map +1 -1
  185. package/dist/handlers/write/create.js +194 -84
  186. package/dist/handlers/write/create.js.map +1 -1
  187. package/dist/handlers/write/rap.d.ts.map +1 -1
  188. package/dist/handlers/write/rap.js +9 -9
  189. package/dist/handlers/write/rap.js.map +1 -1
  190. package/dist/handlers/write/unit-surgery.d.ts +5 -0
  191. package/dist/handlers/write/unit-surgery.d.ts.map +1 -0
  192. package/dist/handlers/write/unit-surgery.js +47 -0
  193. package/dist/handlers/write/unit-surgery.js.map +1 -0
  194. package/dist/handlers/write/update-delete.js +8 -8
  195. package/dist/handlers/write/update-delete.js.map +1 -1
  196. package/dist/handlers/write-helpers.d.ts +7 -4
  197. package/dist/handlers/write-helpers.d.ts.map +1 -1
  198. package/dist/handlers/write-helpers.js +118 -63
  199. package/dist/handlers/write-helpers.js.map +1 -1
  200. package/dist/handlers/write.d.ts.map +1 -1
  201. package/dist/handlers/write.js +35 -13
  202. package/dist/handlers/write.js.map +1 -1
  203. package/dist/lint/config-builder.js.map +1 -1
  204. package/dist/lint/lint.d.ts.map +1 -1
  205. package/dist/lint/lint.js +4 -2
  206. package/dist/lint/lint.js.map +1 -1
  207. package/dist/probe/catalog.js +1 -1
  208. package/dist/probe/catalog.js.map +1 -1
  209. package/dist/public/testing.js.map +1 -1
  210. package/dist/public/ui/app.js +1 -14
  211. package/dist/registry/tool-registry.d.ts.map +1 -1
  212. package/dist/server/audit.d.ts +27 -15
  213. package/dist/server/audit.d.ts.map +1 -1
  214. package/dist/server/audit.js.map +1 -1
  215. package/dist/server/auth-rate-limit.d.ts +18 -9
  216. package/dist/server/auth-rate-limit.d.ts.map +1 -1
  217. package/dist/server/auth-rate-limit.js +31 -6
  218. package/dist/server/auth-rate-limit.js.map +1 -1
  219. package/dist/server/auth-summary.d.ts +4 -0
  220. package/dist/server/auth-summary.d.ts.map +1 -0
  221. package/dist/server/auth-summary.js +67 -0
  222. package/dist/server/auth-summary.js.map +1 -0
  223. package/dist/server/config.d.ts.map +1 -1
  224. package/dist/server/config.js +110 -7
  225. package/dist/server/config.js.map +1 -1
  226. package/dist/server/context.d.ts +12 -0
  227. package/dist/server/context.d.ts.map +1 -1
  228. package/dist/server/context.js.map +1 -1
  229. package/dist/server/destination-discovery.d.ts +36 -0
  230. package/dist/server/destination-discovery.d.ts.map +1 -0
  231. package/dist/server/destination-discovery.js +130 -0
  232. package/dist/server/destination-discovery.js.map +1 -0
  233. package/dist/server/destination-registry.d.ts +111 -0
  234. package/dist/server/destination-registry.d.ts.map +1 -0
  235. package/dist/server/destination-registry.js +449 -0
  236. package/dist/server/destination-registry.js.map +1 -0
  237. package/dist/server/http.d.ts +43 -1
  238. package/dist/server/http.d.ts.map +1 -1
  239. package/dist/server/http.js +283 -73
  240. package/dist/server/http.js.map +1 -1
  241. package/dist/server/logger.d.ts.map +1 -1
  242. package/dist/server/logger.js +58 -13
  243. package/dist/server/logger.js.map +1 -1
  244. package/dist/server/mcp-rate-limit.d.ts +6 -4
  245. package/dist/server/mcp-rate-limit.d.ts.map +1 -1
  246. package/dist/server/mcp-rate-limit.js +6 -4
  247. package/dist/server/mcp-rate-limit.js.map +1 -1
  248. package/dist/server/multi-target-basic-auth.d.ts +49 -0
  249. package/dist/server/multi-target-basic-auth.d.ts.map +1 -0
  250. package/dist/server/multi-target-basic-auth.js +246 -0
  251. package/dist/server/multi-target-basic-auth.js.map +1 -0
  252. package/dist/server/multi-target-catalog.d.ts +33 -0
  253. package/dist/server/multi-target-catalog.d.ts.map +1 -0
  254. package/dist/server/multi-target-catalog.js +130 -0
  255. package/dist/server/multi-target-catalog.js.map +1 -0
  256. package/dist/server/multi-target-destination-config.d.ts +12 -0
  257. package/dist/server/multi-target-destination-config.d.ts.map +1 -0
  258. package/dist/server/multi-target-destination-config.js +37 -0
  259. package/dist/server/multi-target-destination-config.js.map +1 -0
  260. package/dist/server/multi-target-destination-runtime.d.ts +20 -0
  261. package/dist/server/multi-target-destination-runtime.d.ts.map +1 -0
  262. package/dist/server/multi-target-destination-runtime.js +191 -0
  263. package/dist/server/multi-target-destination-runtime.js.map +1 -0
  264. package/dist/server/multi-target-feature-state.d.ts +6 -0
  265. package/dist/server/multi-target-feature-state.d.ts.map +1 -0
  266. package/dist/server/multi-target-feature-state.js +41 -0
  267. package/dist/server/multi-target-feature-state.js.map +1 -0
  268. package/dist/server/multi-target-identity.d.ts +10 -0
  269. package/dist/server/multi-target-identity.d.ts.map +1 -0
  270. package/dist/server/multi-target-identity.js +18 -0
  271. package/dist/server/multi-target-identity.js.map +1 -0
  272. package/dist/server/multi-target-runtime.d.ts +27 -0
  273. package/dist/server/multi-target-runtime.d.ts.map +1 -0
  274. package/dist/server/multi-target-runtime.js +110 -0
  275. package/dist/server/multi-target-runtime.js.map +1 -0
  276. package/dist/server/multi-target-server.d.ts +53 -0
  277. package/dist/server/multi-target-server.d.ts.map +1 -0
  278. package/dist/server/multi-target-server.js +434 -0
  279. package/dist/server/multi-target-server.js.map +1 -0
  280. package/dist/server/multi-target-shared-auth-state.d.ts +82 -0
  281. package/dist/server/multi-target-shared-auth-state.d.ts.map +1 -0
  282. package/dist/server/multi-target-shared-auth-state.js +226 -0
  283. package/dist/server/multi-target-shared-auth-state.js.map +1 -0
  284. package/dist/server/multi-target-tools.d.ts +20 -0
  285. package/dist/server/multi-target-tools.d.ts.map +1 -0
  286. package/dist/server/multi-target-tools.js +191 -0
  287. package/dist/server/multi-target-tools.js.map +1 -0
  288. package/dist/server/oauth-session-recovery.d.ts +22 -0
  289. package/dist/server/oauth-session-recovery.d.ts.map +1 -0
  290. package/dist/server/oauth-session-recovery.js +69 -0
  291. package/dist/server/oauth-session-recovery.js.map +1 -0
  292. package/dist/server/server.d.ts +47 -25
  293. package/dist/server/server.d.ts.map +1 -1
  294. package/dist/server/server.js +490 -301
  295. package/dist/server/server.js.map +1 -1
  296. package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
  297. package/dist/server/sinks/btp-auditlog.js +83 -11
  298. package/dist/server/sinks/btp-auditlog.js.map +1 -1
  299. package/dist/server/sinks/file.d.ts.map +1 -1
  300. package/dist/server/sinks/file.js.map +1 -1
  301. package/dist/server/sinks/stderr.d.ts.map +1 -1
  302. package/dist/server/sinks/stderr.js.map +1 -1
  303. package/dist/server/tool-auth.d.ts +10 -0
  304. package/dist/server/tool-auth.d.ts.map +1 -0
  305. package/dist/server/tool-auth.js +75 -0
  306. package/dist/server/tool-auth.js.map +1 -0
  307. package/dist/server/trace-context.d.ts +51 -0
  308. package/dist/server/trace-context.d.ts.map +1 -0
  309. package/dist/server/trace-context.js +100 -0
  310. package/dist/server/trace-context.js.map +1 -0
  311. package/dist/server/types.d.ts +28 -7
  312. package/dist/server/types.d.ts.map +1 -1
  313. package/dist/server/types.js +5 -2
  314. package/dist/server/types.js.map +1 -1
  315. package/dist/server/ui-log-buffer.d.ts.map +1 -1
  316. package/dist/server/ui-log-buffer.js.map +1 -1
  317. package/dist/server/ui-state.d.ts +0 -2
  318. package/dist/server/ui-state.d.ts.map +1 -1
  319. package/dist/server/ui-state.js +2 -6
  320. package/dist/server/ui-state.js.map +1 -1
  321. package/dist/server/ui.d.ts.map +1 -1
  322. package/dist/server/ui.js +8 -9
  323. package/dist/server/ui.js.map +1 -1
  324. package/package.json +21 -18
  325. package/dist/cache/warmup.d.ts +0 -41
  326. package/dist/cache/warmup.d.ts.map +0 -1
  327. package/dist/cache/warmup.js +0 -337
  328. package/dist/cache/warmup.js.map +0 -1
  329. package/dist/server/elicit.d.ts +0 -43
  330. package/dist/server/elicit.d.ts.map +0 -1
  331. package/dist/server/elicit.js +0 -183
  332. package/dist/server/elicit.js.map +0 -1
@@ -6,6 +6,7 @@
6
6
  * - stdio (default): for local MCP clients (Claude Desktop, Claude Code, Cursor)
7
7
  * - http-streamable: for remote/containerized deployments
8
8
  */
9
+ import { createApiKeyVerifier } from '@arc-mcp/xsuaa-auth';
9
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
12
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -15,28 +16,66 @@ import { AdtApiError } from '../adt/errors.js';
15
16
  import { shouldWarnPreStatefulRelease } from '../adt/release.js';
16
17
  import { deriveUserSafety, deriveUserSafetyFromProfile } from '../adt/safety.js';
17
18
  import { Semaphore } from '../adt/semaphore.js';
18
- import { getActionPolicy, hasRequiredScope } from '../authz/policy.js';
19
+ import { hasRequiredScope } from '../authz/policy.js';
19
20
  import { CachingLayer } from '../cache/caching-layer.js';
20
21
  import { MemoryCache } from '../cache/memory.js';
21
22
  import { getToolRegistry, handleToolCall } from '../handlers/dispatch.js';
22
23
  import { getCachedDiscovery, getCachedFeatures, setCachedDiscovery, setCachedFeatures, } from '../handlers/feature-cache.js';
23
24
  import { getToolDefinitions } from '../handlers/tools.js';
25
+ import { logAuthSummary } from './auth-summary.js';
24
26
  import { API_KEY_PROFILES } from './config.js';
27
+ import { generateRequestId } from './context.js';
25
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';
26
31
  import { authLibLogger, initLogger, logger } from './logger.js';
27
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';
28
40
  import { loadPlugins } from './plugin-loader.js';
29
41
  import { FileSink } from './sinks/file.js';
42
+ import { filterToolsByAuthScope } from './tool-auth.js';
30
43
  import { startLocalUiServer } from './ui.js';
31
44
  import { UiLogBufferSink } from './ui-log-buffer.js';
32
45
  /** ARC-1 version */
33
- export const VERSION = '0.9.25'; // x-release-please-version
46
+ export const VERSION = '1.0.0'; // x-release-please-version
34
47
  // Soft warning for an unusually large served tools/list. It is re-sent on every conversation (a
35
48
  // recurring token + latency cost), and some MCP clients cap tool-list size. CI's
36
49
  // check-tool-schema-budget guards the built-in surface, but plugin (Custom_*) tools are added at
37
50
  // runtime and invisible to CI — so warn once at serve time if the live list crosses the threshold.
38
51
  const TOOLS_LIST_SOFT_WARN_BYTES = 60_000;
39
52
  let warnedLargeToolsList = false;
53
+ /**
54
+ * Resolve API-key provenance from the configured secret, not from AuthInfo.clientId.
55
+ * XSUAA/OIDC also populate clientId, so a claim such as `azp=api-key:viewer` must
56
+ * never make a JWT take the shared API-key path. This is a second, timing-safe
57
+ * provenance check after the upstream verifier has already authenticated the token.
58
+ */
59
+ function createConfiguredApiKeyVerifier(config) {
60
+ const entries = [];
61
+ for (const entry of config.apiKeys ?? []) {
62
+ if (!API_KEY_PROFILES[entry.profile])
63
+ continue;
64
+ entries.push({ key: entry.key, clientId: `api-key:${entry.profile}` });
65
+ }
66
+ return entries.length > 0 ? createApiKeyVerifier(entries) : undefined;
67
+ }
68
+ async function configuredApiKeyProfile(verifier, token) {
69
+ if (!verifier || typeof token !== 'string')
70
+ return undefined;
71
+ try {
72
+ const authInfo = await verifier(token);
73
+ return authInfo.clientId?.startsWith('api-key:') ? authInfo.clientId.slice('api-key:'.length) : undefined;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
40
79
  function warnIfToolsListTooLarge(tools) {
41
80
  if (warnedLargeToolsList)
42
81
  return;
@@ -67,137 +106,32 @@ export function getToolDefinitionOptions(config, client) {
67
106
  export function getConfiguredToolDefinitions(config, textSearchAvailable, resolvedFeatures, client) {
68
107
  return getToolDefinitions(config, textSearchAvailable, resolvedFeatures, getToolDefinitionOptions(config, client));
69
108
  }
70
- /**
71
- * Prune a tool's action OR type enum (or both) based on the user's scopes and
72
- * the server's denyActions list. Uses ACTION_POLICY as the single source of truth.
73
- *
74
- * - For action-bearing tools (SAPWrite, SAPManage, SAPLint, SAPTransport, SAPGit, ...):
75
- * filter the `action` enum to entries the user can actually invoke.
76
- * - For SAPRead (which uses `type` not `action`): filter the `type` enum. The key one is
77
- * TABLE_CONTENTS, which requires the `data` scope — a read-scoped user sees SAPRead
78
- * without TABLE_CONTENTS in the type enum.
79
- */
80
- function pruneToolByPolicy(tool, scopes, denyActions) {
81
- const schema = tool.inputSchema;
82
- const properties = schema.properties ?? {};
83
- // Determine which enum this tool uses: SAPRead uses `type`; others use `action`.
84
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
85
- const enumDef = properties[enumField] ?? {};
86
- const enumValues = Array.isArray(enumDef.enum) ? enumDef.enum.map(String) : null;
87
- if (!enumValues)
88
- return tool; // no pruning needed
89
- const filtered = enumValues.filter((value) => {
90
- const policy = getActionPolicy(tool.name, value);
91
- if (!policy)
92
- return true; // unknown action/type — let it through and fail at runtime
93
- if (!hasRequiredScope(scopes, policy.scope))
94
- return false;
95
- if (isActionDenied(tool.name, value, denyActions))
96
- return false;
97
- return true;
98
- });
99
- return {
100
- ...tool,
101
- inputSchema: {
102
- ...schema,
103
- properties: {
104
- ...properties,
105
- [enumField]: {
106
- ...enumDef,
107
- enum: filtered,
108
- },
109
- },
110
- },
111
- };
112
- }
113
- function hasNonEmptyActionOrTypeEnum(tool) {
114
- const schema = tool.inputSchema;
115
- const properties = schema.properties ?? {};
116
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
117
- const enumDef = properties[enumField] ?? {};
118
- if (!Array.isArray(enumDef.enum))
119
- return true;
120
- 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);
121
115
  }
122
116
  /**
123
- * Filter tools by user scope + server deny list.
117
+ * Describe the final bare-/mcp SAP connection for multi-target overlap checks.
124
118
  *
125
- * Tools are included when the user has the tool-level scope, OR when any action/type
126
- * in the tool has a scope the user satisfies (in which case the enum is pruned to
127
- * just those action/types). After pruning, tools with empty action/type enums are
128
- * 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.
129
121
  */
130
- export function filterToolsByAuthScope(tools, scopes, denyActions = []) {
131
- return tools
132
- .filter((tool) => {
133
- // Tool-level visibility: if the whole tool is tool-level-denied, hide it.
134
- if (isActionDenied(tool.name, undefined, denyActions))
135
- return false;
136
- // Must have scope for at least the tool-level default — otherwise no chance any action succeeds.
137
- const toolPolicy = getActionPolicy(tool.name);
138
- if (toolPolicy && !hasRequiredScope(scopes, toolPolicy.scope)) {
139
- // Still allow if any specific action has a scope the user HAS (e.g., SAPManage default='write'
140
- // but flp_list_* have scope='read' — a read user should see SAPManage with pruned actions).
141
- const schema = tool.inputSchema;
142
- const properties = schema.properties ?? {};
143
- const enumField = tool.name === 'SAPRead' ? 'type' : 'action';
144
- const enumDef = properties[enumField] ?? {};
145
- const enumValues = Array.isArray(enumDef.enum) ? enumDef.enum.map(String) : null;
146
- if (!enumValues)
147
- return false;
148
- const anyAllowed = enumValues.some((v) => {
149
- const p = getActionPolicy(tool.name, v);
150
- return p && hasRequiredScope(scopes, p.scope) && !isActionDenied(tool.name, v, denyActions);
151
- });
152
- if (!anyAllowed)
153
- return false;
154
- }
155
- return true;
156
- })
157
- .map((tool) => pruneToolByPolicy(tool, scopes, denyActions))
158
- .filter(hasNonEmptyActionOrTypeEnum);
159
- }
160
- export function logAuthSummary(config) {
161
- const mcpMethods = [];
162
- if (config.apiKeys?.length)
163
- mcpMethods.push('api-keys');
164
- if (config.oidcIssuer && config.oidcAudience)
165
- mcpMethods.push('oidc');
166
- if (config.xsuaaAuth)
167
- mcpMethods.push('xsuaa');
168
- if (mcpMethods.length === 0)
169
- mcpMethods.push('none');
170
- const hasCookie = !!(config.cookieFile || config.cookieString);
171
- const hasBearer = !!(config.btpServiceKey || config.btpServiceKeyFile);
172
- const hasDestination = !!process.env.SAP_BTP_DESTINATION;
173
- const hasBasic = !!(config.username && config.password);
174
- let sapMethod = 'none';
175
- if (config.ppEnabled) {
176
- if (hasDestination)
177
- sapMethod = 'destination+pp';
178
- else if (hasCookie)
179
- sapMethod = 'cookie+pp';
180
- else
181
- sapMethod = 'pp';
182
- }
183
- else if (hasBearer) {
184
- sapMethod = 'bearer';
185
- }
186
- else if (hasDestination) {
187
- sapMethod = 'destination';
188
- }
189
- else if (hasBasic && hasCookie) {
190
- sapMethod = 'basic+cookie';
191
- }
192
- else if (hasCookie) {
193
- sapMethod = 'cookie';
194
- }
195
- else if (hasBasic) {
196
- sapMethod = 'basic';
197
- }
198
- const scope = config.ppEnabled ? 'per-user' : 'shared';
199
- const samlSuffix = config.disableSaml2 ? ' disable-saml=on' : '';
200
- logger.info(`auth: MCP=[${mcpMethods.join(',')}] SAP=${sapMethod} (${scope})${samlSuffix}`);
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
+ });
201
135
  }
202
136
  /** Build the base ADT client config (without per-user auth) */
203
137
  // When perUser=true, strips shared credentials (username/password/cookies)
@@ -273,21 +207,45 @@ export function selectPerUserProxy(destination, btpProxy) {
273
207
  * The Cloud Connector uses this header to generate an X.509 cert
274
208
  * mapped to the SAP user via CERTRULE.
275
209
  */
276
- async function createPerUserClient(config, btpConfig, btpProxy, userJwt, adtSemaphore) {
277
- const { lookupDestinationWithUserToken } = await import('@arc-mcp/xsuaa-auth/btp');
278
- // Use SAP_BTP_PP_DESTINATION if set, otherwise fall back to SAP_BTP_DESTINATION.
279
- // This enables a dual-destination approach:
280
- // - SAP_BTP_DESTINATION = BasicAuth destination (shared client, startup resolution)
281
- // - SAP_BTP_PP_DESTINATION = PrincipalPropagation destination (per-user, runtime)
282
- 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);
283
220
  if (!destName) {
284
221
  throw new Error('SAP_BTP_PP_DESTINATION or SAP_BTP_DESTINATION is required for principal propagation');
285
222
  }
286
- const { destination, authTokens } = await lookupDestinationWithUserToken(btpConfig, destName, userJwt, authLibLogger);
287
- 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);
288
246
  const adtConfig = buildAdtConfig(config, effectiveProxy, undefined, { perUser: true }, adtSemaphore);
289
247
  // Override URL from destination (in case it differs from startup-resolved URL)
290
- adtConfig.baseUrl = destination.URL;
248
+ adtConfig.baseUrl = resolvedUrl;
291
249
  // Set per-user auth for principal propagation.
292
250
  // Option 1 (Recommended): jwt-bearer exchanged token → Proxy-Authorization
293
251
  // Option 2 (Backward compat): SAML assertion → SAP-Connectivity-Authentication
@@ -362,64 +320,76 @@ export function applyPerUserAuthTokens(adtConfig, authTokens, displayUsername, d
362
320
  * Returns a promise that resolves once probe results are stored in cachedFeatures.
363
321
  * In PP mode (when btpConfig is available for per-user client creation), auth failures
364
322
  * (401/403) on textSearch are treated as "unknown" so the tool schema doesn't hide
365
- * source_code from users who might have authorization. Without btpConfig, PP cannot
366
- * 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.
367
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
+ }
368
385
  export function runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore) {
369
386
  const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
370
387
  return (async () => {
371
388
  try {
372
- const { defaultFeatureConfig } = await import('../adt/config.js');
373
- const { probeFeatures } = await import('../adt/features.js');
374
- const fc = defaultFeatureConfig();
375
- fc.hana = config.featureHana;
376
- fc.abapGit = config.featureAbapGit;
377
- fc.gcts = config.featureGcts;
378
- fc.rap = config.featureRap;
379
- fc.amdp = config.featureAmdp;
380
- fc.ui5 = config.featureUi5;
381
- fc.transport = config.featureTransport;
382
- fc.ui5repo = config.featureUi5Repo;
383
- fc.flp = config.featureFlp;
384
- const features = await probeFeatures(client.http, fc, config.systemType);
385
- if (config.ppEnabled && btpConfig && features.textSearch && !features.textSearch.available) {
386
- const reason = features.textSearch.reason ?? '';
387
- if (reason.includes('authorization') || reason.includes('401') || reason.includes('403')) {
388
- features.textSearch = undefined;
389
- }
390
- }
391
- // Log authorization probe results
392
- if (features.authProbe) {
393
- const ap = features.authProbe;
394
- if (ap.searchAccess) {
395
- logger.info('Authorization probe: object search access is available');
396
- }
397
- else {
398
- logger.warn(`Authorization probe: object search access denied — ${ap.searchReason ?? 'unknown reason'}`);
399
- }
400
- if (ap.transportAccess) {
401
- logger.info('Authorization probe: transport access is available');
402
- }
403
- else {
404
- logger.info(`Authorization probe: transport access is not available — ${ap.transportReason ?? 'unknown reason'}`);
405
- }
406
- }
407
- setCachedFeatures(features);
408
- // Proactive warning: on SAP_BASIS < 7.51 the ADT REST handler does not honor the
409
- // stateful-session header over HTTP, so object writes fail with 423 "invalid lock
410
- // handle" until the abapfs_extensions enhancement is installed. Warn at startup —
411
- // before the first cryptic 423 — but only when writes are enabled (issue #293).
412
- if (shouldWarnPreStatefulRelease(config.allowWrites, features.abapRelease)) {
413
- logger.warn(`SAP_BASIS ${features.abapRelease} is below 7.51 and does not natively honor stateful ADT ` +
414
- 'HTTP sessions — object writes will fail with 423 "invalid lock handle" UNLESS the ' +
415
- 'abapfs_extensions enhancement is installed on the SAP system ' +
416
- '(https://github.com/marcellourbani/abapfs_extensions). If writes already work, this is ' +
417
- 'installed and you can ignore this. See docs/sap-trial-setup.md (423 troubleshooting).');
418
- }
419
- setCachedDiscovery(features.discoveryMap ?? new Map());
389
+ await probeClientFeatures(config, client, btpConfig);
420
390
  }
421
391
  catch {
422
- setCachedDiscovery(new Map());
392
+ setCachedDiscovery(new Map(), config.targetId ?? config.destinationName);
423
393
  // Probe failed (e.g., SAP system unreachable) — continue with default tool set
424
394
  }
425
395
  })();
@@ -517,22 +487,40 @@ export function formatStartupAuthPreflightToolError(preflight) {
517
487
  `Preflight endpoint: ${preflight.endpoint}\n` +
518
488
  `Checked at: ${preflight.checkedAt}`);
519
489
  }
520
- /**
521
- * Create the MCP server with registered tool handlers.
522
- * @param config Server configuration
523
- * @param btpProxy Optional BTP connectivity proxy config (resolved at startup)
524
- * @param btpConfig Optional BTP service config (for per-user destination lookup)
525
- * @param bearerTokenProvider Optional OAuth bearer token provider (BTP ABAP Environment)
526
- * @param cachingLayer Optional object cache layer
527
- * @param startupProbePromise Promise from runStartupProbe() — ListTools waits on this
528
- * @param startupAuthPreflightPromise Promise from runStartupAuthPreflight() — CallTool blocks on auth failure in shared mode
529
- */
530
- export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter) {
531
- 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
+ });
517
+ const apiKeyProvenanceVerifier = createConfiguredApiKeyVerifier(config);
532
518
  // Create default ADT client (shared, uses startup-time credentials or OAuth bearer).
533
519
  // Passes the shared server-wide semaphore so per-user PP clients (created at request
534
520
  // time) share the same Layer 3 concurrency cap.
535
- 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));
536
524
  // Cookie-auth preflight propagation: when startup preflight returned a non-blocking
537
525
  // 401 in SAP_COOKIE_FILE mode, the throwaway preflight client marked itself stale —
538
526
  // but the long-lived defaultClient was constructed independently with cookies read at
@@ -544,13 +532,15 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
544
532
  let schemaNullableAutoClientInfoLogged = false;
545
533
  // Register tool listing — filtered by user's scopes when auth is active
546
534
  server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
547
- // Wait for the startup probe (if provided), but with a timeout so a slow/unreachable
548
- // SAP system doesn't stall the MCP connection setup. If the probe doesn't finish in
549
- // time, fall back to the default tool set (textSearch unknown = show source_code).
550
- if (startupProbePromise) {
551
- await Promise.race([startupProbePromise, new Promise((resolve) => setTimeout(resolve, 10_000))]);
552
- }
553
- 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);
554
544
  const clientVersion = server.getClientVersion();
555
545
  if (config.schemaNullableOptionals === 'auto' && !schemaNullableAutoClientInfoLogged) {
556
546
  schemaNullableAutoClientInfoLogged = true;
@@ -560,6 +550,20 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
560
550
  });
561
551
  }
562
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
+ }
563
567
  // When authenticated, only show tools the user has scopes for
564
568
  if (extra.authInfo) {
565
569
  tools = filterToolsByAuthScope(tools, extra.authInfo.scopes, config.denyActions);
@@ -567,7 +571,7 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
567
571
  // FEAT-61: append plugin (Custom_*) tools, gated identically to built-ins (deny-list + scope +
568
572
  // `availableOn` system-type visibility). Hyperfocused mode is out of scope for plugins (spec §10),
569
573
  // so its single `SAP` tool is the only surface there.
570
- if (config.toolMode !== 'hyperfocused') {
574
+ if (!multiTarget && config.toolMode !== 'hyperfocused') {
571
575
  const systemType = features?.systemType;
572
576
  for (const entry of getToolRegistry().list()) {
573
577
  if (entry.source !== 'plugin' || !entry.listing)
@@ -592,9 +596,32 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
592
596
  });
593
597
  // Register tool call handler — passes authInfo for scope enforcement + audit logging
594
598
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
599
+ const requestId = generateRequestId();
595
600
  const toolName = request.params.name;
596
- const args = (request.params.arguments ?? {});
597
- 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) {
598
625
  const startupAuth = await startupAuthPreflightPromise;
599
626
  if (startupAuth.blocking) {
600
627
  return {
@@ -611,28 +638,103 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
611
638
  // stale so its first call goes straight to the lazy reload path instead of repeating
612
639
  // the failure. Fires once per process; subsequent calls early-return.
613
640
  if (!preflightStalePropagated && startupAuth.status === 'inconclusive' && startupAuth.statusCode === 401) {
614
- defaultClient.http.markCookiesStale();
641
+ defaultClient?.http.markCookiesStale();
615
642
  preflightStalePropagated = true;
616
643
  }
617
644
  }
618
645
  // Principal propagation: create per-user ADT client if enabled and user JWT available.
619
- // Only attempt PP when the token is a JWT (3 dot-separated parts), not a plain API key.
646
+ // Resolve API-key provenance from the configured secret before checking JWT shape,
647
+ // so dotted API keys remain supported without trusting the cross-verifier clientId field.
620
648
  let client = defaultClient;
621
649
  let isPerUserClient = false;
622
650
  const token = extra.authInfo?.token;
623
- const isJwt = token && token.split('.').length === 3;
624
- if (config.ppEnabled && btpConfig && isJwt) {
651
+ const apiKeyProfile = await configuredApiKeyProfile(apiKeyProvenanceVerifier, token);
652
+ const isApiKey = apiKeyProfile !== undefined;
653
+ const isJwt = !isApiKey && typeof token === 'string' && token.split('.').length === 3;
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) {
625
700
  const ppUser = (extra.authInfo?.extra?.userName ?? extra.authInfo?.clientId);
626
- const ppDest = process.env.SAP_BTP_PP_DESTINATION ?? process.env.SAP_BTP_DESTINATION ?? '';
701
+ const ppDest = resolvePpDestinationName(activeConfig) ?? '';
702
+ if (!btpConfig) {
703
+ const errMsg = 'BTP runtime configuration is unavailable for principal propagation';
704
+ logger.emitAudit({
705
+ timestamp: new Date().toISOString(),
706
+ level: 'error',
707
+ event: 'auth_pp_created',
708
+ requestId,
709
+ user: ppUser,
710
+ destination: activeConfig.targetId ? undefined : ppDest,
711
+ target: activeConfig.targetId,
712
+ identity: activeConfig.targetId ? 'per-user' : undefined,
713
+ success: false,
714
+ errorMessage: errMsg,
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
+ }
719
+ return {
720
+ content: [{ type: 'text', text: `Principal propagation failed: ${errMsg}` }],
721
+ isError: true,
722
+ };
723
+ }
627
724
  try {
628
- 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);
629
728
  isPerUserClient = true;
630
729
  logger.emitAudit({
631
730
  timestamp: new Date().toISOString(),
632
731
  level: 'info',
633
732
  event: 'auth_pp_created',
733
+ requestId,
634
734
  user: ppUser,
635
- destination: ppDest,
735
+ destination: activeConfig.targetId ? undefined : ppDest,
736
+ target: activeConfig.targetId,
737
+ identity: activeConfig.targetId ? 'per-user' : undefined,
636
738
  success: true,
637
739
  });
638
740
  }
@@ -642,29 +744,33 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
642
744
  timestamp: new Date().toISOString(),
643
745
  level: 'error',
644
746
  event: 'auth_pp_created',
747
+ requestId,
645
748
  user: ppUser,
646
- destination: ppDest,
749
+ destination: activeConfig.targetId ? undefined : ppDest,
750
+ target: activeConfig.targetId,
751
+ identity: activeConfig.targetId ? 'per-user' : undefined,
647
752
  success: false,
648
753
  errorMessage: errMsg,
649
754
  });
650
- if (config.ppStrict) {
651
- // Strict mode: PP failure is a hard error — never fall back to shared client.
652
- // This ensures every request runs with the authenticated user's identity.
653
- return {
654
- content: [
655
- {
656
- type: 'text',
657
- text: `Principal propagation failed (SAP_PP_STRICT=true): ${errMsg}`,
658
- },
659
- ],
660
- isError: true,
661
- };
755
+ // A JWT-authenticated request must never change SAP identity after a PP error.
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');
662
762
  }
663
- // Fall back to shared client (service account)
763
+ return {
764
+ content: [{ type: 'text', text: `Principal propagation failed: ${errMsg}` }],
765
+ isError: true,
766
+ };
664
767
  }
665
768
  }
666
- else if (config.ppStrictExplicit && config.ppStrict && config.ppEnabled && !isJwt) {
769
+ else if (activeConfig.ppStrictExplicit && activeConfig.ppStrict && activeConfig.ppEnabled && !isJwt) {
667
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
+ }
668
774
  return {
669
775
  content: [
670
776
  {
@@ -675,30 +781,28 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
675
781
  isError: true,
676
782
  };
677
783
  }
678
- // Inject startup discovery MIME map (shared for default and per-user clients).
679
- client.http.setDiscoveryMap(getCachedDiscovery());
680
- // Per-request safety: merge server ceiling with per-user policy.
681
- // - API-key path: clientId starts with "api-key:<profile>" — intersect server with profile's partial SafetyConfig.
682
- // - XSUAA/OIDC path: derive from scopes only (server ceiling, scopes can only tighten).
683
- // API-key intersection is stricter — profile can narrow allowedPackages / feature flags
684
- // that scopes alone cannot (scopes don't encode allowedPackages, etc.).
685
- let effectiveClient = client;
686
- if (extra.authInfo?.clientId?.startsWith('api-key:')) {
687
- const profileName = extra.authInfo.clientId.slice('api-key:'.length);
688
- const profile = API_KEY_PROFILES[profileName];
689
- if (profile) {
690
- const effectiveSafety = deriveUserSafetyFromProfile(client.safety, profile.safety);
691
- effectiveClient = client.withSafety(effectiveSafety);
692
- }
693
- }
694
- else if (extra.authInfo?.scopes) {
695
- const effectiveSafety = deriveUserSafety(client.safety, extra.authInfo.scopes);
696
- 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 });
697
788
  }
698
- effectiveClient.http.setDiscoveryMap(getCachedDiscovery());
699
- const result = await handleToolCall(effectiveClient, config, toolName, args, extra.authInfo, server, cachingLayer, isPerUserClient, mcpRateLimiter);
700
- return { ...result };
789
+ return dispatchWithClient(client, isPerUserClient);
701
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
+ }
702
806
  return server;
703
807
  }
704
808
  /**
@@ -710,13 +814,14 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
710
814
  * memory-cache or no-cache mode even when better-sqlite3 is not installed
711
815
  * (e.g. cross-platform deploys where native binaries were compiled elsewhere).
712
816
  */
713
- async function createCachingLayer(config) {
817
+ export async function createCachingLayer(config) {
714
818
  const mode = config.cacheMode;
715
819
  if (mode === 'none')
716
820
  return undefined;
717
821
  let cache;
718
- if (mode === 'sqlite' || (mode === 'auto' && config.transport === 'http-streamable')) {
719
- // Persistent cache for http-streamable / Docker — load dynamically
822
+ if (mode === 'sqlite') {
823
+ logger.warn('ARC1_CACHE=sqlite stores SAP source in plaintext at rest; use ARC1_CACHE=memory/none or encrypted storage for IP-sensitive landscapes.');
824
+ // Persistent cache is explicit opt-in because SQLite stores full source bodies.
720
825
  try {
721
826
  const { SqliteCache } = await import('../cache/sqlite.js');
722
827
  cache = new SqliteCache(config.cacheFile);
@@ -729,7 +834,7 @@ async function createCachingLayer(config) {
729
834
  }
730
835
  }
731
836
  else {
732
- // Memory cache for stdio (default)
837
+ // Memory cache for auto/default and explicit memory mode. Avoids source-at-rest by default.
733
838
  cache = new MemoryCache();
734
839
  }
735
840
  const maxActivityEntries = config.uiMode === 'off' ? 0 : undefined;
@@ -795,7 +900,10 @@ export async function createAndStartServer(config, sources) {
795
900
  });
796
901
  // Pre-flight: warn clearly when no SAP connection is configured so users know
797
902
  // why all feature probes will fail (rather than seeing cryptic network errors).
798
- 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);
799
907
  if (!config.url && !hasBtpConnection) {
800
908
  logger.warn('SAP_URL is not configured — no SAP system connection available. ' +
801
909
  'Copy .env.example to .env and set SAP_URL, SAP_USER, SAP_PASSWORD (or configure SAP_BTP_DESTINATION / SAP_BTP_SERVICE_KEY_FILE).');
@@ -826,7 +934,7 @@ export async function createAndStartServer(config, sources) {
826
934
  callbackPort: config.btpOAuthCallbackPort || 'auto',
827
935
  });
828
936
  }
829
- // Resolve BTP Destination if configured (overrides SAP_URL/USER/PASSWORD)
937
+ // Resolve the optional single-target destination. It remains independent from discovered targets.
830
938
  let btpProxy;
831
939
  let btpConfig;
832
940
  const btpDestination = process.env.SAP_BTP_DESTINATION;
@@ -848,12 +956,65 @@ export async function createAndStartServer(config, sources) {
848
956
  }
849
957
  logger.info('BTP destination resolved', {
850
958
  destination: btpDestination,
851
- url: resolved.url,
852
- user: resolved.username,
959
+ hasUrl: !!resolved.url,
960
+ hasSharedCredentials: !!(resolved.username && resolved.password),
853
961
  hasProxy: !!btpProxy,
854
962
  ppEnabled: config.ppEnabled,
855
963
  });
856
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;
857
1018
  // ─── Layer 3: shared SAP-bound Semaphore (server-wide cap) ────────
858
1019
  // One Semaphore for the whole process. Threaded into the shared startup client AND
859
1020
  // every per-user PP client built at request time, so ARC1_MAX_CONCURRENT is a true
@@ -878,44 +1039,54 @@ export async function createAndStartServer(config, sources) {
878
1039
  mode: config.cacheMode,
879
1040
  sources: stats.sourceCount,
880
1041
  depGraphs: stats.contractCount,
881
- edges: stats.edgeCount,
882
1042
  });
883
1043
  }
884
- // Run warmup if configured (before starting transport so it completes before serving)
885
- if (config.cacheWarmup && cachingLayer && config.url) {
886
- try {
887
- const { runWarmup } = await import('../cache/warmup.js');
888
- const warmupClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
889
- const result = await runWarmup(warmupClient, cachingLayer, config.cacheWarmupPackages || undefined, config.systemType);
890
- logger.info('Cache warmup completed', {
891
- objects: result.totalObjects,
892
- fetched: result.fetched,
893
- skipped: result.skipped,
894
- failed: result.failed,
895
- edges: result.edgesCreated,
896
- durationMs: result.durationMs,
897
- });
898
- }
899
- catch (err) {
900
- logger.warn('Cache warmup failed — continuing without warm cache', {
901
- error: err instanceof Error ? err.message : String(err),
902
- });
903
- }
904
- }
905
1044
  // Run feature probe once at startup — shared across all requests (stdio and HTTP).
906
1045
  // First run startup auth preflight in shared mode. If it blocks (401/403), skip feature probe
907
1046
  // to avoid firing many failing requests with invalid technical credentials.
908
- const startupAuthPreflightPromise = runStartupAuthPreflight(config, btpProxy, bearerTokenProvider, adtSemaphore);
909
- const startupProbePromise = (async () => {
910
- const authPreflight = await startupAuthPreflightPromise;
911
- if (authPreflight.blocking) {
912
- setCachedFeatures(undefined);
913
- setCachedDiscovery(new Map());
914
- return;
915
- }
916
- await runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore);
917
- })();
918
- 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());
919
1090
  const uiDeps = config.uiMode !== 'off'
920
1091
  ? {
921
1092
  config,
@@ -924,7 +1095,7 @@ export async function createAndStartServer(config, sources) {
924
1095
  startedAt,
925
1096
  cachingLayer,
926
1097
  logBuffer: uiLogBuffer,
927
- getFeatures: getCachedFeatures,
1098
+ getFeatures: () => getCachedFeatures(),
928
1099
  }
929
1100
  : undefined;
930
1101
  // Shutdown hook for SQLite cache cleanup (guard against double-close from multiple signals).
@@ -937,7 +1108,7 @@ export async function createAndStartServer(config, sources) {
937
1108
  return;
938
1109
  cacheClosed = true;
939
1110
  try {
940
- cachingLayer.cache.close();
1111
+ cachingLayer?.cache.close();
941
1112
  }
942
1113
  catch {
943
1114
  // Ignore close errors during shutdown
@@ -999,10 +1170,28 @@ export async function createAndStartServer(config, sources) {
999
1170
  logger.error('Failed to load XSUAA credentials — XSUAA auth will not work', {
1000
1171
  error: err instanceof Error ? err.message : String(err),
1001
1172
  });
1173
+ if (config.multiTargetEndpoints) {
1174
+ throw new Error('ARC1_MULTI_TARGET_ENDPOINTS=true requires a valid bound XSUAA service.');
1175
+ }
1002
1176
  }
1003
1177
  }
1004
1178
  const { startHttpServer } = await import('./http.js');
1005
- 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);
1006
1195
  }
1007
1196
  return server;
1008
1197
  }