sap-ai-dev-toolkit 0.5.6 โ†’ 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.github/agents/abap-developer.agent.md +7 -7
  2. package/.github/agents/abap-runtime-debugger.agent.md +4 -4
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +1 -1
  4. package/.github/agents/rap-service-developer.agent.md +5 -5
  5. package/.github/agents/sap-solution-architect.agent.md +2 -2
  6. package/.github/skills/abap-debugging/SKILL.md +1 -1
  7. package/.github/skills/abap-development/SKILL.md +3 -3
  8. package/.github/skills/abap-runtime-analysis/SKILL.md +2 -2
  9. package/.github/skills/abap-testing-quality/SKILL.md +3 -3
  10. package/.github/skills/cds-development/SKILL.md +3 -3
  11. package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
  12. package/.github/skills/hana-cloud-inspection/SKILL.md +2 -2
  13. package/.github/skills/hana-cloud-native-development/SKILL.md +1 -1
  14. package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
  15. package/.github/skills/rap-development/SKILL.md +3 -3
  16. package/.github/skills/rap-service-delivery/SKILL.md +2 -2
  17. package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
  18. package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
  19. package/.github/skills/sap-transport-release/SKILL.md +2 -2
  20. package/README.md +31 -26
  21. package/inventory.md +5 -3
  22. package/package.json +1 -1
  23. package/src/bas-destination-relay.mjs +13 -41
  24. package/src/launcher.mjs +11 -17
  25. package/src/mcp-proxy.mjs +56 -29
  26. package/src/setup.mjs +2 -110
  27. package/src/terminal-ui.mjs +3 -1
  28. package/tools.md +10 -2
  29. package/src/credential-overrides.mjs +0 -75
  30. package/src/credentials-store.mjs +0 -113
package/README.md CHANGED
@@ -68,7 +68,7 @@ Then connect a destination in SAP Business Application Studio:
68
68
 
69
69
  Use the attached destination-prefixed tools directly in chat. Do not launch `sap-ai-dev` or handcraft MCP JSON-RPC in a terminal to discover or call them.
70
70
 
71
- **Prerequisite:** Node.js 20 or newer. If Go is not already available, the installer can provision the pinned supported Go release automatically.
71
+ **Prerequisite:** Node.js 20 or newer. The pinned VSP runtime ships with the package; no Go toolchain is installed or required.
72
72
 
73
73
  ## ๐Ÿค– Available agents and skills
74
74
 
@@ -78,7 +78,7 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
78
78
  | --- | --- | --- |
79
79
  | ๐Ÿงญ | **SAP Solution Architect** | System investigation, SAP standard/API recommendations, Clean Core and side-by-side design, and SDLC orchestration through implementation handoffs and validation gates |
80
80
  | ๐Ÿง‘โ€๐Ÿ’ป | **ABAP Developer** | General ABAP, CDS, and RAP implementation, validation, and transport-preparation tasks |
81
- | ๐Ÿž | **ABAP Runtime Debugger** | Runtime incidents, dumps, logs, traces, debugger sessions, call graphs, and performance symptoms |
81
+ | ๐Ÿž | **ABAP Runtime Debugger** | Runtime incidents, application logs, debugger sessions, and performance symptoms; call relationships are derived from source inspection |
82
82
  | ๐Ÿš€ | **RAP Service Developer** | RAP business objects, behavior implementations, projections, service definitions, service bindings, and OData validation |
83
83
  | ๐Ÿ—ƒ๏ธ | **HANA Cloud/HDI Specialist** | Read-only HANA Cloud container inspection, CAP/HDI artifact generation, local validation, and user-run deployment handoffs |
84
84
 
@@ -168,7 +168,7 @@ The package ships with five custom agents plus fourteen task-focused Copilot Age
168
168
  | ๐Ÿงฑ | `clean-core-extensibility` | Clean Core, released extensibility, and side-by-side extension design with risk classification |
169
169
  | ๐Ÿ” | `sap-sdlc-orchestration` | Requirement-to-release lifecycle planning, delegated implementation, quality gates, validation, and handover |
170
170
  | ๐Ÿž | `abap-debugging` | Dumps, logs, traces, and runtime failure diagnosis |
171
- | ๐Ÿ”ฌ | `abap-runtime-analysis` | Incident triage, traces, debugger state, call graphs, and performance analysis |
171
+ | ๐Ÿ”ฌ | `abap-runtime-analysis` | Incident triage, debugger state, and performance analysis; call relationships come from source inspection |
172
172
  | ๐Ÿš€ | `rap-service-delivery` | RAP service activation, publication, OData validation, and end-to-end runtime checks |
173
173
  | ๐Ÿšš | `sap-transport-release` | Dependency checks and transport preparation; release itself is intentionally unavailable here |
174
174
  | ๐Ÿ”Ž | `hana-cloud-inspection` | Verify the selected HANA Cloud binding and inspect bounded HDI catalog/data results |
@@ -237,6 +237,7 @@ Once the destination server is enabled in Copilot Chat, ask for outcomes instead
237
237
  | --- | --- |
238
238
  | ๐ŸŽฏ **Explicit target** | SAP changes only proceed when the destination and required target details are known. |
239
239
  | ๐Ÿšฆ **Explicit state change** | Activation, service publication, and transport creation happen only when requested and authorized. |
240
+ | ๐Ÿ‘๏ธ **Optional read-only mode** | `SAP_AI_DEV_TOOLKIT_READ_ONLY=true` removes every write/activate/transport-create tool from the surface and starts VSP with `--transport-read-only`. Recommended for exploration destinations. |
240
241
  | ๐Ÿงช **Verification first** | The agent uses available lint, syntax, unit-test, ATC, and diagnostics workflows and reports what actually ran. |
241
242
  | ๐Ÿ”’ **SAP authorization remains authoritative** | The add-on does not bypass backend SAP permissions. |
242
243
  | ๐Ÿšš **Curated VSP tool surface** | The proxy exposes a cherry-picked developer-lifecycle set from VSP plus local workflow tools, keeping one destination below 60 tools; SAP authorizations and VSP safety checks still apply. |
