sap-ai-dev-toolkit 0.8.2 → 0.8.4

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 CHANGED
@@ -33,7 +33,7 @@
33
33
  SAP AI Dev Toolkit connects your AI coding assistant to the SAP development tools available for each selected destination. Agents can inspect ABAP and CDS, propose changes, run checks, and return the evidence—without losing sight of which SAP system each operation targets.
34
34
 
35
35
  <p align="center">
36
- <strong>Agent chat</strong> → <strong>destination-scoped MCP</strong> → <strong>VSP + BAS destination</strong> → <strong>SAP ADT</strong> → <strong>your SAP system</strong><br>
36
+ <strong>Agent chat</strong> → <strong>destination-scoped MCP</strong> → <strong>VSP + configured route (BAS, CF, or local SAP GUI)</strong> → <strong>SAP ADT</strong> → <strong>your SAP system</strong><br>
37
37
  <em>results and diagnostics flow back along the same path</em>
38
38
  </p>
39
39
 
@@ -52,7 +52,7 @@ To also add optional full-stack SAP companion MCP servers for Fiori, UI5, CAP, a
52
52
  sap-ai-dev --setup --tools
53
53
  ```
54
54
 
55
- Then connect a destination in SAP Business Application Studio, or run the same setup locally to discover SAP GUI systems and create VS Code MCP entries:
55
+ After setup, connect a selected BAS destination or local SAP GUI system from your MCP-capable editor:
56
56
 
57
57
  1. Open the Command Palette.
58
58
  2. Run **MCP: List Servers**.
@@ -79,11 +79,11 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
79
79
 
80
80
  **Included skills:** `abap-development` · `abap-testing-quality` · `cds-development` · `rap-development` · `abap-debugging` · `abap-runtime-analysis` · `rap-service-delivery` · `sap-standard-api-analysis` · `clean-core-extensibility` · `sap-sdlc-orchestration` · `sap-transport-release` · `hana-cloud-inspection` · `hana-cloud-native-development` · `hana-cloud-validation`
81
81
 
82
- Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent. In non-interactive installs they are installed for GitHub Copilot only. Repository-scoped installation instructions appear below.
82
+ Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent. In non-interactive installs they are installed for GitHub Copilot only; run `sap-ai-dev --setup` afterwards to configure destinations or local SAP GUI systems. Repository-scoped installation instructions appear below.
83
83
 
84
84
  ## ✨ Your SAP development cockpit, inside your AI assistant
85
85
 
86
- `sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations, and exposes a curated SAP development toolset through MCP. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
86
+ `sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations or local SAP GUI systems, and exposes a curated SAP development toolset through MCP. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
87
87
 
88
88
  <p align="center"><strong>💬 Request → 🔎 Inspect → 🧠 Reason → 🧑‍💻 Implement → 🧪 Verify → 📋 Report</strong></p>
89
89
 
@@ -94,7 +94,7 @@ The workflow covers ABAP, CDS, RAP, repository analysis, table and query access,
94
94
  | | Feature | What it gives you |
95
95
  | --- | --- | --- |
96
96
  | 🤖 | **AI-driven ABAP development** | Describe the outcome; a task-fit agent coordinates inspection, implementation, validation, and a clear report. |
97
- | 🧭 | **Destination-aware by design** | Discover BAS destinations and connect each MCP server to one selected SAP system. |
97
+ | 🧭 | **Destination-aware by design** | Discover BAS destinations or local SAP GUI systems and connect each MCP server to one selected SAP system. |
98
98
  | 🔎 | **Understand before editing** | Trace source, callers, definitions, package contents, dependencies, and CDS impact in one workflow. |
99
99
  | 🧩 | **CDS + RAP development** | Explore models and dependencies, then build RAP business objects and services with system context. |
100
100
  | 🗃️ | **Ground decisions in SAP data** | Inspect DDIC structures, read table contents, and run controlled ABAP SQL queries. |
@@ -278,7 +278,7 @@ The installer handles VSP provisioning automatically:
278
278
 
279
279
  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.
280
280
 
281
- 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, and you should run `sap-ai-dev --setup` afterwards.
281
+ 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 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, and you should run `sap-ai-dev --setup` afterwards.
282
282
 
283
283
  ### 🖥️ Local machine SAP GUI support
284
284
 
@@ -290,7 +290,7 @@ Supported discovery and runtime behavior:
290
290
  - **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.
291
291
  - **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.
292
292
  - **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`.
293
- - **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.
293
+ - **Windows SSO / smart-card Windows logon:** 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 with the current Windows logon session, including smart-card-backed Windows logon. The toolkit does not prompt for, store, or accept manually pasted bearer/SAML tokens. Use `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER` only to point to a trusted custom SSPI/Negotiate helper. NTLM fallback is rejected.
294
294
  - **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.
295
295
  - **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.
296
296
  - **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.
@@ -308,9 +308,9 @@ sap-ai-dev --setup
308
308
  5. Choose password or Windows SSO when the prompt is available.
309
309
  6. Start each generated MCP server from VS Code's **MCP: List Servers** command.
310
310
 
311
- 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.
311
+ 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.
312
312
 
313
- Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive BAS terminal after installation.
313
+ Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive terminal after installation.
314
314
 
315
315
  The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. The table is a weather report, not a bouncer: green **PASS** means the ADT probe responded, red **FAIL** means it failed, and yellow **SKIPPED** means it was skipped. Probe failures do not block MCP registration or startup for destinations you select.
316
316
 
@@ -379,13 +379,13 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
379
379
  | 🧱 | `hana-cloud-native-development` | Create CAP CDS or required native HDI source artifacts while avoiding duplicate models. |
380
380
  | ✅ | `hana-cloud-validation` | Build locally, review deployment risks, and hand off deployment to the user. |
381
381
 
