sap-ai-dev-toolkit 0.9.9918 → 0.9.9921

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.github/agents/sap-integration-architect.agent.md +32 -18
  2. package/.github/agents/sap-integration-developer.agent.md +28 -12
  3. package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
  4. package/.github/skills/sap-integration-architect/SKILL.md +49 -25
  5. package/README.md +72 -15
  6. package/dist/checksums.txt +5 -5
  7. package/dist/vsp-darwin-arm64 +0 -0
  8. package/dist/vsp-darwin-x64 +0 -0
  9. package/dist/vsp-linux-arm64 +0 -0
  10. package/dist/vsp-linux-x64 +0 -0
  11. package/dist/vsp-win32-x64.exe +0 -0
  12. package/inventory.md +97 -57
  13. package/package.json +1 -1
  14. package/scripts/check-live-integration.mjs +151 -0
  15. package/scripts/install-local.mjs +11 -4
  16. package/scripts/run-live-integration.mjs +55 -63
  17. package/src/apim-bundle.mjs +416 -13
  18. package/src/apim-cleanup-tools.mjs +145 -0
  19. package/src/apim-content.mjs +157 -0
  20. package/src/apim-delete-tools.mjs +139 -0
  21. package/src/apim-documentation.mjs +162 -0
  22. package/src/apim-inspection.mjs +141 -0
  23. package/src/apim-monitoring.mjs +140 -0
  24. package/src/apim-policy-steps.mjs +31 -0
  25. package/src/apim-runtime.mjs +262 -0
  26. package/src/apim-tools.mjs +202 -109
  27. package/src/cf-integration.mjs +46 -7
  28. package/src/cpi-async-monitoring.mjs +177 -0
  29. package/src/cpi-content.mjs +92 -0
  30. package/src/cpi-monitoring.mjs +225 -0
  31. package/src/cpi-operational.mjs +129 -0
  32. package/src/cpi-release.mjs +273 -0
  33. package/src/cpi-security.mjs +48 -0
  34. package/src/cpi-tools.mjs +184 -319
  35. package/src/devhub-retirement.mjs +345 -0
  36. package/src/devhub-tools.mjs +177 -0
  37. package/src/edge-monitoring.mjs +101 -0
  38. package/src/iflow-bundle.mjs +295 -7
  39. package/src/integration-alerts.mjs +248 -0
  40. package/src/integration-analysis.mjs +488 -0
  41. package/src/integration-capabilities.mjs +181 -0
  42. package/src/integration-collections.mjs +203 -0
  43. package/src/integration-common.mjs +140 -35
  44. package/src/integration-config.mjs +20 -14
  45. package/src/integration-credentials.mjs +16 -10
  46. package/src/integration-http.mjs +255 -67
  47. package/src/integration-messaging.mjs +118 -0
  48. package/src/integration-optional-config.mjs +122 -0
  49. package/src/integration-optional-tools.mjs +153 -0
  50. package/src/integration-release-store.mjs +410 -0
  51. package/src/integration-server.mjs +34 -16
  52. package/src/integration-state.mjs +31 -0
  53. package/src/integration-tools.mjs +44 -8
  54. package/src/integration-transport.mjs +184 -0
  55. package/src/integration-validation.mjs +26 -0
  56. package/src/integration-verify.mjs +143 -62
  57. package/src/open-connectors-tools.mjs +143 -0
  58. package/src/setup.mjs +86 -52
  59. package/tools.md +193 -26
