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.
- package/README.md +28 -11
- package/dist/adt/abapgit.d.ts +19 -6
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +141 -36
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/client.d.ts +21 -8
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +47 -7
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +15 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +7 -2
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +12 -4
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +27 -17
- package/dist/adt/ddic-xml.d.ts.map +1 -1
- package/dist/adt/ddic-xml.js +45 -34
- package/dist/adt/ddic-xml.js.map +1 -1
- package/dist/adt/devtools.d.ts +13 -0
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +120 -50
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/errors.d.ts +1 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +18 -0
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts +17 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +102 -19
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/server-driven.d.ts +154 -34
- package/dist/adt/server-driven.d.ts.map +1 -1
- package/dist/adt/server-driven.js +178 -53
- package/dist/adt/server-driven.js.map +1 -1
- package/dist/adt/transport.d.ts +6 -6
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +23 -34
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +47 -6
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +52 -12
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +120 -10
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +8 -0
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +23 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cache/cache.d.ts +2 -34
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +2 -3
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/caching-layer.d.ts +3 -19
- package/dist/cache/caching-layer.d.ts.map +1 -1
- package/dist/cache/caching-layer.js +2 -26
- package/dist/cache/caching-layer.js.map +1 -1
- package/dist/cache/memory.d.ts +1 -12
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +0 -69
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +2 -9
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +8 -84
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/unit-surgery.d.ts +28 -0
- package/dist/context/unit-surgery.d.ts.map +1 -0
- package/dist/context/unit-surgery.js +150 -0
- package/dist/context/unit-surgery.js.map +1 -0
- package/dist/handlers/activate.d.ts.map +1 -1
- package/dist/handlers/activate.js +44 -6
- package/dist/handlers/activate.js.map +1 -1
- package/dist/handlers/context.d.ts +1 -1
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/context.js +98 -28
- package/dist/handlers/context.js.map +1 -1
- package/dist/handlers/diagnose.d.ts.map +1 -1
- package/dist/handlers/diagnose.js +55 -31
- package/dist/handlers/diagnose.js.map +1 -1
- package/dist/handlers/dispatch.d.ts +12 -1
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +160 -25
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/feature-cache.d.ts +27 -14
- package/dist/handlers/feature-cache.d.ts.map +1 -1
- package/dist/handlers/feature-cache.js +63 -25
- package/dist/handlers/feature-cache.js.map +1 -1
- package/dist/handlers/function-processing.d.ts +61 -0
- package/dist/handlers/function-processing.d.ts.map +1 -0
- package/dist/handlers/function-processing.js +45 -0
- package/dist/handlers/function-processing.js.map +1 -0
- package/dist/handlers/git.d.ts.map +1 -1
- package/dist/handlers/git.js +44 -19
- package/dist/handlers/git.js.map +1 -1
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +2 -3
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/lint.d.ts.map +1 -1
- package/dist/handlers/lint.js +7 -7
- package/dist/handlers/lint.js.map +1 -1
- package/dist/handlers/manage.d.ts.map +1 -1
- package/dist/handlers/manage.js +42 -25
- package/dist/handlers/manage.js.map +1 -1
- package/dist/handlers/navigate.d.ts.map +1 -1
- package/dist/handlers/navigate.js +27 -90
- package/dist/handlers/navigate.js.map +1 -1
- package/dist/handlers/object-types.d.ts +2 -0
- package/dist/handlers/object-types.d.ts.map +1 -1
- package/dist/handlers/object-types.js +10 -0
- package/dist/handlers/object-types.js.map +1 -1
- package/dist/handlers/query.d.ts.map +1 -1
- package/dist/handlers/query.js +2 -2
- package/dist/handlers/query.js.map +1 -1
- package/dist/handlers/read.d.ts.map +1 -1
- package/dist/handlers/read.js +64 -41
- package/dist/handlers/read.js.map +1 -1
- package/dist/handlers/schemas.d.ts +64 -16
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +123 -32
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +8 -7
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/shared.d.ts +13 -0
- package/dist/handlers/shared.d.ts.map +1 -1
- package/dist/handlers/shared.js +15 -0
- package/dist/handlers/shared.js.map +1 -1
- package/dist/handlers/tool-descriptions.d.ts +4 -0
- package/dist/handlers/tool-descriptions.d.ts.map +1 -0
- package/dist/handlers/tool-descriptions.js +52 -0
- package/dist/handlers/tool-descriptions.js.map +1 -0
- package/dist/handlers/tool-registry.d.ts +18 -4
- package/dist/handlers/tool-registry.d.ts.map +1 -1
- package/dist/handlers/tool-registry.js +18 -0
- package/dist/handlers/tool-registry.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +113 -90
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +1 -1
- package/dist/handlers/transport.d.ts.map +1 -1
- package/dist/handlers/transport.js +37 -10
- package/dist/handlers/transport.js.map +1 -1
- package/dist/handlers/where-used.d.ts +25 -0
- package/dist/handlers/where-used.d.ts.map +1 -0
- package/dist/handlers/where-used.js +94 -0
- package/dist/handlers/where-used.js.map +1 -0
- package/dist/handlers/write/class-surgery.d.ts.map +1 -1
- package/dist/handlers/write/class-surgery.js +11 -12
- package/dist/handlers/write/class-surgery.js.map +1 -1
- package/dist/handlers/write/create.d.ts.map +1 -1
- package/dist/handlers/write/create.js +194 -84
- package/dist/handlers/write/create.js.map +1 -1
- package/dist/handlers/write/rap.d.ts.map +1 -1
- package/dist/handlers/write/rap.js +9 -9
- package/dist/handlers/write/rap.js.map +1 -1
- package/dist/handlers/write/unit-surgery.d.ts +5 -0
- package/dist/handlers/write/unit-surgery.d.ts.map +1 -0
- package/dist/handlers/write/unit-surgery.js +47 -0
- package/dist/handlers/write/unit-surgery.js.map +1 -0
- package/dist/handlers/write/update-delete.js +8 -8
- package/dist/handlers/write/update-delete.js.map +1 -1
- package/dist/handlers/write-helpers.d.ts +7 -4
- package/dist/handlers/write-helpers.d.ts.map +1 -1
- package/dist/handlers/write-helpers.js +118 -63
- package/dist/handlers/write-helpers.js.map +1 -1
- package/dist/handlers/write.d.ts.map +1 -1
- package/dist/handlers/write.js +35 -13
- package/dist/handlers/write.js.map +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +4 -2
- package/dist/lint/lint.js.map +1 -1
- package/dist/probe/catalog.js +1 -1
- package/dist/probe/catalog.js.map +1 -1
- package/dist/public/ui/app.js +1 -14
- package/dist/server/audit.d.ts +27 -15
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/auth-rate-limit.d.ts +18 -9
- package/dist/server/auth-rate-limit.d.ts.map +1 -1
- package/dist/server/auth-rate-limit.js +31 -6
- package/dist/server/auth-rate-limit.js.map +1 -1
- package/dist/server/auth-summary.d.ts +4 -0
- package/dist/server/auth-summary.d.ts.map +1 -0
- package/dist/server/auth-summary.js +67 -0
- package/dist/server/auth-summary.js.map +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +106 -3
- package/dist/server/config.js.map +1 -1
- package/dist/server/context.d.ts +12 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/destination-discovery.d.ts +36 -0
- package/dist/server/destination-discovery.d.ts.map +1 -0
- package/dist/server/destination-discovery.js +130 -0
- package/dist/server/destination-discovery.js.map +1 -0
- package/dist/server/destination-registry.d.ts +111 -0
- package/dist/server/destination-registry.d.ts.map +1 -0
- package/dist/server/destination-registry.js +449 -0
- package/dist/server/destination-registry.js.map +1 -0
- package/dist/server/http.d.ts +43 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +283 -73
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +58 -13
- package/dist/server/logger.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +6 -4
- package/dist/server/mcp-rate-limit.d.ts.map +1 -1
- package/dist/server/mcp-rate-limit.js +6 -4
- package/dist/server/mcp-rate-limit.js.map +1 -1
- package/dist/server/multi-target-basic-auth.d.ts +49 -0
- package/dist/server/multi-target-basic-auth.d.ts.map +1 -0
- package/dist/server/multi-target-basic-auth.js +246 -0
- package/dist/server/multi-target-basic-auth.js.map +1 -0
- package/dist/server/multi-target-catalog.d.ts +33 -0
- package/dist/server/multi-target-catalog.d.ts.map +1 -0
- package/dist/server/multi-target-catalog.js +130 -0
- package/dist/server/multi-target-catalog.js.map +1 -0
- package/dist/server/multi-target-destination-config.d.ts +12 -0
- package/dist/server/multi-target-destination-config.d.ts.map +1 -0
- package/dist/server/multi-target-destination-config.js +37 -0
- package/dist/server/multi-target-destination-config.js.map +1 -0
- package/dist/server/multi-target-destination-runtime.d.ts +20 -0
- package/dist/server/multi-target-destination-runtime.d.ts.map +1 -0
- package/dist/server/multi-target-destination-runtime.js +191 -0
- package/dist/server/multi-target-destination-runtime.js.map +1 -0
- package/dist/server/multi-target-feature-state.d.ts +6 -0
- package/dist/server/multi-target-feature-state.d.ts.map +1 -0
- package/dist/server/multi-target-feature-state.js +41 -0
- package/dist/server/multi-target-feature-state.js.map +1 -0
- package/dist/server/multi-target-identity.d.ts +10 -0
- package/dist/server/multi-target-identity.d.ts.map +1 -0
- package/dist/server/multi-target-identity.js +18 -0
- package/dist/server/multi-target-identity.js.map +1 -0
- package/dist/server/multi-target-runtime.d.ts +27 -0
- package/dist/server/multi-target-runtime.d.ts.map +1 -0
- package/dist/server/multi-target-runtime.js +110 -0
- package/dist/server/multi-target-runtime.js.map +1 -0
- package/dist/server/multi-target-server.d.ts +53 -0
- package/dist/server/multi-target-server.d.ts.map +1 -0
- package/dist/server/multi-target-server.js +434 -0
- package/dist/server/multi-target-server.js.map +1 -0
- package/dist/server/multi-target-shared-auth-state.d.ts +82 -0
- package/dist/server/multi-target-shared-auth-state.d.ts.map +1 -0
- package/dist/server/multi-target-shared-auth-state.js +226 -0
- package/dist/server/multi-target-shared-auth-state.js.map +1 -0
- package/dist/server/multi-target-tools.d.ts +20 -0
- package/dist/server/multi-target-tools.d.ts.map +1 -0
- package/dist/server/multi-target-tools.js +191 -0
- package/dist/server/multi-target-tools.js.map +1 -0
- package/dist/server/oauth-session-recovery.d.ts +22 -0
- package/dist/server/oauth-session-recovery.d.ts.map +1 -0
- package/dist/server/oauth-session-recovery.js +69 -0
- package/dist/server/oauth-session-recovery.js.map +1 -0
- package/dist/server/server.d.ts +39 -27
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +433 -300
- package/dist/server/server.js.map +1 -1
- package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
- package/dist/server/sinks/btp-auditlog.js +83 -11
- package/dist/server/sinks/btp-auditlog.js.map +1 -1
- package/dist/server/tool-auth.d.ts +10 -0
- package/dist/server/tool-auth.d.ts.map +1 -0
- package/dist/server/tool-auth.js +75 -0
- package/dist/server/tool-auth.js.map +1 -0
- package/dist/server/trace-context.d.ts +51 -0
- package/dist/server/trace-context.d.ts.map +1 -0
- package/dist/server/trace-context.js +100 -0
- package/dist/server/trace-context.js.map +1 -0
- package/dist/server/types.d.ts +27 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +5 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/ui-state.d.ts +0 -2
- package/dist/server/ui-state.d.ts.map +1 -1
- package/dist/server/ui-state.js +0 -4
- package/dist/server/ui-state.js.map +1 -1
- package/dist/server/ui.d.ts.map +1 -1
- package/dist/server/ui.js +0 -7
- package/dist/server/ui.js.map +1 -1
- package/package.json +16 -13
- package/dist/cache/warmup.d.ts +0 -41
- package/dist/cache/warmup.d.ts.map +0 -1
- package/dist/cache/warmup.js +0 -337
- package/dist/cache/warmup.js.map +0 -1
- package/dist/server/elicit.d.ts +0 -43
- package/dist/server/elicit.d.ts.map +0 -1
- package/dist/server/elicit.js +0 -183
- package/dist/server/elicit.js.map +0 -1
package/dist/server/server.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
*
|
|
117
|
+
* Describe the final bare-/mcp SAP connection for multi-target overlap checks.
|
|
151
118
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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 =
|
|
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.
|
|
405
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
561
|
-
*
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 =
|
|
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
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
637
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
908
|
-
|
|
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
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|