382
- ### 📥 Install for your BAS user
382
+ ### 📥 Install agents and skills for your user
383
383
 
384
- After destination setup, the installer prints a 🤖 notice and a checkbox picker asking which AI coding harnesses should receive the bundled agents and skills. **Space** selects or deselects, **a** toggles all, **Enter** confirms. **GitHub Copilot** and **Claude Code** are pre-checked, so pressing **Enter** without changes installs both; confirming with none selected skips the step and leaves your files unchanged.
384
+ During npm postinstall, after the destination or local SAP GUI setup step, the installer prints a 🤖 notice and a checkbox picker asking which AI coding harnesses should receive the bundled agents and skills. **Space** selects or deselects, **a** toggles all, **Enter** confirms. **GitHub Copilot** and **Claude Code** are pre-checked, so pressing **Enter** without changes installs both; confirming with none selected skips the step and leaves your files unchanged.
385
385
 
386
386
  | Harness | Skills land in | Agents land in | MCP auto-wiring |
387
387
  | --- | --- | --- | --- |
388
- | GitHub Copilot | `$HOME/.copilot/skills/` | `$HOME/.copilot/agents/*.agent.md` | Uses the BAS/VS Code `mcp.json` written by destination setup |
388
+ | GitHub Copilot | `$HOME/.copilot/skills/` | `$HOME/.copilot/agents/*.agent.md` | Uses the VS Code/BAS `mcp.json` written by setup |
389
389
  | Claude Code | `$HOME/.claude/skills/` | `$HOME/.claude/agents/<id>.md` (frontmatter reduced to `name` and `description`) | `$HOME/.claude.json` |
390
390
  | OpenAI Codex | `$HOME/.agents/skills/` | — | Not changed automatically |
391
391
  | Cursor | `$HOME/.cursor/skills/` | — | `$HOME/.cursor/mcp.json` |
@@ -397,13 +397,13 @@ All 14 skills use the portable `SKILL.md` format and are installed verbatim. The
397
397
 
398
398
  MCP auto-wiring preserves unrelated servers in each harness config and replaces only entries previously managed by `sap-ai-dev-toolkit`. Generated entries carry `SAP_AI_DEV_TOOLKIT_MANAGED=true` in their server environment so future reinstalls can reconcile them without touching user-owned MCP servers.
399
399
 
400
- These user-level customizations are available across workspaces opened by the same BAS user in the same dev space. The harness must be available in BAS and may need a window reload to discover new files. A non-interactive install skips the picker and installs for GitHub Copilot only; override with `SAP_AI_DEV_TOOLKIT_HARNESSES` (comma-separated harness ids). `npm install --ignore-scripts` skips the postinstall wizard entirely.
400
+ These user-level customizations are available across workspaces opened by the same OS user (or the same BAS user in a dev space). The harness may need a window reload to discover new files. A non-interactive install skips the picker and installs for GitHub Copilot only; override with `SAP_AI_DEV_TOOLKIT_HARNESSES` (comma-separated harness ids). `npm install --ignore-scripts` skips the postinstall wizard entirely.
401
401
 
402
402
  On reinstall or package upgrade, unchanged add-on-managed files are updated per harness. Existing customizations and files edited since the previous install are preserved; postinstall reports paths that need manual review instead of overwriting them.
403
403
 
404
404
  ### 🗂️ Add the agents and skills to a repository
405
405
 
406
- To commit repository-scoped customizations, copy the packaged files from the BAS workspace root without overwriting existing files:
406
+ To commit repository-scoped customizations, copy the packaged files from the installed package without overwriting existing files:
407
407
 
408
408
  ```sh
409
409
  ADDON_ROOT="$(npm root -g)/sap-ai-dev-toolkit"
@@ -423,7 +423,7 @@ Review skipped or conflicting files and merge changes manually. `LintABAP` analy
423
423
  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.
424
424
  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.
425
425
 
426
- 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, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination selection without changing MCP config. Run the interactive setup command above; add `--npx` when the package is not installed globally.
426
+ 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, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). 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; add `--npx` when the package is not installed globally.
427
427
 
428
428
  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.
429
429
 
@@ -528,9 +528,9 @@ The file must be strict JSON with an object-valued `servers` property. Existing
528
528
 
529
529
  With `H2O_URL` set, the normal command starts the MCP proxy. Each generated entry supplies one `SAP_AI_DEV_TOOLKIT_DESTINATION`, so each server stays in its own lane and exposes only its selected SAP system.
530
530
 
531
- The proxy exposes a curated subset of VSP tools, plus local `LintABAP`, destination-scoped workflow tools, and the convenience `GetApplicationLog` mapping when the VSP SAP router is available. Hidden VSP tools remain available only behind local workflow chaining where required; they are not advertised or directly callable through the proxy. Direct VSP invocation remains unchanged.
531
+ The proxy exposes a curated subset of VSP tools, plus local `LintABAP`, destination-scoped workflow tools, and the convenience `GetApplicationLog` mapping when the VSP SAP router is available. Hidden VSP tools remain available only behind local workflow chaining where required; they are not advertised or directly callable through the proxy. Direct VSP invocation remains available outside BAS when you pass an explicit VSP argument.
532
532
 
533
- Without `H2O_URL`, the command passes arguments directly to the installed VSP binary—no BAS proxy detour.
533
+ Without `H2O_URL`, generated local SAP GUI entries start a destination-scoped local MCP server. In an interactive local terminal with no arguments, `sap-ai-dev` starts local setup. Other non-setup arguments are passed directly to the installed VSP binary—no BAS proxy detour.
534
534
 
535
535
  ### 🔄 How an MCP tool call reaches SAP
536
536
 
@@ -684,7 +684,7 @@ The proxy starts VSP with `--enable-transports` and omits `--transport-read-only
684
684
  - `messages: true` adds BALDAT details and T100 message text; otherwise, the tool returns log headers only.