package/src/setup.mjs CHANGED
@@ -12,6 +12,7 @@ import { parseIntegrationServiceKey, resolveIntegrationConfig, resolveIntegratio
12
12
  import { readStoredIntegrationKeys, storeIntegrationKey } from './integration-credentials.mjs';
13
13
  import { INTEGRATION_CF_SERVICES, createIntegrationInstance, findIntegrationInstances, integrationServiceKey } from './cf-integration.mjs';
14
14
  import { createIntegrationClients, verificationLines, verifyIntegrationClients } from './integration-verify.mjs';
15
+ import { OPTIONAL_COMPONENT_KEYS, OPTIONAL_INPUTS, parseOptionalConfig, resolveOptionalConfig } from './integration-optional-config.mjs';
15
16
  import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
16
17
  import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
17
18
 
@@ -285,16 +286,26 @@ async function confirmPrompt(message, { input = stdin, output = stdout, defaultY
285
286
  return !/^n(?:o)?$/i.test(trimmed);
286
287
  }
287
288
 
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.
289
+ // Report configured component identities and source evidence without credential values.
290
290
  export function integrationSuiteCredentialStatus(env = process.env) {
291
291
  try {
292
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)}`] };
293
+ const keys = ['cpi', 'apim', 'devhub', ...OPTIONAL_COMPONENT_KEYS];
294
+ const side = (key, value) => {
295
+ const host = value.url ? new URL(value.url).host : key === 'aem' ? 'api.solace.cloud' : 'unknown';
296
+ return `configured from ${value.source}${value.bindingName ? ` (${value.bindingName})` : ''}, host ${host}`;
297
+ };
298
+ const configured = keys.filter(key => config[key]);
299
+ const errors = (config.optional_config_errors ?? []).map(error => `${error.component.toUpperCase()}: invalid optional configuration (${error.source}); adapter disabled`);
300
+ if (!configured.length && !errors.length) return { found: false, config, lines: ['No Integration Suite credentials configured yet.'] };
301
+ return { found: configured.length > 0, config, lines: [
302
+ ...(config.environment ? [`Environment label: ${config.environment}`] : []),
303
+ ...configured.map(key => `${key.toUpperCase()}: ${side(key, config[key])}`),
304
+ ...errors
305
+ ] };
295
306
  } catch (error) {
296
307
  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.'] };
308
+ if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No Integration Suite credentials configured yet.'] };
298
309
  return { found: false, config: null, lines: [`Credentials found but not usable: ${message}`] };
299
310
  }
300
311
  }
@@ -315,28 +326,32 @@ function pastedServiceKey(side, value, env) {
315
326
  throw new Error('Paste the service key JSON (starting with {) or the path to a readable .json file.');
316
327
  }
317
328
  }
318
- return parseIntegrationServiceKey(side, text);
329
+ return OPTIONAL_COMPONENT_KEYS.includes(side.toLowerCase())
330
+ ? parseOptionalConfig(side.toLowerCase(), text, { source: 'setup', allowRawCtms: true })
331
+ : parseIntegrationServiceKey(side, text);
319
332
  }
320
333
 
321
334
  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' });
335
+ const stop = startProgress('Checking Integration Suite access', { output, env, label: 'Integration Suite' });
323
336
  let report;
324
- try {
325
- report = await verifyIntegration(config, { env });
326
- } finally {
327
- stop();
337
+ try { report = await verifyIntegration(config, { env }); }
338
+ finally { stop(); }
339
+ const host = side => config[side]?.url ? new URL(config[side].url).host : side === 'aem' && config.aem ? 'api.solace.cloud' : '';
340
+ for (const label of ['CPI', 'APIM', 'DEVHUB']) {
341
+ for (const line of verificationLines(label, host(label.toLowerCase()), report[label.toLowerCase()])) print(output, ` ${line}`);
328
342
  }
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'));
343
+ for (const side of OPTIONAL_COMPONENT_KEYS) {
344
+ const access = report[side];
345
+ if (access?.configured) print(output, ` ${side.toUpperCase()} ${host(side)}: ${access.result ?? access.checks?.[0]?.result ?? 'untested'} (optional access; activation/entitlement unknown)`);
339
346
  }
347
+ const core = [report.cpi, report.apim, report.devhub].filter(side => side?.configured);
348
+ const denied = core.flatMap(side => side.checks ?? []).filter(check => check.result === 'forbidden').length;
349
+ const message = !core.length ? 'No core service configured yet.'
350
+ : report.ok ? (denied ? `Connected; ${denied} area(s) were access-denied — the key may lack roles.` : 'Connection verified.')
351
+ : 'Sign-in or core read failed; see the lines above.';
352
+ print(output, formatStatus(message, report.ok && !denied ? 'success' : 'warning', output, 'Integration Suite'));
353
+ const optional = OPTIONAL_COMPONENT_KEYS.filter(side => report[side]?.configured);
354
+ if (optional.length) print(output, ` Optional services: ${report.optional_completeness?.complete === true ? 'all verified' : 'not fully verified'} (separate from core access).`);
340
355
  return report;
341
356
  }
342
357
 
@@ -347,7 +362,7 @@ const defaultIntegrationCloudFoundry = Object.freeze({
347
362
  createInstance: createIntegrationInstance
348
363
  });
349
364
 
350
- const SIDE_LABELS = Object.freeze({ CPI: 'Cloud Integration (CPI)', APIM: 'API Management' });
365
+ const SIDE_LABELS = Object.freeze({ CPI: 'Cloud Integration (CPI)', APIM: 'API Management', DEVHUB: 'Developer Hub' });
351
366
 
352
367
  function cfFailure(error) {
353
368
  const detail = String(error?.stderr || error?.message || '').replace(/\s+/g, ' ').trim();
@@ -359,12 +374,14 @@ async function acceptIntegrationKey(side, config, context) {
359
374
  const { env, input, output, verifyIntegration } = context;
360
375
  const choice = side.toLowerCase();
361
376
  const report = await printIntegrationVerification({ [choice]: config }, { env, output, verifyIntegration });
362
- if (!report[choice].signed_in && !await confirmPrompt('Save this key anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return { saved: false, report };
377
+ if (!report[choice]?.signed_in && !await confirmPrompt('Save these existing credentials despite unverified access? (y/n, Enter=no)', { input, output, defaultYes: false })) return { saved: false, report };
363
378
  const path = await storeIntegrationKey(side, config, env);
364
379
  print(output, formatStatus(`Saved the ${side} key to ${path} (readable only by you; not written to mcp.json).`, 'success', output, 'Integration Suite'));
365
380
  let fromEnvironment = null;
366
381
  try {
367
- fromEnvironment = resolveIntegrationSideFromEnvironment(env, side);
382
+ fromEnvironment = OPTIONAL_COMPONENT_KEYS.includes(choice)
383
+ ? resolveOptionalConfig(env, {})[choice]
384
+ : resolveIntegrationSideFromEnvironment(env, side);
368
385
  } catch {
369
386
  // A broken environment value is reported by the status lines.
370
387
  }
@@ -374,7 +391,7 @@ async function acceptIntegrationKey(side, config, context) {
374
391
  }
375
392
 
376
393
  function lacksRoles(side) {
377
- return Boolean(side?.signed_in) && (!side.ok || side.checks.some(check => check.result === 'forbidden'));
394
+ return Boolean(side?.signed_in) && (side.checks ?? []).some(check => check.result === 'forbidden');
378
395
  }
379
396
 
380
397
  async function cloudFoundryStep(message, operation, { env, output }) {
@@ -418,13 +435,13 @@ async function useCloudFoundrySpace(target, context) {
418
435
  print(output, formatStatus(`Could not list service instances: ${cfFailure(error)}`, 'warning', output, 'Cloud Foundry'));
419
436
  return;
420
437
  }
421
- for (const side of ['CPI', 'APIM']) {
438
+ for (const side of ['CPI', 'APIM', 'DEVHUB']) {
422
439
  const service = INTEGRATION_CF_SERVICES[side];
423
440
  const candidates = instances[side] || [];
424
441
  print(output, '');
425
442
  if (!candidates.length) {
426
443
  print(output, formatStatus(`No ${service.label} instance with plan ${service.plan} (${service.offering}) in this space.`, 'info', output, SIDE_LABELS[side]));
427
- 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 })) {
444
+ if (await confirmPrompt(`Create instance ${service.dedicatedInstance} (${service.offering}, plan ${service.plan}) with ${service.parameters.role || 'the roles the tools need'}, then create or reuse its sap-ai-dev-integration service key? (y/n, Enter=no)`, { input, output, defaultYes: false })) {
428
445
  await createDedicatedInstance(side, context);
429
446
  }
430
447
  continue;
@@ -436,6 +453,7 @@ async function useCloudFoundrySpace(target, context) {
436
453
  choices: [...candidates.map(candidate => ({ name: candidate.name, value: candidate.name })), { name: 'Skip', value: '', shortcut: 's' }]
437
454
  }, { input, output });
438
455
  if (!instanceName) continue;
456
+ if (side === 'DEVHUB' && !await confirmPrompt(`Use ${instanceName} and create or reuse its sap-ai-dev-integration service key? (y/n, Enter=no)`, { input, output, defaultYes: false })) continue;
439
457
  let result;
440
458
  try {
441
459
  result = await useCloudFoundryInstance(side, instanceName, context);
@@ -454,15 +472,35 @@ async function useCloudFoundrySpace(target, context) {
454
472
  }
455
473
  }
456
474
 
457
- // Verify the detected credentials online and let the user take CPI/APIM keys from the logged-in CF space or
475
+ // The paste flow asks which service the key belongs to; APIM and Developer Hub keys share one OAuth shape,
476
+ // so the type is never guessed from the key itself.
477
+ async function chooseKeyType({ input, output, status }) {
478
+ return await selectPrompt({
479
+ message: colorText('Which service is this key for?', 'cyan', output),
480
+ defaultValue: ['CPI', 'APIM', 'DEVHUB'].find(side => !status.config?.[side.toLowerCase()]) ?? 'CPI',
481
+ choices: [
482
+ { value: 'CPI', shortcut: 'i', name: 'Cloud Integration (CPI) — Process Integration Runtime, plan api' },
483
+ { value: 'APIM', shortcut: 'a', name: 'API Management — plan apiportal-apiaccess' },
484
+ { value: 'DEVHUB', shortcut: 'd', name: 'Developer Hub — plan devportal-apiaccess (AuthGroup.API.Admin)' },
485
+ ...OPTIONAL_COMPONENT_KEYS.map(side => ({
486
+ value: side.toUpperCase(),
487
+ shortcut: { ctms: 't', ans: 'n', event_mesh: 'e', aem: 'm', aem_semp: 's', open_connectors: 'o', edge: 'g' }[side],
488
+ name: `Optional: ${OPTIONAL_INPUTS[side].label} (${OPTIONAL_INPUTS[side].variable}, existing credentials only)`
489
+ })),
490
+ { value: '', shortcut: 'b', name: 'Back' }
491
+ ]
492
+ }, { input, output });
493
+ }
494
+
495
+ // Verify the detected credentials online and let the user take CPI/APIM/DEVHUB keys from the logged-in CF space or
458
496
  // paste them. A key is checked online first and saved to the user-only store (never to mcp.json); it is used
459
497
  // when the environment has no key.
460
498
  async function configureIntegrationCredentials({ env, input, output, verifyIntegration, cloudFoundry = defaultIntegrationCloudFoundry }) {
461
499
  let status = integrationSuiteCredentialStatus(env);
462
- // Latest verification per side ({ cpi, apim }); updated whenever a key is checked or saved.
500
+ // Latest verification per side ({ cpi, apim, devhub }); updated whenever a key is checked or saved.
463
501
  const health = {};
464
502
  const recordHealth = report => {
465
- for (const side of ['cpi', 'apim']) if (report?.[side]?.configured) health[side] = report[side];
503
+ for (const side of ['cpi', 'apim', 'devhub', ...OPTIONAL_COMPONENT_KEYS]) if (report?.[side]?.configured) health[side] = report[side];
466
504
  };
467
505
  if (status.found) recordHealth(await printIntegrationVerification(status.config, { env, output, verifyIntegration }));
468
506
  let target = { available: false };
@@ -475,9 +513,9 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
475
513
  for (;;) {
476
514
  print(output, '');
477
515
  const space = target.available ? [target.orgName, target.spaceName].filter(Boolean).join(' / ') || target.spaceGuid : '';
478
- // Sides whose key signs in but lacks roles: a new key cannot fix that, a dedicated instance can.
479
- const roleless = ['CPI', 'APIM'].filter(side => lacksRoles(health[side.toLowerCase()]));
480
- const healthy = status.found && !roleless.length && Object.values(health).every(side => side.ok);
516
+ // Access denial is observed; its role/entitlement cause is not established. Optional setup never provisions.
517
+ const roleless = ['CPI', 'APIM', 'DEVHUB'].filter(side => lacksRoles(health[side.toLowerCase()]));
518
+ const healthy = status.found && !roleless.length && Object.values(health).length > 0 && Object.values(health).every(side => side.ok);
481
519
  const fixes = target.available
482
520
  ? roleless.map(side => ({
483
521
  name: `Create ${INTEGRATION_CF_SERVICES[side].dedicatedInstance} in ${space} with the roles the ${side} tools need, then use it`,
@@ -486,14 +524,13 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
486
524
  : [];
487
525
  const choice = await selectPrompt({
488
526
  message: colorText('🔑 Integration Suite credentials', 'cyan', output),
489
- defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : 'cpi'))),
527
+ defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : 'paste'))),
490
528
  choices: [
491
529
  ...fixes,
492
- { name: !status.found ? 'Continue without credentials (the server will not start until a key is configured)' : (healthy ? 'Continue' : 'Continue with the current key (tools in the ✗ areas will fail with HTTP 403)'), value: 'done', shortcut: 'c' },
493
- ...(target.available ? [{ name: `Use the logged-in CF space (${space}): find instances, reuse or create keys`, value: 'cf', shortcut: 'f' }] : []),
494
- { name: 'Paste a Cloud Integration (CPI) service key — Process Integration Runtime, plan api', value: 'cpi', shortcut: 'p' },
495
- { name: 'Paste an API Management service key — API Management, plan apiportal-apiaccess', value: 'apim', shortcut: 'a' },
496
- ...(status.found ? [{ name: 'Check the configured credentials online again', value: 'verify', shortcut: 'v' }] : [])
530
+ ...(target.available ? [{ value: 'cf', shortcut: 'f', name: `Get keys from Cloud Foundry (${space})` }] : []),
531
+ { value: 'paste', shortcut: 'p', name: 'Paste a service key (JSON or path to a .json file)' },
532
+ ...(status.found ? [{ value: 'verify', shortcut: 'v', name: 'Check credentials again' }] : []),
533
+ { value: 'done', shortcut: 'c', name: status.found ? (healthy ? 'Done' : 'Done (some checks did not pass)') : 'Skip for now (run sap-ai-dev --setup-integration later)' }
497
534
  ]
498
535
  }, { input, output });
499
536
  if (choice === 'done') return status;
@@ -505,10 +542,11 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
505
542
  await createDedicatedInstance(choice.slice('create-'.length).toUpperCase(), context);
506
543
  } else if (choice === 'cf') {
507
544
  await useCloudFoundrySpace(target, context);
508
- } else {
509
- const side = choice.toUpperCase();
545
+ } else if (choice === 'paste') {
546
+ const side = await chooseKeyType({ input, output, status });
547
+ if (!side) continue; // Back: show the menu again without refreshing status.
510
548
  const pasted = await textPrompt({
511
- message: `Paste the ${side} service key JSON or the path to its .json file`,
549
+ message: `Paste existing ${side} credential JSON or the path to its .json file`,
512
550
  secret: true,
513
551
  validate: value => {
514
552
  try {
@@ -528,16 +566,12 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
528
566
 
529
567
  async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, integrationCloudFoundry, ask = true }) {
530
568
  print(output, '');
531
- print(output, formatStatus('SAP Integration Suite tools (Cloud Integration + API Management).', 'step', output, 'Integration Suite'));
532
- 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.');
533
- 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.');
569
+ print(output, formatStatus('SAP Integration Suite tools', 'step', output, 'Integration Suite'));
570
+ print(output, ' Connects your AI agent to Cloud Integration (CPI), API Management and Developer Hub. Keys are checked read-only and stored only for you, never in mcp.json.');
534
571
  if (alreadyConfigured) print(output, ' Already configured in this MCP config; keeping it and refreshing its version.');
535
572
  const status = integrationSuiteCredentialStatus(env);
536
573
  for (const line of status.lines) print(output, ` ${line}`);
537
- if (!status.found) {
538
- print(output, ' The server needs CPI_SERVICE_KEY and/or APIM_SERVICE_KEY (or CPI_*/APIM_* variables, VCAP_SERVICES) in the MCP host environment,');
539
- 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.');
540
- }
574
+ if (!status.found) print(output, ' No credentials found yet. Next you can get them from your Cloud Foundry space or paste a service key.');
541
575
  if (ask && !alreadyConfigured) {
542
576
  print(output, '');
543
577
  const defaultYes = status.found;
@@ -1062,7 +1096,7 @@ export async function runSetup({
1062
1096
  defaultValue: 'manual',
1063
1097
  choices: [
1064
1098
  { name: '✍️ Start manual entry wizard (system name, ADT URL, client, login)', value: 'manual', shortcut: 'm' },
1065
- { name: '🔌 Set up only the SAP Integration Suite tools (paste and verify CPI / API Management service keys)', value: 'integration', shortcut: 'i' },
1099
+ { name: '🔌 Set up only the SAP Integration Suite tools (paste and verify CPI / API Management / Developer Hub service keys)', value: 'integration', shortcut: 'i' },
1066
1100
  { name: 'Exit setup without changes', value: 'exit', shortcut: 'e' }
1067
1101
  ]
1068
1102
  }, { input, output });
@@ -1087,9 +1121,9 @@ export async function runSetup({
1087
1121
  sapDevelopmentServers = ['integration-suite'];
1088
1122
  } else if (includeSapDevelopmentToolsPrompt) {
1089
1123
  print(output, '');
1090
- print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development, HANA inspection, and Integration Suite (CPI/API Management).', 'step', output, 'Tools'));
1124
+ print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development, HANA inspection, and Integration Suite (CPI/API Management/Developer Hub).', 'step', output, 'Tools'));
1091
1125
  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.');
1092
- 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.');
1126
+ print(output, ' Integration Suite uses CPI/APIM/DEVHUB 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.');
1093
1127
  print(output, ' Leave empty to only configure ABAP/ADT destination servers.');
1094
1128
  print(output, '');
1095
1129
  sapDevelopmentServers = await checkboxPrompt({