@@ -270,14 +271,14 @@ The guided setup can also offer companion MCP entries with `sap-ai-dev --setup -
270
271
  | CAP tools | `@cap-js/mcp-server` | CAP CDS/service model inspection and CAP app development |
271
272
  | Browser validation | `@playwright/mcp` | Fiori/UI smoke tests, screenshots, and browser runtime validation |
272
273
 
273
- The installer handles two setup tasks automatically:
274
+ The installer handles VSP provisioning automatically:
274
275
 
275
- - Looks for Go in `GO_BINARY`, `PATH`, or the package-local Go installation. If none is available, it downloads and installs the pinned supported Go release without prompting.
276
- - Uses the package's checksum-verified patched VSP binary for the current platform. A remote download is the fallback only when the package has no bundled asset.
276
+ - Uses the package's bundled patched VSP binary for the current platform, verified against the npm-published `dist/checksums.txt`. A checksum-anchored remote download is the fallback only when the package has no bundled asset.
277
+ - No Go toolchain is downloaded or required at install time; Go is only used by the repository's own `build:vsp` development script.
277
278
 
278
279
  With `H2O_URL` set, an interactive install opens a checkbox picker 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. npm may run its install hook without an interactive terminal, even when the shell is interactive; in that case, selection is skipped without changing MCP config.
279
280
 
280
- 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-toolkit --setup` from an interactive BAS terminal after installation.
281
+ 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.
281
282
 
282
283
  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.
283
284
 
@@ -300,7 +301,7 @@ This lists discovered systems in the same checkbox picker, with nothing selected
300
301
  For a non-interactive installation or a platform without a published VSP asset, provide a trusted binary override:
301
302
 
302
303
  ```sh
303
- BAS_VSP_BINARY=/path/to/vsp npm install --global sap-ai-dev-toolkit
304
+ SAP_AI_DEV_TOOLKIT_BINARY=/path/to/vsp npm install --global sap-ai-dev-toolkit
304
305
  ```
305
306
 
306
307
  At the end of a global install, the color-coded summary shows the MCP config path, each generated entry name, destination/client/authentication, launch command, and environment key names (not values). The installer does not open an editor automatically; in BAS/VS Code:
@@ -339,7 +340,7 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
339
340
  | ๐Ÿงฑ | `clean-core-extensibility` | Design Clean Core compliant in-app, developer, API/event, and side-by-side extension patterns. |
340
341
  | ๐Ÿ” | `sap-sdlc-orchestration` | Orchestrate discovery, design, implementation handoffs, quality gates, transport readiness, and handover. |
341
342
  | ๐Ÿž | `abap-debugging` | Diagnose dumps, application logs, traces, and runtime failures. |
342
- | ๐Ÿ”ฌ | `abap-runtime-analysis` | Analyze incidents, traces, debugger state, call graphs, and performance symptoms. |
343
+ | ๐Ÿ”ฌ | `abap-runtime-analysis` | Analyze incidents, debugger state, and performance symptoms; derive call relationships from source inspection. |
343
344
  | ๐Ÿš€ | `rap-service-delivery` | Validate RAP service bindings, activation, publication, and end-to-end OData behavior. |
344
345
  | ๐Ÿšš | `sap-transport-release` | Check dependencies and prepare changes for transport; release is not available here. |
345
346
  | ๐Ÿ”Ž | `hana-cloud-inspection` | Verify the attached HANA target and inspect bounded HDI metadata and rows. |
@@ -348,7 +349,7 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
348
349
 
349
350
  ### ๐Ÿ“ฅ Install for your BAS user
350
351
 
351
- After destination setup, the installer prints a ๐Ÿค– notice that it is waiting for confirmation, then offers to install all bundled agents and skills under `$HOME/.copilot`. Press **Enter** to install; type **n** then **Enter** to skip. Declining leaves those files unchanged.
352
+ After destination setup, the installer prints a ๐Ÿค– notice that it is waiting for confirmation, then offers to install all bundled agents and skills under `$HOME/.copilot`. Type **y** then **Enter** to install; pressing **Enter** alone skips (the default). Declining leaves those files unchanged.
352
353
 
353
354
  These user-level customizations are available across workspaces opened by the same BAS user in the same dev space. Copilot must be available in BAS and may need a window reload to discover new files. A non-interactive install skips the optional prompt; `npm install --ignore-scripts` skips the postinstall wizard entirely.
354
355
 
@@ -399,11 +400,11 @@ The sample values sketch an on-premise ABAP backend routed through SAP Cloud Con
399
400
  | `WebIDEUsage` | `dev_abap,odata_abap` | Include `dev_abap` for ABAP development; add other usages required by your BAS scenario. |
400
401
  | `CloudConnectorLocationId` | `DEMO-LOCATION` | Optional; set only when the Cloud Connector uses a location ID. |
401
402
 
402
- For on-premise systems, configure Cloud Connector access to the backend host and port first. BAS Destination and Connectivity service credentials remain in their respective services; optional SAP backend overrides are stored in the owner-only credentials file beside `mcp.json`, never in `mcp.json`. The BAS OnPremise override is saved only when its tunnel route returns a CSRF token and session cookie.
403
+ For on-premise systems, configure Cloud Connector access to the backend host and port first. The add-on uses the authentication already configured in BAS or the Cloud Foundry Destination service; manage SAP credentials there.
403
404
 
404
405
  Think of `H2O_URL` as BAS's front door: it must point to the endpoint serving `/api/listDestinations`, not to the SAP backend.
405
406
 
406
- The `/api/listDestinations` response is a JSON array of destination records, like the provided `dests.sample.json`. A representative record:
407
+ The `/api/listDestinations` response is a JSON array of destination records. A representative record:
407
408
 
408
409
  ```json
409
410
  {
@@ -447,7 +448,7 @@ One selected system, one isolated stdio MCP entry. Here's the shape:
447
448
  }
448
449
  ```
449
450
 
450
- The MCP protocol `serverInfo.name` matches `BAS_VSP_DESTINATION`, so each wizard-generated destination has its own identity instead of the shared `sap-ai-dev-toolkit` name.
451
+ The MCP protocol `serverInfo.name` is the lowercased destination slug (from `SAP_AI_DEV_TOOLKIT_DESTINATION`, legacy `BAS_VSP_DESTINATION` accepted), so each wizard-generated destination has its own identity instead of the shared `sap-ai-dev-toolkit` name.
451
452
 
452
453
  Credentials, cookies, SAP usernames, passwords, and raw BAS destination payloads are not written to the MCP configuration.
453
454
 
@@ -489,7 +490,7 @@ Without `H2O_URL`, the command passes arguments directly to the installed VSP bi
489
490
 
