sap-ai-dev-toolkit 0.8.0 → 0.8.1

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 (3) hide show
  1. package/README.md +28 -1
  2. package/package.json +1 -1
  3. package/src/setup.mjs +82 -20
package/README.md CHANGED
@@ -272,8 +272,35 @@ The installer handles VSP provisioning automatically:
272
272
 
273
273
  Setup detects where it is running. With `H2O_URL` set, it treats the environment as SAP Business Application Studio and opens a checkbox picker for BAS destinations, with no destinations selected by default. Use **Space** to choose destinations and **Enter** to confirm. Press **a** to toggle all destinations (select all if any are unchecked; otherwise clear the selection). Confirming with none selected removes this add-on's managed MCP entries. When the `cf` CLI 8.18 or newer is authenticated to a targeted space, setup first offers an optional import from that space's Destination service; type **y** then **Enter** to include it, or press **Enter** to skip. Accepted CF and BAS destinations appear together in the picker.
274
274
 
275
- Without `H2O_URL`, interactive setup looks for local SAP GUI system configuration on macOS and Windows (for example SAP GUI landscape XML and `saplogon.ini`). SAP GUI landscape files describe SAP GUI/DIAG connectivity, not the ADT HTTP(S) endpoint, so setup asks you to confirm the ADT URL for each selected system. The generated MCP entry uses VS Code input prompts for the SAP user and password at server start; raw SAP credentials are not stored in `mcp.json`. On Windows, setup can instead write an experimental `SAP_AUTH_MODE=windows-sso` entry without username/password prompts; this requires the ADT endpoint to support HTTP Negotiate/SPNEGO. Runtime uses the bundled Windows Negotiate helper by default, or a trusted custom helper configured with `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER`. npm may run its install hook without an interactive terminal, even when the shell is interactive; in that case, destination selection is skipped without changing MCP config.
275
+ Without `H2O_URL`, interactive setup looks for local SAP GUI system configuration on macOS and Windows (for example SAP GUI landscape XML and `saplogon.ini`). SAP GUI landscape files describe SAP GUI/DIAG connectivity, not the ADT HTTP(S) endpoint or a complete ABAP client catalog, so setup asks for the SAP client or comma-separated clients to configure for each selected system (for example `100,200`), proposes an ADT URL from the SAP GUI host and instance when it can, and asks you to confirm or edit that value. Multiple entered clients create separate isolated MCP entries, one per system/client pair. When the ADT URL uses an IP address, setup automatically inspects the SAP HTTPS certificate, extracts DNS subjectAltName entries, and offers to enable secure TLS self-healing with a simple yes/no confirmation; you do not need to type certificate names. The generated MCP entry uses VS Code input prompts for the SAP user and password at server start; raw SAP credentials are not stored in `mcp.json`. On Windows, setup can instead write an experimental `SAP_AUTH_MODE=windows-sso` entry without username/password prompts; this requires the ADT endpoint to support HTTP Negotiate/SPNEGO. Runtime uses the bundled Windows Negotiate helper by default, or a trusted custom helper configured with `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER`. npm may run its install hook without an interactive terminal, even when the shell is interactive; in that case, destination selection is skipped without changing MCP config.
276
276
 
