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.
- package/.github/agents/abap-developer.agent.md +7 -7
- package/.github/agents/abap-runtime-debugger.agent.md +4 -4
- package/.github/agents/hana-cloud-hdi-specialist.agent.md +1 -1
- package/.github/agents/rap-service-developer.agent.md +5 -5
- package/.github/agents/sap-solution-architect.agent.md +2 -2
- package/.github/skills/abap-debugging/SKILL.md +1 -1
- package/.github/skills/abap-development/SKILL.md +3 -3
- package/.github/skills/abap-runtime-analysis/SKILL.md +2 -2
- package/.github/skills/abap-testing-quality/SKILL.md +3 -3
- package/.github/skills/cds-development/SKILL.md +3 -3
- package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
- package/.github/skills/hana-cloud-inspection/SKILL.md +2 -2
- package/.github/skills/hana-cloud-native-development/SKILL.md +1 -1
- package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
- package/.github/skills/rap-development/SKILL.md +3 -3
- package/.github/skills/rap-service-delivery/SKILL.md +2 -2
- package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
- package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
- package/.github/skills/sap-transport-release/SKILL.md +2 -2
- package/README.md +31 -26
- package/inventory.md +5 -3
- package/package.json +1 -1
- package/src/bas-destination-relay.mjs +13 -41
- package/src/launcher.mjs +11 -17
- package/src/mcp-proxy.mjs +56 -29
- package/src/setup.mjs +2 -110
- package/src/terminal-ui.mjs +3 -1
- package/tools.md +10 -2
- package/src/credential-overrides.mjs +0 -75
- 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.
|
|
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,
|
|
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,
|
|
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
|
|
274
|
+
The installer handles VSP provisioning automatically:
|
|
274
275
|
|
|
275
|
-
-
|
|
276
|
-
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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`.
|
|
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.
|
|
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
|
|
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`
|
|
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`.
|
|
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
|
|
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
|
|
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 (
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
-
| `
|
|
741
|
-
| `
|
|
742
|
-
| `
|
|
743
|
-
| `
|
|
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. **
|
|
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
|
|
10
|
-
| Local lint tool | 1 per destination | `LintABAP` analyzes caller-supplied ABAP source in memory; it is exposed publicly as
|
|
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>_
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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',
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
?
|
|
222
|
-
: await
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
633
|
-
|
|
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
|
}
|