sap-ai-dev-toolkit 0.9.9921 → 0.9.9923

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/agents/sap-btp-infrastructure-admin.agent.md +23 -0
  2. package/.github/agents/sap-bw-datasphere-migration-specialist.agent.md +22 -0
  3. package/.github/agents/sap-datasphere-developer.agent.md +22 -0
  4. package/.github/agents/sap-devops-observability-specialist.agent.md +54 -0
  5. package/.github/agents/sap-process-automation-developer.agent.md +50 -0
  6. package/.github/skills/btp-platform-administration/SKILL.md +53 -0
  7. package/.github/skills/bw-datasphere-migration/SKILL.md +66 -0
  8. package/.github/skills/cloud-alm-operations/SKILL.md +135 -0
  9. package/.github/skills/datasphere-administration/SKILL.md +40 -0
  10. package/.github/skills/datasphere-development/SKILL.md +55 -0
  11. package/.github/skills/process-automation-development/SKILL.md +113 -0
  12. package/NOTICE +2 -1
  13. package/README.md +152 -8
  14. package/dist/checksums.txt +5 -5
  15. package/dist/vsp-darwin-arm64 +0 -0
  16. package/dist/vsp-darwin-x64 +0 -0
  17. package/dist/vsp-linux-arm64 +0 -0
  18. package/dist/vsp-linux-x64 +0 -0
  19. package/dist/vsp-win32-x64.exe +0 -0
  20. package/inventory.md +60 -0
  21. package/package.json +8 -2
  22. package/patches/vsp-namespaced-class-include-edit.patch +134 -0
  23. package/scripts/build-vsp.mjs +3 -1
  24. package/src/btp-cli.mjs +199 -0
  25. package/src/btp-server.mjs +125 -0
  26. package/src/btp-tools.mjs +776 -0
  27. package/src/cloud-alm-client.mjs +44 -0
  28. package/src/cloud-alm-config.mjs +92 -0
  29. package/src/cloud-alm-credentials.mjs +29 -0
  30. package/src/cloud-alm-data.mjs +129 -0
  31. package/src/cloud-alm-server.mjs +115 -0
  32. package/src/cloud-alm-tools.mjs +682 -0
  33. package/src/datasphere-auth.mjs +279 -0
  34. package/src/datasphere-cli.mjs +220 -0
  35. package/src/datasphere-command.mjs +236 -0
  36. package/src/datasphere-config.mjs +154 -0
  37. package/src/datasphere-credentials.mjs +37 -0
  38. package/src/datasphere-csn.mjs +73 -0
  39. package/src/datasphere-discovery.mjs +151 -0
  40. package/src/datasphere-server.mjs +249 -0
  41. package/src/datasphere-tools.mjs +809 -0
  42. package/src/hana-database.mjs +3 -3
  43. package/src/hana-inspector.mjs +2 -2
  44. package/src/harnesses.mjs +31 -1
  45. package/src/install-user-assets.mjs +1 -1
  46. package/src/integration-common.mjs +7 -7
  47. package/src/integration-config.mjs +5 -0
  48. package/src/integration-credentials.mjs +25 -16
  49. package/src/integration-http.mjs +24 -7
  50. package/src/launcher.mjs +56 -4
  51. package/src/mcp-config.mjs +37 -1
  52. package/src/process-automation-automation-tools.mjs +125 -0
  53. package/src/process-automation-client.mjs +36 -0
  54. package/src/process-automation-common.mjs +69 -0
  55. package/src/process-automation-config.mjs +142 -0
  56. package/src/process-automation-credentials.mjs +37 -0
  57. package/src/process-automation-inbox-tools.mjs +246 -0
  58. package/src/process-automation-server.mjs +139 -0
  59. package/src/process-automation-tools.mjs +727 -0
  60. package/src/process-automation-visibility-tools.mjs +182 -0
  61. package/src/setup.mjs +623 -11
  62. package/tools.md +138 -0