277
+ ### 🖥️ Local machine SAP GUI support
278
+
279
+ Local setup is intended for developers running the MCP server from their own workstation rather than SAP Business Application Studio.
280
+
281
+ Supported discovery and runtime behavior:
282
+
283
+ - **Discovery sources:** SAP GUI landscape XML and `saplogon.ini` style configuration on macOS and Windows. Setup de-duplicates entries, ignores incomplete DIAG records, and shows the discovered SAP systems in the same checkbox picker used for BAS destinations.
284
+ - **Client selection:** SAP GUI entries may contain one default client but cannot reliably list every ABAP client available through ADT. Setup asks for one or more 3-digit clients per selected system; enter comma-separated values such as `100,200` to create separate MCP servers for each client.
285
+ - **ADT endpoint derivation:** SAP GUI entries usually contain DIAG/router information, not the HTTP(S) ADT endpoint. Setup derives the common ADT port from the SAP instance when possible, for example instance `00` → `https://host:44300`, and asks for confirmation only when configuring the selected entry.
286
+ - **Credential handling:** username/password local entries use VS Code MCP input prompts (`${input:...}`) at server start. Plain SAP credentials are not written to `mcp.json`.
287
+ - **Windows SSO:** local Windows entries can use `SAP_AUTH_MODE=windows-sso` instead of username/password prompts when the ABAP ADT endpoint supports HTTP Negotiate/SPNEGO. The runtime starts a loopback ADT proxy and uses the bundled Windows SSPI helper unless `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER` points to a trusted custom helper. NTLM fallback is rejected.
288
+ - **IP-address HTTPS self-healing:** if the confirmed ADT URL is `https://<ip>:<port>` and the certificate is issued to DNS names, setup reads the certificate DNS SANs automatically and stores them as `SAP_TLS_SERVER_NAMES`. At runtime a loopback proxy dials the IP address but verifies TLS against those discovered DNS names, trying them one by one. Certificate verification stays enabled; there is no `--insecure` mode.
289
+ - **Private CAs:** if the SAP HTTPS certificate chains to an internal CA that is not trusted by the local Node runtime, set `SAP_TLS_CA_FILE` or `SAP_AI_DEV_TOOLKIT_TLS_CA_FILE` to a PEM CA bundle. This adds trust roots for the self-healing proxy without disabling certificate validation.
290
+ - **Redirect and request safety:** local loopback proxies reject cross-origin forward-form requests and rewrite same-origin SAP redirects back through the loopback listener, so VSP stays on the safe route.
291
+
292
+ Typical local setup flow:
293
+
294
+ ```sh
295
+ sap-ai-dev --setup
296
+ ```
297
+
298
+ 1. Select the discovered SAP GUI system with **Space** and confirm with **Enter**.
299
+ 2. Confirm the detected/default SAP client, or enter multiple clients such as `100,200`.
300
+ 3. Accept the proposed ADT URL when it is correct, or edit it if your landscape exposes ADT through a different HTTP(S) host/port.
301
+ 4. If setup discovers certificate DNS names for an IP-based HTTPS URL, accept the yes/no TLS self-healing prompt.
302
+ 5. Choose password or Windows SSO when the prompt is available.
303
+ 6. Start each generated MCP server from VS Code's **MCP: List Servers** command.
277
304
 
278
305
  The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all seven, and confirming with none selected skips the step without changing files. When destination MCP setup succeeds, the generated managed MCP servers are also mirrored into selected harnesses that have a stable user-level JSON MCP configuration (Claude Code, Cursor, Gemini CLI, and Pi Coding Agent). GitHub Copilot continues to use the BAS/VS Code MCP configuration written during destination setup. With no interactive terminal, the assets are installed for GitHub Copilot only; set `SAP_AI_DEV_TOOLKIT_HARNESSES` to a comma-separated list of harness ids (`github-copilot`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `opencode`, `pi-coding-agent`) to choose non-interactively.
279
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
package/src/setup.mjs CHANGED
@@ -45,6 +45,51 @@ async function confirmCloudFoundryImport({ input = stdin, output = stdout } = {}
45
45
  }
46
46
  }
47
47
 
