arc-1 0.9.5 → 0.9.7
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 +21 -3
- package/dist/adt/abapgit.d.ts +2 -1
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +2 -2
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +7 -3
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/class-structure.d.ts +176 -0
- package/dist/adt/class-structure.d.ts.map +1 -0
- package/dist/adt/class-structure.js +317 -0
- package/dist/adt/class-structure.js.map +1 -0
- package/dist/adt/client.d.ts +150 -8
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +345 -12
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +7 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +38 -0
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +73 -1
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/errors.d.ts +2 -2
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +50 -6
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +27 -3
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/gcts.d.ts +3 -2
- package/dist/adt/gcts.d.ts.map +1 -1
- package/dist/adt/gcts.js +4 -4
- package/dist/adt/gcts.js.map +1 -1
- package/dist/adt/http.d.ts +41 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +132 -45
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/package-hierarchy.d.ts +67 -0
- package/dist/adt/package-hierarchy.d.ts.map +1 -0
- package/dist/adt/package-hierarchy.js +100 -0
- package/dist/adt/package-hierarchy.js.map +1 -0
- package/dist/adt/release.d.ts +35 -0
- package/dist/adt/release.d.ts.map +1 -0
- package/dist/adt/release.js +48 -0
- package/dist/adt/release.js.map +1 -0
- package/dist/adt/safety.d.ts +39 -3
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +136 -15
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/types.d.ts +74 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +68 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +263 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +12 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/context/grep.d.ts +48 -0
- package/dist/context/grep.d.ts.map +1 -0
- package/dist/context/grep.js +146 -0
- package/dist/context/grep.js.map +1 -0
- package/dist/handlers/intent.d.ts +2 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +614 -50
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +52 -6
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +90 -9
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +73 -12
- package/dist/handlers/tools.js.map +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +6 -0
- package/dist/lint/lint.js.map +1 -1
- package/dist/lint/pre-write-hints.d.ts +45 -0
- package/dist/lint/pre-write-hints.d.ts.map +1 -0
- package/dist/lint/pre-write-hints.js +145 -0
- package/dist/lint/pre-write-hints.js.map +1 -0
- package/dist/server/audit.d.ts +27 -1
- 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 +78 -0
- package/dist/server/auth-rate-limit.d.ts.map +1 -0
- package/dist/server/auth-rate-limit.js +95 -0
- package/dist/server/auth-rate-limit.js.map +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +32 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +73 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +69 -0
- package/dist/server/mcp-rate-limit.d.ts.map +1 -0
- package/dist/server/mcp-rate-limit.js +92 -0
- package/dist/server/mcp-rate-limit.js.map +1 -0
- package/dist/server/server.d.ts +26 -6
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +87 -28
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +20 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +14 -12
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2 — Per-user MCP tool-call rate limiter.
|
|
3
|
+
*
|
|
4
|
+
* Applied at the top of `handleToolCall` in `src/handlers/intent.ts`. Returns an MCP
|
|
5
|
+
* tool error (NOT HTTP 429) on denial so the LLM client surfaces it as a tool failure
|
|
6
|
+
* and the agent loop backs off correctly. Per-user token bucket keyed on the resolved
|
|
7
|
+
* user identity (userName / clientId / __anon__).
|
|
8
|
+
*
|
|
9
|
+
* Design choices:
|
|
10
|
+
* - Per-instance, in-memory only. Multi-instance attackers cost `limit × instances` —
|
|
11
|
+
* acceptable trade-off, matches stateless-DCR philosophy from PR #212.
|
|
12
|
+
* - Stdio mode is exempt because there's no authInfo to key on; the caller is
|
|
13
|
+
* responsible for skipping the consume in that case.
|
|
14
|
+
* - When `perMinute === 0`, the factory returns a stub whose `consume` resolves
|
|
15
|
+
* immediately with `{ allowed: true }` — no allocation, no per-key bookkeeping.
|
|
16
|
+
* This is the clean opt-out for single-user deployments.
|
|
17
|
+
* - Cost weighting per tool is intentionally deferred to v2 — every consume call is
|
|
18
|
+
* one point. See ADR-0004 for the rationale.
|
|
19
|
+
*/
|
|
20
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
21
|
+
export type RateLimitDecision = {
|
|
22
|
+
allowed: true;
|
|
23
|
+
} | {
|
|
24
|
+
allowed: false;
|
|
25
|
+
retryAfterMs: number;
|
|
26
|
+
limitPerMinute: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the per-user rate-limit key from an `AuthInfo`, walking the most-
|
|
30
|
+
* specific identity claims first so distinct users never share a quota when
|
|
31
|
+
* they share an auth client / application.
|
|
32
|
+
*
|
|
33
|
+
* Order, by descending specificity:
|
|
34
|
+
* 1. `extra.userName` — XSUAA logon name (`securityContext.getLogonName()`)
|
|
35
|
+
* 2. `extra.email` — XSUAA / OIDC email when populated
|
|
36
|
+
* 3. `extra.sub` — OIDC subject claim (guaranteed unique per user within issuer)
|
|
37
|
+
* 4. `extra.preferred_username` — sometimes set on OIDC tokens
|
|
38
|
+
* 5. `clientId` — last resort. Note for OIDC this is `azp`
|
|
39
|
+
* (the app's client id), shared by all users of that app — so falling here
|
|
40
|
+
* collapses them into one bucket. The earlier checks exist specifically
|
|
41
|
+
* to avoid that. Acceptable only for the API-key path where the clientId
|
|
42
|
+
* is `api-key:<profile>` and the operator has chosen the profile granularity.
|
|
43
|
+
* 6. `'__anon__'` — token with no usable identity claim. Single
|
|
44
|
+
* shared bucket for anonymous traffic. Operators should configure auth so
|
|
45
|
+
* this branch is never reached in production.
|
|
46
|
+
*
|
|
47
|
+
* Why not just `sub`? Because XSUAA tokens don't put `sub` on `extra`; they put
|
|
48
|
+
* the SAP logon name on `extra.userName`. OIDC does the inverse. We accept both
|
|
49
|
+
* shapes rather than forcing every auth provider to align on one claim.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveRateLimitUserKey(authInfo: AuthInfo | undefined): string;
|
|
52
|
+
export interface McpRateLimiter {
|
|
53
|
+
/**
|
|
54
|
+
* Try to consume one point for `userKey`. Resolves `{ allowed: true }` when the
|
|
55
|
+
* bucket has tokens, `{ allowed: false, retryAfterMs, limitPerMinute }` when it
|
|
56
|
+
* doesn't. Never throws — internal RateLimiterRes rejection is caught here.
|
|
57
|
+
*
|
|
58
|
+
* `tool` is recorded for the audit event at the call site; it doesn't affect
|
|
59
|
+
* the bucket.
|
|
60
|
+
*/
|
|
61
|
+
consume(userKey: string, tool: string): Promise<RateLimitDecision>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a per-user MCP rate limiter.
|
|
65
|
+
*
|
|
66
|
+
* @param perMinute Per-user requests per minute. `0` returns a no-op stub.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createMcpRateLimiter(perMinute: number): McpRateLimiter;
|
|
69
|
+
//# sourceMappingURL=mcp-rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-rate-limit.d.ts","sourceRoot":"","sources":["../../src/server/mcp-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAG/E,MAAM,MAAM,iBAAiB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AAErH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAa9E;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACpE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAgCtE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2 — Per-user MCP tool-call rate limiter.
|
|
3
|
+
*
|
|
4
|
+
* Applied at the top of `handleToolCall` in `src/handlers/intent.ts`. Returns an MCP
|
|
5
|
+
* tool error (NOT HTTP 429) on denial so the LLM client surfaces it as a tool failure
|
|
6
|
+
* and the agent loop backs off correctly. Per-user token bucket keyed on the resolved
|
|
7
|
+
* user identity (userName / clientId / __anon__).
|
|
8
|
+
*
|
|
9
|
+
* Design choices:
|
|
10
|
+
* - Per-instance, in-memory only. Multi-instance attackers cost `limit × instances` —
|
|
11
|
+
* acceptable trade-off, matches stateless-DCR philosophy from PR #212.
|
|
12
|
+
* - Stdio mode is exempt because there's no authInfo to key on; the caller is
|
|
13
|
+
* responsible for skipping the consume in that case.
|
|
14
|
+
* - When `perMinute === 0`, the factory returns a stub whose `consume` resolves
|
|
15
|
+
* immediately with `{ allowed: true }` — no allocation, no per-key bookkeeping.
|
|
16
|
+
* This is the clean opt-out for single-user deployments.
|
|
17
|
+
* - Cost weighting per tool is intentionally deferred to v2 — every consume call is
|
|
18
|
+
* one point. See ADR-0004 for the rationale.
|
|
19
|
+
*/
|
|
20
|
+
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the per-user rate-limit key from an `AuthInfo`, walking the most-
|
|
23
|
+
* specific identity claims first so distinct users never share a quota when
|
|
24
|
+
* they share an auth client / application.
|
|
25
|
+
*
|
|
26
|
+
* Order, by descending specificity:
|
|
27
|
+
* 1. `extra.userName` — XSUAA logon name (`securityContext.getLogonName()`)
|
|
28
|
+
* 2. `extra.email` — XSUAA / OIDC email when populated
|
|
29
|
+
* 3. `extra.sub` — OIDC subject claim (guaranteed unique per user within issuer)
|
|
30
|
+
* 4. `extra.preferred_username` — sometimes set on OIDC tokens
|
|
31
|
+
* 5. `clientId` — last resort. Note for OIDC this is `azp`
|
|
32
|
+
* (the app's client id), shared by all users of that app — so falling here
|
|
33
|
+
* collapses them into one bucket. The earlier checks exist specifically
|
|
34
|
+
* to avoid that. Acceptable only for the API-key path where the clientId
|
|
35
|
+
* is `api-key:<profile>` and the operator has chosen the profile granularity.
|
|
36
|
+
* 6. `'__anon__'` — token with no usable identity claim. Single
|
|
37
|
+
* shared bucket for anonymous traffic. Operators should configure auth so
|
|
38
|
+
* this branch is never reached in production.
|
|
39
|
+
*
|
|
40
|
+
* Why not just `sub`? Because XSUAA tokens don't put `sub` on `extra`; they put
|
|
41
|
+
* the SAP logon name on `extra.userName`. OIDC does the inverse. We accept both
|
|
42
|
+
* shapes rather than forcing every auth provider to align on one claim.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveRateLimitUserKey(authInfo) {
|
|
45
|
+
if (!authInfo)
|
|
46
|
+
return '__anon__';
|
|
47
|
+
const extra = (authInfo.extra ?? {});
|
|
48
|
+
const candidates = [extra.userName, extra.email, extra.sub, extra.preferred_username, authInfo.clientId];
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (typeof c === 'string' && c.length > 0)
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
return '__anon__';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a per-user MCP rate limiter.
|
|
57
|
+
*
|
|
58
|
+
* @param perMinute Per-user requests per minute. `0` returns a no-op stub.
|
|
59
|
+
*/
|
|
60
|
+
export function createMcpRateLimiter(perMinute) {
|
|
61
|
+
if (perMinute === 0) {
|
|
62
|
+
return {
|
|
63
|
+
async consume(_userKey, _tool) {
|
|
64
|
+
return { allowed: true };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const limiter = new RateLimiterMemory({ points: perMinute, duration: 60 });
|
|
69
|
+
return {
|
|
70
|
+
async consume(userKey, _tool) {
|
|
71
|
+
try {
|
|
72
|
+
await limiter.consume(userKey, 1);
|
|
73
|
+
return { allowed: true };
|
|
74
|
+
}
|
|
75
|
+
catch (rejected) {
|
|
76
|
+
// RateLimiterRes is thrown on overflow; anything else is unexpected.
|
|
77
|
+
if (rejected instanceof RateLimiterRes) {
|
|
78
|
+
return {
|
|
79
|
+
allowed: false,
|
|
80
|
+
retryAfterMs: rejected.msBeforeNext,
|
|
81
|
+
limitPerMinute: perMinute,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Defensive: treat unexpected errors as "allowed" so a misbehaving limiter
|
|
85
|
+
// can never wedge legitimate traffic. The exception itself bubbles up via
|
|
86
|
+
// logging when the limiter is fixed; in the meantime users still get through.
|
|
87
|
+
return { allowed: true };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=mcp-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-rate-limit.js","sourceRoot":"","sources":["../../src/server/mcp-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI1E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAA8B;IACpE,IAAI,CAAC,QAAQ;QAAE,OAAO,UAAU,CAAC;IACjC,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAKlC,CAAC;IACF,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,kBAAkB,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzG,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAcD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO;YACL,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,KAAa;gBAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC;SACF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAE3E,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,KAAa;YAC1C,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC;YAAC,OAAO,QAAQ,EAAE,CAAC;gBAClB,qEAAqE;gBACrE,IAAI,QAAQ,YAAY,cAAc,EAAE,CAAC;oBACvC,OAAO;wBACL,OAAO,EAAE,KAAK;wBACd,YAAY,EAAE,QAAQ,CAAC,YAAY;wBACnC,cAAc,EAAE,SAAS;qBAC1B,CAAC;gBACJ,CAAC;gBACD,2EAA2E;gBAC3E,0EAA0E;gBAC1E,8EAA8E;gBAC9E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/server/server.d.ts
CHANGED
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* - http-streamable: for remote/containerized deployments
|
|
8
8
|
*/
|
|
9
9
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
-
import type { BTPConfig, BTPProxyConfig } from '../adt/btp.js';
|
|
10
|
+
import type { BTPConfig, BTPProxyConfig, PerUserAuthTokens } from '../adt/btp.js';
|
|
11
11
|
import type { AdtClientConfig } from '../adt/config.js';
|
|
12
|
+
import { Semaphore } from '../adt/semaphore.js';
|
|
12
13
|
import { CachingLayer } from '../cache/caching-layer.js';
|
|
13
14
|
import { type ToolDefinition } from '../handlers/tools.js';
|
|
15
|
+
import { type McpRateLimiter } from './mcp-rate-limit.js';
|
|
14
16
|
import type { ServerConfig } from './types.js';
|
|
15
17
|
/** ARC-1 version */
|
|
16
|
-
export declare const VERSION = "0.9.
|
|
18
|
+
export declare const VERSION = "0.9.7";
|
|
17
19
|
/**
|
|
18
20
|
* Filter tools by user scope + server deny list.
|
|
19
21
|
*
|
|
@@ -27,7 +29,25 @@ export declare function logAuthSummary(config: ServerConfig): void;
|
|
|
27
29
|
/** Build the base ADT client config (without per-user auth) */
|
|
28
30
|
export declare function buildAdtConfig(config: ServerConfig, btpProxy?: BTPProxyConfig, bearerTokenProvider?: () => Promise<string>, opts?: {
|
|
29
31
|
perUser?: boolean;
|
|
30
|
-
}): Partial<AdtClientConfig>;
|
|
32
|
+
}, adtSemaphore?: Semaphore): Partial<AdtClientConfig>;
|
|
33
|
+
/**
|
|
34
|
+
* Map per-user auth tokens from the BTP Destination Service onto an AdtClientConfig.
|
|
35
|
+
* Mutates and returns `adtConfig`. Exported for unit testing.
|
|
36
|
+
*
|
|
37
|
+
* Precedence (most-specific first):
|
|
38
|
+
* 1. ppProxyAuth — Option 1: jwt-bearer exchanged token → Proxy-Authorization (Cloud Connector)
|
|
39
|
+
* 2. sapConnectivityAuth — Option 2: SAML assertion → SAP-Connectivity-Authentication (Cloud Connector)
|
|
40
|
+
* 3. bearerToken — OAuth2UserTokenExchange / OAuth2SAMLBearerAssertion: a user-context Bearer
|
|
41
|
+
* token minted at the target's XSUAA → `Authorization: Bearer` (cloud-to-cloud,
|
|
42
|
+
* e.g. a BTP ABAP Environment over the Internet — no Cloud Connector / proxy).
|
|
43
|
+
*
|
|
44
|
+
* Throws when none is present (PP could not produce a usable per-user credential).
|
|
45
|
+
*
|
|
46
|
+
* In every success branch the SAP password is cleared and `username` is set to a display-only
|
|
47
|
+
* value — it is never used for auth or access control; the real SAP identity rides in the
|
|
48
|
+
* chosen token/assertion.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyPerUserAuthTokens(adtConfig: Partial<AdtClientConfig>, authTokens: PerUserAuthTokens, displayUsername: string | undefined, destName: string): Partial<AdtClientConfig>;
|
|
31
51
|
/**
|
|
32
52
|
* Run a one-time feature probe against the SAP system using the shared/default client.
|
|
33
53
|
* Returns a promise that resolves once probe results are stored in cachedFeatures.
|
|
@@ -36,7 +56,7 @@ export declare function buildAdtConfig(config: ServerConfig, btpProxy?: BTPProxy
|
|
|
36
56
|
* source_code from users who might have authorization. Without btpConfig, PP cannot
|
|
37
57
|
* create per-user clients, so shared-client auth failures are definitive.
|
|
38
58
|
*/
|
|
39
|
-
export declare function runStartupProbe(config: ServerConfig, btpProxy?: BTPProxyConfig, bearerTokenProvider?: () => Promise<string>, btpConfig?: BTPConfig): Promise<void>;
|
|
59
|
+
export declare function runStartupProbe(config: ServerConfig, btpProxy?: BTPProxyConfig, bearerTokenProvider?: () => Promise<string>, btpConfig?: BTPConfig, adtSemaphore?: Semaphore): Promise<void>;
|
|
40
60
|
export interface StartupAuthPreflightResult {
|
|
41
61
|
status: 'ok' | 'failed' | 'inconclusive' | 'skipped';
|
|
42
62
|
/** When true, shared-client SAP tool calls must be blocked to prevent repeated auth failures. */
|
|
@@ -58,7 +78,7 @@ export interface StartupAuthPreflightResult {
|
|
|
58
78
|
* - 401/403 are blocking failures
|
|
59
79
|
* - Network/other failures are inconclusive (non-blocking)
|
|
60
80
|
*/
|
|
61
|
-
export declare function runStartupAuthPreflight(config: ServerConfig, btpProxy?: BTPProxyConfig, bearerTokenProvider?: () => Promise<string
|
|
81
|
+
export declare function runStartupAuthPreflight(config: ServerConfig, btpProxy?: BTPProxyConfig, bearerTokenProvider?: () => Promise<string>, adtSemaphore?: Semaphore): Promise<StartupAuthPreflightResult>;
|
|
62
82
|
export declare function formatStartupAuthPreflightToolError(preflight: StartupAuthPreflightResult): string;
|
|
63
83
|
/**
|
|
64
84
|
* Create the MCP server with registered tool handlers.
|
|
@@ -70,7 +90,7 @@ export declare function formatStartupAuthPreflightToolError(preflight: StartupAu
|
|
|
70
90
|
* @param startupProbePromise Promise from runStartupProbe() — ListTools waits on this
|
|
71
91
|
* @param startupAuthPreflightPromise Promise from runStartupAuthPreflight() — CallTool blocks on auth failure in shared mode
|
|
72
92
|
*/
|
|
73
|
-
export declare function createServer(config: ServerConfig, btpProxy?: BTPProxyConfig, btpConfig?: BTPConfig, bearerTokenProvider?: () => Promise<string>, cachingLayer?: CachingLayer, startupProbePromise?: Promise<void>, startupAuthPreflightPromise?: Promise<StartupAuthPreflightResult
|
|
93
|
+
export declare function createServer(config: ServerConfig, btpProxy?: BTPProxyConfig, btpConfig?: BTPConfig, bearerTokenProvider?: () => Promise<string>, cachingLayer?: CachingLayer, startupProbePromise?: Promise<void>, startupAuthPreflightPromise?: Promise<StartupAuthPreflightResult>, adtSemaphore?: Semaphore, mcpRateLimiter?: McpRateLimiter): Server;
|
|
74
94
|
/**
|
|
75
95
|
* Create and start the MCP server.
|
|
76
96
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server/server.js
CHANGED
|
@@ -12,7 +12,9 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
12
12
|
import { AdtClient } from '../adt/client.js';
|
|
13
13
|
import { resolveCookies } from '../adt/cookies.js';
|
|
14
14
|
import { AdtApiError } from '../adt/errors.js';
|
|
15
|
+
import { shouldWarnPreStatefulRelease } from '../adt/release.js';
|
|
15
16
|
import { deriveUserSafety, deriveUserSafetyFromProfile } from '../adt/safety.js';
|
|
17
|
+
import { Semaphore } from '../adt/semaphore.js';
|
|
16
18
|
import { getActionPolicy, hasRequiredScope } from '../authz/policy.js';
|
|
17
19
|
import { CachingLayer } from '../cache/caching-layer.js';
|
|
18
20
|
import { MemoryCache } from '../cache/memory.js';
|
|
@@ -21,9 +23,10 @@ import { getToolDefinitions } from '../handlers/tools.js';
|
|
|
21
23
|
import { API_KEY_PROFILES } from './config.js';
|
|
22
24
|
import { isActionDenied } from './deny-actions.js';
|
|
23
25
|
import { initLogger, logger } from './logger.js';
|
|
26
|
+
import { createMcpRateLimiter } from './mcp-rate-limit.js';
|
|
24
27
|
import { FileSink } from './sinks/file.js';
|
|
25
28
|
/** ARC-1 version */
|
|
26
|
-
export const VERSION = '0.9.
|
|
29
|
+
export const VERSION = '0.9.7'; // x-release-please-version
|
|
27
30
|
/**
|
|
28
31
|
* Prune a tool's action OR type enum (or both) based on the user's scopes and
|
|
29
32
|
* the server's denyActions list. Uses ACTION_POLICY as the single source of truth.
|
|
@@ -159,7 +162,11 @@ export function logAuthSummary(config) {
|
|
|
159
162
|
/** Build the base ADT client config (without per-user auth) */
|
|
160
163
|
// When perUser=true, strips shared credentials (username/password/cookies)
|
|
161
164
|
// so per-user PP clients never inherit admin auth.
|
|
162
|
-
|
|
165
|
+
//
|
|
166
|
+
// adtSemaphore (Layer 3): when provided, the constructed AdtClient shares this single
|
|
167
|
+
// server-wide semaphore with every other client built from this server. This is what
|
|
168
|
+
// makes ARC1_MAX_CONCURRENT a true server-wide cap rather than per-client.
|
|
169
|
+
export function buildAdtConfig(config, btpProxy, bearerTokenProvider, opts, adtSemaphore) {
|
|
163
170
|
const adtConfig = {
|
|
164
171
|
baseUrl: config.url,
|
|
165
172
|
client: config.client,
|
|
@@ -169,6 +176,7 @@ export function buildAdtConfig(config, btpProxy, bearerTokenProvider, opts) {
|
|
|
169
176
|
btpProxy,
|
|
170
177
|
bearerTokenProvider,
|
|
171
178
|
maxConcurrent: config.maxConcurrent,
|
|
179
|
+
adtSemaphore,
|
|
172
180
|
safety: {
|
|
173
181
|
allowWrites: config.allowWrites,
|
|
174
182
|
allowDataPreview: config.allowDataPreview,
|
|
@@ -203,7 +211,7 @@ export function buildAdtConfig(config, btpProxy, bearerTokenProvider, opts) {
|
|
|
203
211
|
* The Cloud Connector uses this header to generate an X.509 cert
|
|
204
212
|
* mapped to the SAP user via CERTRULE.
|
|
205
213
|
*/
|
|
206
|
-
async function createPerUserClient(config, btpConfig, btpProxy, userJwt) {
|
|
214
|
+
async function createPerUserClient(config, btpConfig, btpProxy, userJwt, adtSemaphore) {
|
|
207
215
|
const { lookupDestinationWithUserToken } = await import('../adt/btp.js');
|
|
208
216
|
// Use SAP_BTP_PP_DESTINATION if set, otherwise fall back to SAP_BTP_DESTINATION.
|
|
209
217
|
// This enables a dual-destination approach:
|
|
@@ -222,7 +230,7 @@ async function createPerUserClient(config, btpConfig, btpProxy, userJwt) {
|
|
|
222
230
|
const effectiveProxy = btpProxy && destination.CloudConnectorLocationId !== undefined
|
|
223
231
|
? { ...btpProxy, locationId: destination.CloudConnectorLocationId }
|
|
224
232
|
: btpProxy;
|
|
225
|
-
const adtConfig = buildAdtConfig(config, effectiveProxy, undefined, { perUser: true });
|
|
233
|
+
const adtConfig = buildAdtConfig(config, effectiveProxy, undefined, { perUser: true }, adtSemaphore);
|
|
226
234
|
// Override URL from destination (in case it differs from startup-resolved URL)
|
|
227
235
|
adtConfig.baseUrl = destination.URL;
|
|
228
236
|
// Set per-user auth for principal propagation.
|
|
@@ -240,22 +248,42 @@ async function createPerUserClient(config, btpConfig, btpProxy, userJwt) {
|
|
|
240
248
|
catch {
|
|
241
249
|
displayUsername = undefined;
|
|
242
250
|
}
|
|
251
|
+
applyPerUserAuthTokens(adtConfig, authTokens, displayUsername, destName);
|
|
252
|
+
return new AdtClient(adtConfig);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Map per-user auth tokens from the BTP Destination Service onto an AdtClientConfig.
|
|
256
|
+
* Mutates and returns `adtConfig`. Exported for unit testing.
|
|
257
|
+
*
|
|
258
|
+
* Precedence (most-specific first):
|
|
259
|
+
* 1. ppProxyAuth — Option 1: jwt-bearer exchanged token → Proxy-Authorization (Cloud Connector)
|
|
260
|
+
* 2. sapConnectivityAuth — Option 2: SAML assertion → SAP-Connectivity-Authentication (Cloud Connector)
|
|
261
|
+
* 3. bearerToken — OAuth2UserTokenExchange / OAuth2SAMLBearerAssertion: a user-context Bearer
|
|
262
|
+
* token minted at the target's XSUAA → `Authorization: Bearer` (cloud-to-cloud,
|
|
263
|
+
* e.g. a BTP ABAP Environment over the Internet — no Cloud Connector / proxy).
|
|
264
|
+
*
|
|
265
|
+
* Throws when none is present (PP could not produce a usable per-user credential).
|
|
266
|
+
*
|
|
267
|
+
* In every success branch the SAP password is cleared and `username` is set to a display-only
|
|
268
|
+
* value — it is never used for auth or access control; the real SAP identity rides in the
|
|
269
|
+
* chosen token/assertion.
|
|
270
|
+
*/
|
|
271
|
+
export function applyPerUserAuthTokens(adtConfig, authTokens, displayUsername, destName) {
|
|
243
272
|
if (authTokens.ppProxyAuth) {
|
|
244
|
-
// Option 1: exchanged token replaces Proxy-Authorization
|
|
245
273
|
adtConfig.ppProxyAuth = authTokens.ppProxyAuth;
|
|
246
|
-
adtConfig.username = displayUsername;
|
|
247
|
-
adtConfig.password = undefined;
|
|
248
274
|
}
|
|
249
275
|
else if (authTokens.sapConnectivityAuth) {
|
|
250
|
-
// Option 2: SAML assertion from Destination Service
|
|
251
276
|
adtConfig.sapConnectivityAuth = authTokens.sapConnectivityAuth;
|
|
252
|
-
adtConfig.username = displayUsername;
|
|
253
|
-
adtConfig.password = undefined;
|
|
254
277
|
}
|
|
255
278
|
else if (authTokens.bearerToken) {
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
279
|
+
// createPerUserClient runs per request and the Cloud SDK caches the exchanged token per
|
|
280
|
+
// user (TTL-bounded), so a provider returning the already-resolved token is fresh for the
|
|
281
|
+
// request's lifetime.
|
|
282
|
+
const bearer = authTokens.bearerToken;
|
|
283
|
+
adtConfig.bearerTokenProvider = async () => bearer;
|
|
284
|
+
logger.debug('PP: using destination-exchanged Bearer token (OAuth2UserTokenExchange)', {
|
|
285
|
+
destination: destName,
|
|
286
|
+
});
|
|
259
287
|
}
|
|
260
288
|
else {
|
|
261
289
|
// No per-user auth token received.
|
|
@@ -263,7 +291,9 @@ async function createPerUserClient(config, btpConfig, btpProxy, userJwt) {
|
|
|
263
291
|
'no SAP-Connectivity-Authentication header, Bearer token, or jwt-bearer exchange token returned. ' +
|
|
264
292
|
'Check Cloud Connector status, destination configuration, and user JWT validity.');
|
|
265
293
|
}
|
|
266
|
-
|
|
294
|
+
adtConfig.username = displayUsername;
|
|
295
|
+
adtConfig.password = undefined;
|
|
296
|
+
return adtConfig;
|
|
267
297
|
}
|
|
268
298
|
/**
|
|
269
299
|
* Run a one-time feature probe against the SAP system using the shared/default client.
|
|
@@ -273,8 +303,8 @@ async function createPerUserClient(config, btpConfig, btpProxy, userJwt) {
|
|
|
273
303
|
* source_code from users who might have authorization. Without btpConfig, PP cannot
|
|
274
304
|
* create per-user clients, so shared-client auth failures are definitive.
|
|
275
305
|
*/
|
|
276
|
-
export function runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig) {
|
|
277
|
-
const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider));
|
|
306
|
+
export function runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore) {
|
|
307
|
+
const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
|
|
278
308
|
return (async () => {
|
|
279
309
|
try {
|
|
280
310
|
const { defaultFeatureConfig } = await import('../adt/config.js');
|
|
@@ -313,6 +343,17 @@ export function runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig
|
|
|
313
343
|
}
|
|
314
344
|
}
|
|
315
345
|
setCachedFeatures(features);
|
|
346
|
+
// Proactive warning: on SAP_BASIS < 7.51 the ADT REST handler does not honor the
|
|
347
|
+
// stateful-session header over HTTP, so object writes fail with 423 "invalid lock
|
|
348
|
+
// handle" until the abapfs_extensions enhancement is installed. Warn at startup —
|
|
349
|
+
// before the first cryptic 423 — but only when writes are enabled (issue #293).
|
|
350
|
+
if (shouldWarnPreStatefulRelease(config.allowWrites, features.abapRelease)) {
|
|
351
|
+
logger.warn(`SAP_BASIS ${features.abapRelease} is below 7.51 and does not natively honor stateful ADT ` +
|
|
352
|
+
'HTTP sessions — object writes will fail with 423 "invalid lock handle" UNLESS the ' +
|
|
353
|
+
'abapfs_extensions enhancement is installed on the SAP system ' +
|
|
354
|
+
'(https://github.com/marcellourbani/abapfs_extensions). If writes already work, this is ' +
|
|
355
|
+
'installed and you can ignore this. See docs/sap-trial-setup.md (423 troubleshooting).');
|
|
356
|
+
}
|
|
316
357
|
setCachedDiscovery(features.discoveryMap ?? new Map());
|
|
317
358
|
}
|
|
318
359
|
catch {
|
|
@@ -358,7 +399,7 @@ function buildStartupAuthFailureReason(statusCode, config) {
|
|
|
358
399
|
* - 401/403 are blocking failures
|
|
359
400
|
* - Network/other failures are inconclusive (non-blocking)
|
|
360
401
|
*/
|
|
361
|
-
export async function runStartupAuthPreflight(config, btpProxy, bearerTokenProvider) {
|
|
402
|
+
export async function runStartupAuthPreflight(config, btpProxy, bearerTokenProvider, adtSemaphore) {
|
|
362
403
|
const checkedAt = new Date().toISOString();
|
|
363
404
|
const endpoint = STARTUP_AUTH_ENDPOINT;
|
|
364
405
|
if (config.ppEnabled) {
|
|
@@ -372,7 +413,7 @@ export async function runStartupAuthPreflight(config, btpProxy, bearerTokenProvi
|
|
|
372
413
|
return { status: 'skipped', blocking: false, endpoint, checkedAt, reason };
|
|
373
414
|
}
|
|
374
415
|
try {
|
|
375
|
-
const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider));
|
|
416
|
+
const client = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
|
|
376
417
|
await client.http.get(endpoint);
|
|
377
418
|
const reason = 'Startup auth preflight succeeded for shared SAP credentials.';
|
|
378
419
|
logger.info(reason, { endpoint });
|
|
@@ -424,10 +465,12 @@ export function formatStartupAuthPreflightToolError(preflight) {
|
|
|
424
465
|
* @param startupProbePromise Promise from runStartupProbe() — ListTools waits on this
|
|
425
466
|
* @param startupAuthPreflightPromise Promise from runStartupAuthPreflight() — CallTool blocks on auth failure in shared mode
|
|
426
467
|
*/
|
|
427
|
-
export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise) {
|
|
468
|
+
export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter) {
|
|
428
469
|
const server = new Server({ name: 'arc-1', version: VERSION }, { capabilities: { tools: {} } });
|
|
429
|
-
// Create default ADT client (shared, uses startup-time credentials or OAuth bearer)
|
|
430
|
-
|
|
470
|
+
// Create default ADT client (shared, uses startup-time credentials or OAuth bearer).
|
|
471
|
+
// Passes the shared server-wide semaphore so per-user PP clients (created at request
|
|
472
|
+
// time) share the same Layer 3 concurrency cap.
|
|
473
|
+
const defaultClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
|
|
431
474
|
// Cookie-auth preflight propagation: when startup preflight returned a non-blocking
|
|
432
475
|
// 401 in SAP_COOKIE_FILE mode, the throwaway preflight client marked itself stale —
|
|
433
476
|
// but the long-lived defaultClient was constructed independently with cookies read at
|
|
@@ -487,7 +530,7 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
|
|
|
487
530
|
const ppUser = (extra.authInfo?.extra?.userName ?? extra.authInfo?.clientId);
|
|
488
531
|
const ppDest = process.env.SAP_BTP_PP_DESTINATION ?? process.env.SAP_BTP_DESTINATION ?? '';
|
|
489
532
|
try {
|
|
490
|
-
client = await createPerUserClient(config, btpConfig, btpProxy, token);
|
|
533
|
+
client = await createPerUserClient(config, btpConfig, btpProxy, token, adtSemaphore);
|
|
491
534
|
isPerUserClient = true;
|
|
492
535
|
logger.emitAudit({
|
|
493
536
|
timestamp: new Date().toISOString(),
|
|
@@ -558,7 +601,7 @@ export function createServer(config, btpProxy, btpConfig, bearerTokenProvider, c
|
|
|
558
601
|
effectiveClient = client.withSafety(effectiveSafety);
|
|
559
602
|
}
|
|
560
603
|
effectiveClient.http.setDiscoveryMap(getCachedDiscovery());
|
|
561
|
-
const result = await handleToolCall(effectiveClient, config, toolName, args, extra.authInfo, server, cachingLayer, isPerUserClient);
|
|
604
|
+
const result = await handleToolCall(effectiveClient, config, toolName, args, extra.authInfo, server, cachingLayer, isPerUserClient, mcpRateLimiter);
|
|
562
605
|
return { ...result };
|
|
563
606
|
});
|
|
564
607
|
return server;
|
|
@@ -705,6 +748,22 @@ export async function createAndStartServer(config, sources) {
|
|
|
705
748
|
ppEnabled: config.ppEnabled,
|
|
706
749
|
});
|
|
707
750
|
}
|
|
751
|
+
// ─── Layer 3: shared SAP-bound Semaphore (server-wide cap) ────────
|
|
752
|
+
// One Semaphore for the whole process. Threaded into the shared startup client AND
|
|
753
|
+
// every per-user PP client built at request time, so ARC1_MAX_CONCURRENT is a true
|
|
754
|
+
// server-wide ceiling rather than a per-client one (the latter would multiply the cap
|
|
755
|
+
// by the number of active PP users — see ADR-0004).
|
|
756
|
+
const adtSemaphore = new Semaphore(config.maxConcurrent);
|
|
757
|
+
logger.info('SAP semaphore', { maxConcurrent: config.maxConcurrent, scope: 'server-wide' });
|
|
758
|
+
// ─── Layer 2: per-user MCP tool-call rate limiter ─────────────────
|
|
759
|
+
// Applied inside handleToolCall. Stdio (no authInfo) is exempt — there's no user
|
|
760
|
+
// identity to key on. When rateLimit=0 the factory returns a no-op stub.
|
|
761
|
+
// See docs_page/rate-limiting.md.
|
|
762
|
+
const mcpRateLimiter = createMcpRateLimiter(config.rateLimit);
|
|
763
|
+
logger.info('MCP rate limiting', {
|
|
764
|
+
perMinute: config.rateLimit,
|
|
765
|
+
disabled: config.rateLimit === 0,
|
|
766
|
+
});
|
|
708
767
|
// ─── Cache Setup ───────────────────────────────────────────────────
|
|
709
768
|
const cachingLayer = await createCachingLayer(config);
|
|
710
769
|
if (cachingLayer) {
|
|
@@ -720,7 +779,7 @@ export async function createAndStartServer(config, sources) {
|
|
|
720
779
|
if (config.cacheWarmup && cachingLayer && config.url) {
|
|
721
780
|
try {
|
|
722
781
|
const { runWarmup } = await import('../cache/warmup.js');
|
|
723
|
-
const warmupClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider));
|
|
782
|
+
const warmupClient = new AdtClient(buildAdtConfig(config, btpProxy, bearerTokenProvider, undefined, adtSemaphore));
|
|
724
783
|
const result = await runWarmup(warmupClient, cachingLayer, config.cacheWarmupPackages || undefined, config.systemType);
|
|
725
784
|
logger.info('Cache warmup completed', {
|
|
726
785
|
objects: result.totalObjects,
|
|
@@ -740,7 +799,7 @@ export async function createAndStartServer(config, sources) {
|
|
|
740
799
|
// Run feature probe once at startup — shared across all requests (stdio and HTTP).
|
|
741
800
|
// First run startup auth preflight in shared mode. If it blocks (401/403), skip feature probe
|
|
742
801
|
// to avoid firing many failing requests with invalid technical credentials.
|
|
743
|
-
const startupAuthPreflightPromise = runStartupAuthPreflight(config, btpProxy, bearerTokenProvider);
|
|
802
|
+
const startupAuthPreflightPromise = runStartupAuthPreflight(config, btpProxy, bearerTokenProvider, adtSemaphore);
|
|
744
803
|
const startupProbePromise = (async () => {
|
|
745
804
|
const authPreflight = await startupAuthPreflightPromise;
|
|
746
805
|
if (authPreflight.blocking) {
|
|
@@ -748,9 +807,9 @@ export async function createAndStartServer(config, sources) {
|
|
|
748
807
|
setCachedDiscovery(new Map());
|
|
749
808
|
return;
|
|
750
809
|
}
|
|
751
|
-
await runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig);
|
|
810
|
+
await runStartupProbe(config, btpProxy, bearerTokenProvider, btpConfig, adtSemaphore);
|
|
752
811
|
})();
|
|
753
|
-
const server = createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise);
|
|
812
|
+
const server = createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter);
|
|
754
813
|
// Shutdown hook for SQLite cache cleanup (guard against double-close from multiple signals).
|
|
755
814
|
// IMPORTANT: registering a SIGINT/SIGTERM listener suppresses Node's default exit behavior,
|
|
756
815
|
// so we must call process.exit() explicitly after cleanup — otherwise Ctrl+C hangs the process.
|
|
@@ -820,7 +879,7 @@ export async function createAndStartServer(config, sources) {
|
|
|
820
879
|
}
|
|
821
880
|
}
|
|
822
881
|
const { startHttpServer } = await import('./http.js');
|
|
823
|
-
await startHttpServer(() => createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise), config, xsuaaCredentials);
|
|
882
|
+
await startHttpServer(() => createServer(config, btpProxy, btpConfig, bearerTokenProvider, cachingLayer, startupProbePromise, startupAuthPreflightPromise, adtSemaphore, mcpRateLimiter), config, xsuaaCredentials);
|
|
824
883
|
}
|
|
825
884
|
return server;
|
|
826
885
|
}
|