@@ -0,0 +1,44 @@
1
+ // SAP Cloud ALM API REST client, shared by the MCP server, the tools and the
2
+ // setup verification: XSUAA client credentials with token retry, on-demand CSRF
3
+ // and SAP error details.
4
+ import { list } from './integration-common.mjs';
5
+ import { createServiceClient } from './integration-http.mjs';
6
+
7
+ export const CLOUD_ALM_PATHS = Object.freeze({
8
+ analytics: '/calm-analytics/v1/analytics',
9
+ logs: '/calm-logs/v1/logs',
10
+ landscape: '/calm-landscape/v1/landscapeObjects',
11
+ features: '/calm-features/v1',
12
+ eventSituations: '/calm-event-situations/v1/event-situations'
13
+ });
14
+
15
+ export function createCloudAlmRestClient(config, { env = process.env, fetchImpl } = {}) {
16
+ return createServiceClient({
17
+ label: 'CLOUD_ALM',
18
+ url: config.url,
19
+ tokenUrl: config.tokenUrl,
20
+ clientId: config.clientId,
21
+ clientSecret: config.clientSecret,
22
+ csrfPath: `${CLOUD_ALM_PATHS.analytics}/providers`,
23
+ env,
24
+ ...(fetchImpl ? { fetchImpl } : {})
25
+ });
26
+ }
27
+
28
+ /** Read-only reachability check of the Landscape and Analytics APIs; never throws. */
29
+ export async function verifyCloudAlmAccess(rest) {
30
+ const report = {};
31
+ try {
32
+ const objects = list(await rest.request('GET', CLOUD_ALM_PATHS.landscape, { query: { limit: 1 } }));
33
+ report.landscape_api = `ok (${objects.length} object(s) on first page)`;
34
+ } catch (error) {
35
+ report.landscape_api = `failed: ${error?.message || 'Landscape API request failed'}`;
36
+ }
37
+ try {
38
+ const providers = list(await rest.request('GET', `${CLOUD_ALM_PATHS.analytics}/providers`));
39
+ report.analytics_api = `ok (${providers.length} authorized providers)`;
40
+ } catch (error) {
41
+ report.analytics_api = `failed: ${error?.message || 'Analytics API request failed'}`;
42
+ }
43
+ return report;
44
+ }
@@ -0,0 +1,92 @@
1
+ // SAP Cloud ALM API service-key / OAuth client configuration.
2
+ // Pure: no I/O, no network. SAP documents only client credentials for the
3
+ // Cloud ALM API, so there is no interactive mode or refresh token.
4
+ import { finalizeClientCredentials } from './integration-config.mjs';
5
+
6
+ const BASE_KEYS = ['CLOUD_ALM_URL', 'CLOUD_ALM_TOKEN_URL', 'CLOUD_ALM_CLIENT_ID', 'CLOUD_ALM_CLIENT_SECRET'];
7
+
8
+ function fail(message) {
9
+ throw new Error(message);
10
+ }
11
+
12
+ function text(value) {
13
+ return typeof value === 'string' ? value.trim() : '';
14
+ }
15
+
16
+ /**
17
+ * Parse a "SAP Cloud ALM API" service key (JSON text or object, optionally wrapped
18
+ * as `cf service-key --json` output) into `{ url, tokenUrl, clientId, clientSecret }`.
19
+ */
20
+ export function parseCloudAlmServiceKey(value) {
21
+ let parsed = value;
22
+ if (typeof value === 'string') {
23
+ try {
24
+ parsed = JSON.parse(value);
25
+ } catch {
26
+ fail('CLOUD_ALM_SERVICE_KEY is not valid JSON');
27
+ }
28
+ }
29
+ if (!parsed || typeof parsed !== 'object') fail('CLOUD_ALM_SERVICE_KEY is not valid JSON');
30
+ const key = parsed.credentials ?? parsed.service_key?.credentials ?? parsed;
31
+ let api = text(key?.endpoints?.Api) || text(key?.endpoints?.api);
32
+ if (!api) {
33
+ fail('CLOUD_ALM_SERVICE_KEY has no endpoints.Api; use a service key of the "SAP Cloud ALM API" instance (plan standard) from your Cloud ALM subaccount');
34
+ }
35
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(api)) api = `https://${api}`;
36
+ const uaa = key.uaa ?? {};
37
+ if (!text(uaa.clientsecret) && (uaa.certificate || uaa.key)) {
38
+ fail('CLOUD_ALM_SERVICE_KEY is a certificate (x509) binding; create a binding with client secret credentials (credential type binding-secret)');
39
+ }
40
+ const missing = ['url', 'clientid', 'clientsecret'].filter(field => !text(uaa[field])).map(field => `uaa.${field}`);
41
+ if (missing.length) fail(`CLOUD_ALM_SERVICE_KEY is missing ${missing.join(', ')}`);
42
+ return { url: api, tokenUrl: text(uaa.url), clientId: text(uaa.clientid), clientSecret: text(uaa.clientsecret) };
43
+ }
44
+
45
+ /** Validate raw credentials `{ url, tokenUrl, clientId, clientSecret }` into a frozen config. */
46
+ export function parseCloudAlmCredentials(raw, source) {
47
+ const clientId = text(raw?.clientId);
48
+ const clientSecret = text(raw?.clientSecret);
49
+ if (!clientId || !clientSecret) fail('CLOUD_ALM_CLIENT_ID and CLOUD_ALM_CLIENT_SECRET are required');
50
+ const base = finalizeClientCredentials('CLOUD_ALM', source, {
51
+ url: text(raw?.url),
52
+ tokenUrl: text(raw?.tokenUrl),
53
+ clientId,
54
+ clientSecret
55
+ });
56
+ return Object.freeze({
57
+ source,
58
+ url: base.url,
59
+ tokenUrl: base.tokenUrl,
60
+ clientId: base.clientId,
61
+ clientSecret: base.clientSecret
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Resolve credentials: explicit CLOUD_ALM_* variables, then CLOUD_ALM_SERVICE_KEY,
67
+ * then the record stored by `sap-ai-dev --setup-cloud-alm`.
68
+ */
69
+ export function resolveCloudAlmConfig(env = process.env, { stored } = {}) {
70
+ if (BASE_KEYS.some(key => env[key] !== undefined && env[key] !== '')) {
71
+ const missing = BASE_KEYS.filter(key => !text(env[key]));
72
+ if (missing.length) fail(`Explicit CLOUD_ALM configuration is incomplete: ${missing.join(', ')}`);
73
+ return parseCloudAlmCredentials({
74
+ url: env.CLOUD_ALM_URL,
75
+ tokenUrl: env.CLOUD_ALM_TOKEN_URL,
76
+ clientId: env.CLOUD_ALM_CLIENT_ID,
77
+ clientSecret: env.CLOUD_ALM_CLIENT_SECRET
78
+ }, 'environment');
79
+ }
80
+ if (text(env.CLOUD_ALM_SERVICE_KEY)) {
81
+ return parseCloudAlmCredentials(parseCloudAlmServiceKey(env.CLOUD_ALM_SERVICE_KEY), 'service-key');
82
+ }
83
+ if (stored) {
84
+ return parseCloudAlmCredentials({
85
+ url: stored.url,
86
+ tokenUrl: stored.tokenurl,
87
+ clientId: stored.clientid,
88
+ clientSecret: stored.clientsecret
89
+ }, 'stored-key');
90
+ }
91
+ return fail('Configure SAP Cloud ALM (CLOUD_ALM_SERVICE_KEY, or CLOUD_ALM_URL/CLOUD_ALM_TOKEN_URL/CLOUD_ALM_CLIENT_ID/CLOUD_ALM_CLIENT_SECRET) in the MCP host environment, or run sap-ai-dev --setup-cloud-alm.');
92
+ }
@@ -0,0 +1,29 @@
1
+ // User-only store for SAP Cloud ALM API client credentials (never mcp.json):
2
+ // ~/.config/sap-ai-dev-toolkit/cloud-alm.json, mode 0600, atomic writes.
3
+ import { readFileSync } from 'node:fs';
4
+ import { userConfigFilePath, writeUserOnlyJson } from './integration-credentials.mjs';
5
+
6
+ export function cloudAlmCredentialsPath(env = process.env) {
7
+ return userConfigFilePath(env, 'cloud-alm.json');
8
+ }
9
+
10
+ /** Stored record ({ url, tokenurl, clientid, clientsecret }); undefined when absent. */
11
+ export function readStoredCloudAlmCredentials(env = process.env) {
12
+ const path = cloudAlmCredentialsPath(env);
13
+ let text;
14
+ try {
15
+ text = readFileSync(path, 'utf8');
16
+ } catch (error) {
17
+ if (error?.code === 'ENOENT') return undefined;
18
+ throw new Error(`Stored SAP Cloud ALM credentials at ${path} could not be read`);
19
+ }
20
+ try {
21
+ return JSON.parse(text);
22
+ } catch {
23
+ throw new Error(`Stored SAP Cloud ALM credentials at ${path} are not valid JSON; rerun sap-ai-dev --setup-cloud-alm`);
24
+ }
25
+ }
26
+
27
+ export async function storeCloudAlmCredentials(record, env = process.env) {
28
+ return writeUserOnlyJson(cloudAlmCredentialsPath(env), record);
29
+ }
@@ -0,0 +1,129 @@
1
+ // Pure SAP Cloud ALM data helpers: API timestamps, lookback windows, OTLP log
2
+ // record flattening and analytics table reading/aggregation. No I/O.
3
+
4
+ /** Cloud ALM `YYYYMMDDHHmmss` UTC timestamp. */
5
+ export function calmTimestamp(ms) {
6
+ return new Date(ms).toISOString().slice(0, 19).replace(/[-:T]/g, '');
7
+ }
8
+
9
+ /** Window ending at `end` (ISO, default now) and spanning `spanMs`. */
10
+ export function lookbackWindow({ end, spanMs, now = Date.now }) {
11
+ const endMs = end === undefined ? now() : Date.parse(end);
12
+ const startMs = endMs - spanMs;
13
+ return {
14
+ startMs,
15
+ endMs,
16
+ from: calmTimestamp(startMs),
17
+ to: calmTimestamp(endMs),
18
+ from_iso: new Date(startMs).toISOString(),
19
+ to_iso: new Date(endMs).toISOString()
20
+ };
21
+ }
22
+
23
+ function cap(value, max) {
24
+ return typeof value === 'string' && value.length > max ? `${value.slice(0, max)}…` : value;
25
+ }
26
+
27
+ function nanosToIso(value) {
28
+ if (value === undefined || value === null || value === '') return undefined;
29
+ try {
30
+ const date = new Date(Number(BigInt(value) / 1000000n));
31
+ return Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ function anyValue(value, max) {
38
+ if (!value || typeof value !== 'object') return value;
39
+ if ('stringValue' in value) return cap(value.stringValue, max);
40
+ if ('boolValue' in value) return value.boolValue;
41
+ if ('intValue' in value) {
42
+ const number = Number(value.intValue);
43
+ return Number.isSafeInteger(number) ? number : String(value.intValue);
44
+ }
45
+ if ('doubleValue' in value) return Number(value.doubleValue);
46
+ if ('arrayValue' in value) return (value.arrayValue?.values ?? []).map(entry => anyValue(entry, max));
47
+ if ('kvlistValue' in value) return keyValues(value.kvlistValue?.values, max);
48
+ if ('bytesValue' in value) return '[bytes]';
49
+ return undefined;
50
+ }
51
+
52
+ function keyValues(entries, max) {
53
+ const result = {};
54
+ for (const entry of Array.isArray(entries) ? entries : []) {
55
+ if (typeof entry?.key === 'string') result[entry.key] = anyValue(entry.value, max);
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /** Flatten an OTLP JSON logs payload into one plain record per log record. */
61
+ export function flattenLogRecords(payload, { bodyMax = 2048, valueMax = 512 } = {}) {
62
+ const records = [];
63
+ for (const resourceLog of payload?.resourceLogs ?? []) {
64
+ const resource = keyValues(resourceLog?.resource?.attributes, valueMax);
65
+ const service = {
66
+ name: resource['service.name'],
67
+ type: resource['service.namespace'],
68
+ display_name: resource['sap.service.display_name'],
69
+ instance_id: resource['service.instance.id']
70
+ };
71
+ for (const scopeLog of resourceLog?.scopeLogs ?? []) {
72
+ for (const record of scopeLog?.logRecords ?? []) {
73
+ records.push({
74
+ time: nanosToIso(record.timeUnixNano),
75
+ observed_time: nanosToIso(record.observedTimeUnixNano),
76
+ severity: record.severityText || record.severityNumber,
77
+ body: anyValue(record.body, bodyMax),
78
+ service,
79
+ attributes: keyValues(record.attributes, valueMax)
80
+ });
81
+ }
82
+ }
83
+ }
84
+ return records;
85
+ }
86
+
87
+ /** Read one analytics table entry into row objects keyed by column text. */
88
+ export function readAnalyticsTable(entry) {
89
+ const columns = entry?.columns ?? entry?.COLUMNS ?? [];
90
+ const data = entry?.rows ?? entry?.ROWS ?? [];
91
+ const rows = data.map(values => Object.fromEntries(columns.map((column, index) => {
92
+ const value = values?.[index];
93
+ return [column.text, column.type === 'time' && value !== null && value !== undefined
94
+ ? new Date(Number(value)).toISOString()
95
+ : value];
96
+ })));
97
+ const total = Number.isFinite(Number(entry?.totalCount)) && entry?.totalCount !== null && entry?.totalCount !== undefined
98
+ ? Number(entry.totalCount)
99
+ : null;
100
+ const count = entry?.count;
101
+ const truncated = total !== null && count !== null && count !== undefined && Number.isFinite(Number(count)) && total > Number(count);
102
+ return { columns, rows, total, truncated };
103
+ }
104
+
105
+ /** Group rows by dimension columns (time dropped) and combine measures by sum or max; sorted by the first measure. */
106
+ export function aggregateAnalyticsRows(table, mode) {
107
+ const columns = table.columns.filter(column => column.type !== 'time');
108
+ const measures = columns.filter(column => column.type === 'number').map(column => column.text);
109
+ const dimensions = columns.filter(column => column.type !== 'number').map(column => column.text);
110
+ const groups = new Map();
111
+ for (const row of table.rows) {
112
+ const id = JSON.stringify(dimensions.map(name => row[name]));
113
+ let group = groups.get(id);
114
+ if (!group) {
115
+ group = Object.fromEntries(dimensions.map(name => [name, row[name]]));
116
+ for (const name of measures) group[name] = null;
117
+ groups.set(id, group);
118
+ }
119
+ for (const name of measures) {
120
+ const value = Number(row[name]);
121
+ if (row[name] === null || row[name] === undefined || !Number.isFinite(value)) continue;
122
+ group[name] = group[name] === null ? value : mode === 'max' ? Math.max(group[name], value) : group[name] + value;
123
+ }
124
+ }
125
+ const first = measures[0];
126
+ const result = [...groups.values()];
127
+ if (first !== undefined) result.sort((a, b) => (b[first] ?? -Infinity) - (a[first] ?? -Infinity));
128
+ return result;
129
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ // sap-ai-cloud-alm: stdio MCP server for SAP Cloud ALM operations (landscape,
3
+ // Integration & Exception Monitoring, analytics, features/transports, event
4
+ // situations) with service-key client credentials.
5
+ import { realpathSync } from 'node:fs';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { createCloudAlmRestClient, verifyCloudAlmAccess } from './cloud-alm-client.mjs';
11
+ import { resolveCloudAlmConfig } from './cloud-alm-config.mjs';
12
+ import { cloudAlmCredentialsPath, readStoredCloudAlmCredentials } from './cloud-alm-credentials.mjs';
13
+ import { registerCloudAlmTools } from './cloud-alm-tools.mjs';
14
+
15
+ const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
16
+
17
+ /** Create an injectable server runtime for tests and the stdio executable. */
18
+ export function createCloudAlmServer({ env = process.env, fetchImpl } = {}) {
19
+ const config = resolveCloudAlmConfig(env, { stored: readStoredCloudAlmCredentials(env) });
20
+ const rest = createCloudAlmRestClient(config, { env, ...(fetchImpl ? { fetchImpl } : {}) });
21
+
22
+ const summary = Object.freeze({
23
+ api_host: new URL(config.url).host,
24
+ token_host: new URL(config.tokenUrl).host,
25
+ auth: 'client_credentials',
26
+ source: config.source
27
+ });
28
+
29
+ const verify = () => verifyCloudAlmAccess(rest);
30
+
31
+ const server = new McpServer({
32
+ name: 'sap-ai-cloud-alm',
33
+ version: packageMetadata.version
34
+ }, { capabilities: { tools: {} } });
35
+ registerCloudAlmTools(server, { rest, summary, verify });
36
+
37
+ let closing;
38
+ const close = () => {
39
+ if (!closing) closing = server.close().catch(() => {});
40
+ return closing;
41
+ };
42
+
43
+ return { server, close, verify, connectionSummary: summary };
44
+ }
45
+
46
+ export async function runCloudAlmServer({ env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr } = {}) {
47
+ if (process.argv.slice(2).some(argument => argument === '--help' || argument === '-h')) {
48
+ stdout.write([
49
+ 'Usage: sap-ai-cloud-alm',
50
+ '',
51
+ 'Starts a stdio MCP server for SAP Cloud ALM operations: landscape objects and tiers, health,',
52
+ 'exception and alert hotspots, Integration & Exception Monitoring records, monitoring events,',
53
+ 'incident triage with CPI message log and SLG1 handoffs, analytics queries, feature and',
54
+ 'transport status. Closing an event situation requires an exact confirmation string.',
55
+ '',
56
+ 'Credentials come from a service key of a "SAP Cloud ALM API" instance (plan standard) in the',
57
+ 'MCP host environment, or from the user-only store filled by sap-ai-dev --setup-cloud-alm:',
58
+ ' CLOUD_ALM_SERVICE_KEY service key JSON (endpoints.Api and uaa client secret)',
59
+ ' CLOUD_ALM_URL API base URL including /api; takes precedence over the key',
60
+ ' CLOUD_ALM_TOKEN_URL XSUAA URL or token URL (uaa.url)',
61
+ ' CLOUD_ALM_CLIENT_ID OAuth client id (all four variables required together)',
62
+ ' CLOUD_ALM_CLIENT_SECRET OAuth client secret',
63
+ ` ${cloudAlmCredentialsPath(env)}`,
64
+ ' service key stored by sap-ai-dev --setup-cloud-alm (never mcp.json)',
65
+ '',
66
+ 'HTTP_PROXY, HTTPS_PROXY and NO_PROXY are honored.',
67
+ '',
68
+ ' --check verify Landscape and Analytics API access; exit 1 when a check fails',
69
+ ''
70
+ ].join('\n'));
71
+ return;
72
+ }
73
+ let runtime;
74
+ try {
75
+ runtime = createCloudAlmServer({ env });
76
+ } catch (error) {
77
+ stderr.write(`[sap-ai-cloud-alm] ${error.message}\n`);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ if (process.argv.slice(2).includes('--check')) {
82
+ const report = await runtime.verify();
83
+ stdout.write(`${Object.entries(report).map(([name, result]) => `${name}: ${result}`).join('\n')}\n`);
84
+ await runtime.close();
85
+ if (Object.values(report).some(result => String(result).startsWith('failed'))) process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize: 1024 * 1024 });
90
+ let closing;
91
+ const shutdown = () => {
92
+ if (!closing) closing = runtime.close().catch(() => {});
93
+ return closing;
94
+ };
95
+ const onStdinEnd = () => { void shutdown(); };
96
+ const onSignal = signal => {
97
+ void shutdown().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
98
+ };
99
+ stdin.once('end', onStdinEnd);
100
+ process.once('SIGINT', onSignal);
101
+ process.once('SIGTERM', onSignal);
102
+ transport.onerror = () => stderr.write('[sap-ai-cloud-alm] MCP stdio transport failed.\n');
103
+
104
+ try {
105
+ await runtime.server.connect(transport);
106
+ } catch {
107
+ stderr.write('[sap-ai-cloud-alm] MCP server startup failed.\n');
108
+ await shutdown();
109
+ process.exitCode = 1;
110
+ }
111
+ }
112
+
113
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
114
+ await runCloudAlmServer();
115
+ }