48
+ export function parseSapClientList(value, fallback = '001') {
49
+ const supplied = String(value ?? '').trim();
50
+ const raw = supplied || String(fallback || '001').trim() || '001';
51
+ const clients = raw.split(',').map(client => client.trim()).filter(Boolean);
52
+ const unique = [];
53
+ const seen = new Set();
54
+ for (const client of clients) {
55
+ if (!/^\d{3}$/.test(client)) throw new Error('Enter one or more SAP clients as 3 digits, separated by commas, for example 100 or 100,200.');
56
+ if (!seen.has(client)) {
57
+ seen.add(client);
58
+ unique.push(client);
59
+ }
60
+ }
61
+ if (!unique.length) throw new Error('Enter at least one SAP client.');
62
+ return unique;
63
+ }
64
+
65
+ async function promptSapClients(destination, { input = stdin, output = stdout } = {}) {
66
+ const fallback = destination.client || '001';
67
+ const answer = await textPrompt({
68
+ message: `SAP client(s) for ${destination.name}`,
69
+ placeholder: fallback,
70
+ required: false,
71
+ validate: value => {
72
+ try { parseSapClientList(value, fallback); return true; }
73
+ catch (error) { return error.message; }
74
+ }
75
+ }, { input, output });
76
+ return parseSapClientList(answer, fallback);
77
+ }
78
+
79
+ function localSystemDiscriminator(destination) {
80
+ return [destination.systemId, destination.host, destination.instance].map(value => String(value || '').trim()).filter(Boolean).join(' ');
81
+ }
82
+
83
+ function localClientDestinationName(destination, client, { multipleClients = false, duplicateSystemName = false } = {}) {
84
+ const suffix = [];
85
+ if (multipleClients || duplicateSystemName) suffix.push(client);
86
+ if (duplicateSystemName) {
87
+ const discriminator = localSystemDiscriminator(destination);
88
+ if (discriminator) suffix.push(discriminator);
89
+ }
90
+ return suffix.length ? `${destination.name} ${suffix.join(' ')}` : destination.name;
91
+ }
92
+
48
93
 
