sap-ai-dev-toolkit 0.9.9922 → 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.
- package/.github/agents/sap-btp-infrastructure-admin.agent.md +23 -0
- package/.github/agents/sap-bw-datasphere-migration-specialist.agent.md +22 -0
- package/.github/agents/sap-datasphere-developer.agent.md +22 -0
- package/.github/agents/sap-devops-observability-specialist.agent.md +54 -0
- package/.github/agents/sap-process-automation-developer.agent.md +50 -0
- package/.github/skills/btp-platform-administration/SKILL.md +53 -0
- package/.github/skills/bw-datasphere-migration/SKILL.md +66 -0
- package/.github/skills/cloud-alm-operations/SKILL.md +135 -0
- package/.github/skills/datasphere-administration/SKILL.md +40 -0
- package/.github/skills/datasphere-development/SKILL.md +55 -0
- package/.github/skills/process-automation-development/SKILL.md +113 -0
- package/README.md +152 -8
- package/inventory.md +60 -0
- package/package.json +8 -2
- package/src/btp-cli.mjs +199 -0
- package/src/btp-server.mjs +125 -0
- package/src/btp-tools.mjs +776 -0
- package/src/cloud-alm-client.mjs +44 -0
- package/src/cloud-alm-config.mjs +92 -0
- package/src/cloud-alm-credentials.mjs +29 -0
- package/src/cloud-alm-data.mjs +129 -0
- package/src/cloud-alm-server.mjs +115 -0
- package/src/cloud-alm-tools.mjs +682 -0
- package/src/datasphere-auth.mjs +279 -0
- package/src/datasphere-cli.mjs +220 -0
- package/src/datasphere-command.mjs +236 -0
- package/src/datasphere-config.mjs +154 -0
- package/src/datasphere-credentials.mjs +37 -0
- package/src/datasphere-csn.mjs +73 -0
- package/src/datasphere-discovery.mjs +151 -0
- package/src/datasphere-server.mjs +249 -0
- package/src/datasphere-tools.mjs +809 -0
- package/src/hana-database.mjs +3 -3
- package/src/hana-inspector.mjs +2 -2
- package/src/harnesses.mjs +31 -1
- package/src/install-user-assets.mjs +1 -1
- package/src/integration-common.mjs +7 -7
- package/src/integration-config.mjs +5 -0
- package/src/integration-credentials.mjs +25 -16
- package/src/integration-http.mjs +24 -7
- package/src/launcher.mjs +56 -4
- package/src/mcp-config.mjs +37 -1
- package/src/process-automation-automation-tools.mjs +125 -0
- package/src/process-automation-client.mjs +36 -0
- package/src/process-automation-common.mjs +69 -0
- package/src/process-automation-config.mjs +142 -0
- package/src/process-automation-credentials.mjs +37 -0
- package/src/process-automation-inbox-tools.mjs +246 -0
- package/src/process-automation-server.mjs +139 -0
- package/src/process-automation-tools.mjs +727 -0
- package/src/process-automation-visibility-tools.mjs +182 -0
- package/src/setup.mjs +623 -11
- package/tools.md +138 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Shared schemas and helpers for the SAP Build Process Automation tool modules.
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { IntegrationError, odataKey } from './integration-http.mjs';
|
|
6
|
+
|
|
7
|
+
export const WORKFLOW = '/public/workflow/rest/v1';
|
|
8
|
+
export const INBOX = '/public/workflow/odata/v1/tcm';
|
|
9
|
+
export const RULES = '/public/rule/runtime/rest/v2/rule-services';
|
|
10
|
+
export const UNIFIED = '/public/unified/v1';
|
|
11
|
+
export const IRPA_RUNTIME = '/public/irpa/runtime/v1';
|
|
12
|
+
export const IRPA_MONITORING = '/public/irpa/monitoring/v1';
|
|
13
|
+
export const VISIBILITY = '/public/visibility/runtime';
|
|
14
|
+
export const EVENTS = '/internal/be/v1/events';
|
|
15
|
+
export const CONTEXT_MAX_BYTES = 48 * 1024;
|
|
16
|
+
|
|
17
|
+
// Workflow ids are UUID v1; a plain shape check avoids strict variant rejection.
|
|
18
|
+
export const guid = z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'expected a BPA id (UUID)');
|
|
19
|
+
export const identifier = (label = 'identifier') => z.string().regex(/^[A-Za-z0-9_.:-]{1,255}$/, `expected a ${label}`);
|
|
20
|
+
export const payload = z.record(z.string(), z.unknown());
|
|
21
|
+
export const environmentId = z.string().regex(/^[A-Za-z0-9_.-]{1,128}$/, 'expected an environment id').optional()
|
|
22
|
+
.describe('Environment id (Control Tower > Environments); defaults to PROCESS_AUTOMATION_ENVIRONMENT_ID when configured.');
|
|
23
|
+
export const timestamp = z.string().max(40).regex(/^\d{4}-\d\d-\d\d(?:T\d\d:\d\d(?::\d\d(?:\.\d{1,3})?)?(?:Z|[+-]\d\d:\d\d)?)?$/, 'expected an ISO 8601 date or timestamp');
|
|
24
|
+
export const top = (max, fallback) => z.number().int().min(1).max(max).default(fallback);
|
|
25
|
+
export const skip = z.number().int().min(0).default(0);
|
|
26
|
+
export const outputFile = z.string().min(1).max(1024).refine(isAbsolute, 'must be an absolute path')
|
|
27
|
+
.describe('Absolute path of a new local file (must not exist); written with mode 0600.');
|
|
28
|
+
|
|
29
|
+
export const rows = value => (Array.isArray(value) ? value : []);
|
|
30
|
+
export const enc = encodeURIComponent;
|
|
31
|
+
/** OData string literal ('x', quotes doubled) for keys and function-import parameters. */
|
|
32
|
+
export const odataString = odataKey;
|
|
33
|
+
|
|
34
|
+
export function capContext(value, field = 'context') {
|
|
35
|
+
const bytes = Buffer.byteLength(JSON.stringify(value ?? null));
|
|
36
|
+
if (bytes <= CONTEXT_MAX_BYTES) return { [field]: value };
|
|
37
|
+
return {
|
|
38
|
+
[`${field}_truncated`]: true,
|
|
39
|
+
[`${field}_bytes`]: bytes,
|
|
40
|
+
[`${field}_keys`]: value && typeof value === 'object' ? Object.keys(value) : []
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Header carrying the environment API key; `required` APIs fail before any tenant call without one. */
|
|
45
|
+
export function apiKeyHeaders(access, name, { required = false } = {}) {
|
|
46
|
+
if (access?.apiKey) return { [name]: access.apiKey };
|
|
47
|
+
if (required) {
|
|
48
|
+
throw new IntegrationError('This SAP Build Process Automation API needs an environment API key (Control Tower > Environments > API Keys); set PROCESS_AUTOMATION_API_KEY or rerun sap-ai-dev --setup-process-automation.');
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function environmentQuery(access, given) {
|
|
54
|
+
const value = given ?? access?.environmentId ?? undefined;
|
|
55
|
+
return value ? { environmentId: value } : {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Write downloaded bytes to a new user-only file; never overwrites. */
|
|
59
|
+
export async function writeExportFile(path, data) {
|
|
60
|
+
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data ?? new Uint8Array(0));
|
|
61
|
+
try {
|
|
62
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code === 'EEXIST') throw new IntegrationError(`output file ${path} already exists; choose a new path`);
|
|
65
|
+
if (error?.code === 'ENOENT') throw new IntegrationError(`output directory for ${path} does not exist`);
|
|
66
|
+
throw new IntegrationError(`output file ${path} could not be written (${error?.code || 'error'})`);
|
|
67
|
+
}
|
|
68
|
+
return { output_file: path, bytes: bytes.byteLength };
|
|
69
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SAP Build Process Automation service-key / OAuth client configuration.
|
|
2
|
+
// Pure: no I/O, no network. Two auth modes: 'technical' (service-key client
|
|
3
|
+
// credentials) and 'interactive' (authorization code + refresh token with the
|
|
4
|
+
// same service-key client, so actions are attributed to the signed-in user).
|
|
5
|
+
import { finalizeClientCredentials } from './integration-config.mjs';
|
|
6
|
+
|
|
7
|
+
const BASE_KEYS = ['PROCESS_AUTOMATION_URL', 'PROCESS_AUTOMATION_TOKEN_URL', 'PROCESS_AUTOMATION_CLIENT_ID', 'PROCESS_AUTOMATION_CLIENT_SECRET'];
|
|
8
|
+
|
|
9
|
+
export const PROCESS_AUTOMATION_OAUTH = Object.freeze({
|
|
10
|
+
product: 'SAP Build Process Automation',
|
|
11
|
+
setupCommand: 'sap-ai-dev --setup-process-automation',
|
|
12
|
+
sacCustomAuth: false
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new Error(message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function text(value) {
|
|
20
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a process-automation-service key (JSON text or object, optionally wrapped
|
|
25
|
+
* as `cf service-key --json` output) into `{ url, tokenUrl, clientId, clientSecret }`.
|
|
26
|
+
*/
|
|
27
|
+
export function parseProcessAutomationServiceKey(value) {
|
|
28
|
+
let parsed = value;
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(value);
|
|
32
|
+
} catch {
|
|
33
|
+
fail('PROCESS_AUTOMATION_SERVICE_KEY is not valid JSON');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!parsed || typeof parsed !== 'object') fail('PROCESS_AUTOMATION_SERVICE_KEY is not valid JSON');
|
|
37
|
+
const key = parsed.credentials ?? parsed.service_key?.credentials ?? parsed;
|
|
38
|
+
let api = text(key?.endpoints?.api);
|
|
39
|
+
if (!api) {
|
|
40
|
+
fail('PROCESS_AUTOMATION_SERVICE_KEY has no endpoints.api; use a service key of the SAP Build Process Automation instance (process-automation-service)');
|
|
41
|
+
}
|
|
42
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(api)) api = `https://${api}`;
|
|
43
|
+
const uaa = key.uaa ?? {};
|
|
44
|
+
if (!text(uaa.clientsecret) && (uaa.certificate || uaa.key)) {
|
|
45
|
+
fail('PROCESS_AUTOMATION_SERVICE_KEY is a certificate-based key; create a service key with client secret credentials');
|
|
46
|
+
}
|
|
47
|
+
const missing = ['url', 'clientid', 'clientsecret'].filter(field => !text(uaa[field])).map(field => `uaa.${field}`);
|
|
48
|
+
if (missing.length) fail(`PROCESS_AUTOMATION_SERVICE_KEY is missing ${missing.join(', ')}`);
|
|
49
|
+
return { url: api, tokenUrl: text(uaa.url), clientId: text(uaa.clientid), clientSecret: text(uaa.clientsecret) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ENVIRONMENT_ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
53
|
+
|
|
54
|
+
/** Optional environment-scoped API key (Control Tower > Environments > API Keys) and default environment. */
|
|
55
|
+
function parseOptionalAccess(raw) {
|
|
56
|
+
const apiKey = text(raw?.apiKey);
|
|
57
|
+
if (apiKey && (apiKey.length > 512 || /[^\x21-\x7e]/.test(apiKey))) fail('PROCESS_AUTOMATION_API_KEY must be a single printable token');
|
|
58
|
+
const environmentId = text(raw?.environmentId);
|
|
59
|
+
if (environmentId && !ENVIRONMENT_ID.test(environmentId)) fail('PROCESS_AUTOMATION_ENVIRONMENT_ID must be 1-128 letters, digits, dots, dashes or underscores');
|
|
60
|
+
return { apiKey: apiKey || null, environmentId: environmentId || null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate raw credentials into a frozen config.
|
|
65
|
+
* `raw = { auth, url, tokenUrl, clientId, clientSecret, refreshToken?, apiKey?, environmentId? }`.
|
|
66
|
+
*/
|
|
67
|
+
export function parseProcessAutomationCredentials(raw, source) {
|
|
68
|
+
const auth = text(raw?.auth) || 'technical';
|
|
69
|
+
if (auth !== 'technical' && auth !== 'interactive') fail('PROCESS_AUTOMATION_AUTH must be technical or interactive');
|
|
70
|
+
const clientId = text(raw?.clientId);
|
|
71
|
+
const clientSecret = text(raw?.clientSecret);
|
|
72
|
+
if (!clientId || !clientSecret) fail('PROCESS_AUTOMATION_CLIENT_ID and PROCESS_AUTOMATION_CLIENT_SECRET are required');
|
|
73
|
+
const base = finalizeClientCredentials('PROCESS_AUTOMATION', source, {
|
|
74
|
+
url: text(raw?.url),
|
|
75
|
+
tokenUrl: text(raw?.tokenUrl),
|
|
76
|
+
clientId,
|
|
77
|
+
clientSecret
|
|
78
|
+
});
|
|
79
|
+
let authorizationUrl = null;
|
|
80
|
+
let refreshToken = null;
|
|
81
|
+
if (auth === 'interactive') {
|
|
82
|
+
authorizationUrl = `${new URL(base.tokenUrl).origin}/oauth/authorize`;
|
|
83
|
+
refreshToken = text(raw?.refreshToken);
|
|
84
|
+
if (!refreshToken) fail('PROCESS_AUTOMATION_REFRESH_TOKEN is required for interactive sign-in; run sap-ai-dev --setup-process-automation');
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
source,
|
|
88
|
+
auth,
|
|
89
|
+
url: base.url,
|
|
90
|
+
tokenUrl: base.tokenUrl,
|
|
91
|
+
clientId: base.clientId,
|
|
92
|
+
clientSecret: base.clientSecret,
|
|
93
|
+
authorizationUrl,
|
|
94
|
+
refreshToken,
|
|
95
|
+
...parseOptionalAccess(raw)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve credentials: explicit PROCESS_AUTOMATION_* variables, then
|
|
101
|
+
* PROCESS_AUTOMATION_SERVICE_KEY, then the record stored by
|
|
102
|
+
* `sap-ai-dev --setup-process-automation`. PROCESS_AUTOMATION_API_KEY and
|
|
103
|
+
* PROCESS_AUTOMATION_ENVIRONMENT_ID always win over stored values.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveProcessAutomationConfig(env = process.env, { stored } = {}) {
|
|
106
|
+
if (BASE_KEYS.some(key => env[key] !== undefined && env[key] !== '')) {
|
|
107
|
+
const missing = BASE_KEYS.filter(key => !text(env[key]));
|
|
108
|
+
if (missing.length) fail(`Explicit PROCESS_AUTOMATION configuration is incomplete: ${missing.join(', ')}`);
|
|
109
|
+
return parseProcessAutomationCredentials({
|
|
110
|
+
auth: env.PROCESS_AUTOMATION_AUTH,
|
|
111
|
+
url: env.PROCESS_AUTOMATION_URL,
|
|
112
|
+
tokenUrl: env.PROCESS_AUTOMATION_TOKEN_URL,
|
|
113
|
+
clientId: env.PROCESS_AUTOMATION_CLIENT_ID,
|
|
114
|
+
clientSecret: env.PROCESS_AUTOMATION_CLIENT_SECRET,
|
|
115
|
+
refreshToken: env.PROCESS_AUTOMATION_REFRESH_TOKEN,
|
|
116
|
+
apiKey: env.PROCESS_AUTOMATION_API_KEY,
|
|
117
|
+
environmentId: env.PROCESS_AUTOMATION_ENVIRONMENT_ID
|
|
118
|
+
}, 'environment');
|
|
119
|
+
}
|
|
120
|
+
if (text(env.PROCESS_AUTOMATION_SERVICE_KEY)) {
|
|
121
|
+
return parseProcessAutomationCredentials({
|
|
122
|
+
auth: env.PROCESS_AUTOMATION_AUTH,
|
|
123
|
+
...parseProcessAutomationServiceKey(env.PROCESS_AUTOMATION_SERVICE_KEY),
|
|
124
|
+
refreshToken: env.PROCESS_AUTOMATION_REFRESH_TOKEN,
|
|
125
|
+
apiKey: env.PROCESS_AUTOMATION_API_KEY,
|
|
126
|
+
environmentId: env.PROCESS_AUTOMATION_ENVIRONMENT_ID
|
|
127
|
+
}, 'service-key');
|
|
128
|
+
}
|
|
129
|
+
if (stored) {
|
|
130
|
+
return parseProcessAutomationCredentials({
|
|
131
|
+
auth: stored.auth,
|
|
132
|
+
url: stored.url,
|
|
133
|
+
tokenUrl: stored.tokenurl,
|
|
134
|
+
clientId: stored.clientid,
|
|
135
|
+
clientSecret: stored.clientsecret,
|
|
136
|
+
refreshToken: stored.refreshtoken,
|
|
137
|
+
apiKey: text(env.PROCESS_AUTOMATION_API_KEY) || stored.apikey,
|
|
138
|
+
environmentId: text(env.PROCESS_AUTOMATION_ENVIRONMENT_ID) || stored.environmentid
|
|
139
|
+
}, 'stored-key');
|
|
140
|
+
}
|
|
141
|
+
return fail('Configure SAP Build Process Automation (PROCESS_AUTOMATION_SERVICE_KEY, or PROCESS_AUTOMATION_URL/PROCESS_AUTOMATION_TOKEN_URL/PROCESS_AUTOMATION_CLIENT_ID/PROCESS_AUTOMATION_CLIENT_SECRET) in the MCP host environment, or run sap-ai-dev --setup-process-automation.');
|
|
142
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// User-only store for SAP Build Process Automation OAuth client credentials (never
|
|
2
|
+
// mcp.json): ~/.config/sap-ai-dev-toolkit/process-automation.json, mode 0600, atomic writes.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { userConfigFilePath, writeUserOnlyJson } from './integration-credentials.mjs';
|
|
5
|
+
|
|
6
|
+
export function processAutomationCredentialsPath(env = process.env) {
|
|
7
|
+
return userConfigFilePath(env, 'process-automation.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Stored record ({ auth, url, tokenurl, clientid, clientsecret, refreshtoken?, apikey?, environmentid? }); undefined when absent. */
|
|
11
|
+
export function readStoredProcessAutomationCredentials(env = process.env) {
|
|
12
|
+
const path = processAutomationCredentialsPath(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 Build Process Automation credentials at ${path} could not be read`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(`Stored SAP Build Process Automation credentials at ${path} are not valid JSON; rerun sap-ai-dev --setup-process-automation`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function storeProcessAutomationCredentials(record, env = process.env) {
|
|
28
|
+
return writeUserOnlyJson(processAutomationCredentialsPath(env), record);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Persist a rotated refresh token without touching the rest of the record. */
|
|
32
|
+
export async function updateStoredProcessAutomationRefreshToken(refreshToken, env = process.env) {
|
|
33
|
+
const path = processAutomationCredentialsPath(env);
|
|
34
|
+
const stored = readStoredProcessAutomationCredentials(env);
|
|
35
|
+
if (!stored) throw new Error(`Stored SAP Build Process Automation credentials at ${path} could not be read`);
|
|
36
|
+
return writeUserOnlyJson(path, { ...stored, refreshtoken: refreshToken });
|
|
37
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// SAP Build Process Automation Inbox API (OData v2 Task Consumption Model) tools: the signed-in
|
|
2
|
+
// user's task inbox, task details, task definitions, claim/release/forward and substitution rules.
|
|
3
|
+
// The API is user-centric: with technical (client-credentials) auth it shows the technical
|
|
4
|
+
// client's own inbox, which is normally empty.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { CREATE_ANNOTATIONS, OVERWRITE_ANNOTATIONS, READ_ONLY_ANNOTATIONS, confirmation, requireConfirmation } from './integration-common.mjs';
|
|
7
|
+
import { INBOX, enc, odataString, rows, skip, timestamp, top } from './process-automation-common.mjs';
|
|
8
|
+
|
|
9
|
+
export const INBOX_TOOL_NAMES = Object.freeze([
|
|
10
|
+
'bpa_inbox_list_tasks',
|
|
11
|
+
'bpa_inbox_get_task',
|
|
12
|
+
'bpa_inbox_list_task_definitions',
|
|
13
|
+
'bpa_inbox_task_action',
|
|
14
|
+
'bpa_inbox_list_substitution_rules',
|
|
15
|
+
'bpa_inbox_create_substitution_rule',
|
|
16
|
+
'bpa_inbox_change_substitution_rule'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const USER_SCOPE = 'Needs the ProcessAutomationParticipant role; returns the signed-in user\'s inbox (with technical auth the technical client\'s, normally empty).';
|
|
20
|
+
|
|
21
|
+
// Status values the TCM $filter accepts (other values fail with HTTP 400).
|
|
22
|
+
const FILTER_STATUS = z.enum(['READY', 'RESERVED', 'IN_PROGRESS', 'EXECUTED']);
|
|
23
|
+
const PRIORITY = z.enum(['LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']);
|
|
24
|
+
const ORDER_BY = z.enum([
|
|
25
|
+
'TaskDefinitionName asc', 'TaskDefinitionName desc', 'TaskTitle asc', 'TaskTitle desc', 'Status asc', 'Status desc',
|
|
26
|
+
'Priority asc', 'Priority desc', 'PriorityNumber asc', 'PriorityNumber desc', 'CreatedOn asc', 'CreatedOn desc',
|
|
27
|
+
'CreatedByName asc', 'CreatedByName desc', 'CompletionDeadLine asc', 'CompletionDeadLine desc'
|
|
28
|
+
]);
|
|
29
|
+
const TASK_INCLUDES = Object.freeze({
|
|
30
|
+
task_definition: '/TaskDefinitionData',
|
|
31
|
+
workflow_logs: '/WorkflowLogs',
|
|
32
|
+
description: '/Description',
|
|
33
|
+
custom_attributes: '/CustomAttributeData'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const origin = z.string().min(1).max(100).describe('SAP__Origin of the task; \'NA\' for SAP Build Process Automation.');
|
|
37
|
+
const instanceId = z.string().min(1).max(255).describe('Task InstanceID.');
|
|
38
|
+
const text = max => z.string().min(1).max(max);
|
|
39
|
+
|
|
40
|
+
/** UTC milliseconds of an ISO timestamp; a timestamp without zone is read as UTC (TCM expects UTC). */
|
|
41
|
+
function utcMillis(value) {
|
|
42
|
+
const zoned = /T/.test(value) && !/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? `${value}Z` : value;
|
|
43
|
+
return Date.parse(zoned);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** OData v2 datetime literal, e.g. datetime'2026-11-05T00:00:00'. */
|
|
47
|
+
const datetimeLiteral = value => `datetime'${new Date(utcMillis(value)).toISOString().slice(0, 19)}'`;
|
|
48
|
+
|
|
49
|
+
/** Drop OData v2 `__metadata` and deferred navigation stubs so results stay compact. */
|
|
50
|
+
function clean(value) {
|
|
51
|
+
if (Array.isArray(value)) return value.map(clean);
|
|
52
|
+
if (!value || typeof value !== 'object') return value;
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [key, item] of Object.entries(value)) {
|
|
55
|
+
if (key === '__metadata') continue;
|
|
56
|
+
if (item && typeof item === 'object' && !Array.isArray(item) && Object.keys(item).length === 1 && item.__deferred) continue;
|
|
57
|
+
result[key] = clean(item);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const anyOf = (field, values) => {
|
|
63
|
+
const terms = values.map(value => `${field} eq ${odataString(value)}`);
|
|
64
|
+
return terms.length > 1 ? `(${terms.join(' or ')})` : terms[0];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** One $filter from typed inputs; attributes are and-combined, multiple values of one attribute or-combined. */
|
|
68
|
+
function taskFilter({ status, task_definition_id, priority, created_from, created_up_to, due_from, due_up_to, custom_attributes }) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (status?.length) parts.push(anyOf('Status', status));
|
|
71
|
+
if (task_definition_id?.length) parts.push(anyOf('TaskDefinitionID', task_definition_id));
|
|
72
|
+
if (priority?.length) parts.push(anyOf('Priority', priority));
|
|
73
|
+
if (created_from) parts.push(`CreatedOn ge ${datetimeLiteral(created_from)}`);
|
|
74
|
+
if (created_up_to) parts.push(`CreatedOn le ${datetimeLiteral(created_up_to)}`);
|
|
75
|
+
if (due_from) parts.push(`CompletionDeadLine ge ${datetimeLiteral(due_from)}`);
|
|
76
|
+
if (due_up_to) parts.push(`CompletionDeadLine le ${datetimeLiteral(due_up_to)}`);
|
|
77
|
+
for (const { name, value } of custom_attributes ?? []) {
|
|
78
|
+
parts.push(`(CustomAttributeData/Name eq ${odataString(name)} and CustomAttributeData/Value eq ${odataString(value)})`);
|
|
79
|
+
}
|
|
80
|
+
return parts.length ? parts.join(' and ') : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const taskKey = (sapOrigin, id) => `/TaskCollection(SAP__Origin=${enc(odataString(sapOrigin))},InstanceID=${enc(odataString(id))})`;
|
|
84
|
+
|
|
85
|
+
export function registerInboxTools(register, { rest }) {
|
|
86
|
+
const get = (path, query = {}) => rest.request('GET', `${INBOX}${path}`, { query: { ...query, $format: 'json' } });
|
|
87
|
+
const count = async (path, query = {}) => {
|
|
88
|
+
const body = await rest.request('GET', `${INBOX}${path}/$count`, { query, accept: 'text/plain', responseType: 'text' });
|
|
89
|
+
const value = Number(String(body ?? '').trim());
|
|
90
|
+
return { count: Number.isFinite(value) ? value : null };
|
|
91
|
+
};
|
|
92
|
+
// Function imports take OData literals as query parameters and answer 202 with the changed entity.
|
|
93
|
+
const functionImport = async (name, query) => clean(await rest.request('POST', `${INBOX}/${name}`, {
|
|
94
|
+
query: { ...query, $format: 'json' },
|
|
95
|
+
expect: [200, 202, 204]
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
register('bpa_inbox_list_tasks', 'List inbox tasks',
|
|
99
|
+
`List or count tasks in the Inbox (Task Consumption Model) with typed filters. ${USER_SCOPE}`,
|
|
100
|
+
z.object({
|
|
101
|
+
status: z.array(FILTER_STATUS).min(1).max(4).optional().describe('Defaults to the API default (READY or RESERVED).'),
|
|
102
|
+
task_definition_id: z.array(text(255)).min(1).max(20).optional().describe('TaskDefinitionID values ({task name}@{workflow definition id}, lowercase).'),
|
|
103
|
+
priority: z.array(PRIORITY).min(1).max(4).optional(),
|
|
104
|
+
created_from: timestamp.optional().describe('CreatedOn ge (UTC).'),
|
|
105
|
+
created_up_to: timestamp.optional().describe('CreatedOn le (UTC).'),
|
|
106
|
+
due_from: timestamp.optional().describe('CompletionDeadLine ge (UTC).'),
|
|
107
|
+
due_up_to: timestamp.optional().describe('CompletionDeadLine le (UTC).'),
|
|
108
|
+
custom_attributes: z.array(z.object({ name: text(255), value: z.string().max(255) })).min(1).max(10).optional()
|
|
109
|
+
.describe('Custom attribute name/value pairs, all must match; also filter task_definition_id for performance.'),
|
|
110
|
+
orderby: z.array(ORDER_BY).min(1).max(2).default(['CreatedOn desc'])
|
|
111
|
+
.describe('Up to two sort keys; TaskDefinitionName must come first when used.'),
|
|
112
|
+
expand: z.array(z.enum(['Description', 'CustomAttributeData'])).max(2).optional(),
|
|
113
|
+
top: top(200, 50),
|
|
114
|
+
skip,
|
|
115
|
+
count_only: z.boolean().default(false).describe('Return only the number of matching tasks (TaskCollection/$count).')
|
|
116
|
+
}).refine(args => !args.orderby.slice(1).some(key => key.startsWith('TaskDefinitionName')), {
|
|
117
|
+
message: 'TaskDefinitionName must be the first orderby key', path: ['orderby']
|
|
118
|
+
}).refine(args => !(args.count_only && args.custom_attributes), {
|
|
119
|
+
message: 'custom_attributes filters are not supported with count_only', path: ['custom_attributes']
|
|
120
|
+
}),
|
|
121
|
+
READ_ONLY_ANNOTATIONS,
|
|
122
|
+
async args => {
|
|
123
|
+
const filter = taskFilter(args);
|
|
124
|
+
if (args.count_only) return count('/TaskCollection', filter ? { $filter: filter } : {});
|
|
125
|
+
const tasks = await get('/TaskCollection', {
|
|
126
|
+
...(filter ? { $filter: filter } : {}),
|
|
127
|
+
$orderby: args.orderby.join(','),
|
|
128
|
+
...(args.expand?.length ? { $expand: args.expand.join(',') } : {}),
|
|
129
|
+
$top: args.top,
|
|
130
|
+
$skip: args.skip
|
|
131
|
+
});
|
|
132
|
+
return { tasks: clean(rows(tasks)) };
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
register('bpa_inbox_get_task', 'Get inbox task',
|
|
136
|
+
`Get one inbox task with optional task definition, workflow log, description and custom attributes. ${USER_SCOPE}`,
|
|
137
|
+
{
|
|
138
|
+
origin,
|
|
139
|
+
instance_id: instanceId,
|
|
140
|
+
include: z.array(z.enum(Object.keys(TASK_INCLUDES))).max(4).default([])
|
|
141
|
+
},
|
|
142
|
+
READ_ONLY_ANNOTATIONS,
|
|
143
|
+
async ({ origin: sapOrigin, instance_id, include }) => {
|
|
144
|
+
const key = taskKey(sapOrigin, instance_id);
|
|
145
|
+
const result = { task: clean(await get(key)) };
|
|
146
|
+
for (const name of new Set(include)) result[name] = clean(await get(`${key}${TASK_INCLUDES[name]}`));
|
|
147
|
+
return result;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
register('bpa_inbox_list_task_definitions', 'List inbox task definitions',
|
|
151
|
+
`List (or count) the task definitions of the user's inbox tasks, or with origin and task_definition_id the custom attribute definitions of one. ${USER_SCOPE}`,
|
|
152
|
+
z.object({
|
|
153
|
+
origin: origin.optional(),
|
|
154
|
+
task_definition_id: text(255).optional(),
|
|
155
|
+
count_only: z.boolean().default(false).describe('Return only the number of task definitions (TaskDefinitionCollection/$count).')
|
|
156
|
+
}).refine(args => Boolean(args.origin) === Boolean(args.task_definition_id), {
|
|
157
|
+
message: 'provide both origin and task_definition_id, or neither', path: ['task_definition_id']
|
|
158
|
+
}).refine(args => !(args.count_only && args.task_definition_id), {
|
|
159
|
+
message: 'count_only lists all task definitions; omit origin and task_definition_id', path: ['count_only']
|
|
160
|
+
}),
|
|
161
|
+
READ_ONLY_ANNOTATIONS,
|
|
162
|
+
async ({ origin: sapOrigin, task_definition_id, count_only }) => {
|
|
163
|
+
if (count_only) return count('/TaskDefinitionCollection');
|
|
164
|
+
if (task_definition_id) {
|
|
165
|
+
const path = `/TaskDefinitionCollection(SAP__Origin=${enc(odataString(sapOrigin))},TaskDefinitionID=${enc(odataString(task_definition_id))})/CustomAttributeDefinitionData`;
|
|
166
|
+
return { custom_attribute_definitions: clean(rows(await get(path))) };
|
|
167
|
+
}
|
|
168
|
+
return { task_definitions: clean(rows(await get('/TaskDefinitionCollection'))) };
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
register('bpa_inbox_task_action', 'Claim, release or forward inbox task',
|
|
172
|
+
`Claim (to RESERVED), release (to READY) or forward (to another user, who becomes processor and recipient; the user id is not validated) an inbox task. ${USER_SCOPE}`,
|
|
173
|
+
z.object({
|
|
174
|
+
origin,
|
|
175
|
+
instance_id: instanceId,
|
|
176
|
+
action: z.enum(['claim', 'release', 'forward']),
|
|
177
|
+
forward_to: text(255).optional().describe('User id to forward to; required for forward only.'),
|
|
178
|
+
confirmation
|
|
179
|
+
}).refine(args => (args.action === 'forward') === Boolean(args.forward_to), {
|
|
180
|
+
message: 'forward_to is required for forward and not allowed otherwise', path: ['forward_to']
|
|
181
|
+
}),
|
|
182
|
+
OVERWRITE_ANNOTATIONS,
|
|
183
|
+
async ({ origin: sapOrigin, instance_id, action, forward_to, confirmation: given }) => {
|
|
184
|
+
requireConfirmation(given, `${action.toUpperCase()} BPA INBOX TASK ${instance_id}`);
|
|
185
|
+
const name = { claim: 'Claim', release: 'Release', forward: 'Forward' }[action];
|
|
186
|
+
const task = await functionImport(name, {
|
|
187
|
+
SAP__Origin: odataString(sapOrigin),
|
|
188
|
+
InstanceID: odataString(instance_id),
|
|
189
|
+
...(action === 'forward' ? { ForwardTo: odataString(forward_to) } : {})
|
|
190
|
+
});
|
|
191
|
+
return { action, instance_id, task };
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
register('bpa_inbox_list_substitution_rules', 'List substitution rules',
|
|
195
|
+
`List the user's own substitution rules (my_rules) or the rules where the user is the substitute (substituting_for). ${USER_SCOPE}`,
|
|
196
|
+
{ scope: z.enum(['my_rules', 'substituting_for']).default('my_rules') },
|
|
197
|
+
READ_ONLY_ANNOTATIONS,
|
|
198
|
+
async ({ scope }) => ({
|
|
199
|
+
rules: clean(rows(await get(scope === 'my_rules' ? '/SubstitutionRuleCollection' : '/SubstitutesRuleCollection')))
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
register('bpa_inbox_create_substitution_rule', 'Create substitution rule',
|
|
203
|
+
`Create a substitution rule for the signed-in user: RECEIVE_TASKS (planned, enabled, needs end_date) or TAKE_OVER (unplanned, disabled until the substitute enables it). ${USER_SCOPE}`,
|
|
204
|
+
z.object({
|
|
205
|
+
user: text(255).describe('User id of the substitute.'),
|
|
206
|
+
begin_date: timestamp.describe('UTC begin; yesterday, today or later.'),
|
|
207
|
+
end_date: timestamp.optional().describe('UTC end; required for RECEIVE_TASKS, ignored for TAKE_OVER.'),
|
|
208
|
+
mode: z.enum(['RECEIVE_TASKS', 'TAKE_OVER']),
|
|
209
|
+
enabled: z.boolean().optional().describe('Must be true for RECEIVE_TASKS and false for TAKE_OVER; derived from mode when omitted.'),
|
|
210
|
+
origin: origin.default('NA'),
|
|
211
|
+
confirmation
|
|
212
|
+
}).refine(args => args.mode !== 'RECEIVE_TASKS' || Boolean(args.end_date), {
|
|
213
|
+
message: 'end_date is required for RECEIVE_TASKS', path: ['end_date']
|
|
214
|
+
}).refine(args => args.enabled === undefined || args.enabled === (args.mode === 'RECEIVE_TASKS'), {
|
|
215
|
+
message: 'enabled must be true for RECEIVE_TASKS and false for TAKE_OVER', path: ['enabled']
|
|
216
|
+
}),
|
|
217
|
+
CREATE_ANNOTATIONS,
|
|
218
|
+
async ({ user, begin_date, end_date, mode, origin: sapOrigin, confirmation: given }) => {
|
|
219
|
+
requireConfirmation(given, `CREATE BPA SUBSTITUTION FOR ${user}`);
|
|
220
|
+
const rule = await rest.request('POST', `${INBOX}/SubstitutionRuleCollection`, {
|
|
221
|
+
query: { $format: 'json' },
|
|
222
|
+
json: {
|
|
223
|
+
SAP__Origin: sapOrigin,
|
|
224
|
+
User: user,
|
|
225
|
+
BeginDate: `/Date(${utcMillis(begin_date)})/`,
|
|
226
|
+
...(mode === 'RECEIVE_TASKS' ? { EndDate: `/Date(${utcMillis(end_date)})/` } : {}),
|
|
227
|
+
Mode: mode,
|
|
228
|
+
IsEnabled: mode === 'RECEIVE_TASKS'
|
|
229
|
+
},
|
|
230
|
+
expect: [200, 201]
|
|
231
|
+
});
|
|
232
|
+
return { rule: clean(rule) };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
register('bpa_inbox_change_substitution_rule', 'Enable, disable or delete substitution rule',
|
|
236
|
+
`Enable or disable a passive (TAKE_OVER) substitution rule where the user is the substitute, or delete one of the user's substitution rules. ${USER_SCOPE}`,
|
|
237
|
+
{ rule_id: text(255).describe('SubstitutionRuleID.'), action: z.enum(['enable', 'disable', 'delete']), confirmation },
|
|
238
|
+
OVERWRITE_ANNOTATIONS,
|
|
239
|
+
async ({ rule_id, action, confirmation: given }) => {
|
|
240
|
+
requireConfirmation(given, `${action.toUpperCase()} BPA SUBSTITUTION ${rule_id}`);
|
|
241
|
+
const rule = action === 'delete'
|
|
242
|
+
? await functionImport('DeleteSubstitutionRule', { SubstitutionRuleID: odataString(rule_id) })
|
|
243
|
+
: await functionImport('EnableSubstitutionRule', { SubstitutionRuleID: odataString(rule_id), Enabled: action === 'enable' ? 'true' : 'false' });
|
|
244
|
+
return { action, rule_id, rule: rule ?? null };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sap-ai-process-automation: stdio MCP server for SAP Build Process Automation.
|
|
3
|
+
// Technical (service-key client credentials) or interactive (authorization code +
|
|
4
|
+
// refresh token with the service-key client) OAuth; Workflow, decision runtime and
|
|
5
|
+
// business event APIs.
|
|
6
|
+
import { realpathSync } from 'node:fs';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import { createProcessAutomationRestClient } from './process-automation-client.mjs';
|
|
12
|
+
import { resolveProcessAutomationConfig } from './process-automation-config.mjs';
|
|
13
|
+
import {
|
|
14
|
+
processAutomationCredentialsPath,
|
|
15
|
+
readStoredProcessAutomationCredentials,
|
|
16
|
+
updateStoredProcessAutomationRefreshToken
|
|
17
|
+
} from './process-automation-credentials.mjs';
|
|
18
|
+
import { registerProcessAutomationTools } from './process-automation-tools.mjs';
|
|
19
|
+
|
|
20
|
+
const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
21
|
+
|
|
22
|
+
/** Create an injectable server runtime for tests and the stdio executable. */
|
|
23
|
+
export function createProcessAutomationServer({ env = process.env, fetchImpl } = {}) {
|
|
24
|
+
const config = resolveProcessAutomationConfig(env, { stored: readStoredProcessAutomationCredentials(env) });
|
|
25
|
+
const rest = createProcessAutomationRestClient(config, {
|
|
26
|
+
env,
|
|
27
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
28
|
+
persistRefreshToken: token => (config.source === 'stored-key' ? updateStoredProcessAutomationRefreshToken(token, env) : undefined)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const summary = Object.freeze({
|
|
32
|
+
api_host: new URL(config.url).host,
|
|
33
|
+
uaa_host: new URL(config.tokenUrl).host,
|
|
34
|
+
auth: config.auth,
|
|
35
|
+
source: config.source,
|
|
36
|
+
task_attribution: config.auth === 'technical' ? 'technical client' : 'signed-in user',
|
|
37
|
+
api_key_configured: Boolean(config.apiKey),
|
|
38
|
+
environment_id: config.environmentId
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const verify = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const definitions = await rest.request('GET', '/public/workflow/rest/v1/workflow-definitions', { query: { $top: 1 } });
|
|
44
|
+
return { workflow_api: `ok (${Array.isArray(definitions) ? definitions.length : 0} definitions on first page)` };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { workflow_api: `failed: ${error?.message || 'Workflow API request failed'}` };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const server = new McpServer({
|
|
51
|
+
name: 'sap-ai-process-automation',
|
|
52
|
+
version: packageMetadata.version
|
|
53
|
+
}, { capabilities: { tools: {} } });
|
|
54
|
+
registerProcessAutomationTools(server, { rest, summary, verify, access: { apiKey: config.apiKey, environmentId: config.environmentId } });
|
|
55
|
+
|
|
56
|
+
let closing;
|
|
57
|
+
const close = () => {
|
|
58
|
+
if (!closing) closing = server.close().catch(() => {});
|
|
59
|
+
return closing;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { server, close, verify, connectionSummary: summary };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runProcessAutomationServer({ env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
66
|
+
if (process.argv.slice(2).some(argument => argument === '--help' || argument === '-h')) {
|
|
67
|
+
stdout.write([
|
|
68
|
+
'Usage: sap-ai-process-automation',
|
|
69
|
+
'',
|
|
70
|
+
'Starts a stdio MCP server covering the SAP Build Process Automation APIs: Workflow (definitions,',
|
|
71
|
+
'instances, contexts, logs, errors, user tasks, export, purge, jobs, messages), Inbox (TCM),',
|
|
72
|
+
'Decisions, Unified API triggers, Automation Execution/Monitoring, Visibility Scenarios and',
|
|
73
|
+
'Workspace, and business events. Writes require exact confirmation strings.',
|
|
74
|
+
'',
|
|
75
|
+
'Credentials come from a service key of a process-automation-service instance in the MCP host',
|
|
76
|
+
'environment, or from the user-only store filled by sap-ai-dev --setup-process-automation:',
|
|
77
|
+
' PROCESS_AUTOMATION_SERVICE_KEY service key JSON (endpoints.api and uaa client secret)',
|
|
78
|
+
' PROCESS_AUTOMATION_URL API URL (endpoints.api); takes precedence over the key',
|
|
79
|
+
' PROCESS_AUTOMATION_TOKEN_URL XSUAA URL or token URL (uaa.url)',
|
|
80
|
+
' PROCESS_AUTOMATION_CLIENT_ID OAuth client id (all four variables required together)',
|
|
81
|
+
' PROCESS_AUTOMATION_CLIENT_SECRET OAuth client secret',
|
|
82
|
+
' PROCESS_AUTOMATION_AUTH technical (default) | interactive',
|
|
83
|
+
' PROCESS_AUTOMATION_REFRESH_TOKEN refresh token (required for interactive)',
|
|
84
|
+
' PROCESS_AUTOMATION_API_KEY environment API key (Control Tower > Environments > API Keys);',
|
|
85
|
+
' required for automation triggers/jobs/exports, optional elsewhere',
|
|
86
|
+
' PROCESS_AUTOMATION_ENVIRONMENT_ID default environment id for API triggers and process starts',
|
|
87
|
+
` ${processAutomationCredentialsPath(env)}`,
|
|
88
|
+
' sign-in stored by sap-ai-dev --setup-process-automation (never mcp.json)',
|
|
89
|
+
'',
|
|
90
|
+
'HTTP_PROXY, HTTPS_PROXY and NO_PROXY are honored.',
|
|
91
|
+
'',
|
|
92
|
+
' --check verify Workflow API access; exit 1 when the check fails',
|
|
93
|
+
''
|
|
94
|
+
].join('\n'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let runtime;
|
|
98
|
+
try {
|
|
99
|
+
runtime = createProcessAutomationServer({ env });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
stderr.write(`[sap-ai-process-automation] ${error.message}\n`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (process.argv.slice(2).includes('--check')) {
|
|
106
|
+
const report = await runtime.verify();
|
|
107
|
+
stdout.write(`${Object.entries(report).map(([name, result]) => `${name}: ${result}`).join('\n')}\n`);
|
|
108
|
+
await runtime.close();
|
|
109
|
+
if (Object.values(report).some(result => String(result).startsWith('failed'))) process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize: 1024 * 1024 });
|
|
114
|
+
let closing;
|
|
115
|
+
const shutdown = () => {
|
|
116
|
+
if (!closing) closing = runtime.close().catch(() => {});
|
|
117
|
+
return closing;
|
|
118
|
+
};
|
|
119
|
+
const onStdinEnd = () => { void shutdown(); };
|
|
120
|
+
const onSignal = signal => {
|
|
121
|
+
void shutdown().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
|
|
122
|
+
};
|
|
123
|
+
stdin.once('end', onStdinEnd);
|
|
124
|
+
process.once('SIGINT', onSignal);
|
|
125
|
+
process.once('SIGTERM', onSignal);
|
|
126
|
+
transport.onerror = () => stderr.write('[sap-ai-process-automation] MCP stdio transport failed.\n');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await runtime.server.connect(transport);
|
|
130
|
+
} catch {
|
|
131
|
+
stderr.write('[sap-ai-process-automation] MCP server startup failed.\n');
|
|
132
|
+
await shutdown();
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
138
|
+
await runProcessAutomationServer();
|
|
139
|
+
}
|