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.
- package/README.md +29 -12
- 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/authorization-trace.d.ts +23 -0
- package/dist/adt/authorization-trace.d.ts.map +1 -0
- package/dist/adt/authorization-trace.js +196 -0
- package/dist/adt/authorization-trace.js.map +1 -0
- 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/diagnostics.d.ts +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +1 -0
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +8 -8
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +30 -11
- 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/oauth.d.ts +2 -2
- package/dist/adt/oauth.d.ts.map +1 -1
- package/dist/adt/oauth.js +5 -3
- package/dist/adt/oauth.js.map +1 -1
- package/dist/adt/package-hierarchy.d.ts.map +1 -1
- package/dist/adt/package-hierarchy.js.map +1 -1
- package/dist/adt/rap-generate.js.map +1 -1
- package/dist/adt/rap-handlers.js.map +1 -1
- package/dist/adt/rap-preflight.js.map +1 -1
- package/dist/adt/safety.d.ts +13 -13
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/semaphore.d.ts.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/structure-hierarchy.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 +94 -6
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/version-diff.js.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/aff/validator.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 +24 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cache/cache.d.ts +4 -36
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +4 -5
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/caching-layer.d.ts +6 -22
- package/dist/cache/caching-layer.d.ts.map +1 -1
- package/dist/cache/caching-layer.js +5 -29
- package/dist/cache/caching-layer.js.map +1 -1
- package/dist/cache/inactive-list-cache.d.ts.map +1 -1
- package/dist/cache/memory.d.ts +2 -13
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +1 -70
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +3 -10
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +9 -85
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/compressor.js.map +1 -1
- package/dist/context/grep.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/extract-sap-cookies.js.map +1 -1
- 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 +74 -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 +47 -28
- 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-errors.d.ts +11 -0
- package/dist/handlers/query-errors.d.ts.map +1 -0
- package/dist/handlers/query-errors.js +216 -0
- package/dist/handlers/query-errors.js.map +1 -0
- package/dist/handlers/query.d.ts.map +1 -1
- package/dist/handlers/query.js +62 -67
- 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 +285 -234
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +126 -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 +20 -6
- 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 +124 -92
- 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/config-builder.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/testing.js.map +1 -1
- package/dist/public/ui/app.js +1 -14
- package/dist/registry/tool-registry.d.ts.map +1 -1
- 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 +110 -7
- 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 +47 -25
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +490 -301
- 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/sinks/file.d.ts.map +1 -1
- package/dist/server/sinks/file.js.map +1 -1
- package/dist/server/sinks/stderr.d.ts.map +1 -1
- package/dist/server/sinks/stderr.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 +28 -7
- 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-log-buffer.d.ts.map +1 -1
- package/dist/server/ui-log-buffer.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 +2 -6
- package/dist/server/ui-state.js.map +1 -1
- package/dist/server/ui.d.ts.map +1 -1
- package/dist/server/ui.js +8 -9
- package/dist/server/ui.js.map +1 -1
- package/package.json +21 -18
- 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
|
@@ -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 {
|
|
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.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
117
|
+
* Describe the final bare-/mcp SAP connection for multi-target overlap checks.
|
|
124
118
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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 =
|
|
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.
|
|
366
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
522
|
-
*
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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 =
|
|
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
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
597
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
624
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
763
|
+
return {
|
|
764
|
+
content: [{ type: 'text', text: `Principal propagation failed: ${errMsg}` }],
|
|
765
|
+
isError: true,
|
|
766
|
+
};
|
|
664
767
|
}
|
|
665
768
|
}
|
|
666
|
-
else if (
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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'
|
|
719
|
-
|
|
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
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
852
|
-
|
|
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
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|