sap-ai-dev-toolkit 0.9.9913 → 0.9.9915
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-integration-architect.agent.md +38 -0
- package/.github/agents/sap-integration-developer.agent.md +31 -0
- package/.github/agents/sap-solution-architect.agent.md +1 -0
- package/.github/skills/integration-suite-delivery/SKILL.md +74 -0
- package/.github/skills/sap-integration-architect/SKILL.md +76 -0
- package/NOTICE +10 -0
- package/README.md +35 -8
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/inventory.md +62 -0
- package/package.json +5 -2
- package/scripts/run-live-integration.mjs +1079 -0
- package/src/apim-bundle.mjs +381 -0
- package/src/apim-tools.mjs +641 -0
- package/src/cf-destination.mjs +5 -5
- package/src/cf-integration.mjs +74 -0
- package/src/cpi-tools.mjs +1042 -0
- package/src/iflow-bundle.mjs +282 -0
- package/src/integration-common.mjs +232 -0
- package/src/integration-config.mjs +167 -0
- package/src/integration-credentials.mjs +65 -0
- package/src/integration-http.mjs +282 -0
- package/src/integration-server.mjs +125 -0
- package/src/integration-templates/https-request-reply-http.iflw +391 -0
- package/src/integration-tools.mjs +27 -0
- package/src/integration-verify.mjs +118 -0
- package/src/launcher.mjs +14 -2
- package/src/mcp-config.mjs +18 -3
- package/src/setup.mjs +310 -11
- package/tools.md +76 -0
package/src/setup.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { createInterface } from 'node:readline/promises';
|
|
2
3
|
import { stdin, stdout } from 'node:process';
|
|
3
4
|
import { platform } from 'node:os';
|
|
@@ -7,6 +8,10 @@ import { discoverDestinations, remediation } from './bas-discovery.mjs';
|
|
|
7
8
|
import { adtBaseUrl, adtPort, defaultAdtUrl, discoverSapGuiSystems, hasExplicitPort, hasPathPrefix, messageServerAppServers } from './local-sap-gui.mjs';
|
|
8
9
|
import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
|
|
9
10
|
import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
|
|
11
|
+
import { parseIntegrationServiceKey, resolveIntegrationConfig, resolveIntegrationSideFromEnvironment } from './integration-config.mjs';
|
|
12
|
+
import { readStoredIntegrationKeys, storeIntegrationKey } from './integration-credentials.mjs';
|
|
13
|
+
import { INTEGRATION_CF_SERVICES, createIntegrationInstance, findIntegrationInstances, integrationServiceKey } from './cf-integration.mjs';
|
|
14
|
+
import { createIntegrationClients, verificationLines, verifyIntegrationClients } from './integration-verify.mjs';
|
|
10
15
|
import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
|
|
11
16
|
import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
|
|
12
17
|
|
|
@@ -268,14 +273,286 @@ function warnSsoIpHost(destination, label, output) {
|
|
|
268
273
|
print(output, formatStatus(`${label} uses an IP address; SSO needs the DNS host name for Kerberos and the browser certificate check. Enter the host name if the sign-in fails.`, 'warning', output, 'SSO'));
|
|
269
274
|
}
|
|
270
275
|
|
|
271
|
-
async function confirmPrompt(message, { input = stdin, output = stdout } = {}) {
|
|
276
|
+
async function confirmPrompt(message, { input = stdin, output = stdout, defaultYes = true } = {}) {
|
|
272
277
|
const answer = await textPrompt({
|
|
273
278
|
message,
|
|
274
|
-
placeholder: 'y',
|
|
279
|
+
placeholder: defaultYes ? 'y' : 'n',
|
|
275
280
|
required: false,
|
|
276
281
|
validate: value => /^(?:|y|yes|n|no)$/i.test(String(value || '').trim()) || 'Enter y or n.'
|
|
277
282
|
}, { input, output });
|
|
278
|
-
|
|
283
|
+
const trimmed = String(answer || '').trim();
|
|
284
|
+
if (!trimmed) return defaultYes;
|
|
285
|
+
return !/^n(?:o)?$/i.test(trimmed);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Credential status for the Integration Suite companion, without exposing any value: which sides
|
|
289
|
+
// (CPI, APIM) resolve from this environment or the stored-key file, and from which source.
|
|
290
|
+
export function integrationSuiteCredentialStatus(env = process.env) {
|
|
291
|
+
try {
|
|
292
|
+
const config = resolveIntegrationConfig(env, { stored: readStoredIntegrationKeys(env) });
|
|
293
|
+
const side = value => (value ? `configured from ${value.source}${value.bindingName ? ` (${value.bindingName})` : ''}, host ${new URL(value.url).host}` : 'not configured');
|
|
294
|
+
return { found: true, config, lines: [`CPI: ${side(config.cpi)}`, `APIM: ${side(config.apim)}`] };
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const message = String(error?.message || '');
|
|
297
|
+
if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No CPI or APIM credentials were found in this environment.'] };
|
|
298
|
+
return { found: false, config: null, lines: [`Credentials found but not usable: ${message}`] };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function defaultVerifyIntegration(config, { env }) {
|
|
303
|
+
return verifyIntegrationClients(createIntegrationClients(config, { env }));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Read a pasted service key: JSON text (line breaks from a multi-line paste are dropped) or a path to a .json file. */
|
|
307
|
+
function pastedServiceKey(side, value, env) {
|
|
308
|
+
const trimmed = String(value || '').trim();
|
|
309
|
+
let text = trimmed;
|
|
310
|
+
if (!trimmed.startsWith('{')) {
|
|
311
|
+
const path = trimmed.replace(/^~(?=$|[\\/])/, env.HOME || env.USERPROFILE || '~');
|
|
312
|
+
try {
|
|
313
|
+
text = readFileSync(path, 'utf8');
|
|
314
|
+
} catch {
|
|
315
|
+
throw new Error('Paste the service key JSON (starting with {) or the path to a readable .json file.');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return parseIntegrationServiceKey(side, text);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function printIntegrationVerification(config, { env, output, verifyIntegration }) {
|
|
322
|
+
const stop = startProgress('Signing in to Integration Suite and checking each tool area', { output, env, label: 'Integration Suite' });
|
|
323
|
+
let report;
|
|
324
|
+
try {
|
|
325
|
+
report = await verifyIntegration(config, { env });
|
|
326
|
+
} finally {
|
|
327
|
+
stop();
|
|
328
|
+
}
|
|
329
|
+
const host = side => (config[side] ? new URL(config[side].url).host : '');
|
|
330
|
+
for (const line of [...verificationLines('CPI', host('cpi'), report.cpi), ...verificationLines('APIM', host('apim'), report.apim)]) print(output, ` ${line}`);
|
|
331
|
+
const missing = [report.cpi, report.apim].flatMap(side => side?.checks || []).filter(check => check.result === 'forbidden').length;
|
|
332
|
+
if (report.ok) {
|
|
333
|
+
print(output, formatStatus(`Integration Suite credentials verified${missing ? `; ${missing} area${missing === 1 ? ' needs' : 's need'} an extra role on the service key` : ''}.`, missing ? 'warning' : 'success', output, 'Integration Suite'));
|
|
334
|
+
} else {
|
|
335
|
+
const signedIn = [report.cpi, report.apim].some(side => side?.signed_in);
|
|
336
|
+
print(output, formatStatus(signedIn
|
|
337
|
+
? 'The key signs in but lacks the roles for the core area; use an instance with the needed roles (the Cloud Foundry option can create one) or replace the key.'
|
|
338
|
+
: 'Integration Suite sign-in failed; fix or replace the service key before using the tools.', 'warning', output, 'Integration Suite'));
|
|
339
|
+
}
|
|
340
|
+
return report;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const defaultIntegrationCloudFoundry = Object.freeze({
|
|
344
|
+
getTarget: getCloudFoundryTarget,
|
|
345
|
+
findInstances: findIntegrationInstances,
|
|
346
|
+
serviceKey: integrationServiceKey,
|
|
347
|
+
createInstance: createIntegrationInstance
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const SIDE_LABELS = Object.freeze({ CPI: 'Cloud Integration (CPI)', APIM: 'API Management' });
|
|
351
|
+
|
|
352
|
+
function cfFailure(error) {
|
|
353
|
+
const detail = String(error?.stderr || error?.message || '').replace(/\s+/g, ' ').trim();
|
|
354
|
+
return detail ? detail.slice(0, 300) : 'Cloud Foundry CLI command failed';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Verify one side's key online, then save it user-only. A key that cannot sign in is saved only when confirmed.
|
|
358
|
+
async function acceptIntegrationKey(side, config, { env, input, output, verifyIntegration }) {
|
|
359
|
+
const choice = side.toLowerCase();
|
|
360
|
+
const report = await printIntegrationVerification({ [choice]: config }, { env, output, verifyIntegration });
|
|
361
|
+
if (!report[choice].signed_in && !await confirmPrompt('Save this key anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return { saved: false, report };
|
|
362
|
+
const path = await storeIntegrationKey(side, config, env);
|
|
363
|
+
print(output, formatStatus(`Saved the ${side} key to ${path} (readable only by you; not written to mcp.json).`, 'success', output, 'Integration Suite'));
|
|
364
|
+
let fromEnvironment = null;
|
|
365
|
+
try {
|
|
366
|
+
fromEnvironment = resolveIntegrationSideFromEnvironment(env, side);
|
|
367
|
+
} catch {
|
|
368
|
+
// A broken environment value is reported by the status lines.
|
|
369
|
+
}
|
|
370
|
+
if (fromEnvironment) print(output, formatStatus(`${side} credentials from ${fromEnvironment.source} in this environment take precedence over the saved key.`, 'warning', output, 'Integration Suite'));
|
|
371
|
+
return { saved: true, report };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function lacksRoles(side) {
|
|
375
|
+
return Boolean(side?.signed_in) && (!side.ok || side.checks.some(check => check.result === 'forbidden'));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function cloudFoundryStep(message, operation, { env, output }) {
|
|
379
|
+
const stop = startProgress(message, { output, env, label: 'Cloud Foundry' });
|
|
380
|
+
try {
|
|
381
|
+
return await operation();
|
|
382
|
+
} finally {
|
|
383
|
+
stop();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Key from an instance: reuse or create the toolkit's service key, then verify and save it.
|
|
388
|
+
async function useCloudFoundryInstance(side, instanceName, context) {
|
|
389
|
+
const { env, output, cloudFoundry } = context;
|
|
390
|
+
const key = await cloudFoundryStep(`Reading service key ${instanceName}/sap-ai-dev-integration`, () => cloudFoundry.serviceKey(instanceName, { env }), context);
|
|
391
|
+
print(output, formatStatus(`${key.created ? 'Created' : 'Reusing'} service key ${key.keyName} on ${instanceName}.`, 'info', output, 'Cloud Foundry'));
|
|
392
|
+
return acceptIntegrationKey(side, parseIntegrationServiceKey(side, key.credentials), context);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function createDedicatedInstance(side, context) {
|
|
396
|
+
const { env, output, cloudFoundry } = context;
|
|
397
|
+
const service = INTEGRATION_CF_SERVICES[side];
|
|
398
|
+
try {
|
|
399
|
+
await cloudFoundryStep(`Creating ${service.offering}/${service.plan} instance ${service.dedicatedInstance}`, () => cloudFoundry.createInstance(side, { env }), context);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
print(output, formatStatus(`Could not create ${service.dedicatedInstance}: ${cfFailure(error)}`, 'warning', output, 'Cloud Foundry'));
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
print(output, formatStatus(`Created service instance ${service.dedicatedInstance}.`, 'success', output, 'Cloud Foundry'));
|
|
405
|
+
return useCloudFoundryInstance(side, service.dedicatedInstance, context);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Use the logged-in CF space: per side pick the matching instance (or offer to create a dedicated one with the
|
|
409
|
+
// roles the tools need), reuse or create the toolkit's service key, verify it online and save it.
|
|
410
|
+
async function useCloudFoundrySpace(target, context) {
|
|
411
|
+
const { env, input, output, cloudFoundry } = context;
|
|
412
|
+
let instances;
|
|
413
|
+
try {
|
|
414
|
+
instances = await cloudFoundryStep('Looking for Integration Suite service instances in the CF space', () => cloudFoundry.findInstances({ env, spaceGuid: target.spaceGuid }), context);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
print(output, formatStatus(`Could not list service instances: ${cfFailure(error)}`, 'warning', output, 'Cloud Foundry'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
for (const side of ['CPI', 'APIM']) {
|
|
420
|
+
const service = INTEGRATION_CF_SERVICES[side];
|
|
421
|
+
const candidates = instances[side] || [];
|
|
422
|
+
print(output, '');
|
|
423
|
+
if (!candidates.length) {
|
|
424
|
+
print(output, formatStatus(`No ${service.label} instance with plan ${service.plan} (${service.offering}) in this space.`, 'info', output, SIDE_LABELS[side]));
|
|
425
|
+
if (await confirmPrompt(`Create instance ${service.dedicatedInstance} (${service.offering}, plan ${service.plan}) with the roles the tools need? (y/n, Enter=no)`, { input, output, defaultYes: false })) {
|
|
426
|
+
await createDedicatedInstance(side, context);
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const dedicated = candidates.find(candidate => candidate.name === service.dedicatedInstance);
|
|
431
|
+
const instanceName = candidates.length === 1 ? candidates[0].name : await selectPrompt({
|
|
432
|
+
message: colorText(`☁️ ${SIDE_LABELS[side]}: choose the ${service.offering}/${service.plan} instance to use`, 'cyan', output),
|
|
433
|
+
defaultValue: (dedicated || candidates[0]).name,
|
|
434
|
+
choices: [...candidates.map(candidate => ({ name: candidate.name, value: candidate.name })), { name: 'Skip', value: '', shortcut: 's' }]
|
|
435
|
+
}, { input, output });
|
|
436
|
+
if (!instanceName) continue;
|
|
437
|
+
let result;
|
|
438
|
+
try {
|
|
439
|
+
result = await useCloudFoundryInstance(side, instanceName, context);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
print(output, formatStatus(`Could not use ${instanceName}: ${cfFailure(error)}`, 'warning', output, SIDE_LABELS[side]));
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const sideReport = result.report[side.toLowerCase()];
|
|
445
|
+
if (instanceName !== service.dedicatedInstance && !dedicated && lacksRoles(sideReport)) {
|
|
446
|
+
print(output, ` The roles of ${instanceName} are fixed on the instance; a new key cannot add them.`);
|
|
447
|
+
print(output, ` A dedicated instance gets: ${service.parameters.roles ? service.parameters.roles.join(', ') : service.parameters.role}.`);
|
|
448
|
+
if (await confirmPrompt(`Create instance ${service.dedicatedInstance} with these roles and use it instead? (y/n, Enter=no)`, { input, output, defaultYes: false })) {
|
|
449
|
+
await createDedicatedInstance(side, context);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Verify the detected credentials online and let the user take CPI/APIM keys from the logged-in CF space or
|
|
456
|
+
// paste them. A key is checked online first and saved to the user-only store (never to mcp.json); it is used
|
|
457
|
+
// when the environment has no key.
|
|
458
|
+
async function configureIntegrationCredentials({ env, input, output, verifyIntegration, cloudFoundry = defaultIntegrationCloudFoundry }) {
|
|
459
|
+
let status = integrationSuiteCredentialStatus(env);
|
|
460
|
+
if (status.found) await printIntegrationVerification(status.config, { env, output, verifyIntegration });
|
|
461
|
+
let target = { available: false };
|
|
462
|
+
try {
|
|
463
|
+
target = await cloudFoundryStep('Checking the Cloud Foundry login', () => cloudFoundry.getTarget({ env }), { env, output });
|
|
464
|
+
} catch {
|
|
465
|
+
// No CF CLI or no login: the CF option is simply not offered.
|
|
466
|
+
}
|
|
467
|
+
const context = { env, input, output, verifyIntegration, cloudFoundry };
|
|
468
|
+
for (;;) {
|
|
469
|
+
print(output, '');
|
|
470
|
+
const space = target.available ? [target.orgName, target.spaceName].filter(Boolean).join(' / ') || target.spaceGuid : '';
|
|
471
|
+
const choice = await selectPrompt({
|
|
472
|
+
message: colorText('🔑 Integration Suite credentials', 'cyan', output),
|
|
473
|
+
defaultValue: status.found ? 'done' : (target.available ? 'cf' : 'cpi'),
|
|
474
|
+
choices: [
|
|
475
|
+
{ name: status.found ? 'Continue' : 'Continue without credentials (the server will not start until a key is configured)', value: 'done', shortcut: 'c' },
|
|
476
|
+
...(target.available ? [{ name: `Use the logged-in Cloud Foundry space (${space}): find the instances, create or reuse service keys, verify`, value: 'cf', shortcut: 'f' }] : []),
|
|
477
|
+
{ name: 'Paste a Cloud Integration (CPI) service key — Process Integration Runtime, plan api', value: 'cpi', shortcut: 'p' },
|
|
478
|
+
{ name: 'Paste an API Management service key — API Management, plan apiportal-apiaccess', value: 'apim', shortcut: 'a' },
|
|
479
|
+
...(status.found ? [{ name: 'Check the configured credentials online again', value: 'verify', shortcut: 'v' }] : [])
|
|
480
|
+
]
|
|
481
|
+
}, { input, output });
|
|
482
|
+
if (choice === 'done') return status;
|
|
483
|
+
if (choice === 'verify') {
|
|
484
|
+
await printIntegrationVerification(status.config, { env, output, verifyIntegration });
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (choice === 'cf') {
|
|
488
|
+
await useCloudFoundrySpace(target, context);
|
|
489
|
+
} else {
|
|
490
|
+
const side = choice.toUpperCase();
|
|
491
|
+
const pasted = await textPrompt({
|
|
492
|
+
message: `Paste the ${side} service key JSON or the path to its .json file`,
|
|
493
|
+
secret: true,
|
|
494
|
+
validate: value => {
|
|
495
|
+
try {
|
|
496
|
+
pastedServiceKey(side, value, env);
|
|
497
|
+
return true;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return error.message;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}, { input, output });
|
|
503
|
+
await acceptIntegrationKey(side, pastedServiceKey(side, pasted, env), context);
|
|
504
|
+
}
|
|
505
|
+
status = integrationSuiteCredentialStatus(env);
|
|
506
|
+
for (const line of status.lines) print(output, ` ${line}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, integrationCloudFoundry, ask = true }) {
|
|
511
|
+
print(output, '');
|
|
512
|
+
print(output, formatStatus('SAP Integration Suite tools (Cloud Integration + API Management).', 'step', output, 'Integration Suite'));
|
|
513
|
+
print(output, ' Adds the sap-ai-integration MCP server: iFlows, value mappings, security material and monitoring in CPI; API providers, proxies, policies, products and key value maps in APIM.');
|
|
514
|
+
print(output, ' Every write needs an exact confirmation string; there are no delete tools and secrets are write-only. Agents: SAP Integration Architect, SAP Integration Developer.');
|
|
515
|
+
if (alreadyConfigured) print(output, ' Already configured in this MCP config; keeping it and refreshing its version.');
|
|
516
|
+
const status = integrationSuiteCredentialStatus(env);
|
|
517
|
+
for (const line of status.lines) print(output, ` ${line}`);
|
|
518
|
+
if (!status.found) {
|
|
519
|
+
print(output, ' The server needs CPI_SERVICE_KEY and/or APIM_SERVICE_KEY (or CPI_*/APIM_* variables, VCAP_SERVICES) in the MCP host environment,');
|
|
520
|
+
print(output, ' or a service key pasted in the next step (verified online, then saved for your user only). Credentials are never written to mcp.json.');
|
|
521
|
+
}
|
|
522
|
+
if (ask && !alreadyConfigured) {
|
|
523
|
+
print(output, '');
|
|
524
|
+
const defaultYes = status.found;
|
|
525
|
+
if (!await confirmPrompt(`Add the SAP Integration Suite tools? (y/n, Enter=${defaultYes ? 'yes' : 'no'})`, { input, output, defaultYes })) return false;
|
|
526
|
+
}
|
|
527
|
+
await configureIntegrationCredentials({ env, input, output, verifyIntegration, ...(integrationCloudFoundry ? { cloudFoundry: integrationCloudFoundry } : {}) });
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** `sap-ai-dev --setup-integration`: only the Integration Suite step (paste, verify, add the server), keeping every other MCP entry. */
|
|
532
|
+
export async function runIntegrationSuiteSetup({
|
|
533
|
+
env = process.env,
|
|
534
|
+
input = stdin,
|
|
535
|
+
output = stdout,
|
|
536
|
+
install = installMcpConfig,
|
|
537
|
+
verifyIntegration = defaultVerifyIntegration,
|
|
538
|
+
integrationCloudFoundry
|
|
539
|
+
} = {}) {
|
|
540
|
+
if (!input.isTTY || !output.isTTY) {
|
|
541
|
+
print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-integration from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
542
|
+
return { skipped: true, reason: 'non-tty' };
|
|
543
|
+
}
|
|
544
|
+
let alreadyConfigured = false;
|
|
545
|
+
try {
|
|
546
|
+
alreadyConfigured = Boolean((await readMcpConfig(await resolveMcpConfigPath(env)))?.servers?.['integration-suite']);
|
|
547
|
+
} catch {
|
|
548
|
+
// A missing or unreadable config is created or healed by install.
|
|
549
|
+
}
|
|
550
|
+
await promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
|
|
551
|
+
const result = await install([], { env, sapDevelopmentServers: ['integration-suite'], additive: true });
|
|
552
|
+
print(output, '');
|
|
553
|
+
print(output, formatStatus(`Configured the Integration Suite MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'Integration Suite'));
|
|
554
|
+
for (const message of Array.isArray(result?.warnings) ? result.warnings : []) print(output, formatStatus(message, 'warning', output, 'Warning'));
|
|
555
|
+
return { selected: [], ...(result || {}) };
|
|
279
556
|
}
|
|
280
557
|
|
|
281
558
|
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false, browserPath } = {}) {
|
|
@@ -613,7 +890,9 @@ export async function runSetup({
|
|
|
613
890
|
install = installMcpConfig,
|
|
614
891
|
includeSapDevelopmentToolsPrompt = false,
|
|
615
892
|
detectAdtUrl,
|
|
616
|
-
discoverSsoBrowsers = findRemoteControllableSsoBrowsers
|
|
893
|
+
discoverSsoBrowsers = findRemoteControllableSsoBrowsers,
|
|
894
|
+
verifyIntegration = defaultVerifyIntegration,
|
|
895
|
+
integrationCloudFoundry
|
|
617
896
|
} = {}) {
|
|
618
897
|
const isBas = Boolean(env.H2O_URL);
|
|
619
898
|
if (!isBas && (!input.isTTY || !output.isTTY)) {
|
|
@@ -661,11 +940,13 @@ export async function runSetup({
|
|
|
661
940
|
let configPath;
|
|
662
941
|
let managedKeys = [];
|
|
663
942
|
let previousAdtUrls = new Map();
|
|
943
|
+
let integrationSuiteConfigured = false;
|
|
664
944
|
try {
|
|
665
945
|
configPath = await resolveMcpConfigPath(env);
|
|
666
946
|
const existingConfig = await readMcpConfig(configPath);
|
|
667
947
|
managedKeys = collectManagedCloudFoundryKeyReferences(existingConfig);
|
|
668
948
|
previousAdtUrls = previousLocalAdtUrls(existingConfig);
|
|
949
|
+
integrationSuiteConfigured = Boolean(existingConfig?.servers?.['integration-suite']);
|
|
669
950
|
} catch {
|
|
670
951
|
warnings.push('Existing MCP config could not be read before setup; existing CF service keys will not be reused.');
|
|
671
952
|
}
|
|
@@ -750,6 +1031,7 @@ export async function runSetup({
|
|
|
750
1031
|
};
|
|
751
1032
|
let selected;
|
|
752
1033
|
let additiveSetup = false;
|
|
1034
|
+
let integrationOnly = false;
|
|
753
1035
|
if (!selectable.length) {
|
|
754
1036
|
// Nothing was detected — no BAS/CF destinations, or no SAP GUI entries.
|
|
755
1037
|
// Offer the manual MCP entry wizard before giving up.
|
|
@@ -761,23 +1043,34 @@ export async function runSetup({
|
|
|
761
1043
|
defaultValue: 'manual',
|
|
762
1044
|
choices: [
|
|
763
1045
|
{ name: '✍️ Start manual entry wizard (system name, ADT URL, client, login)', value: 'manual', shortcut: 'm' },
|
|
1046
|
+
{ name: '🔌 Set up only the SAP Integration Suite tools (paste and verify CPI / API Management service keys)', value: 'integration', shortcut: 'i' },
|
|
764
1047
|
{ name: 'Exit setup without changes', value: 'exit', shortcut: 'e' }
|
|
765
1048
|
]
|
|
766
1049
|
}, { input, output });
|
|
767
|
-
if (choice
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1050
|
+
if (choice === 'integration') {
|
|
1051
|
+
selected = [];
|
|
1052
|
+
additiveSetup = true;
|
|
1053
|
+
integrationOnly = true;
|
|
1054
|
+
} else {
|
|
1055
|
+
if (choice !== 'manual') return await exitWithoutDestinations();
|
|
1056
|
+
selected = await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser });
|
|
1057
|
+
additiveSetup = true;
|
|
1058
|
+
if (!selected.length) return await exitWithoutDestinations();
|
|
1059
|
+
}
|
|
771
1060
|
} else {
|
|
772
1061
|
selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls, selectSsoBrowser });
|
|
773
1062
|
additiveSetup = selected.length > 0;
|
|
774
1063
|
}
|
|
775
1064
|
|
|
776
1065
|
let sapDevelopmentServers = [];
|
|
777
|
-
if (
|
|
1066
|
+
if (integrationOnly) {
|
|
1067
|
+
await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
|
|
1068
|
+
sapDevelopmentServers = ['integration-suite'];
|
|
1069
|
+
} else if (includeSapDevelopmentToolsPrompt) {
|
|
778
1070
|
print(output, '');
|
|
779
|
-
print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development and
|
|
1071
|
+
print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development, HANA inspection, and Integration Suite (CPI/API Management).', 'step', output, 'Tools'));
|
|
780
1072
|
print(output, ' HANA Cloud inspector is read-only and uses HANA_RO_* or VCAP_SERVICES already present in the MCP host environment; credentials are not written to mcp.json.');
|
|
1073
|
+
print(output, ' Integration Suite uses CPI/APIM service keys from the MCP host environment or keys you paste next (verified online, saved for your user only); credentials are not written to mcp.json.');
|
|
781
1074
|
print(output, ' Leave empty to only configure ABAP/ADT destination servers.');
|
|
782
1075
|
print(output, '');
|
|
783
1076
|
sapDevelopmentServers = await checkboxPrompt({
|
|
@@ -785,11 +1078,17 @@ export async function runSetup({
|
|
|
785
1078
|
choices: SAP_DEVELOPMENT_MCP_SERVERS.map(server => ({
|
|
786
1079
|
value: server.id,
|
|
787
1080
|
name: `${server.name} — ${server.description}`,
|
|
788
|
-
checked:
|
|
1081
|
+
checked: server.id === 'integration-suite' && integrationSuiteConfigured
|
|
789
1082
|
})),
|
|
790
1083
|
required: false,
|
|
791
1084
|
shortcuts: { all: 'a' }
|
|
792
1085
|
}, { input, output });
|
|
1086
|
+
if (sapDevelopmentServers.includes('integration-suite')) {
|
|
1087
|
+
await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
|
|
1088
|
+
}
|
|
1089
|
+
} else if (await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry })) {
|
|
1090
|
+
// Without --tools, Integration Suite still gets its own yes/no step instead of being hidden.
|
|
1091
|
+
sapDevelopmentServers = ['integration-suite'];
|
|
793
1092
|
}
|
|
794
1093
|
|
|
795
1094
|
try {
|
package/tools.md
CHANGED
|
@@ -110,3 +110,79 @@ The optional `hana-cloud-inspector` companion starts `sap-ai-hana` as a separate
|
|
|
110
110
|
- `hana_read_rows` — perform parameterized reads of selected columns, capped at 200 rows; credential-like columns are blocked and arbitrary SQL is not accepted.
|
|
111
111
|
|
|
112
112
|
Connection secrets are read from the MCP host environment via explicit `HANA_RO_*` variables or a selected `VCAP_SERVICES` binding and are not written to `mcp.json`. TLS certificate verification is mandatory. No HANA write, DDL/DML, deploy/undeploy, grants, or service-key tools are exposed; deployment remains a user-run project workflow.
|
|
113
|
+
|
|
114
|
+
## Separate optional Integration Suite server
|
|
115
|
+
|
|
116
|
+
The optional `integration-suite` companion starts `sap-ai-integration` as a separate stdio MCP server for SAP Cloud Integration (CPI) and API Management (APIM). It does not use the BAS proxy or a destination prefix. Tools for a side appear only when that side is configured:
|
|
117
|
+
|
|
118
|
+
- `integration_connection_info` — show which of CPI/APIM are configured, the credential source, and tenant hosts; with `verify: true` it signs in and reports per tool area whether the service key has the needed role. Never returns client ids or secrets.
|
|
119
|
+
|
|
120
|
+
Cloud Integration — design time:
|
|
121
|
+
|
|
122
|
+
- `cpi_list_packages` — list Cloud Integration packages.
|
|
123
|
+
- `cpi_list_iflows` — list the iFlows of one package.
|
|
124
|
+
- `cpi_list_package_artifacts` — list a package's iFlows, value mappings, script collections, and message mappings.
|
|
125
|
+
- `cpi_get_iflow` — read iFlow metadata, externalized configuration, and bundle files (or one file by path).
|
|
126
|
+
- `cpi_get_artifact` — read a value mapping, script collection, or message mapping with its bundle files.
|
|
127
|
+
- `cpi_get_value_mappings` — read value-mapping agency pairs with their value and default pairs.
|
|
128
|
+
- `cpi_create_package` — create a package.
|
|
129
|
+
- `cpi_update_package` — change package name, short text, description, or version.
|
|
130
|
+
- `cpi_copy_package` — copy a Discover package into Design (new copy with suffix, or overwrite-merge keeping configuration).
|
|
131
|
+
- `cpi_create_iflow` — create an iFlow from the vetted HTTPS → Request Reply → HTTP template or caller-supplied BPMN.
|
|
132
|
+
- `cpi_update_iflow` — replace/add bundle files and parameters with an expected-version check and patch-version bump.
|
|
133
|
+
- `cpi_update_iflow_configuration` — set externalized configuration parameters.
|
|
134
|
+
- `cpi_upsert_value_mappings` — add or change value-mapping pairs.
|
|
135
|
+
- `cpi_update_script_collection` — add or replace scripts in a script collection.
|
|
136
|
+
|
|
137
|
+
Cloud Integration — deployment and monitoring:
|
|
138
|
+
|
|
139
|
+
- `cpi_deploy_iflow` — deploy the active iFlow version and wait for build and runtime status.
|
|
140
|
+
- `cpi_deploy_artifact` — deploy a value mapping, script collection, or message mapping and wait for runtime status.
|
|
141
|
+
- `cpi_undeploy_artifact` — undeploy any runtime artifact and wait until it is gone.
|
|
142
|
+
- `cpi_list_runtime_artifacts` — list deployed artifacts, optionally by status.
|
|
143
|
+
- `cpi_get_iflow_runtime_status` — read deployed runtime status, including error text when the iFlow failed to start.
|
|
144
|
+
- `cpi_list_service_endpoints` — list deployed endpoint URLs, optionally for one iFlow.
|
|
145
|
+
- `cpi_list_message_logs` — list message processing logs by iFlow, status, time window, correlation id, application message id, or custom header.
|
|
146
|
+
- `cpi_get_message_log` — read one message log with error text and custom header properties; payload attachments are never read.
|
|
147
|
+
- `cpi_get_message_log_steps` — read the processing steps of a message's latest run with step errors.
|
|
148
|
+
- `cpi_get_jms_broker_status` — read JMS broker capacity and queues that are not healthy.
|
|
149
|
+
- `cpi_list_data_stores` — list data stores with message and overdue counts.
|
|
150
|
+
- `cpi_list_data_store_entries` — list entry metadata of one data store; payloads are never read.
|
|
151
|
+
- `cpi_list_variables` — list global and iFlow variables (metadata only).
|
|
152
|
+
|
|
153
|
+
Cloud Integration — security material and tenant configuration:
|
|
154
|
+
|
|
155
|
+
- `cpi_list_security_material` — list user credentials, OAuth2 client credentials, or keystore entries with secret fields redacted.
|
|
156
|
+
- `cpi_set_user_credential` — create or update a user credential; the password is write-only.
|
|
157
|
+
- `cpi_set_oauth2_client_credential` — create or update an OAuth2 client credential; the client secret is write-only.
|
|
158
|
+
- `cpi_upload_certificate` — add or replace a public certificate in the tenant keystore.
|
|
159
|
+
- `cpi_list_number_ranges` — list number ranges.
|
|
160
|
+
- `cpi_set_number_range` — create or update a number range.
|
|
161
|
+
- `cpi_list_partner_parameters` — list Partner Directory string parameters and binary parameter metadata.
|
|
162
|
+
- `cpi_set_partner_parameter` — create or update a Partner Directory string or binary parameter.
|
|
163
|
+
- `cpi_odata_get` — guarded read-only GET for other CPI entity sets (for example access policies or JMS queues) and the service metadata.
|
|
164
|
+
|
|
165
|
+
API Management:
|
|
166
|
+
|
|
167
|
+
- `apim_list_api_providers` — list API providers without credential fields.
|
|
168
|
+
- `apim_get_api_provider` — read one API provider with credential properties removed.
|
|
169
|
+
- `apim_create_api_provider` — create an Internet or On-Premise (Cloud Connector) API provider without credentials.
|
|
170
|
+
- `apim_update_api_provider` — change host, port, type, location, path prefix, or sap-client of a credential-free provider.
|
|
171
|
+
- `apim_list_api_proxies` — list API proxies.
|
|
172
|
+
- `apim_get_api_proxy` — read one API proxy with endpoints, virtual hosts, and the computed runtime URL.
|
|
173
|
+
- `apim_export_api_proxy` — export an API proxy bundle and return its files (policies, endpoints, scripts).
|
|
174
|
+
- `apim_list_virtual_hosts` — list virtual hosts.
|
|
175
|
+
- `apim_create_api_proxy` — import (and thereby deploy) a policy-free API proxy for a provider or https URL target.
|
|
176
|
+
- `apim_update_api_proxy` — change base path, target, title, or description; existing policies and steps are kept.
|
|
177
|
+
- `apim_attach_api_proxy_policies` — add or replace policies (API key, quota, spike arrest, CORS, headers, OAuth, IP allow list, response cache, Basic auth from a key value map, raise fault, JavaScript, custom XML) and their flow steps, then redeploy.
|
|
178
|
+
- `apim_list_api_products` — list API products.
|
|
179
|
+
- `apim_get_api_product` — read one API product with its proxies.
|
|
180
|
+
- `apim_create_api_product` — create (and by default publish) an API product over existing proxies with an optional quota.
|
|
181
|
+
- `apim_update_api_product` — change product title, description, scope, quota, or publish state.
|
|
182
|
+
- `apim_list_key_value_maps` — list key value maps.
|
|
183
|
+
- `apim_get_key_value_map` — read one key value map; encrypted values are redacted.
|
|
184
|
+
- `apim_create_key_value_map` — create a key value map; values are write-only.
|
|
185
|
+
- `apim_set_key_value_map_entries` — add or change key value map entries; values are write-only.
|
|
186
|
+
- `apim_odata_get` — guarded read-only GET for other Management.svc entity sets (for example applications, developers, certificate store references) and the service metadata.
|
|
187
|
+
|
|
188
|
+
Credentials come from CPI/APIM OAuth client service keys in the MCP host environment (`CPI_SERVICE_KEY`/`APIM_SERVICE_KEY`, explicit `CPI_*`/`APIM_*` variables, or `VCAP_SERVICES`) and are never written to `mcp.json`. Every write requires an exact confirmation string (for example `CREATE IFLOW <iflow_id>` or `UNDEPLOY ARTIFACT <artifact_id>`) and is checked before any network call. Scope is create, update, deploy, and undeploy: there are no delete tools, and deletions stay manual in the Integration Suite UI. Secrets (passwords, client secrets, key value map values) are write-only: tools accept them as input and never return them. Message payloads, attachments, and data-store payloads are never read.
|