49
94
  function safeProbe(probe) {
50
95
  const result = {};
@@ -293,7 +338,7 @@ export async function runSetup({
293
338
  print(output, ' Space = select/deselect · a = toggle all · Enter = confirm.');
294
339
  print(output, ' Nothing selected removes this add-on’s MCP entries.');
295
340
  print(output, '');
296
- const selected = await checkboxPrompt({
341
+ let selected = await checkboxPrompt({
297
342
  message: colorText('🧭 Select destinations', 'cyan', output),
298
343
  choices,
299
344
  required: false,
@@ -302,12 +347,23 @@ export async function runSetup({
302
347
 
303
348
  if (!isBas && selected.length) {
304
349
  print(output, '');
305
- print(output, formatStatus('SAP GUI landscapes do not contain ADT HTTP(S) endpoints. Confirm the ADT URL and authentication for each selected system.', 'step', output, 'Local SAP GUI'));
350
+ print(output, formatStatus('SAP GUI landscapes do not contain ADT HTTP(S) endpoints or a complete client catalog. Confirm the SAP client(s), ADT URL, and authentication for each selected system.', 'step', output, 'Local SAP GUI'));
351
+ print(output, ' Enter multiple clients as comma-separated 3-digit values, for example 100,200. Setup creates one isolated MCP server per system/client pair.');
306
352
  if (windowsSsoSetupAvailable(env)) print(output, ' Windows SSO is experimental and only applies to ADT systems configured for HTTP Integrated Authentication. Username/password remains the default.');
353
+ const selectedNameCounts = selected.reduce((counts, destination) => {
354
+ const key = String(destination.name).toLowerCase();
355
+ counts.set(key, (counts.get(key) || 0) + 1);
356
+ return counts;
357
+ }, new Map());
358
+ const expanded = [];
307
359
  for (const destination of selected) {
360
+ const duplicateSystemName = (selectedNameCounts.get(String(destination.name).toLowerCase()) || 0) > 1;
361
+ const discriminator = duplicateSystemName ? localSystemDiscriminator(destination) : '';
362
+ const promptLabel = discriminator ? `${destination.name} (${discriminator})` : destination.name;
363
+ const clients = await promptSapClients({ ...destination, name: promptLabel }, { input, output });
308
364
  const fallback = defaultAdtUrl(destination);
309
365
  const url = await textPrompt({
310
- message: `ADT URL for ${destination.name}`,
366
+ message: `ADT URL for ${promptLabel}`,
311
367
  placeholder: fallback || 'https://host:44300',
312
368
  validate: value => {
313
369
  const candidate = value || fallback;
@@ -333,7 +389,7 @@ export async function runSetup({
333
389
  if (discoveredNames.length) {
334
390
  const preview = discoveredNames.slice(0, 5).join(', ');
335
391
  const answer = await textPrompt({
336
- message: `Use discovered certificate DNS name${discoveredNames.length === 1 ? '' : 's'} for ${destination.name}: ${preview}${discoveredNames.length > 5 ? ', ...' : ''}? (y/Enter=yes, n=no)`,
392
+ message: `Use discovered certificate DNS name${discoveredNames.length === 1 ? '' : 's'} for ${promptLabel}: ${preview}${discoveredNames.length > 5 ? ', ...' : ''}? (y/Enter=yes, n=no)`,
337
393
  placeholder: 'y',
338
394
  required: false,
339
395
  validate: value => /^(?:|y|yes|n|no)$/i.test(String(value || '').trim()) || 'Enter y or n.'
@@ -346,26 +402,32 @@ export async function runSetup({
346
402
  print(output, formatStatus('No DNS subjectAltName entries could be read from the SAP HTTPS certificate. TLS self-healing was not configured automatically.', 'warning', output, 'TLS self-heal'));
347
403
  }
348
404
  }
349
- const authMode = await chooseLocalAuthMode(destination, { input, output, env });
405
+ const authMode = await chooseLocalAuthMode({ ...destination, name: promptLabel }, { input, output, env });
350
406
  destination.authentication = authMode === 'windows-sso' ? 'WindowsSSO' : 'Basic';
351
407
  destination.authMode = authMode;
352
- if (authMode === 'windows-sso') {
353
- destination.childEnv = { SAP_AUTH_MODE: 'windows-sso' };
354
- destination.inputs = [];
355
- } else {
356
- const userInputId = `sap-ai-dev-${destination.serverName || destination.name}-user`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
357
- const passwordInputId = `sap-ai-dev-${destination.serverName || destination.name}-password`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
358
- destination.childEnv = {
359
- SAP_AUTH_MODE: 'basic',
360
- SAP_USER: `\${input:${userInputId}}`,
361
- SAP_PASSWORD: `\${input:${passwordInputId}}`
362
- };
363
- destination.inputs = [
364
- { id: userInputId, type: 'promptString', description: `SAP user for ${destination.name}` },
365
- { id: passwordInputId, type: 'promptString', description: `SAP password for ${destination.name}`, password: true }
366
- ];
408
+ for (const client of clients) {
409
+ const name = localClientDestinationName(destination, client, { multipleClients: clients.length > 1, duplicateSystemName });
410
+ const localDestination = { ...destination, name, serverName: name, client };
411
+ if (authMode === 'windows-sso') {
412
+ localDestination.childEnv = { SAP_AUTH_MODE: 'windows-sso' };
413
+ localDestination.inputs = [];
414
+ } else {
415
+ const userInputId = `sap-ai-dev-${localDestination.serverName || localDestination.name}-user`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
416
+ const passwordInputId = `sap-ai-dev-${localDestination.serverName || localDestination.name}-password`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
417
+ localDestination.childEnv = {
418
+ SAP_AUTH_MODE: 'basic',
419
+ SAP_USER: `\${input:${userInputId}}`,
420
+ SAP_PASSWORD: `\${input:${passwordInputId}}`
421
+ };
422
+ localDestination.inputs = [
423
+ { id: userInputId, type: 'promptString', description: `SAP user for ${localDestination.name}` },
424
+ { id: passwordInputId, type: 'promptString', description: `SAP password for ${localDestination.name}`, password: true }
425
+ ];
426
+ }
427
+ expanded.push(localDestination);
367
428
  }
368
429
  }
430
+ selected = expanded;
369
431
  }
370
432
 
371
433
  let sapDevelopmentServers = [];