490
491
  `sap-ai-dev` is an MCP stdio server and destination router, not a terminal command for individual SAP operations. No shell incantations needed: your MCP client discovers the tools, picks one for the chat request, and sends the call over stdio.
491
492
 
492
- Each generated MCP server entry is named with the lowercased destination slug: `DEMO_ABAP` becomes `demo-abap`. Tool names are `<destination-slug>_<tool>` with a lowercase snake_case tool segment (`demo-abap_get_source`, `demo-abap_run_query`, `demo-abap_lint_abap`), so every tool states which SAP system it targets. Lowercase names are deliberate: BAS and VS Code derive chat tool references from the server and tool names and only bind lowercase identifiers, so mixed-case names show up in the tools picker but never bind to the chat session. Use the exact names shown by your MCP client; punctuation can change during slugification.
493
+ Each generated MCP server entry is named with the lowercased destination slug: `DEMO_ABAP` becomes `demo-abap`. Because every generated entry is scoped to exactly one destination, tool names carry no redundant prefix โ€” they are plain lowercase snake_case (`get_source`, `run_query`, `lint_abap`); the server name already states which SAP system it targets. Only when one server fronts several destinations (manual multi-destination startup) does each name gain its destination slug (`demo-abap_run_query`) to stay unambiguous. Lowercase names are deliberate: BAS and VS Code derive chat tool references from the server and tool names and only bind lowercase identifiers, so mixed-case names show up in the tools picker but never bind to the chat session. Use the exact names shown by your MCP client; punctuation can change during slugification.
493
494
 
494
495
  Upgrading from an earlier release that wrote mixed-case entry names (for example `ActionS4D` or `cf:<guid>:<guid>:<name>`)? Re-run `sap-ai-dev --setup` to replace legacy entries โ€” this is the only migration path for Cloud Foundry entries โ€” or run `sap-ai-dev --doctor` to rename BAS entries in place. Then reload the BAS window and start a new chat; an existing chat keeps its stale tool binding. If setup reports that the lowercase name already exists and is not managed by this package, rename or remove that user-owned server entry first.
495
496
 
@@ -505,7 +506,7 @@ The menu includes the tools registered by the active VSP mode, the `GetApplicati
505
506
 
506
507
  ### ๐Ÿงน Lint submitted ABAP source locally
507
508
 
508
- The per-destination tool name is `<destination-slug>_lint_abap`, for example `demo-abap_lint_abap` (the local workflow name `LintABAP` is exposed in snake_case). It accepts caller-supplied abapGit-serialized source files; config is optional and, when present, is the full abaplint configuration rather than a merge with defaults.
509
+ The tool is named `lint_abap` (the local workflow name `LintABAP` exposed in snake_case; on a multi-destination server it is `<destination-slug>_lint_abap`). It accepts caller-supplied abapGit-serialized source files; config is optional and, when present, is the full abaplint configuration rather than a merge with defaults.
509
510
 
510
511
  ```json
