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
package/src/btp-cli.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Child-process runner for the SAP BTP CLI. It uses the user's own
|
|
2
|
+
// `btp login --sso` session from the CLI configuration and never logs in by
|
|
3
|
+
// itself (except loginBtpCli, called from the setup wizard). It never runs
|
|
4
|
+
// `btp target`; every subaccount-scoped call passes --subaccount. Calls are
|
|
5
|
+
// serialized so session refreshes inside the CLI cannot race.
|
|
6
|
+
import { execFile, spawn } from 'node:child_process';
|
|
7
|
+
import { constants, readFileSync } from 'node:fs';
|
|
8
|
+
import { access } from 'node:fs/promises';
|
|
9
|
+
import { delimiter, resolve } from 'node:path';
|
|
10
|
+
import { IntegrationError } from './integration-http.mjs';
|
|
11
|
+
import { diagnosticText } from './redact.mjs';
|
|
12
|
+
import { userConfigFilePath, writeUserOnlyJson } from './integration-credentials.mjs';
|
|
13
|
+
|
|
14
|
+
const CLI_TIMEOUT_MS = 120_000;
|
|
15
|
+
// Global-account entitlement JSON carries an iconBase64 per service.
|
|
16
|
+
const MAX_BUFFER = 32 * 1024 * 1024;
|
|
17
|
+
const ERROR_CAP = 800;
|
|
18
|
+
const LOGIN_REQUIRED = /Unknown session|Please log in|log in again|session (?:has )?expired/i;
|
|
19
|
+
|
|
20
|
+
export const BTP_LOGIN_REQUIRED_MESSAGE = 'The btp CLI session is missing or expired (sessions last at most 12 hours). Run "btp login --sso" in a terminal, or sap-ai-dev --setup-btp, then retry.';
|
|
21
|
+
|
|
22
|
+
/** Parse btp CLI JSON output; tolerates notices around the JSON. null for empty output, undefined when not JSON. */
|
|
23
|
+
export function parseCliJson(text) {
|
|
24
|
+
const trimmed = String(text ?? '').trim();
|
|
25
|
+
if (!trimmed) return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(trimmed);
|
|
28
|
+
} catch {
|
|
29
|
+
// Retry on the outermost JSON value below.
|
|
30
|
+
}
|
|
31
|
+
const starts = [trimmed.indexOf('{'), trimmed.indexOf('[')].filter(index => index >= 0);
|
|
32
|
+
const end = Math.max(trimmed.lastIndexOf('}'), trimmed.lastIndexOf(']'));
|
|
33
|
+
if (!starts.length || end < 0) return undefined;
|
|
34
|
+
const start = Math.min(...starts);
|
|
35
|
+
if (end <= start) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function btpSettingsPath(env = process.env) {
|
|
44
|
+
return userConfigFilePath(env, 'btp.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Stored { cli?, config? } written by sap-ai-dev --setup-btp; {} when absent. */
|
|
48
|
+
export function readBtpSettings(env = process.env) {
|
|
49
|
+
const path = btpSettingsPath(env);
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(path, 'utf8');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'ENOENT') return {};
|
|
55
|
+
throw new Error(`Stored btp CLI settings at ${path} could not be read`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(text);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`Stored btp CLI settings at ${path} are not valid JSON; rerun sap-ai-dev --setup-btp`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function storeBtpSettings(settings, env = process.env) {
|
|
65
|
+
return writeUserOnlyJson(btpSettingsPath(env), settings);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const trimmed = value => (typeof value === 'string' ? value.trim() : '');
|
|
69
|
+
|
|
70
|
+
export function resolveBtpCliSettings(env = process.env, { stored = readBtpSettings(env) } = {}) {
|
|
71
|
+
const fromEnv = trimmed(env.BTP_CLI);
|
|
72
|
+
const fromStore = trimmed(stored?.cli);
|
|
73
|
+
const cli = fromEnv || fromStore || 'btp';
|
|
74
|
+
const cliSource = fromEnv ? 'environment' : fromStore ? 'stored' : 'PATH';
|
|
75
|
+
const config = trimmed(env.BTP_CLIENTCONFIG) || trimmed(stored?.config) || null;
|
|
76
|
+
return Object.freeze({ cli, cliSource, config });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Absolute path of the btp executable on PATH, or null. */
|
|
80
|
+
export async function locateBtpCli(env = process.env) {
|
|
81
|
+
const executableNames = process.platform === 'win32' ? ['btp.exe', 'btp'] : ['btp'];
|
|
82
|
+
const searchPath = String(env.PATH || env.Path || '').split(delimiter).filter(Boolean);
|
|
83
|
+
for (const directory of searchPath) {
|
|
84
|
+
for (const executableName of executableNames) {
|
|
85
|
+
const candidate = resolve(directory, executableName);
|
|
86
|
+
try {
|
|
87
|
+
await access(candidate, constants.X_OK);
|
|
88
|
+
return candidate;
|
|
89
|
+
} catch {
|
|
90
|
+
// Continue searching PATH.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const childEnvironment = (env, config) => (config ? { ...env, BTP_CLIENTCONFIG: config } : env);
|
|
98
|
+
|
|
99
|
+
const missingCliError = cli => new IntegrationError(
|
|
100
|
+
`btp CLI not found (${cli}). Install the SAP BTP command line interface from https://tools.hana.ondemand.com/#cloud, then rerun sap-ai-dev --setup-btp or set BTP_CLI to its path.`,
|
|
101
|
+
{ code: 'BTP_CLI_MISSING' }
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
function flatten(flags = {}) {
|
|
105
|
+
const argv = [];
|
|
106
|
+
for (const [name, value] of Object.entries(flags)) {
|
|
107
|
+
if (value === undefined || value === null || value === false) continue;
|
|
108
|
+
if (value === true) argv.push(`--${name}`);
|
|
109
|
+
else argv.push(`--${name}`, String(value));
|
|
110
|
+
}
|
|
111
|
+
return argv;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function errorDetail(stderr, stdout, error) {
|
|
115
|
+
const raw = String(stderr || '').trim() || String(stdout || '').trim() || String(error?.message || '');
|
|
116
|
+
const lines = raw.split(/\r?\n/)
|
|
117
|
+
.map(line => line.trim())
|
|
118
|
+
.filter(line => line && !/^(?:✗ ERROR|ERROR|OK)$/.test(line) && !line.startsWith('SAP BTP command line interface'));
|
|
119
|
+
return diagnosticText(lines.join(' '), ERROR_CAP);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createBtpCli({ cli, config = null, env = process.env, execFileImpl = execFile } = {}) {
|
|
123
|
+
if (!cli) throw new Error('btp CLI runner needs the btp executable');
|
|
124
|
+
const childEnv = childEnvironment(env, config);
|
|
125
|
+
let queue = Promise.resolve();
|
|
126
|
+
|
|
127
|
+
const enqueue = operation => {
|
|
128
|
+
const result = queue.then(operation);
|
|
129
|
+
queue = result.then(() => undefined, () => undefined);
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const exec = (words, argv) => enqueue(() => new Promise((resolvePromise, reject) => {
|
|
134
|
+
const label = words.join(' ');
|
|
135
|
+
const child = execFileImpl(cli, argv, {
|
|
136
|
+
env: childEnv,
|
|
137
|
+
timeout: CLI_TIMEOUT_MS,
|
|
138
|
+
maxBuffer: MAX_BUFFER,
|
|
139
|
+
windowsHide: true
|
|
140
|
+
}, (error, stdout, stderr) => {
|
|
141
|
+
if (error) {
|
|
142
|
+
if (error.code === 'ENOENT') {
|
|
143
|
+
reject(missingCliError(cli));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
147
|
+
reject(new IntegrationError(`btp CLI "${label}" output exceeded 32 MiB; narrow the request`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (error.killed) {
|
|
151
|
+
reject(new IntegrationError(`btp CLI "${label}" timed out after ${CLI_TIMEOUT_MS / 1000} s`, { code: 'BTP_CLI_TIMEOUT' }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (LOGIN_REQUIRED.test(`${stderr ?? ''}\n${stdout ?? ''}`)) {
|
|
155
|
+
reject(new IntegrationError(BTP_LOGIN_REQUIRED_MESSAGE, { code: 'BTP_LOGIN_REQUIRED' }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
reject(new IntegrationError(`btp CLI "${label}" failed: ${errorDetail(stderr, stdout, error)}`));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
resolvePromise(String(stdout ?? ''));
|
|
162
|
+
});
|
|
163
|
+
child?.stdin?.end();
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const text = (words, flags) => exec(words, ['--format', 'json', ...words, ...flatten(flags)]);
|
|
167
|
+
|
|
168
|
+
const json = async (words, flags) => {
|
|
169
|
+
const stdout = await text(words, flags);
|
|
170
|
+
const parsed = parseCliJson(stdout);
|
|
171
|
+
if (parsed === undefined) throw new IntegrationError(`btp CLI "${words.join(' ')}" returned output that is not JSON`);
|
|
172
|
+
return parsed;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const version = async () => {
|
|
176
|
+
const stdout = await exec(['--version'], ['--version']);
|
|
177
|
+
return stdout.match(/client v(\d+(?:\.\d+)+)/)?.[1] ?? null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return { json, text, version };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Interactive `btp login --sso` on the caller's terminal (setup wizard only). */
|
|
184
|
+
export function loginBtpCli({ cli, config = null, env = process.env, spawnImpl = spawn } = {}) {
|
|
185
|
+
return new Promise((resolvePromise, reject) => {
|
|
186
|
+
let child;
|
|
187
|
+
try {
|
|
188
|
+
child = spawnImpl(cli, ['login', '--sso'], { env: childEnvironment(env, config), stdio: 'inherit' });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
reject(error?.code === 'ENOENT' ? missingCliError(cli) : error);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
child.once('error', error => reject(error?.code === 'ENOENT' ? missingCliError(cli) : error));
|
|
194
|
+
child.once('exit', code => {
|
|
195
|
+
if (code === 0) resolvePromise();
|
|
196
|
+
else reject(new IntegrationError(`btp login --sso exited with code ${code}`));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sap-ai-btp: stdio MCP server for SAP BTP core platform administration through
|
|
3
|
+
// the user's own btp CLI login (btp login --sso): account hierarchy, entitlements
|
|
4
|
+
// and quotas, Service Manager instances and keys, destinations, trust and Cloud
|
|
5
|
+
// Connectors. The toolkit stores no credentials for it.
|
|
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 { btpSettingsPath, createBtpCli, resolveBtpCliSettings } from './btp-cli.mjs';
|
|
12
|
+
import { registerBtpTools } from './btp-tools.mjs';
|
|
13
|
+
|
|
14
|
+
const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
|
+
|
|
16
|
+
/** Create an injectable server runtime for tests and the stdio executable. */
|
|
17
|
+
export function createBtpServer({ env = process.env, execFileImpl } = {}) {
|
|
18
|
+
const settings = resolveBtpCliSettings(env);
|
|
19
|
+
const cli = createBtpCli({ cli: settings.cli, config: settings.config, env, ...(execFileImpl ? { execFileImpl } : {}) });
|
|
20
|
+
|
|
21
|
+
const summary = Object.freeze({
|
|
22
|
+
cli: settings.cli,
|
|
23
|
+
cli_source: settings.cliSource,
|
|
24
|
+
config_file: settings.config ?? 'btp CLI default',
|
|
25
|
+
acting_user: 'the user signed in to the btp CLI (btp login --sso)'
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const verify = async () => {
|
|
29
|
+
let cliVersion;
|
|
30
|
+
try {
|
|
31
|
+
cliVersion = await cli.version() ?? 'unknown';
|
|
32
|
+
} catch (error) {
|
|
33
|
+
cliVersion = `failed: ${error?.message || 'btp --version failed'}`;
|
|
34
|
+
}
|
|
35
|
+
let globalAccount;
|
|
36
|
+
try {
|
|
37
|
+
const account = await cli.json(['get', 'accounts/global-account']);
|
|
38
|
+
globalAccount = `ok (${account?.displayName}, subdomain ${account?.subdomain})`;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
globalAccount = `failed: ${error?.message || 'reading the global account failed'}`;
|
|
41
|
+
}
|
|
42
|
+
return { cli_version: cliVersion, global_account: globalAccount };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const server = new McpServer({
|
|
46
|
+
name: 'sap-ai-btp',
|
|
47
|
+
version: packageMetadata.version
|
|
48
|
+
}, { capabilities: { tools: {} } });
|
|
49
|
+
registerBtpTools(server, { cli, summary, verify, env });
|
|
50
|
+
|
|
51
|
+
let closing;
|
|
52
|
+
const close = () => {
|
|
53
|
+
if (!closing) closing = server.close().catch(() => {});
|
|
54
|
+
return closing;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { server, close, verify, connectionSummary: summary };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runBtpServer({ env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
61
|
+
if (process.argv.slice(2).some(argument => argument === '--help' || argument === '-h')) {
|
|
62
|
+
stdout.write([
|
|
63
|
+
'Usage: sap-ai-btp',
|
|
64
|
+
'',
|
|
65
|
+
'Starts a stdio MCP server for SAP BTP core platform administration through your own btp CLI',
|
|
66
|
+
'login: global account hierarchy, entitlements and quotas, Service Manager instances and service',
|
|
67
|
+
'keys, destinations, identity and destination trust, and Cloud Connectors. Creating instances and',
|
|
68
|
+
'service keys requires exact confirmation strings; credentials are always redacted.',
|
|
69
|
+
'',
|
|
70
|
+
'Sign in with "btp login --sso" (sessions last at most 12 hours); the toolkit stores no credentials.',
|
|
71
|
+
' BTP_CLI path to the btp executable (else the location saved by sap-ai-dev --setup-btp, else btp on PATH)',
|
|
72
|
+
' BTP_CLIENTCONFIG btp CLI configuration file (else the saved value, else the btp CLI default)',
|
|
73
|
+
` ${btpSettingsPath(env)}`,
|
|
74
|
+
' btp CLI location saved by sap-ai-dev --setup-btp (never mcp.json)',
|
|
75
|
+
'',
|
|
76
|
+
'HTTP_PROXY, HTTPS_PROXY and NO_PROXY are honored.',
|
|
77
|
+
'',
|
|
78
|
+
' --check verify the btp CLI and its login session; exit 1 when the check fails',
|
|
79
|
+
''
|
|
80
|
+
].join('\n'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let runtime;
|
|
84
|
+
try {
|
|
85
|
+
runtime = createBtpServer({ env });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
stderr.write(`[sap-ai-btp] ${error.message}\n`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (process.argv.slice(2).includes('--check')) {
|
|
92
|
+
const report = await runtime.verify();
|
|
93
|
+
stdout.write(`${Object.entries(report).map(([name, result]) => `${name}: ${result}`).join('\n')}\n`);
|
|
94
|
+
await runtime.close();
|
|
95
|
+
if (Object.values(report).some(result => String(result).startsWith('failed'))) process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize: 1024 * 1024 });
|
|
100
|
+
let closing;
|
|
101
|
+
const shutdown = () => {
|
|
102
|
+
if (!closing) closing = runtime.close().catch(() => {});
|
|
103
|
+
return closing;
|
|
104
|
+
};
|
|
105
|
+
const onStdinEnd = () => { void shutdown(); };
|
|
106
|
+
const onSignal = signal => {
|
|
107
|
+
void shutdown().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
|
|
108
|
+
};
|
|
109
|
+
stdin.once('end', onStdinEnd);
|
|
110
|
+
process.once('SIGINT', onSignal);
|
|
111
|
+
process.once('SIGTERM', onSignal);
|
|
112
|
+
transport.onerror = () => stderr.write('[sap-ai-btp] MCP stdio transport failed.\n');
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await runtime.server.connect(transport);
|
|
116
|
+
} catch {
|
|
117
|
+
stderr.write('[sap-ai-btp] MCP server startup failed.\n');
|
|
118
|
+
await shutdown();
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
124
|
+
await runBtpServer();
|
|
125
|
+
}
|