685
685
  - The proxy maps this convenience tool to the single upstream `SAP(action="analyze", type="application_log")` operation. The general-purpose `SAP` router itself is hidden from direct calls.
686
686
 
687
- The proxy intentionally does not expose the full child VSP process. Object deletion, broad trace surfaces, arbitrary SAP router/RFC calls, debugger helper/delete-breakpoint routes, and transport release/delete tools are hidden from direct MCP calls. The curated debugger tools (`SetBreakpoint`, `GetBreakpoints`, `DebuggerAttach`, `DebuggerGetStack`, `DebuggerGetVariables`, `DebuggerStep`, and `DebuggerDetach`) remain available when VSP and the target system expose them. Direct VSP invocation without `H2O_URL` retains the VSP binary's own tool surface.
687
+ The proxy intentionally does not expose the full child VSP process. Object deletion, broad trace surfaces, arbitrary SAP router/RFC calls, debugger helper/delete-breakpoint routes, and transport release/delete tools are hidden from direct MCP calls. The curated debugger tools (`SetBreakpoint`, `GetBreakpoints`, `DebuggerAttach`, `DebuggerGetStack`, `DebuggerGetVariables`, `DebuggerStep`, and `DebuggerDetach`) remain available when VSP and the target system expose them. Direct VSP invocation without `H2O_URL` retains the VSP binary's own tool surface; in an interactive local terminal, use an explicit VSP argument because plain `sap-ai-dev` starts local setup.
688
688
 
689
689
  ### 🚀 Ready to put the tools to work from chat?
690
690
 
@@ -738,9 +738,9 @@ The MCP client discovers the schemas; callers do not need to memorize every argu
738
738
  | What to inspect | How |
739
739
  | --- | --- |
740
740
  | Add-on command options | `sap-ai-dev --help` |
741
- | BAS destinations and probe status | `sap-ai-dev --list-destinations --json` |
741
+ | BAS destinations and probe status | `sap-ai-dev --list-destinations --json` (BAS only) |
742
742
  | Destination availability only | `sap-ai-dev --check` |
743
- | Generated server names and destination mapping | **MCP: Open User Configuration**; look for lowercase slug entries (for example `demo-abap`) whose `SAP_AI_DEV_TOOLKIT_DESTINATION` names the BAS destination. |
743
+ | Generated server names and destination mapping | **MCP: Open User Configuration**; look for lowercase slug entries (for BAS, for example `demo-abap`; for local SAP GUI, system/client slugs such as `t4d-100`) whose `SAP_AI_DEV_TOOLKIT_DESTINATION` names the BAS destination or local SAP GUI system. |
744
744
  | Running server | **MCP: List Servers**; select the server and choose **Start Server**. |
745
745
  | Tools and exact schemas | Expand that server in the Chat tools picker. The MCP host requests `tools/list`, whose entries include `name`, `description`, and `inputSchema`; the agent should call those tools through chat, not reproduce the protocol in a terminal. |
746
746
  | Runtime and tool-call logs | Select the MCP server in the Output view. Startup, call lifecycle, and child stderr logs are written to stderr; child MCP log notifications are forwarded to the client. Tool arguments and result contents are not logged. |
@@ -782,11 +782,11 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
782
782
  | `SAP_AI_DEV_TOOLKIT_DESTINATION` | Comma-separated destination allowlist for normal runtime discovery. Setup clears this temporarily so it can display all eligible systems. |
783
783
  | `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`. |
784
784
  | `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. |
785
- | `SAP_AUTH_MODE=windows-sso` | Local SAP GUI entry mode for experimental Windows Integrated Authentication. Runtime starts a loopback ADT proxy and does not pass SAP username/password credentials to VSP. |
785
+ | `SAP_AUTH_MODE=windows-sso` | Local SAP GUI entry mode for experimental Windows Integrated Authentication over HTTP Negotiate/SPNEGO. Runtime starts a loopback ADT proxy, uses the current Windows logon session (including smart-card-backed Windows logon), and does not pass SAP username/password credentials or manual bearer/SAML tokens to VSP. |
786
786
  | `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. |
787
787
  | `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. |
788
788
  | `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. |
789
- | `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 convert an HTTP `WWW-Authenticate: Negotiate` challenge into a Kerberos/SPNEGO token. 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 token on stdout. |
789
+ | `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. |
790
790
  | `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. |
791
791
  | `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. |
792
792
  | `SAP_AI_DEV_MCP_CONFIG` | Explicit MCP configuration path; highest precedence. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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/launcher.mjs CHANGED