511
512
  {
@@ -545,7 +546,7 @@ For an ABAP Cloud assessment, get object URIs from `SearchObject` and pass them
545
546
 
546
547
  `tools/list` confirms what this MCP server exposes; it does not prove that VS Code attached the server to the active chat. Start the selected destination in **MCP: List Servers**, enable it in the Chat tools picker, and reload/reselect the agent if the host binding is stale. This add-on cannot inspect or repair another MCP server's registration or a host-wide tool budget.
547
548
 
548
- `sap-ai-dev-toolkit --demo` starts an isolated server with sample ABAP objects, test and ATC results, a sample transport, and synthetic OData metadata and rows. Source edits and transport creation stay in process memory and disappear on exit. The demo does not discover or contact BAS or SAP destinations.
549
+ `sap-ai-dev --demo` starts an isolated server with sample ABAP objects, test and ATC results, a sample transport, and synthetic OData metadata and rows. Source edits and transport creation stay in process memory and disappear on exit. The demo does not discover or contact BAS or SAP destinations.
549
550
 
550
551
  ### ๐Ÿ”Ž Find and understand ABAP objects
551
552
 
@@ -605,6 +606,7 @@ The write and activation tools change SAP state. Confirm the target, package, an
605
606
  | `GetSystemInfo` | Read system ID, SAP release, kernel, and database details. |
606
607
  | `GetInstalledComponents` | List installed software components and versions. |
607
608
  | `GetFeatures` | Probe optional system capabilities, including abapGit, RAP/OData, AMDP debugging, UI5/BSP, and CTS transports. |
609
+ | `GetConnectionInfo` | Show the connected user, client, URL, mode, and feature-probe summary for the current destination session. |
608
610
  | `PrettyPrint` | Format ABAP source text without saving it to SAP. |
609
611
 
610
612
  ### ๐Ÿšš Inspect and create transports
@@ -642,7 +644,7 @@ The proxy intentionally does not expose the full child VSP process. Object delet
642
644
  3. Ask for the operation in plain language. The MCP client sends `tools/call`; no need to type a tool such as `GetSource` into a terminal.
643
645
  4. Check the response in chat. For source edits, ask for a syntax check and tests before activation when that matches your workflow.
644
646
 
645
- For a quick table readโ€”say, company codes from `T001`โ€”select the destination's `RunQuery` tool (for `DEMO_ABAP`, `demo-abap_run_query`) and pass:
647
+ For a quick table readโ€”say, company codes from `T001`โ€”select the destination's `RunQuery` tool (exposed as `run_query`; `demo-abap_run_query` only on a multi-destination server) and pass:
646
648
 
647
649
  ```json
648
650
  {
@@ -659,7 +661,7 @@ The same request can be expressed to an MCP client as:
659
661
  "id": 2,
660
662
  "method": "tools/call",
661
663
  "params": {
662
- "name": "demo-abap_run_query",
664
+ "name": "run_query",
663
665
  "arguments": {
664
666
  "sql_query": "SELECT BUKRS, BUTXT, WAERS, LAND1 FROM T001",
665
667
  "max_rows": 100
@@ -707,7 +709,7 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
707
709
 
708
710
  ```json
709
711
  {
710
- "name": "demo-abap_run_query",
712
+ "name": "run_query",
711
713
  "description": "Execute an ABAP SQL query [destination: DEMO_ABAP]",
712
714
  "inputSchema": {
713
715
  "type": "object",
@@ -732,15 +734,19 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
732
734
  | `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`. |
733
735
  | `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. |
734
736
  | `SAP_AI_DEV_TOOLKIT_DISABLE_BAS_RELAY=true` | Disable the built-in BAS destination relay. By default the add-on self-heals `.dest` destinations through a local relay that keeps all access destination-based while handling ADT CSRF fetch/retry behavior before VSP calls SAP. |
735
- | `SAP_AI_DEV_TOOLKIT_HTTP_PROXY` | Egress proxy for relay and discovery traffic. Takes precedence over `HTTP_PROXY`/`http_proxy`; unset means the default BAS proxy for `.dest` hosts, empty means direct except for OnPremise credential overrides, which require a BAS proxy tunnel. |
737
+ | `SAP_AI_DEV_TOOLKIT_HTTP_PROXY` | Egress proxy for relay and discovery traffic. Takes precedence over `HTTP_PROXY`/`http_proxy`; unset means the default BAS proxy for `.dest` hosts, empty means direct. |
736
738
  | `SAP_AI_DEV_TOOLKIT_MAX_CSRF_RETRIES` | Bounded retries after explicit CSRF-session rejection per unsafe request (default 3, maximum 10). Invalid values use the default. |
739
+ | `SAP_AI_DEV_TOOLKIT_READ_ONLY=true` | Read-only mode: hides `WriteSource`, `EditSource`, `Activate`, `ActivateMultiple`, `CreateTransport`, `SetBreakpoint`, and the change-set workflow tools, and starts VSP with `--transport-read-only`. |
740
+ | `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. |
737
741
  | `SAP_AI_DEV_MCP_CONFIG` | Explicit MCP configuration path; highest precedence. |
738
742
  | `SAP_AI_DEV_TOOLKIT_MCP_CONFIG` | Branded compatibility alias for the MCP configuration path. |
739
743
  | `BAS_VSP_MCP_CONFIG` | Backward-compatible alias for the MCP configuration path. |
740
- | `BAS_VSP_BINARY` | Trusted prebuilt VSP executable; skips Go and binary provisioning. |
741
- | `BAS_VSP_BINARY_URL` | Alternate VSP binary download URL. |
742
- | `BAS_VSP_CACHE_DIR` | Binary cache directory. |
743
- | `GO_BINARY` | Explicit Go executable used for provisioning when automatic Go installation is unavailable. |
744
+ | `SAP_AI_DEV_TOOLKIT_BINARY` | Trusted prebuilt VSP executable; skips binary provisioning. Legacy `BAS_VSP_BINARY` still works. |
745
+ | `SAP_AI_DEV_TOOLKIT_BINARY_URL` | Alternate VSP binary download URL (checksum-verified). Legacy `BAS_VSP_BINARY_URL` still works. |
746
+ | `SAP_AI_DEV_TOOLKIT_RELEASE_BASE_URL` | Override the base URL that VSP release binaries are downloaded from. |
747
+ | `SAP_AI_DEV_TOOLKIT_CACHE_DIR` | Binary cache directory. Legacy `BAS_VSP_CACHE_DIR` still works. |
748
+ | `BAS_CF_SPACE_GUID`, `BAS_CF_DESTINATION_INSTANCE_GUID`, `BAS_CF_DESTINATION_INSTANCE`, `BAS_CF_DESTINATION_KEY`, `BAS_CF_DESTINATION_NAME`, `BAS_CF_CONNECTIVITY_INSTANCE_GUID`, `BAS_CF_CONNECTIVITY_INSTANCE`, `BAS_CF_CONNECTIVITY_KEY` | Written into generated Cloud Foundry destination entries; they reference service instances and key **names** (never credentials). |
749
+ | `GO_BINARY` | Explicit Go executable for the repository-only `build:vsp` script; not used during installation. |
744
750
  | `SAP_AI_DEV_TOOLKIT_SKIP_PROBE=true` | Skip destination probes; useful for controlled diagnostics or fixtures. |
745
751
  | `HTTP_PROXY` / `HTTPS_PROXY` | BAS proxy settings used for destination-list requests, destination probing, and child processes. |
746
752
  | `NO_PROXY` | Proxy bypass list for BAS destination-list requests; `.dest` hosts remain routed through the BAS proxy. |
@@ -753,8 +759,7 @@ The relay between the VSP child and each BAS destination recovers from the failu
753
759
  2. **Session refresh** โ€” when SAP explicitly rejects a CSRF token, the cached session is dropped, a fresh token+cookie pair is fetched, and the request is retried within the configured bound. Generic authorization 401/403 responses are returned without being mislabeled or retried as CSRF failures.
754
760
  3. **Proxy tunnel fallback** โ€” when the BAS proxy refuses absolute-form requests (502/504 or transport errors), the relay switches to a CONNECT tunnel through the same proxy and keeps going.
755
761
  4. **Child crash recovery** โ€” a crashed VSP child is restarted transparently, re-initialized, tools re-registered, and the interrupted `tools/call` retried once before any error reaches the client.
756
- 5. **Direct connect for Internet destinations** โ€” the BAS `.dest` proxy strips SAP `Set-Cookie` headers, which makes CSRF token/session binding impossible for ADT writes (`403 CSRF token validation failed`). When you select an Internet destination with BasicAuthentication, setup asks whether to override its credentials. A Yes prompts for SAP user/password and stores them in `sap-ai-dev-toolkit-credentials.json` **next to** `mcp.json` (never inside it, permissions `0600`). The relay connects straight to the backend host so cookies survive and ADT writes can use the paired CSRF session.
757
- 6. **Cloud Connector credential overrides** โ€” setup also offers per-destination overrides for BasicAuthentication OnPremise destinations. BAS OnPremise credentials travel through an HTTP CONNECT tunnel to the BAS `.dest` endpoint, retaining its Cloud Connector mapping; setup saves the override only after a read-only probe returns both a CSRF token and a session cookie. If that route check fails, the override is not saved. Cloud Foundry OnPremise overrides replace only the SAP backend user/password; Connectivity service-key authentication and its proxy route remain unchanged. PrincipalPropagation is never overridden. Rerun `sap-ai-dev --setup` to change credentials or answer No to disable a stored override. Deselecting a destination (or selecting none) removes stale overrides.
762
+ 5. **Configured destination authentication** โ€” the add-on always uses the URL, authentication mode, and credentials already configured for the selected destination. Setup does not prompt for or store SAP usernames/passwords, and the relay does not connect directly to a backend host as a credential fallback. Correct the credentials in BAS or the Cloud Foundry Destination service if SAP rejects them.
758
763
  Existing installs that still set the previous `BAS_VSP_*` environment variables remain supported. The setup wizard writes new MCP entries with the `SAP_AI_DEV_TOOLKIT_*` names.
759
764
 
760
765
  #### Server logging
package/inventory.md CHANGED
@@ -6,8 +6,8 @@ This inventory reflects the current SAP AI Dev Toolkit proxy behavior in `src/mc
6
6
 
7
7
  | Status | Count | Notes |
8
8
  | --- | ---: | --- |
9
- | VSP tools | Up to 51 | Curated from the child VSP tool list and exposed with a destination prefix in lowercase snake_case, for example `<destination>_get_source`. |
10
- | Local lint tool | 1 per destination | `LintABAP` analyzes caller-supplied ABAP source in memory; it is exposed publicly as `<destination>_lint_abap`. |
9
+ | VSP tools | Up to 51 | Curated from the child VSP tool list and exposed in lowercase snake_case, unprefixed (`get_source`) on single-destination servers; a destination slug prefix (`demo-abap_get_source`) appears only when one server fronts multiple destinations. |
10
+ | Local lint tool | 1 per destination | `LintABAP` analyzes caller-supplied ABAP source in memory; it is exposed publicly as `lint_abap`. |
11
11
  | Workflow tools | Up to 6 per destination | Review/apply change sets, transport evidence, Clean Core release assessment, and read-only RAP regression suites. Some are exposed only when their upstream VSP tools are registered. |
12
12
  | Convenience mapping | Dynamic | `GetApplicationLog` maps to VSP `SAP(action="analyze", type="application_log")` when the SAP router is registered. |
13
13
  | Intentionally filtered VSP tools | Dynamic | Destructive, broad-router, trace, and unsupported VSP operations are hidden from direct calls. |
@@ -161,8 +161,10 @@ These logical names are exposed through MCP in lowercase snake_case (`lint_abap`
161
161
 
162
162
  ## Notes
163
163
 
164
- - Runtime tool names are `<destination-slug>_<tool>`, for example `demo-abap_run_query`; `tools/call` accepts the exact name returned by `tools/list`.
164
+ - Runtime tool names are unprefixed lowercase snake_case (`run_query`) on the normal single-destination server; a `<destination-slug>_` prefix appears only on multi-destination servers. `tools/call` accepts the exact name returned by `tools/list`.
165
165
  - The proxy starts VSP with `--enable-transports`; generated MCP entries set `SAP_ALLOW_TRANSPORTABLE_EDITS=true`.
166
+ - `SAP_AI_DEV_TOOLKIT_READ_ONLY=true` switches a server to read-only: the write/activate/transport-create/breakpoint tools above are removed from the surface, change-set workflows are not registered, and VSP starts with `--transport-read-only` instead.
167
+ - `SAP_AI_DEV_TOOLKIT_REQUEST_TIMEOUT_MS` (default 600000) bounds each forwarded call; stalled requests fail without killing the child.
166
168
  - Transport release/deletion and the general-purpose `SAP` router are intentionally hidden from direct proxy calls. `GetApplicationLog` is the bounded convenience mapping for the SAP application-log route.
167
169
 
168
170
  ## Separate optional HANA Cloud inspector
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.5.6",
3
+ "version": "0.6.0",
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",
@@ -133,14 +133,7 @@ async function readRequestBody(req) {
133
133
  export function createBasDestinationRelay(destination, { env = process.env, fetchImpl = undiciFetch, log = () => {} } = {}) {
134
134
  if (!destination?.url) throw new Error('Destination relay requires a BAS destination URL');
135
135
  const base = new URL(destination.url);
136
- const credentials = destination.credentials;
137
- const tunnelMode = credentials?.mode === 'bas-tunnel';
138
- const directHost = tunnelMode ? '' : credentials?.host;
139
- // Internet overrides retain the direct-backend path. OnPremise overrides
140
- // must keep the BAS .dest route (and its Cloud Connector mapping), so they
141
- // use HTTP CONNECT through the BAS proxy instead of credentials.host.
142
- const directBase = directHost ? new URL(String(directHost)) : null;
143
- const cookieBase = directBase || (destination.backendUrl
136
+ const cookieBase = destination.backendUrl
144
137
  ? new URL(String(destination.backendUrl))
145
138
  : (() => {
146
139
  const target = new URL(base);
@@ -149,9 +142,8 @@ export function createBasDestinationRelay(destination, { env = process.env, fetc
149
142
  // omits the backend URL so Secure session cookies are not discarded.
150
143
  if (target.protocol === 'http:' && /\.dest$/i.test(target.hostname)) target.protocol = 'https:';
151
144
  return target;
152
- })());
153
- if (directBase) log(`[${destination.name}] BAS relay direct connect active (${directBase.origin}); BAS proxy cookie stripping bypassed`);
154
- if (tunnelMode) log(`[${destination.name}] BAS relay using a BAS proxy tunnel for the OnPremise destination`);
145
+ })();
146
+ // BAS destinations must keep their configured .dest route and authentication.
155
147
  // Proxy resolution: an explicitly configured proxy wins (an explicit empty
156
148
  // value means "go direct"). Without configuration, BAS virtual .dest hosts
157
149
  // imply the default BAS proxy; any other host (tests, direct URLs) is
@@ -159,16 +151,15 @@ export function createBasDestinationRelay(destination, { env = process.env, fetc
159
151
  // foreign proxy.
160
152
  const brandedProxy = brandedEnvValue(env, 'HTTP_PROXY');
161
153
  const explicitProxy = brandedProxy !== undefined ? brandedProxy : (env.HTTP_PROXY ?? env.http_proxy);
162
- const proxyUrl = directBase ? '' : (explicitProxy !== undefined ? String(explicitProxy) : (/\.dest$/i.test(base.hostname) ? DEFAULT_PROXY : ''));
163
- if (tunnelMode && !proxyUrl) throw new Error('OnPremise credential overrides require the BAS destination proxy to preserve the Cloud Connector route');
154
+ const proxyUrl = explicitProxy !== undefined ? String(explicitProxy) : (/\.dest$/i.test(base.hostname) ? DEFAULT_PROXY : '');
164
155
  const dispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, proxyTunnel: false }) : undefined;
165
156
  // Self-healing mode 2: if the BAS proxy refuses absolute-form proxied
166
157
  // requests (its own 502/504), retry through a CONNECT tunnel so the relay
167
158
  // keeps working through the same egress.
168
159
  const tunnelDispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, proxyTunnel: true }) : undefined;
169
- let tunneled = tunnelMode;
160
+ let tunneled = false;
170
161
  // Observable self-healing counters, surfaced by --doctor and tests.
171
- const stats = { requests: 0, csrfFetches: 0, csrfRetries: 0, csrfSessionFailures: 0, tunnelFallbacks: 0, direct: Boolean(directBase), proxyTunnel: tunnelMode };
162
+ const stats = { requests: 0, csrfFetches: 0, csrfRetries: 0, csrfSessionFailures: 0, tunnelFallbacks: 0, direct: false, proxyTunnel: false };
172
163
  const maxFailures = maxTokenFailures(env, log, destination.name);
173
164
  // The shared jar captures SAP cookies from safe reads as well as CSRF fetches.
174
165
  // Each cached token gets its own snapshot so later responses cannot silently
@@ -210,16 +201,16 @@ export function createBasDestinationRelay(destination, { env = process.env, fetc
210
201
  async function fetchTokenFresh(requestHeaders) {
211
202
  const key = CSRF_FETCH_PATH;
212
203
  stats.csrfFetches += 1;
213
- const target = resolveTarget(new URL(CSRF_FETCH_PATH, base));
204
+ const target = new URL(CSRF_FETCH_PATH, base);
214
205
  const cookieTarget = resolveCookieTarget(target);
215
206
  const jar = copyCookieJar(sharedCookieJar);
216
207
  await seedRequestCookies(jar, requestHeaders.cookie, cookieTarget);
217
208
  const outgoingCookie = await cookieHeader(jar, cookieTarget);
218
- const response = await sendWithFallback(target, 'GET', authHeaders(fetchHeaders(requestHeaders, {
209
+ const response = await sendWithFallback(target, 'GET', fetchHeaders(requestHeaders, {
219
210
  'x-csrf-token': 'Fetch',
220
211
  accept: requestHeaders.accept || 'application/xml,*/*',
221
212
  ...(outgoingCookie ? { cookie: outgoingCookie } : {})
222
- })));
213
+ }));
223
214
  await response.arrayBuffer().catch(() => undefined);
224
215
  const token = response.headers.get('x-csrf-token');
225
216
  await recordCookies(jar, response.headers, cookieTarget, log, destination.name);
@@ -265,36 +256,17 @@ export function createBasDestinationRelay(destination, { env = process.env, fetc
265
256
  };
266
257
  }
267
258
 
268
- // Rewrite an incoming .dest target onto the direct backend host when
269
- // credentials are configured, keeping path and query intact.
270
- function resolveTarget(target) {
271
- if (!directBase) return target;
272
- return new URL(`${target.pathname}${target.search}`, directBase);
273
- }
274
-
275
- function authHeaders(extra = {}) {
276
- if (!credentials?.user || !credentials?.password) return extra;
277
- const basic = Buffer.from(`${credentials.user}:${credentials.password}`).toString('base64');
278
- // The stored credential override wins over any inbound authorization
279
- // header: the override exists precisely to pair Basic auth with the
280
- // relay's CSRF session, and silently forwarding a caller-supplied header
281
- // would disable that pairing without any diagnostic.
282
- const headers = { ...extra };
283
- delete headers.authorization;
284
- delete headers.Authorization;
285
- return { ...headers, authorization: `Basic ${basic}` };
286
- }
287
259
 
288
260
  async function forward(method, requestUrl, requestHeaders, body, session) {
289
- const target = resolveTarget(requestUrl);
261
+ const target = requestUrl;
290
262
  const cookieTarget = resolveCookieTarget(target);
291
263
  const cookieJar = session?.jar || sharedCookieJar;
292
264
  if (!session) await seedRequestCookies(cookieJar, requestHeaders.cookie, cookieTarget);
293
265
  const sessionCookie = await cookieHeader(cookieJar, cookieTarget);
294
- const headers = authHeaders(headersFrom(requestHeaders, {
266
+ const headers = headersFrom(requestHeaders, {
295
267
  ...(session?.token ? { 'x-csrf-token': session.token } : {}),
296
268
  ...(sessionCookie ? { cookie: sessionCookie } : {})
297
- }));
269
+ });
298
270
  const response = await sendWithFallback(target, method, headers, body);
299
271
  const responseBody = Buffer.from(await response.arrayBuffer());
300
272
  if (session) await recordCookies(session.jar, response.headers, cookieTarget, log, destination.name);
@@ -332,7 +304,7 @@ export function createBasDestinationRelay(destination, { env = process.env, fetc
332
304
  return forwardWithRetry(method, requestUrl, requestHeaders, body, failures + 1);
333
305
  }
334
306
  if (unsafe && rejected && tokenOnly) {
335
- log(`[${destination.name}] BAS relay CSRF token rejected without a session cookie for ${requestUrl.pathname}; not retried (route delivers no cookies; a credential override enables a paired session)`);
307
+ log(`[${destination.name}] BAS relay CSRF token rejected without a session cookie for ${requestUrl.pathname}; not retried (the configured destination route delivered no cookies)`);
336
308
  } else if (unsafe && rejected && failures >= maxFailures) {
337
309
  log(`[${destination.name}] BAS relay stopped after ${maxFailures} CSRF session retries for ${requestUrl.pathname}`);
338
310
  }
package/src/launcher.mjs CHANGED
@@ -10,7 +10,6 @@ import { installMcpConfig, repairManagedMcpConfig } from './mcp-config.mjs';
10
10
  import { runSetup } from './setup.mjs';
11
11
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
12
12
  import { withBrandedEnvironment } from './branding.mjs';
13
- import { enrichWithStoredCredentials } from './credential-overrides.mjs';
14
13
  import { redactText } from './redact.mjs';
15
14
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
16
15
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
@@ -100,19 +99,18 @@ async function runDoctor(destinations) {
100
99
  } else {
101
100
  checks.push(doctorRow(destination.name, 'BAS destination relay', 'skipped', 'Not applicable for this destination.'));
102
101
  }
103
- if (relayedDestination?.credentials?.mode === 'bas-tunnel') {
104
- checks.push(doctorRow(destination.name, 'OnPremise credential override', 'passed', 'Basic auth is routed through a BAS proxy tunnel; Cloud Connector routing is retained'));
105
- } else if (relayedDestination?.credentials?.host) {
106
- checks.push(doctorRow(destination.name, 'Direct connect', 'passed', `active (${new URL(relayedDestination.credentials.host).origin}); ADT writes use stored credentials`));
107
- }
108
102
  const listed = await proxy.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
109
103
  if (listed?.error) throw new Error(listed.error.message || 'MCP tools/list failed');
110
104
  const tools = listed.result?.tools || [];
105
+ // Doctor inspects one destination per proxy, so tool names are
106
+ // unprefixed (get_source, run_query); match the slug-prefixed form too
107
+ // in case this ever runs against a multi-destination server.
111
108
  const toolPrefix = `${slugifyDestination(destination.name)}_`;
112
109
  const localSegments = new Set(['lint_abap', 'get_application_log', 'prepare_abap_change_set', 'apply_abap_change_set', 'check_transport_readiness', 'plan_abap_cloud_migration', 'generate_rap_regression_suite', 'run_rap_regression_suite']);
113
- const upstreamCount = tools.filter(tool => tool.name.startsWith(toolPrefix) && !localSegments.has(tool.name.slice(toolPrefix.length))).length;
110
+ const isUpstreamTool = tool => (tool.name.startsWith(toolPrefix) ? tool.name.slice(toolPrefix.length) : tool.name);
111
+ const upstreamCount = tools.filter(tool => !localSegments.has(isUpstreamTool(tool))).length;
114
112
  checks.push(doctorRow(destination.name, 'MCP tools/list', upstreamCount ? 'passed' : 'failed', `${upstreamCount} VSP tools and ${tools.length} total MCP tools returned; chat-picker binding is host-managed`));
115
- const systemInfo = tools.find(tool => tool.name === `${toolPrefix}get_system_info`);
113
+ const systemInfo = tools.find(tool => tool.name === 'get_system_info' || tool.name === `${toolPrefix}get_system_info`);
116
114
  if (!systemInfo) {
117
115
  checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'GetSystemInfo is not exposed by this VSP mode.'));
118
116
  } else {
@@ -133,7 +131,7 @@ async function runDoctor(destinations) {
133
131
  : `HTTP ${session?.httpStatus || 0}; token=${session?.tokenReceived ? 'received' : 'missing'}; cookies=${session?.cookieCount || 0}; usableSession=${sessionReady}; csrfFetches=${stats.csrfFetches || 0}; csrfRetries=${stats.csrfRetries || 0}; sessionFailures=${stats.csrfSessionFailures || 0}; tunnelFallbacks=${stats.tunnelFallbacks || 0}`;
134
132
  checks.push(doctorRow(destination.name, 'BAS relay CSRF preflight', status, detail));
135
133
  if (!sessionReady) {
136
- const remediation = 'GetSystemInfo was not called because the relay could not establish a paired CSRF token/session. For OnPremise BasicAuth, run sap-ai-dev --setup and use a validated BAS proxy tunnel credential override.';
134
+ const remediation = 'GetSystemInfo was not called because the relay could not establish a paired CSRF token/session. Confirm the configured destination authentication and BAS proxy route.';
137
135
  checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', preflightError ? `${remediation} Relay error: ${preflightError}` : remediation));
138
136
  }
139
137
  }
@@ -218,8 +216,8 @@ async function main() {
218
216
  let report;
219
217
  try {
220
218
  const destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
221
- ? await enrichWithStoredCredentials([await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })], runtimeEnv)
222
- : await enrichWithStoredCredentials(await discoverForCommand(), runtimeEnv);
219
+ ? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
220
+ : await discoverForCommand();
223
221
  report = await runDoctor(destinations);
224
222
  } catch (error) {
225
223
  report = { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', redactText(error.message || error).slice(0, 300))] };
@@ -255,7 +253,7 @@ async function main() {
255
253
  }
256
254
 
257
255
  if (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry') {
258
- const destination = (await enrichWithStoredCredentials([await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })], runtimeEnv))[0];
256
+ const destination = await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv });
259
257
  let proxy;
260
258
  try {
261
259
  const binary = await binaryOrError();
@@ -285,7 +283,7 @@ async function main() {
285
283
 
286
284
  const discovered = await discoverDestinations({ env: runtimeEnv });
287
285
  for (const destination of discovered) logLine(probeDiagnostic(destination));
288
- const destinations = await enrichWithStoredCredentials(discovered, runtimeEnv);
286
+ const destinations = discovered;
289
287
  if (!destinations.length) {
290
288
  logLine('[sap-ai-dev] destination discovery returned no named BAS destinations');
291
289
  throw new Error(remediation);
@@ -293,10 +291,6 @@ async function main() {
293
291
  const binary = await binaryOrError();
294
292
  logLine(`[sap-ai-dev] sap-ai-dev-toolkit v${pkg.version} (node ${process.version}, pid ${process.pid})`);
295
293
  logLine(`[sap-ai-dev] VSP binary: ${binary}`);
296
- for (const destination of destinations) {
297
- if (destination.credentials?.mode === 'bas-tunnel') logLine(`[sap-ai-dev] ${destination.name}: Basic credentials enabled through the BAS proxy tunnel`);
298
- else if (destination.credentials?.host) logLine(`[sap-ai-dev] ${destination.name}: direct connect enabled through stored credentials`);
299
- }
300
294
  logLine(`[sap-ai-dev] starting MCP proxy for ${destinations.map(destination => `${destination.name} (client=${destination.client})`).join(', ')}`);
301
295
  const proxy = new MCPProxy({ binary, destinations, env: process.env, log: logLine, version: pkg.version });
302
296
  const shutdown = signal => { void proxy.close().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143)); };
package/src/mcp-proxy.mjs CHANGED
@@ -613,6 +613,13 @@ export class MCPProxy {
613
613
  const namespace = new Map();
614
614
  const usedSlugs = new Map();
615
615
  const merged = [];
616
+ // Generated MCP entries pin exactly one destination per server, so tool
617
+ // names are unprefixed (run_query, get_source): the server name already
618
+ // identifies the target and a slug prefix would be redundant in chat
619
+ // references. Only when one server fronts several destinations (manual
620
+ // multi-destination startup) does each name carry its destination slug
621
+ // so every tool stays unambiguous.
622
+ const prefixed = this.children.length > 1;
616
623
  const publish = (name, mapping, definition) => {
617
624
  // A duplicate public name must not shadow the first registration: the
618
625
  // namespace map would keep the first handler while advertising both.
@@ -625,41 +632,54 @@ export class MCPProxy {
625
632
  };
626
633
  for (const entry of this.children) {
627
634
  const slug = slugifyDestination(entry.destination.name, usedSlugs);
628
- const publicName = name => `${slug}_${publicToolSegment(name)}`;
635
+ const publicName = prefixed ? (name => `${slug}_${publicToolSegment(name)}`) : (name => publicToolSegment(name));
629
636
  const lintName = publicName(ABAP_LINT_TOOL.name);
630
637
  publish(lintName, { handler: runABAPLint }, { ...ABAP_LINT_TOOL, name: lintName });
638
+ let upstreamTools;
631
639
  try {
632
- const upstreamTools = await entry.child.listToolsCached();
633
- for (const tool of upstreamTools) {
634
- if (tool.name === 'SAP') {
635
- const applicationLogName = publicName('GetApplicationLog');
636
- publish(applicationLogName, {
637
- entry,
638
- upstream: 'SAP',
639
- publicName: 'GetApplicationLog',
640
- transformArguments: applicationLogArguments
641
- }, {
642
- name: applicationLogName,
643
- description: `${APPLICATION_LOG_DESCRIPTION} [destination: ${entry.destination.name}]`,
644
- inputSchema: APPLICATION_LOG_SCHEMA
645
- });
646
- }
647
-
648
- if (!exposeVspTool(tool, this.readOnly)) continue;
649
- const name = publicName(tool.name);
650
- publish(name, { entry, upstream: tool.name }, { ...tool, name, description: `${tool.description || tool.name} [destination: ${entry.destination.name}]` });
651
- }
652
- for (const localTool of createEngineeringTools(entry, upstreamTools, { env: this.env, log: this.log })) {
653
- const name = publicName(localTool.definition.name);
654
- publish(name, { handler: localTool.handler }, {
655
- ...localTool.definition,
656
- name,
657
- description: `${localTool.definition.description} [destination: ${entry.destination.name}]`
658
- });
659
- }
640
+ upstreamTools = await entry.child.listToolsCached();
641
+ entry.lastUpstreamTools = upstreamTools;
660
642
  } catch (error) {
661
643
  buildState.complete = false;
662
644
  this.eventSink(`[${entry.destination.name}] tools/list failed: ${redactText(error.message)}`, 'error');
645
+ // A crashed child must not silently remove its tools from the
646
+ // surface: fall back to the last successful listing so the names stay
647
+ // callable and the tools/call self-heal path can restart the child.
648
+ upstreamTools = entry.lastUpstreamTools;
649
+ }
650
+ if (!upstreamTools) continue;
651
+ for (const tool of upstreamTools) {
652
+ if (tool.name === 'SAP') {
653
+ const applicationLogName = publicName('GetApplicationLog');
654
+ publish(applicationLogName, {
655
+ entry,
656
+ upstream: 'SAP',
657
+ publicName: 'GetApplicationLog',
658
+ transformArguments: applicationLogArguments
659
+ }, {
660
+ name: applicationLogName,
661
+ description: `${APPLICATION_LOG_DESCRIPTION} [destination: ${entry.destination.name}]`,
662
+ inputSchema: APPLICATION_LOG_SCHEMA
663
+ });
664
+ }
665
+
666
+ if (!exposeVspTool(tool, this.readOnly)) continue;
667
+ const name = publicName(tool.name);
668
+ publish(name, { entry, upstream: tool.name }, { ...tool, name, description: `${tool.description || tool.name} [destination: ${entry.destination.name}]` });
669
+ }
670
+ // Local workflow tools gate on the upstream surface they can use; in
671
+ // read-only mode the hidden write tools must not enable change-set
672
+ // staging either.
673
+ const effectiveUpstream = this.readOnly
674
+ ? upstreamTools.filter(tool => !READ_ONLY_HIDDEN_VSP_TOOLS.has(tool.name))
675
+ : upstreamTools;
676
+ for (const localTool of createEngineeringTools(entry, effectiveUpstream, { env: this.env, log: this.log })) {
677
+ const name = publicName(localTool.definition.name);
678
+ publish(name, { handler: localTool.handler }, {
679
+ ...localTool.definition,
680
+ name,
681
+ description: `${localTool.definition.description} [destination: ${entry.destination.name}]`
682
+ });
663
683
  }
664
684
  }
665
685
  if (!merged.length && this.children.length) throw new Error('No destination child provided tools');
@@ -729,6 +749,7 @@ export class MCPProxy {
729
749
  if (!retriable) {
730
750
  this.eventSink(`[${mapped.entry.destination.name}] tools/call ${toolName} is state-changing; not retried after a child crash (possible duplicate write)`, 'warning');
731
751
  }
752
+ let restartFailure;
732
753
  if (childBroken && retriable && !this.shuttingDown) {
733
754
  try {
734
755
  await this.restartChild(mapped.entry);
@@ -736,9 +757,15 @@ export class MCPProxy {
736
757
  const retried = await mapped.entry.child.request('tools/call', upstreamParams);
737
758
  return retried.error ? { ...retried, id: message.id } : rpcResult(message.id, retried.result);
738
759
  } catch (restartError) {
760
+ restartFailure = restartError;
739
761
  this.eventSink(`[${mapped.entry.destination.name}] tools/call ${toolName} self-healing restart failed: ${diagnosticText(restartError.message)}`, 'error');
740
762
  }
741
763
  }
764
+ if (restartFailure) {
765
+ // The restart refusal (budget exhausted, shutdown) is the
766
+ // actionable cause; keep the original failure as context.
767
+ return rpcError(message.id, -32001, `Destination ${mapped.entry.destination.name} failed: ${error.message}; self-healing restart did not run: ${restartFailure.message}`);
768
+ }
742
769
  return error.rpcError ? rpcError(message.id, error.rpcError.code || -32001, error.rpcError.message || error.message, error.rpcError.data) : rpcError(message.id, -32001, `Destination ${mapped.entry.destination.name} failed: ${error.message}`);
743
770
  }
744
771
  }