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.
- package/README.md +28 -1
- package/package.json +1 -1
- 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
|
|
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
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 = [];
|