@@ -195,6 +195,7 @@ async function configuredLocalSapGuiDestination(env) {
195
195
  url: route.url,
196
196
  backendUrl: env.SAP_URL,
197
197
  client: env.SAP_CLIENT || '001',
198
+ systemId: env.SAP_SYSTEM_ID || '',
198
199
  authentication: 'WindowsSSO',
199
200
  childEnv: {},
200
201
  close: () => closeRoute(route),
@@ -207,6 +208,7 @@ async function configuredLocalSapGuiDestination(env) {
207
208
  url: adtUrl,
208
209
  backendUrl: tlsRoute ? env.SAP_URL : undefined,
209
210
  client: env.SAP_CLIENT || '001',
211
+ systemId: env.SAP_SYSTEM_ID || '',
210
212
  authentication: 'Basic',
211
213
  childEnv: {
212
214
  SAP_USER: env.SAP_USER || env.SAP_USERNAME || '',
@@ -1,7 +1,12 @@
1
1
  import { platform, homedir } from 'node:os';
2
- import { join } from 'node:path';
2
+ import { promisify } from 'node:util';
3
+ import { execFile } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { basename, dirname, extname, join } from 'node:path';
3
6
  import { readdir, readFile, stat } from 'node:fs/promises';
4
7
 
8
+ const execFileAsync = promisify(execFile);
9
+
5
10
  function text(value) { return value == null ? '' : String(value).trim(); }
6
11
  function decodeXml(value) {
7
12
  return text(value)
@@ -18,25 +23,88 @@ async function exists(path) {
18
23
  }
19
24
 
20
25
  function unique(values) {
21
- return [...new Set(values.filter(Boolean))];
26
+ const seen = new Set();
27
+ return values.map(pathFromEnv).filter(Boolean).filter(value => {
28
+ const key = String(value).toLowerCase();
29
+ if (seen.has(key)) return false;
30
+ seen.add(key);
31
+ return true;
32
+ });
33
+ }
34
+
35
+ function pathFromEnv(value) {
36
+ const candidate = text(value);
37
+ if (!candidate) return '';
38
+ if (/^file:\/\//i.test(candidate)) {
39
+ try { return fileURLToPath(candidate); } catch { return candidate.replace(/^file:\/\/\/?/i, ''); }
40
+ }
41
+ return candidate;
42
+ }
43
+
44
+ const WINDOWS_LANDSCAPE_FILES = [
45
+ 'SAPUILandscape.xml',
46
+ 'SAPUILandscapeGlobal.xml',
47
+ 'SAPGUILandscape.xml',
48
+ 'SAPGUILandscapeGlobal.xml',
49
+ 'SAPLogonTree.xml',
50
+ 'SAPLogonTreeGlobal.xml',
51
+ 'saplogon.ini'
52
+ ];
53
+
54
+ const WINDOWS_EXPLICIT_PATH_ENV = [
55
+ 'SAP_AI_DEV_SAP_GUI_LANDSCAPE',
56
+ 'SAP_AI_DEV_SAP_GUI_CONFIG',
57
+ 'SAPUILANDSCAPE',
58
+ 'SAPGUI_LANDSCAPE',
59
+ 'SAPLOGON_LSXML_FILE',
60
+ 'SAPLOGON_INI_FILE'
61
+ ];
62
+
63
+ function addWindowsSapGuiFiles(candidates, directory) {
64
+ if (!directory) return;
65
+ for (const name of WINDOWS_LANDSCAPE_FILES) candidates.push(join(directory, name));
66
+ }
67
+
68
+ function addSapGuiJavaFiles(candidates, directory) {
69
+ if (!directory) return;
70
+ candidates.push(join(directory, 'connections'));
71
+ addWindowsSapGuiFiles(candidates, directory);
22
72
  }
23
73
 
24
74
  export function sapGuiLandscapeCandidates(env = process.env) {
25
75
  const home = env.HOME || env.USERPROFILE || homedir();
26
- const appData = env.APPDATA || (env.USERPROFILE ? join(env.USERPROFILE, 'AppData', 'Roaming') : '');
27
- const candidates = [];
28
- if (platform() === 'win32' || appData) {
76
+ const userProfile = env.USERPROFILE || home;
77
+ const appData = env.APPDATA || (userProfile ? join(userProfile, 'AppData', 'Roaming') : '');
78
+ const localAppData = env.LOCALAPPDATA || (userProfile ? join(userProfile, 'AppData', 'Local') : '');
79
+ const programData = env.PROGRAMDATA || (platform() === 'win32' ? 'C:\\ProgramData' : '');
80
+ const publicProfile = env.PUBLIC || (platform() === 'win32' ? 'C:\\Users\\Public' : '');
81
+ const windir = env.WINDIR || env.SystemRoot || (platform() === 'win32' ? 'C:\\Windows' : '');
82
+ const candidates = WINDOWS_EXPLICIT_PATH_ENV.map(name => env[name]);
83
+ if (platform() === 'win32' || appData || localAppData || programData) {
84
+ for (const base of [appData, localAppData, programData, publicProfile]) {
85
+ if (!base) continue;
86
+ addWindowsSapGuiFiles(candidates, join(base, 'SAP', 'Common'));
87
+ addWindowsSapGuiFiles(candidates, join(base, 'SAP'));
88
+ }
29
89
  candidates.push(
30
- join(appData || '', 'SAP', 'Common', 'SAPUILandscape.xml'),
31
- join(appData || '', 'SAP', 'Common', 'SAPUILandscapeGlobal.xml'),
32
- join(appData || '', 'SAP', 'Common', 'saplogon.ini')
90
+ join(windir || '', 'saplogon.ini'),
91
+ join(windir || '', 'SAPLogon.ini'),
92
+ join(userProfile || '', 'saplogon.ini'),
93
+ join(userProfile || '', 'SAPLogon.ini')
33
94
  );
95
+ addSapGuiJavaFiles(candidates, join(userProfile || '', '.SAPGUI'));
96
+ addSapGuiJavaFiles(candidates, join(appData || '', 'SAPGUI'));
97
+ addSapGuiJavaFiles(candidates, join(localAppData || '', 'SAPGUI'));
98
+ if (env.ProgramFiles) addWindowsSapGuiFiles(candidates, join(env.ProgramFiles, 'SAP', 'FrontEnd', 'SAPgui'));
99
+ if (env['ProgramFiles(x86)']) addWindowsSapGuiFiles(candidates, join(env['ProgramFiles(x86)'], 'SAP', 'FrontEnd', 'SAPgui'));
34
100
  }
35
101
  candidates.push(
36
102
  join(home, 'Library', 'Preferences', 'SAP', 'SAPGUILandscape.xml'),
37
103
  join(home, 'Library', 'Preferences', 'SAP', 'SAPUILandscape.xml'),
38
104
  join(home, 'Library', 'Preferences', 'SAP', 'connections'),
39
- join(home, '.SAPGUI', 'SAPGUILandscape.xml')
105
+ join(home, '.SAPGUI', 'SAPGUILandscape.xml'),
106
+ join(home, '.SAPGUI', 'SAPUILandscape.xml'),
107
+ join(home, '.SAPGUI', 'connections')
40
108
  );
41
109
  return unique(candidates);
42
110
  }
@@ -162,6 +230,113 @@ async function readGuiFile(path) {
162
230
  return bytes.toString('utf8').replace(/^\uFEFF/, '');
163
231
  }
164
232
 
233
+ function sapGuiSearchRoots(env = process.env) {
234
+ const home = env.HOME || env.USERPROFILE || homedir();
235
+ const userProfile = env.USERPROFILE || home;
236
+ return unique([
237
+ env.APPDATA && join(env.APPDATA, 'SAP'),
238
+ env.APPDATA && join(env.APPDATA, 'SAPGUI'),
239
+ userProfile && join(userProfile, 'AppData', 'Roaming', 'SAP'),
240
+ userProfile && join(userProfile, 'AppData', 'Roaming', 'SAPGUI'),
241
+ env.LOCALAPPDATA && join(env.LOCALAPPDATA, 'SAP'),
242
+ env.LOCALAPPDATA && join(env.LOCALAPPDATA, 'SAPGUI'),
243
+ userProfile && join(userProfile, 'AppData', 'Local', 'SAP'),
244
+ userProfile && join(userProfile, 'AppData', 'Local', 'SAPGUI'),
245
+ env.PROGRAMDATA && join(env.PROGRAMDATA, 'SAP'),
246
+ env.PUBLIC && join(env.PUBLIC, 'SAP'),
247
+ userProfile && join(userProfile, '.SAPGUI'),
248
+ home && join(home, 'Library', 'Preferences', 'SAP'),
249
+ home && join(home, '.SAPGUI')
250
+ ]);
251
+ }
252
+
253
+ async function findSapGuiFilesUnder(root, { maxDepth = 4, maxEntries = 500 } = {}) {
254
+ const wanted = new Set(WINDOWS_LANDSCAPE_FILES.map(value => value.toLowerCase()).concat(['connections']));
255
+ const found = [];
256
+ let visited = 0;
257
+ async function walk(directory, depth) {
258
+ if (depth < 0 || visited >= maxEntries) return;
259
+ let entries;
260
+ try { entries = await readdir(directory, { withFileTypes: true }); } catch { return; }
261
+ for (const entry of entries) {
262
+ if (++visited > maxEntries) return;
263
+ const child = join(directory, entry.name);
264
+ if (entry.isFile() && wanted.has(basename(entry.name).toLowerCase())) found.push(child);
265
+ else if (entry.isDirectory()) {
266
+ if (basename(entry.name).toLowerCase() === 'connections') found.push(child);
267
+ await walk(child, depth - 1);
268
+ }
269
+ }
270
+ }
271
+ await walk(root, maxDepth);
272
+ return found;
273
+ }
274
+
275
+ const SAP_GUI_REGISTRY_ROOTS = [
276
+ 'HKCU\\Software\\SAP\\SAPLogon',
277
+ 'HKCU\\Software\\SAP\\SAPLogon\\Options',
278
+ 'HKCU\\Software\\WOW6432Node\\SAP\\SAPLogon',
279
+ 'HKCU\\Software\\WOW6432Node\\SAP\\SAPLogon\\Options',
280
+ 'HKLM\\Software\\SAP\\SAPLogon',
281
+ 'HKLM\\Software\\SAP\\SAPLogon\\Options',
282
+ 'HKLM\\Software\\WOW6432Node\\SAP\\SAPLogon',
283
+ 'HKLM\\Software\\WOW6432Node\\SAP\\SAPLogon\\Options'
284
+ ];
285
+
286
+ function registryPathCandidates(value) {
287
+ const raw = pathFromEnv(value);
288
+ if (!raw) return [];
289
+ const lower = raw.toLowerCase();
290
+ if (/\.(?:xml|ini)$/i.test(lower)) return [raw];
291
+ if (/\\|\//.test(raw) && !extname(raw)) {
292
+ const result = [];
293
+ addWindowsSapGuiFiles(result, raw);
294
+ result.push(join(raw, 'connections'));
295
+ return result;
296
+ }
297
+ if (/\.(?:xml|ini)(?:\s|$)/i.test(lower)) return [raw.split(/\s+/)[0]];
298
+ return [];
299
+ }
300
+
301
+ async function windowsRegistrySapGuiCandidates(env = process.env) {
302
+ if (platform() !== 'win32') return [];
303
+ const candidates = [];
304
+ for (const root of SAP_GUI_REGISTRY_ROOTS) {
305
+ try {
306
+ const { stdout } = await execFileAsync('reg.exe', ['query', root, '/s'], { windowsHide: true, timeout: 2000, maxBuffer: 256 * 1024 });
307
+ for (const line of stdout.split(/\r?\n/)) {
308
+ const match = line.match(/^\s*\S+\s+REG_(?:SZ|EXPAND_SZ)\s+(.+?)\s*$/i);
309
+ if (!match) continue;
310
+ candidates.push(...registryPathCandidates(match[1].replace(/%([^%]+)%/g, (_, name) => env[name] || env[name.toUpperCase()] || `%${name}%`)));
311
+ }
312
+ } catch {
313
+ // Registry access can be blocked by policy or absent on non-standard installs.
314
+ }
315
+ }
316
+ return unique(candidates);
317
+ }
318
+
319
+ async function selfHealingSapGuiCandidates(env, candidates) {
320
+ const expanded = [...candidates, ...await windowsRegistrySapGuiCandidates(env)];
321
+ for (const root of sapGuiSearchRoots(env)) {
322
+ expanded.push(...await findSapGuiFilesUnder(root));
323
+ }
324
+ return unique(expanded);
325
+ }
326
+
327
+ function broadSapGuiSearchRoots(env = process.env) {
328
+ const home = env.HOME || env.USERPROFILE || homedir();
329
+ return unique([home, env.USERPROFILE, env.APPDATA, env.LOCALAPPDATA, env.PROGRAMDATA, env.PUBLIC]);
330
+ }
331
+
332
+ async function broadSelfHealingSapGuiCandidates(env) {
333
+ const expanded = [];
334
+ for (const root of broadSapGuiSearchRoots(env)) {
335
+ expanded.push(...await findSapGuiFilesUnder(root, { maxDepth: 6, maxEntries: 5000 }));
336
+ }
337
+ return unique(expanded);
338
+ }
339
+
165
340
  async function parseConnectionsFile(path) {
166
341
  const content = await readGuiFile(path);
167
342
  if (content.includes('<')) return parseLandscapeXml(content, path);
@@ -172,9 +347,9 @@ async function parseConnectionsFile(path) {
172
347
  }).filter(Boolean);
173
348
  }
174
349
 
175
- export async function discoverSapGuiSystems({ env = process.env, paths = sapGuiLandscapeCandidates(env) } = {}) {
350
+ async function parseSapGuiCandidatePaths(candidatePaths) {
176
351
  const systems = [];
177
- for (const path of unique(paths)) {
352
+ for (const path of unique(candidatePaths)) {
178
353
  if (!path) continue;
179
354
  try {
180
355
  if (!await exists(path)) continue;
@@ -197,6 +372,13 @@ export async function discoverSapGuiSystems({ env = process.env, paths = sapGuiL
197
372
  // Ignore unreadable or unfamiliar SAP GUI files; discovery is best-effort.
198
373
  }
199
374
  }
375
+ return systems;
376
+ }
377
+
378
+ export async function discoverSapGuiSystems({ env = process.env, paths } = {}) {
379
+ const candidatePaths = paths ? unique(paths) : await selfHealingSapGuiCandidates(env, sapGuiLandscapeCandidates(env));
380
+ let systems = await parseSapGuiCandidatePaths(candidatePaths);
381
+ if (!systems.length && !paths) systems = await parseSapGuiCandidatePaths(await broadSelfHealingSapGuiCandidates(env));
200
382
  const seen = new Set();
201
383
  return systems.filter(system => {
202
384
  const key = `${system.name}\0${system.host}\0${system.systemId}\0${system.instance}\0${system.client}`.toLowerCase();
@@ -129,6 +129,15 @@ export function generatedServerName(destinationName) {
129
129
  return slugifyDestination(destinationName);
130
130
  }
131
131
 
132
+ export function generatedDestinationServerName(destination) {
133
+ if (destination?.source === 'sap-gui-local') {
134
+ const systemId = String(destination.systemId || '').trim();
135
+ const client = String(destination.client || '001').trim() || '001';
136
+ if (systemId) return generatedServerName(`${systemId}-${client}`);
137
+ }
138
+ return generatedServerName(destination?.source === 'cloud-foundry' ? destination.serverName : destination?.name);
139
+ }
140
+
132
141
  export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SERVERS.map(server => server.id), { packageVersions = {}, packageManager = 'npx' } = {}) {
133
142
  const selected = new Set(serverIds);
134
143
  const entries = Object.create(null);
@@ -159,7 +168,7 @@ export function buildMcpEntries(destinations, env = process.env) {
159
168
  for (const destination of selected) {
160
169
  const isCf = destination.source === 'cloud-foundry';
161
170
  const isLocalSapGui = destination.source === 'sap-gui-local';
162
- const name = generatedServerName(isCf ? destination.serverName : destination.name);
171
+ const name = generatedDestinationServerName(destination);
163
172
  if (Object.hasOwn(entries, name)) throw new Error(`Duplicate MCP destination server name: ${name} (two destination names normalize to the same lowercase server name)`);
164
173
  const entryEnv = {
165
174
  ...(h2oUrl ? { H2O_URL: String(h2oUrl) } : {}),
@@ -402,9 +411,9 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
402
411
  for (const [serverName, entry] of Object.entries(config.servers)) {
403
412
  if (!isManagedBasDestinationEntry(entry)) continue;
404
413
  const destinationName = destinationValue(entry.env);
405
- const expectedName = generatedServerName(destinationName);
406
414
  const destination = basDestinations.get(destinationName);
407
415
  if (!destination) continue;
416
+ const expectedName = generatedDestinationServerName(destination);
408
417
  if (expectedName !== serverName) {
409
418
  // Legacy mixed-case entries migrate to the normalized lowercase key.
410
419
  // A user-owned entry on the target key blocks the rename; merging is
package/src/setup.mjs CHANGED
@@ -2,7 +2,7 @@ import { createInterface } from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import { platform } from 'node:os';
4
4
  import { isIP } from 'node:net';
5
- import { checkboxPrompt, colorText, formatStatus, textPrompt } from './terminal-ui.mjs';
5
+ import { checkboxPrompt, colorText, formatStatus, selectPrompt, textPrompt } from './terminal-ui.mjs';
6
6
  import { discoverDestinations, remediation } from './bas-discovery.mjs';
7
7
  import { defaultAdtUrl, discoverSapGuiSystems } from './local-sap-gui.mjs';
8
8
  import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
@@ -15,23 +15,55 @@ function print(output, message) {
15
15
  output.write(`${message}\n`);
16
16
  }
17
17
 
18
+ function startSapGuiLandscapeAnimation(output, env = process.env) {
19
+ if (!output?.isTTY || !Number.isFinite(output?.columns)) return () => {};
20
+ if (env.TERM === 'dumb' || env.CI || env.SAP_AI_DEV_TOOLKIT_DISABLE_SCAN_ANIMATION === 'true') return () => {};
21
+ const frames = [
22
+ '=^.^= ~(o:>',
23
+ ' =^.^= ~(o:>',
24
+ ' =^.^= ~(o:>',
25
+ ' =^.^= ~(o:>',
26
+ ' =^.^= ~(o:>',
27
+ ' =^.^= caught it!'
28
+ ];
29
+ let index = 0;
30
+ let active = false;
31
+ const render = () => {
32
+ active = true;
33
+ const columns = Math.max(20, output.columns || 80);
34
+ const frame = frames[index % frames.length];
35
+ index += 1;
36
+ const line = `Scanning SAP GUI landscapes ${frame}`.slice(0, Math.max(1, columns - 1));
37
+ output.write(`\r\u001b[2K${line}`);
38
+ };
39
+ const delay = setTimeout(() => {
40
+ render();
41
+ interval = setInterval(render, 160);
42
+ if (typeof interval.unref === 'function') interval.unref();
43
+ }, 150);
44
+ if (typeof delay.unref === 'function') delay.unref();
45
+ let interval;
46
+ return () => {
47
+ clearTimeout(delay);
48
+ if (interval) clearInterval(interval);
49
+ if (active) output.write('\r\u001b[2K');
50
+ };
51
+ }
52
+
18
53
  function windowsSsoSetupAvailable(env = process.env) {
19
54
  return platform() === 'win32' || env.SAP_AI_DEV_TOOLKIT_ENABLE_WINDOWS_SSO_SETUP === 'true';
20
55
  }
21
56
 
22
57
  async function chooseLocalAuthMode(destination, { input = stdin, output = stdout, env = process.env } = {}) {
23
58
  if (!windowsSsoSetupAvailable(env)) return 'basic';
24
- const answer = await textPrompt({
25
- message: `Authentication for ${destination.name} (password/sso)`,
26
- placeholder: 'password',
27
- required: false,
28
- validate: value => {
29
- const candidate = String(value || 'password').trim().toLowerCase();
30
- return ['password', 'basic', 'sso', 'windows-sso'].includes(candidate) || 'Enter password or sso.';
31
- }
59
+ return await selectPrompt({
60
+ message: `Authentication for ${destination.name}`,
61
+ defaultValue: 'basic',
62
+ choices: [
63
+ { name: 'password', value: 'basic', shortcut: 'p' },
64
+ { name: 'windows-sso (Kerberos/SPNEGO, including smart-card Windows logon)', value: 'windows-sso', shortcut: 's' }
65
+ ]
32
66
  }, { input, output });
33
- const normalized = String(answer || 'password').trim().toLowerCase();
34
- return ['sso', 'windows-sso'].includes(normalized) ? 'windows-sso' : 'basic';
35
67
  }
36
68
 
37
69
  async function confirmCloudFoundryImport({ input = stdin, output = stdout } = {}) {
@@ -214,10 +246,13 @@ export async function runSetup({
214
246
  destinations = basDestinations.map(safeBasDestination);
215
247
  } else {
216
248
  let localSystems;
249
+ const stopScanAnimation = startSapGuiLandscapeAnimation(output, env);
217
250
  try {
218
251
  localSystems = await discoverLocalSapGui({ env });
219
252
  } catch (error) {
220
253
  throw new Error(`SAP GUI discovery failed: ${error.message}`);
254
+ } finally {
255
+ stopScanAnimation();
221
256
  }
222
257
  destinations = localSystems.map(safeLocalSapGuiDestination);
223
258
  }
@@ -349,7 +384,7 @@ export async function runSetup({
349
384
  print(output, '');
350
385
  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
386
  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.');
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.');
387
+ if (windowsSsoSetupAvailable(env)) print(output, ' Windows SSO is experimental and only applies to ADT systems configured for HTTP Integrated Authentication (Negotiate/SPNEGO). It uses the current Windows logon session, including smart-card-backed Windows logon; the toolkit cannot prompt for or accept manual bearer/SAML tokens. Username/password remains the default.');
353
388
  const selectedNameCounts = selected.reduce((counts, destination) => {
354
389
  const key = String(destination.name).toLowerCase();
355
390
  counts.set(key, (counts.get(key) || 0) + 1);
@@ -115,6 +115,102 @@ function checkboxPageSize(output, choiceCount) {
115
115
  return clamp(Math.min(choiceCount, rows - 7), 1, Math.max(1, choiceCount));
116
116
  }
117
117
 
118
+ export async function selectPrompt({ message, choices, defaultValue } = {}, { input = process.stdin, output = process.stdout } = {}) {
119
+ if (!Array.isArray(choices) || !choices.length) return undefined;
120
+ let cursor = choices.findIndex(choice => !choice.disabled && (choice.value === defaultValue || choice.checked));
121
+ if (cursor === -1) cursor = firstEnabledIndex(choices);
122
+ let top = 0;
123
+ let renderedLines = 0;
124
+ let done = false;
125
+ let previousRawMode;
126
+
127
+ const write = text => output.write(text);
128
+ const clearRendered = () => {
129
+ if (!renderedLines) return;
130
+ write(`\u001b[${renderedLines}A\u001b[J`);
131
+ renderedLines = 0;
132
+ };
133
+ const ensureVisible = pageSize => {
134
+ if (cursor < top) top = cursor;
135
+ if (cursor >= top + pageSize) top = cursor - pageSize + 1;
136
+ top = clamp(top, 0, Math.max(0, choices.length - pageSize));
137
+ };
138
+ const render = () => {
139
+ const columns = Number.isFinite(output?.columns) ? output.columns : 80;
140
+ const pageSize = checkboxPageSize(output, choices.length);
141
+ ensureVisible(pageSize);
142
+ const lines = [];
143
+ lines.push(truncateToColumns(`${message} (↑/↓: choose, Enter: confirm)`, columns));
144
+ const end = Math.min(choices.length, top + pageSize);
145
+ for (let index = top; index < end; index += 1) {
146
+ const choice = choices[index];
147
+ const pointer = index === cursor ? '❯' : ' ';
148
+ const marker = index === cursor ? colorText('●', 'green', output) : '○';
149
+ const disabled = choice.disabled ? ` — ${choice.disabled}` : '';
150
+ const label = `${pointer}${marker} ${choice.name}${disabled}`;
151
+ lines.push(truncateToColumns(label, columns));
152
+ }
153
+ if (choices.length > pageSize) {
154
+ lines.push(truncateToColumns(colorText(` Showing ${top + 1}-${end} of ${choices.length}; use ↑/↓ to scroll.`, 'cyan', output), columns));
155
+ }
156
+ clearRendered();
157
+ write(`${lines.join('\n')}\n`);
158
+ renderedLines = physicalRows(lines, columns);
159
+ };
160
+
161
+ return new Promise((resolve, reject) => {
162
+ const cleanup = () => {
163
+ input.off('keypress', onKeypress);
164
+ if (typeof input.setRawMode === 'function' && previousRawMode !== undefined) input.setRawMode(previousRawMode);
165
+ if (typeof input.pause === 'function') input.pause();
166
+ write('\u001b[?25h');
167
+ };
168
+ const finish = choice => {
169
+ if (done) return;
170
+ done = true;
171
+ clearRendered();
172
+ write(`${stripAnsi(message)} ${choice?.name || choice?.value || ''}\n`);
173
+ cleanup();
174
+ resolve(choice?.value);
175
+ };
176
+ const fail = error => {
177
+ if (done) return;
178
+ done = true;
179
+ cleanup();
180
+ reject(error);
181
+ };
182
+ const onKeypress = (chunk, key = {}) => {
183
+ if (key.ctrl && key.name === 'c') return fail(new Error('Prompt interrupted'));
184
+ if (key.name === 'up' || key.name === 'k') cursor = nextEnabledIndex(choices, cursor, -1);
185
+ else if (key.name === 'down' || key.name === 'j') cursor = nextEnabledIndex(choices, cursor, 1);
186
+ else if (key.name === 'return' || key.name === 'enter') return finish(choices[cursor]);
187
+ else if (chunk) {
188
+ const typed = String(chunk).toLowerCase();
189
+ const match = choices.findIndex(choice => !choice.disabled && (
190
+ String(choice.shortcut || '').toLowerCase() === typed ||
191
+ String(choice.value || '').toLowerCase().startsWith(typed) ||
192
+ String(choice.name || '').toLowerCase().startsWith(typed)
193
+ ));
194
+ if (match !== -1) cursor = match;
195
+ else return;
196
+ } else return;
197
+ render();
198
+ };
199
+
200
+ try {
201
+ readline.emitKeypressEvents(input);
202
+ previousRawMode = input.isRaw;
203
+ if (typeof input.setRawMode === 'function') input.setRawMode(true);
204
+ if (typeof input.resume === 'function') input.resume();
205
+ write('\u001b[?25l');
206
+ input.on('keypress', onKeypress);
207
+ render();
208
+ } catch (error) {
209
+ fail(error);
210
+ }
211
+ });
212
+ }
213
+
118
214
  export async function checkboxPrompt({ message, choices, required = false, shortcuts = { all: 'a' }, validate } = {}, { input = process.stdin, output = process.stdout } = {}) {
119
215
  if (!Array.isArray(choices) || !choices.length) return [];
120
216
  const selectable = enabledChoices(choices);
@@ -122,7 +122,7 @@ function execHelper(helper, payload, execFileImpl = nodeExecFile, env = process.
122
122
  env: { ...process.env, ...env }
123
123
  }, (error, stdout) => {
124
124
  // execFile errors can contain helper stdout/stderr, including tokens.
125
- if (error) return reject(safeError('Windows SSO helper failed; verify the helper and Windows Kerberos configuration.'));
125
+ if (error) return reject(safeError('Windows SSO helper failed; verify the ADT endpoint supports HTTP Negotiate/SPNEGO for the signed-in Windows identity (including smart-card-backed Windows logon) and that Kerberos/SPN configuration is correct. The toolkit cannot prompt for or accept manually pasted bearer/SAML tokens.'));
126
126
  const token = String(stdout || '').trim();
127
127
  if (!/^[A-Za-z0-9+/=_-]+$/.test(token)) return reject(safeError('Windows SSO helper returned an invalid Negotiate token'));
128
128
  if (tokenLooksLikeNtlm(token)) return reject(safeError('Windows SSO helper returned an NTLM token. ADT SSO requires Kerberos/SPNEGO; verify the backend SPN and Kerberos configuration, or rerun sap-ai-dev --setup and choose username/password.'));
@@ -135,7 +135,7 @@ function execHelper(helper, payload, execFileImpl = nodeExecFile, env = process.
135
135
  async function acquireNegotiateToken({ targetUrl, challenge, env, execFileImpl }) {
136
136
  const helper = helperCommand(env);
137
137
  if (!helper) {
138
- throw safeError('Windows SSO requires an SSPI/Negotiate helper. Set SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER to a trusted helper executable, or rerun sap-ai-dev --setup and choose username/password.');
138
+ throw safeError('Windows SSO requires an SSPI/Negotiate helper that uses the current Windows logon session (including smart-card-backed logon). Set SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER to a trusted helper executable, or rerun sap-ai-dev --setup and choose username/password. Manual token entry is not supported.');
139
139
  }
140
140
  return execHelper(helper, { url: targetUrl, challenge, mechanism: 'Negotiate' }, execFileImpl, env);
141
141
  }