sap-ai-dev-toolkit 0.9.3 → 0.9.5
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 +11 -14
- package/package.json +1 -1
- package/src/bas-discovery.mjs +55 -13
- package/src/launcher.mjs +25 -42
- package/src/mcp-proxy.mjs +1 -1
- package/src/setup.mjs +241 -156
- package/src/terminal-ui.mjs +46 -4
- package/scripts/windows-negotiate-helper.ps1 +0 -182
- package/src/windows-sso-adt-proxy.mjs +0 -257
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ Prefer a persistent global install? Install globally from SAP Business Applicati
|
|
|
262
262
|
| --- | --- | --- |
|
|
263
263
|
| **Any environment (default — no global install)** | `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` | Runs the guided setup through npm and writes generated entries that launch the pinned package through `npx` (`cmd /c npx` on Windows). Add `--npx` to force the npx launcher even when a global `sap-ai-dev` command exists. |
|
|
264
264
|
| **SAP Business Application Studio dev space (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers BAS destinations, optionally imports Cloud Foundry Destination service records, and writes VS Code/BAS MCP entries. |
|
|
265
|
-
| **Windows workstation with SAP GUI (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries. Windows
|
|
265
|
+
| **Windows workstation with SAP GUI (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries. Windows / smart card SSO (Edge) or username/password can be selected. |
|
|
266
266
|
| **macOS workstation with SAP GUI (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries with secure login prompts. |
|
|
267
267
|
| **Any local interactive terminal, already installed** | `sap-ai-dev --setup` | Re-runs the wizard to add, update, or remove generated MCP entries. Plain `sap-ai-dev` with no arguments also starts local setup when no BAS environment is detected. |
|
|
268
268
|
| **npm install hook did not show a wizard** | `sap-ai-dev --setup` | This is expected on some npm/Windows combinations because lifecycle scripts may run without an interactive console. The explicit setup command is the reliable path. |
|
|
@@ -276,7 +276,7 @@ Everything works natively on Windows — PowerShell, cmd, and Windows Terminal a
|
|
|
276
276
|
- The pinned VSP runtime ships inside the package as a native `vsp-win32-x64.exe`; no Go toolchain, WSL, or extra downloads are required.
|
|
277
277
|
- MCP entries that launch through npm are written as `cmd /c npx ...` on Windows — the form VS Code, Claude Code, and Cursor can spawn there. Entries written on Linux/macOS use plain `npx`.
|
|
278
278
|
- Setup reads `mcp.json` even when Windows tools saved it with a UTF-8/UTF-16 BOM (PowerShell `>`/`Set-Content`, Notepad "UTF-8 with BOM") and rewrites it as plain UTF-8. If the existing file cannot be used as-is, setup self-heals before falling back to a reset: VS Code-style comments and trailing commas (JSONC) are recovered in place, torn reads from a concurrent editor save are re-read once, and broken `servers`/`inputs` fields are repaired individually. Every lossy change first backs up the original next to the real file as `mcp.json.healed|repaired|invalid-<timestamp>.bak` (exclusive create, so reruns never overwrite an earlier backup; symlinked configs keep their link). `sap-ai-dev --doctor` stays strict and reports such files instead of rewriting them — run `sap-ai-dev --setup` to heal.
|
|
279
|
-
- Local SAP GUI discovery reads the Windows landscape files and registry, and setup can write
|
|
279
|
+
- Local SAP GUI discovery reads the Windows landscape files and registry, and setup can write a Windows / smart card SSO entry that signs in through Microsoft Edge; see the “Local machine SAP GUI support” section below.
|
|
280
280
|
|
|
281
281
|
If you previously installed the old package, remove it first because the legacy `bas-vsp-mcp` command is no longer shipped:
|
|
282
282
|
|
|
@@ -302,7 +302,7 @@ The installer handles VSP provisioning automatically:
|
|
|
302
302
|
|
|
303
303
|
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.
|
|
304
304
|
|
|
305
|
-
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. Local SAP GUI MCP server keys use the SAP system ID and client (for example `t4d-100`) instead of the longer SAP GUI description, while `SAP_AI_DEV_TOOLKIT_DESTINATION` still preserves the display name. 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
|
|
305
|
+
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. Local SAP GUI MCP server keys use the SAP system ID and client (for example `t4d-100`) instead of the longer SAP GUI description, while `SAP_AI_DEV_TOOLKIT_DESTINATION` still preserves the display name. 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 offers Windows / smart card SSO (the default) instead, which writes `SAP_AUTH_MODE=sso` without username/password prompts. At server start, VSP opens Microsoft Edge on the ADT URL; Edge completes the login the same way it does in your browser (smart card certificate and PIN, Kerberos, or company SAML/Entra login), and the session is cached and refreshed automatically. SSO entries need an ADT URL with a host name, not an IP address. 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, and you should run `sap-ai-dev --setup` afterwards.
|
|
306
306
|
|
|
307
307
|
### 🖥️ Local machine SAP GUI support
|
|
308
308
|
|
|
@@ -314,7 +314,7 @@ Supported discovery and runtime behavior:
|
|
|
314
314
|
- **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.
|
|
315
315
|
- **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.
|
|
316
316
|
- **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`.
|
|
317
|
-
- **Login methods:** local entries
|
|
317
|
+
- **Login methods:** local entries use either username/password or Windows / smart card SSO. SSO sets `SAP_SSO=true` for the VSP runtime, which drives Microsoft Edge for the login and re-opens an Edge window only when the cached session can no longer be refreshed silently.
|
|
318
318
|
- **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.
|
|
319
319
|
- **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.
|
|
320
320
|
- **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.
|
|
@@ -325,11 +325,11 @@ Typical local setup flow:
|
|
|
325
325
|
sap-ai-dev --setup
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
-
1. Select the discovered SAP GUI system with **Space** and confirm with **Enter**.
|
|
328
|
+
1. Select the discovered SAP GUI system with **Space** and confirm with **Enter**. If no SAP GUI entries are found, setup offers the manual entry wizard: enter a system name, ADT URL, client(s), and login mode instead.
|
|
329
329
|
2. Confirm the detected/default SAP client, or enter multiple clients such as `100,200`.
|
|
330
330
|
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.
|
|
331
331
|
4. If setup discovers certificate DNS names for an IP-based HTTPS URL, accept the yes/no TLS self-healing prompt.
|
|
332
|
-
5. Choose
|
|
332
|
+
5. Choose Windows / smart card SSO or username/password when the prompt is available.
|
|
333
333
|
6. Start each generated MCP server from VS Code's **MCP: List Servers** command.
|
|
334
334
|
|
|
335
335
|
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 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 VS Code/BAS MCP configuration written during 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.
|
|
@@ -352,7 +352,7 @@ To configure without a global install, run the guided setup directly through npm
|
|
|
352
352
|
npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
-
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm,
|
|
355
|
+
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, **a** to toggle all (select all if any are unchecked; otherwise clear the selection), and **m** to configure an undiscovered system manually in the manual entry wizard; configured systems join the picker pre-selected. Because no global `sap-ai-dev` command is on `PATH`, it writes MCP entries that launch the pinned package version through npx — through `cmd /c npx` on Windows, which is the form VS Code, Claude Code, and Cursor can spawn there. `--ignore-scripts` avoids running the install-time wizard a second time; add `--npx` to force this launcher shape even when the toolkit is installed globally. After setup, in BAS or VS Code run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
|
|
356
356
|
|
|
357
357
|
For a non-interactive installation or a platform without a published VSP asset, provide a trusted binary override:
|
|
358
358
|
|
|
@@ -447,7 +447,9 @@ Review skipped or conflicting files and merge changes manually. `LintABAP` analy
|
|
|
447
447
|
If the `cf` CLI is version 8.18 or newer and authenticated to a targeted space, setup offers an opt-in import from that space's SAP Destination service. It does not create service keys unless you accept. The imported records are limited to the current space and are merged with BAS destinations for selection. OnPremise destinations require choosing a Connectivity service instance; runtime traffic uses that service's proxy, and PrincipalPropagation uses the current CF user's token. Internet destinations use the configured HTTP(S) proxy environment. Setup stores only service-instance/key references in `mcp.json`, not the service-key credentials. At runtime the generated entry verifies the active CF space and resolves those references. Setup removes an add-on-created key only when no remaining CF entry references it and the CLI is targeted to the key's recorded space; keys are left untouched when the space cannot be verified.
|
|
448
448
|
Cloud Foundry HTTP destination probes use forward-form HTTP requests, not CONNECT tunnels; SAP Connectivity requires HTTP from the application to its proxy. OnPremise requests are canceled when the client disconnects or the runtime route closes.
|
|
449
449
|
|
|
450
|
-
Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm,
|
|
450
|
+
Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm, **a** to toggle all (select all if any are unchecked; otherwise clear the selection), and **m** to configure an undiscovered system manually in the manual entry wizard; configured systems join the picker pre-selected. Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination or local SAP GUI system selection without changing MCP config. Run the interactive setup command above; generated entries fall back to the pinned npx launcher automatically when the package is not installed globally.
|
|
451
|
+
|
|
452
|
+
When nothing is detected — no BAS destinations, no Cloud Foundry imports, or no local SAP GUI entries — setup offers the **manual entry wizard** instead of stopping. The same wizard is available at any point in the destination picker via the **m** shortcut. It asks for a system name, the ADT HTTP(S) base URL (for example `https://myhost.example:44300`), one or more 3-digit SAP clients, and (on Windows) the login mode. You can add several systems in one run; each becomes the same isolated per-system/client MCP server the discovery flows generate, including credential input prompts and TLS self-healing for IP-based HTTPS URLs. Declining the wizard when nothing was detected exits setup without changes; aborting it inside the picker (Ctrl+C) returns to the selection unchanged.
|
|
451
453
|
|
|
452
454
|
Setup tidies its own footprint: it reconciles MCP entries managed by this package and removes legacy `sapAiDev_*` / `basVspMcp_*` entries from earlier releases. Existing unrelated MCP servers and top-level configuration such as `inputs` stay untouched.
|
|
453
455
|
|
|
@@ -806,15 +808,10 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
|
|
|
806
808
|
| `SAP_AI_DEV_TOOLKIT_DESTINATION` | Comma-separated destination allowlist for normal runtime discovery. Setup clears this temporarily so it can display all eligible systems. |
|
|
807
809
|
| `SAP_AI_DEV_TOOLKIT_MODE` | VSP child mode (`expert` by default; `focused` omits `ActivateMultiple`, `GetUserTransports`, and `GetTransportInfo`). The proxy exposes its curated tool subset and local workflow tools, including `LintABAP`. |
|
|
808
810
|
| `SAP_ALLOW_TRANSPORTABLE_EDITS` | Generated MCP entries set this to `true` to permit source edits in transportable packages; VSP safety checks and SAP authorizations still apply. |
|
|
809
|
-
| `SAP_AUTH_MODE=
|
|
811
|
+
| `SAP_AUTH_MODE=sso` | Local SAP GUI entry mode for Windows / smart card SSO. Runtime starts VSP with `SAP_SSO=true`, which signs in through Microsoft Edge and caches the session per system and client. Older `windows-sso`, `browser-saml`, and `saml-password` entries are treated as `sso`. |
|
|
810
812
|
| `SAP_TLS_SERVER_NAMES` / `SAP_AI_DEV_TOOLKIT_TLS_SERVER_NAMES` | Comma-separated local SAP GUI TLS self-healing names, normally auto-discovered during setup from the SAP HTTPS certificate when `SAP_URL` uses an IP address. Runtime tries them one by one while keeping certificate validation enabled. |
|
|
811
813
|
| `SAP_TLS_SERVER_NAME` / `SAP_AI_DEV_TOOLKIT_TLS_SERVER_NAME` | Single-name compatibility form of the local SAP GUI TLS self-healing override. Prefer the auto-discovered plural variable written by setup. |
|
|
812
814
|
| `SAP_TLS_CA_FILE` / `SAP_AI_DEV_TOOLKIT_TLS_CA_FILE` | Optional PEM CA bundle for the local TLS self-healing proxy when the SAP HTTPS certificate chains to a private CA that is not in the OS/Node trust store. Certificate verification remains enabled. |
|
|
813
|
-
| `SAP_AUTH_MODE=browser-saml` with `SAP_BROWSER_AUTH=true` / `SAP_SAML_AUTH=true` | Local browser SAML / web SSO mode. The generated entry delegates browser login handling to the VSP runtime instead of storing SAP credentials. |
|
|
814
|
-
| `SAP_AUTH_MODE=saml-password` with `SAP_SAML_AUTH=true` | Local SAML username/password mode. Generated entries use MCP input prompts for `SAP_SAML_USER` and `SAP_SAML_PASSWORD`; raw credentials are not written to `mcp.json`. |
|
|
815
|
-
| `SAP_AI_DEV_TOOLKIT_WINDOWS_CREDENTIAL_UI=true` / `SAP_WINDOWS_CREDENTIAL_UI=true` | Allows the bundled Windows SSPI helper to run without PowerShell `-NonInteractive`, enabling native Windows credential UI / smartcard PIN prompts when Windows and the MCP host desktop session permit them. |
|
|
816
|
-
| `SAP_AUTH_FALLBACK_MODE=basic` / `SAP_AI_DEV_TOOLKIT_AUTH_FALLBACK_MODE=basic` | Enables Windows SSO self-healing fallback for local SAP GUI entries. If Negotiate/SPNEGO is unavailable or the SSPI helper cannot acquire a token, the loopback proxy retries with Basic auth using `SAP_USER`/`SAP_PASSWORD` from the MCP input prompts. |
|
|
817
|
-
| `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER` / `SAP_AI_DEV_TOOLKIT_SSPI_HELPER` | Optional trusted Windows helper executable used by the local Windows SSO proxy to answer an HTTP `WWW-Authenticate: Negotiate` challenge with the current Windows SSPI/Kerberos/SPNEGO credentials. It is not a prompt for a user-entered token; the helper must obtain credentials from Windows. NTLM fallback is rejected because it is connection-oriented and unsafe for this proxy shape. When unset on Windows, the bundled PowerShell SSPI helper is used. A custom helper receives one JSON line on stdin (`url`, `challenge`, `mechanism`) and prints the base64 Negotiate response on stdout. |
|
|
818
815
|
| `SAP_AI_DEV_TOOLKIT_HTTP_PROXY` | Egress proxy for BAS discovery and probe traffic. Takes precedence over `HTTP_PROXY`/`http_proxy`; unset means the default BAS proxy for `.dest` hosts, empty means direct. |
|
|
819
816
|
| `SAP_AI_DEV_TOOLKIT_REQUEST_TIMEOUT_MS` | Per-request timeout for calls forwarded to a VSP child (default 600000 = 10 minutes; `0` disables). A stalled request fails with a timeout error; the child is left running. |
|
|
820
817
|
| `SAP_AI_DEV_MCP_CONFIG` | Explicit MCP configuration path; highest precedence. |
|
package/package.json
CHANGED
package/src/bas-discovery.mjs
CHANGED
|
@@ -17,19 +17,26 @@ function scalar(value) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function propertiesOf(item) {
|
|
20
|
+
// Nested property maps (BAS wraps destination attributes in `properties`,
|
|
21
|
+
// some proxies use `configuration` or a key/value array) only fill gaps:
|
|
22
|
+
// a nested `Name` — e.g. the Cloud Connector system name `_SIDCLNTOX` —
|
|
23
|
+
// must never overwrite the real destination name at the top level.
|
|
20
24
|
const result = {};
|
|
21
|
-
|
|
22
|
-
const maps =
|
|
25
|
+
const entries = Object.entries(item || {});
|
|
26
|
+
const maps = entries.filter(([key]) => ['properties', 'propertymap', 'destinationproperties', 'configuration'].includes(keyName(key))).map(([, value]) => value);
|
|
23
27
|
for (const map of maps) {
|
|
24
28
|
if (Array.isArray(map)) {
|
|
25
29
|
for (const entry of map) {
|
|
26
30
|
const key = entry?.key ?? entry?.name ?? entry?.property;
|
|
27
|
-
if (key != null) result[keyName(key)] = scalar(entry?.value ?? entry?.val);
|
|
31
|
+
if (key != null && !(keyName(key) in result)) result[keyName(key)] = scalar(entry?.value ?? entry?.val);
|
|
28
32
|
}
|
|
29
33
|
} else if (map && typeof map === 'object') {
|
|
30
|
-
for (const [key, value] of Object.entries(map))
|
|
34
|
+
for (const [key, value] of Object.entries(map)) {
|
|
35
|
+
if (!(keyName(key) in result)) result[keyName(key)] = scalar(value);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
}
|
|
39
|
+
for (const [key, value] of entries) result[keyName(key)] = scalar(value);
|
|
33
40
|
return result;
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -42,23 +49,36 @@ function listFromBody(body) {
|
|
|
42
49
|
if (Array.isArray(body)) return body;
|
|
43
50
|
if (!body || typeof body !== 'object') return null;
|
|
44
51
|
for (const [key, value] of Object.entries(body)) {
|
|
45
|
-
if (['destinations', 'data'].includes(keyName(key))
|
|
52
|
+
if (!['destinations', 'destinationlist', 'data', 'items', 'value'].includes(keyName(key))) continue;
|
|
53
|
+
if (Array.isArray(value)) return value;
|
|
54
|
+
// Some destination services unwrap records into a name-keyed map
|
|
55
|
+
// instead of an array; heal the shape so discovery still proceeds.
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
const records = Object.entries(value).filter(([, entry]) => entry && typeof entry === 'object').map(([mapName, entry]) => ({ name: mapName, ...entry }));
|
|
58
|
+
if (records.length) return records;
|
|
59
|
+
}
|
|
46
60
|
}
|
|
47
61
|
return null;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
export function normalizeDestination(item) {
|
|
51
65
|
const values = propertiesOf(item);
|
|
52
|
-
const name = text(values.name || values.destinationname || values.destname);
|
|
66
|
+
const name = text(values.name || values.destinationname || values.destname || values.destination || values.sapdestinationname);
|
|
53
67
|
const authentication = text(values.authentication || values.authtype || values.auth);
|
|
54
|
-
const client = text(values.sapclient || values.client) || '001';
|
|
68
|
+
const client = text(values.sapclient || values.client || values.mandt) || '001';
|
|
55
69
|
const backendUrl = text(values.url || values.host || values.webideexposedhost);
|
|
56
70
|
const proxyType = text(values.proxytype || values.proxy);
|
|
71
|
+
// One destination with an unusable name must never abort discovery; the
|
|
72
|
+
// probe reports `invalid-name` for entries whose url stays null.
|
|
73
|
+
let url = null;
|
|
74
|
+
if (name) {
|
|
75
|
+
try { url = destinationUrl(name); } catch { url = null; }
|
|
76
|
+
}
|
|
57
77
|
return {
|
|
58
78
|
name,
|
|
59
79
|
authentication: authentication || 'Unknown',
|
|
60
80
|
client,
|
|
61
|
-
url
|
|
81
|
+
url,
|
|
62
82
|
backendUrl: backendUrl || null,
|
|
63
83
|
proxyType: proxyType || null,
|
|
64
84
|
rawKeys: Object.keys(item || {}).map(String)
|
|
@@ -67,7 +87,10 @@ export function normalizeDestination(item) {
|
|
|
67
87
|
|
|
68
88
|
export function destinationUrl(name) {
|
|
69
89
|
const value = text(name);
|
|
70
|
-
|
|
90
|
+
// A leading underscore is valid: BAS destination names allow it (Cloud
|
|
91
|
+
// Connector system names like `_SIDCLNTOX`), and `<name>.dest` resolution
|
|
92
|
+
// inside a dev space is Host-header matching in the BAS proxy, not DNS.
|
|
93
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(value)) throw new Error(`Invalid BAS destination name: ${value || '<empty>'}`);
|
|
71
94
|
return `http://${value}.dest`;
|
|
72
95
|
}
|
|
73
96
|
|
|
@@ -137,7 +160,7 @@ function proxyDispatcher(target, env) {
|
|
|
137
160
|
|
|
138
161
|
export async function fetchDestinationList(h2oUrl, options = {}) {
|
|
139
162
|
if (!h2oUrl) throw new Error('H2O_URL is required for BAS destination discovery');
|
|
140
|
-
const url = `${String(h2oUrl).replace(
|
|
163
|
+
const url = `${String(h2oUrl).trim().replace(/\/+$/, '')}/api/listDestinations`;
|
|
141
164
|
const env = options.env || process.env;
|
|
142
165
|
const dispatcher = proxyDispatcher(url, env);
|
|
143
166
|
try {
|
|
@@ -147,7 +170,15 @@ export async function fetchDestinationList(h2oUrl, options = {}) {
|
|
|
147
170
|
...(dispatcher ? { dispatcher } : {})
|
|
148
171
|
});
|
|
149
172
|
if (!response.ok) throw new Error(`BAS destination discovery failed with HTTP ${response.status}`);
|
|
150
|
-
|
|
173
|
+
// Parse from text so UTF-8 BOMs and text/plain content types from
|
|
174
|
+
// proxies in front of BAS do not break discovery.
|
|
175
|
+
const payload = await response.text();
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(payload.replace(/^\uFEFF/, ''));
|
|
178
|
+
} catch {
|
|
179
|
+
const preview = payload.trim().slice(0, 200);
|
|
180
|
+
throw new Error(`BAS destination discovery returned unparsable JSON${preview ? `: ${preview}` : ''}`);
|
|
181
|
+
}
|
|
151
182
|
} finally {
|
|
152
183
|
await dispatcher?.close();
|
|
153
184
|
}
|
|
@@ -186,7 +217,12 @@ function requestProbe(target, proxyUrl, timeoutMs) {
|
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
export async function probeADT(destination, options = {}) {
|
|
189
|
-
|
|
220
|
+
let url;
|
|
221
|
+
try {
|
|
222
|
+
url = `${destinationUrl(destination.name)}/sap/bc/adt/discovery`;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return { status: 'invalid-name', available: false, warning: error.message };
|
|
225
|
+
}
|
|
190
226
|
if (options.skipProbe) return { status: 'skipped', available: true, url };
|
|
191
227
|
try {
|
|
192
228
|
const result = options.probeImpl ? await options.probeImpl(url, options) : await requestProbe(url, options.proxyUrl ?? DEFAULT_PROXY, options.timeoutMs || 5000);
|
|
@@ -204,7 +240,13 @@ export async function discoverDestinations(options = {}) {
|
|
|
204
240
|
const list = listFromBody(body);
|
|
205
241
|
if (!list) throw new Error(`Unrecognized BAS destination response shape; top-level keys: ${Object.keys(body || {}).map(String).join(', ') || '<none>'}`);
|
|
206
242
|
const allow = text(brandedEnvValue(env, 'DESTINATION')).split(',').map(value => value.trim().toLowerCase()).filter(Boolean);
|
|
207
|
-
const normalized = list
|
|
243
|
+
const normalized = list
|
|
244
|
+
.map(item => {
|
|
245
|
+
try { return normalizeDestination(item); }
|
|
246
|
+
catch (error) { return { name: '', probe: { status: 'invalid-name', available: false, warning: error.message } }; }
|
|
247
|
+
})
|
|
248
|
+
.filter(item => item.name)
|
|
249
|
+
.filter(item => !allow.length || allow.includes(item.name.toLowerCase()));
|
|
208
250
|
const brandedProxy = brandedEnvValue(env, 'HTTP_PROXY');
|
|
209
251
|
const configuredProxy = brandedProxy !== undefined ? String(brandedProxy) : (env.HTTP_PROXY ?? env.http_proxy);
|
|
210
252
|
const proxyUrl = configuredProxy !== undefined ? configuredProxy : DEFAULT_PROXY;
|
package/src/launcher.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import { MCPProxy } from './mcp-proxy.mjs';
|
|
|
10
10
|
import { installMcpConfig, npxMcpLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
|
|
11
11
|
import { runSetup } from './setup.mjs';
|
|
12
12
|
import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
|
|
13
|
-
import { createWindowsSsoAdtProxy } from './windows-sso-adt-proxy.mjs';
|
|
14
13
|
import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
|
|
15
14
|
import { withBrandedEnvironment } from './branding.mjs';
|
|
16
15
|
import { redactText } from './redact.mjs';
|
|
@@ -184,57 +183,41 @@ async function discoverForCommand() {
|
|
|
184
183
|
async function configuredLocalSapGuiDestination(env) {
|
|
185
184
|
const name = env.SAP_AI_DEV_TOOLKIT_DESTINATION || env.SAP_SYSTEM_ID || 'local-sap';
|
|
186
185
|
if (!env.SAP_URL) throw new Error('SAP_URL is required for local SAP GUI MCP entries. Rerun sap-ai-dev --setup and enter the ADT URL.');
|
|
187
|
-
const
|
|
186
|
+
const client = env.SAP_CLIENT || '001';
|
|
187
|
+
const systemId = env.SAP_SYSTEM_ID || '';
|
|
188
|
+
// Older entries used windows-sso, browser-saml or saml-password; all of them now mean browser SSO.
|
|
189
|
+
if (!['', 'basic'].includes(String(env.SAP_AUTH_MODE || '').toLowerCase())) {
|
|
190
|
+
const cacheKey = `${systemId || new URL(env.SAP_URL).hostname}-${client}`.toLowerCase().replace(/[^a-z0-9.-]+/g, '-');
|
|
191
|
+
return {
|
|
192
|
+
source: 'sap-gui-local',
|
|
193
|
+
name,
|
|
194
|
+
url: env.SAP_URL,
|
|
195
|
+
client,
|
|
196
|
+
systemId,
|
|
197
|
+
authentication: 'SSO',
|
|
198
|
+
// SAP_SAML_AUTH survives sanitizeChildEnv and would make VSP demand SAML credentials.
|
|
199
|
+
childEnv: { SAP_SSO: 'true', SAP_SSO_SYSTEM: cacheKey, SAP_SAML_AUTH: '' },
|
|
200
|
+
probe: { status: 'configured', available: true }
|
|
201
|
+
};
|
|
202
|
+
}
|
|
188
203
|
const tlsServerName = String(env.SAP_TLS_SERVER_NAME || env.SAP_AI_DEV_TOOLKIT_TLS_SERVER_NAME || '').trim();
|
|
189
204
|
const tlsServerNames = String(env.SAP_TLS_SERVER_NAMES || env.SAP_AI_DEV_TOOLKIT_TLS_SERVER_NAMES || tlsServerName).split(',').map(value => value.trim()).filter(Boolean);
|
|
190
205
|
const tlsCaFile = String(env.SAP_TLS_CA_FILE || env.SAP_AI_DEV_TOOLKIT_TLS_CA_FILE || '').trim();
|
|
191
206
|
const tlsRoute = tlsServerNames.length
|
|
192
207
|
? await createTlsServerNameAdtProxy({ destinationUrl: env.SAP_URL, tlsServerNames, caFile: tlsCaFile || undefined })
|
|
193
208
|
: null;
|
|
194
|
-
const adtUrl = tlsRoute?.url || env.SAP_URL;
|
|
195
|
-
const closeRoute = async route => {
|
|
196
|
-
await Promise.all([route?.close?.(), tlsRoute?.close?.()].filter(Boolean).map(close => Promise.resolve().then(close).catch(() => {})));
|
|
197
|
-
};
|
|
198
|
-
if (authMode === 'windows-sso') {
|
|
199
|
-
const route = await createWindowsSsoAdtProxy({ destinationUrl: adtUrl, env });
|
|
200
|
-
return {
|
|
201
|
-
source: 'sap-gui-local',
|
|
202
|
-
name,
|
|
203
|
-
url: route.url,
|
|
204
|
-
backendUrl: env.SAP_URL,
|
|
205
|
-
client: env.SAP_CLIENT || '001',
|
|
206
|
-
systemId: env.SAP_SYSTEM_ID || '',
|
|
207
|
-
authentication: 'WindowsSSO',
|
|
208
|
-
childEnv: {},
|
|
209
|
-
close: () => closeRoute(route),
|
|
210
|
-
probe: { status: tlsRoute ? 'configured-tls-server-name' : 'configured', available: true }
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
const childEnv = (() => {
|
|
214
|
-
if (authMode === 'browser-saml') {
|
|
215
|
-
return { SAP_BROWSER_AUTH: 'true', SAP_SAML_AUTH: 'true' };
|
|
216
|
-
}
|
|
217
|
-
if (authMode === 'saml-password') {
|
|
218
|
-
return {
|
|
219
|
-
SAP_SAML_AUTH: 'true',
|
|
220
|
-
SAP_SAML_USER: env.SAP_SAML_USER || env.SAP_USER || env.SAP_USERNAME || '',
|
|
221
|
-
SAP_SAML_PASSWORD: env.SAP_SAML_PASSWORD || env.SAP_PASSWORD || env.SAP_PASS || ''
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
SAP_USER: env.SAP_USER || env.SAP_USERNAME || '',
|
|
226
|
-
SAP_PASSWORD: env.SAP_PASSWORD || env.SAP_PASS || ''
|
|
227
|
-
};
|
|
228
|
-
})();
|
|
229
209
|
return {
|
|
230
210
|
source: 'sap-gui-local',
|
|
231
211
|
name,
|
|
232
|
-
url:
|
|
212
|
+
url: tlsRoute?.url || env.SAP_URL,
|
|
233
213
|
backendUrl: tlsRoute ? env.SAP_URL : undefined,
|
|
234
|
-
client
|
|
235
|
-
systemId
|
|
236
|
-
authentication:
|
|
237
|
-
childEnv
|
|
214
|
+
client,
|
|
215
|
+
systemId,
|
|
216
|
+
authentication: 'Basic',
|
|
217
|
+
childEnv: {
|
|
218
|
+
SAP_USER: env.SAP_USER || env.SAP_USERNAME || '',
|
|
219
|
+
SAP_PASSWORD: env.SAP_PASSWORD || env.SAP_PASS || ''
|
|
220
|
+
},
|
|
238
221
|
close: tlsRoute?.close,
|
|
239
222
|
probe: { status: tlsRoute ? 'configured-tls-server-name' : 'configured', available: true }
|
|
240
223
|
};
|
package/src/mcp-proxy.mjs
CHANGED
|
@@ -168,7 +168,7 @@ function closeDestinationRoute(destination) {
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
function useProxyAuthentication(destination) {
|
|
171
|
-
if (destination.source === 'sap-gui-local') return
|
|
171
|
+
if (destination.source === 'sap-gui-local') return false;
|
|
172
172
|
return destination.source !== 'cloud-foundry' || destination.authentication === 'PrincipalPropagation';
|
|
173
173
|
}
|
|
174
174
|
|