sap-ai-dev-toolkit 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -6
- package/package.json +2 -1
- package/scripts/postinstall.mjs +7 -3
- package/src/launcher.mjs +42 -12
- package/src/mcp-config.mjs +67 -22
- package/src/mcp-proxy.mjs +4 -1
- package/src/setup.mjs +8 -2
- package/src/terminal-ui.mjs +40 -0
package/README.md
CHANGED
|
@@ -46,6 +46,12 @@ npm install --global sap-ai-dev-toolkit
|
|
|
46
46
|
sap-ai-dev --setup
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Or run the whole toolkit once with npx — no global install, nothing left behind except the generated MCP configuration:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
To also add optional full-stack SAP companion MCP servers for Fiori, UI5, CAP, and browser validation, run:
|
|
50
56
|
|
|
51
57
|
```sh
|
|
@@ -252,7 +258,16 @@ Install globally from SAP Business Application Studio or from a local workstatio
|
|
|
252
258
|
| **Any local interactive terminal, already installed** | `sap-ai-dev --setup` | Re-runs the wizard to add, update, or remove generated MCP entries. Plain `sap-ai-dev` with no arguments also starts local setup when no BAS environment is detected. |
|
|
253
259
|
| **npm install hook did not show a wizard** | `sap-ai-dev --setup` | This is expected on some npm/Windows combinations because lifecycle scripts may run without an interactive console. The explicit setup command is the reliable path. |
|
|
254
260
|
| **Force npm to show install-time script output** | `npm install --global --foreground-scripts sap-ai-dev-toolkit` | Optional troubleshooting mode if you specifically want to see postinstall output during installation. Still run `sap-ai-dev --setup` afterwards if no destinations were configured. |
|
|
255
|
-
| **No global install / temporary setup** | `npx --yes --ignore-scripts
|
|
261
|
+
| **No global install / temporary setup** | `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` | Runs the guided setup through npm and writes generated entries that launch the pinned package through `npx` (`cmd /c npx` on Windows). Add `--npx` to force the npx launcher even when a global `sap-ai-dev` command exists. |
|
|
262
|
+
|
|
263
|
+
### 🪟 Windows notes
|
|
264
|
+
|
|
265
|
+
Everything works natively on Windows — PowerShell, cmd, and Windows Terminal are all supported:
|
|
266
|
+
|
|
267
|
+
- `npm install --global sap-ai-dev-toolkit` puts three commands on `PATH`: `sap-ai-dev`, its alias `sap-ai-dev-toolkit`, and `sap-ai-hana`. If `sap-ai-dev` is reported as unknown, npm's global bin directory is missing from `PATH` (run `npm prefix -g` to find it) or the install did not complete — `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` works without any global install.
|
|
268
|
+
- The pinned VSP runtime ships inside the package as a native `vsp-win32-x64.exe`; no Go toolchain, WSL, or extra downloads are required.
|
|
269
|
+
- MCP entries that launch through npm are written as `cmd /c npx ...` on Windows — the form VS Code, Claude Code, and Cursor can spawn there. Entries written on Linux/macOS use plain `npx`.
|
|
270
|
+
- Local SAP GUI discovery reads the Windows landscape files and registry, and setup can write an experimental Windows SSO (`Negotiate`/SPNEGO) entry; see the “Local machine SAP GUI support” section below.
|
|
256
271
|
|
|
257
272
|
If you previously installed the old package, remove it first because the legacy `bas-vsp-mcp` command is no longer shipped:
|
|
258
273
|
|
|
@@ -312,9 +327,9 @@ The same postinstall offers the bundled agents and skills for several AI coding
|
|
|
312
327
|
|
|
313
328
|
Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive terminal after installation.
|
|
314
329
|
|
|
315
|
-
The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. The table is a weather report, not a bouncer: green **PASS** means the ADT probe responded, red **FAIL** means it failed, and yellow **SKIPPED** means it was skipped. Probe failures do not block MCP registration or startup for destinations you select.
|
|
330
|
+
The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. Long-running steps (BAS destination discovery, ADT probes, VSP runtime provisioning, `--doctor` checks) show an animated progress line so the terminal never looks stuck; it appears only on interactive terminals and is disabled automatically on CI — set `SAP_AI_DEV_TOOLKIT_DISABLE_SCAN_ANIMATION=true` to turn it off. The table is a weather report, not a bouncer: green **PASS** means the ADT probe responded, red **FAIL** means it failed, and yellow **SKIPPED** means it was skipped. Probe failures do not block MCP registration or startup for destinations you select.
|
|
316
331
|
|
|
317
|
-
Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools.
|
|
332
|
+
Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools. Generated destination entries use the global `sap-ai-dev` command when it is on `PATH`; otherwise (for example after a pure npx setup) they automatically launch the pinned package version through `npx`, routed through `cmd /c npx` on Windows so every MCP host can spawn them. Pass `--npx` to force the npx launcher even when a global command exists; companion tools already use `npx` with their own npm packages.
|
|
318
333
|
|
|
319
334
|
If a setup step is skipped or fails, rerun it from an interactive terminal:
|
|
320
335
|
|
|
@@ -325,10 +340,10 @@ sap-ai-dev --setup
|
|
|
325
340
|
To configure without a global install, run the guided setup directly through npm:
|
|
326
341
|
|
|
327
342
|
```sh
|
|
328
|
-
npx --yes --ignore-scripts
|
|
343
|
+
npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
|
|
329
344
|
```
|
|
330
345
|
|
|
331
|
-
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection).
|
|
346
|
+
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Because no global `sap-ai-dev` command is on `PATH`, it writes MCP entries that launch the pinned package version through npx — through `cmd /c npx` on Windows, which is the form VS Code, Claude Code, and Cursor can spawn there. `--ignore-scripts` avoids running the install-time wizard a second time; add `--npx` to force this launcher shape even when the toolkit is installed globally. After setup, in BAS or VS Code run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
|
|
332
347
|
|
|
333
348
|
For a non-interactive installation or a platform without a published VSP asset, provide a trusted binary override:
|
|
334
349
|
|
|
@@ -423,7 +438,7 @@ Review skipped or conflicting files and merge changes manually. `LintABAP` analy
|
|
|
423
438
|
If the `cf` CLI is version 8.18 or newer and authenticated to a targeted space, setup offers an opt-in import from that space's SAP Destination service. It does not create service keys unless you accept. The imported records are limited to the current space and are merged with BAS destinations for selection. OnPremise destinations require choosing a Connectivity service instance; runtime traffic uses that service's proxy, and PrincipalPropagation uses the current CF user's token. Internet destinations use the configured HTTP(S) proxy environment. Setup stores only service-instance/key references in `mcp.json`, not the service-key credentials. At runtime the generated entry verifies the active CF space and resolves those references. Setup removes an add-on-created key only when no remaining CF entry references it and the CLI is targeted to the key's recorded space; keys are left untouched when the space cannot be verified.
|
|
424
439
|
Cloud Foundry HTTP destination probes use forward-form HTTP requests, not CONNECT tunnels; SAP Connectivity requires HTTP from the application to its proxy. OnPremise requests are canceled when the client disconnects or the runtime route closes.
|
|
425
440
|
|
|
426
|
-
Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination or local SAP GUI system selection without changing MCP config. Run the interactive setup command above;
|
|
441
|
+
Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination or local SAP GUI system selection without changing MCP config. Run the interactive setup command above; generated entries fall back to the pinned npx launcher automatically when the package is not installed globally.
|
|
427
442
|
|
|
428
443
|
Setup tidies its own footprint: it reconciles MCP entries managed by this package and removes legacy `sapAiDev_*` / `basVspMcp_*` entries from earlier releases. Existing unrelated MCP servers and top-level configuration such as `inputs` stay untouched.
|
|
429
444
|
|
|
@@ -857,6 +872,8 @@ NPM_PUBLISH_TOKEN=npm_...
|
|
|
857
872
|
- To publish a patch bump, run `npm run publish:npm -- --patch`; it updates `package.json` and `package-lock.json` before publishing. If publishing fails after the bump, retry without `--patch`.
|
|
858
873
|
- npm's `prepublishOnly` hook builds the pinned VSP binaries for all platforms (including the Go toolchain if missing) and refuses to ship a tarball whose `dist/checksums.txt` does not list all five assets; it guards `npm run publish:npm` and a plain `npm publish` alike. Pass `--skip-build` to republish an already-built `dist/` without rebuilding.
|
|
859
874
|
- Preview the package without publishing, changing its version, or building with `npm run publish:npm -- --dry-run`.
|
|
875
|
+
- GitHub Actions runs the test suite on pushes to `main` and pull requests. A push to `main` that changes the `package.json` version runs the same checks, builds the VSP binaries, publishes that version to npm, and creates a `v<version>` GitHub release with generated notes.
|
|
876
|
+
- The publish workflow uses the `NPM_PUBLISH_TOKEN` GitHub repository secret. Verify it with `gh secret list`; the token value is never stored in the repository.
|
|
860
877
|
|
|
861
878
|
The token is not printed or stored in the repository.
|
|
862
879
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-ai-dev-toolkit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
19
|
"sap-ai-dev": "src/launcher.mjs",
|
|
20
|
+
"sap-ai-dev-toolkit": "src/launcher.mjs",
|
|
20
21
|
"sap-ai-hana": "src/hana-inspector.mjs"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { installMcpServersForHarnesses } from '../src/harness-mcp-config.mjs';
|
|
|
9
9
|
import { DEFAULT_HARNESS_IDS, HARNESSES, HARNESS_ENV_VAR, NON_INTERACTIVE_HARNESS_IDS, envHarnessSelection, harnessRoot } from '../src/harnesses.mjs';
|
|
10
10
|
import { ReadStream as TTYReadStream, WriteStream as TTYWriteStream } from 'node:tty';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
|
-
import { checkboxPrompt, colorText, formatStatus } from '../src/terminal-ui.mjs';
|
|
12
|
+
import { checkboxPrompt, colorText, formatStatus, startProgress } from '../src/terminal-ui.mjs';
|
|
13
13
|
import { brandedEnvValue, withBrandedEnvironment } from '../src/branding.mjs';
|
|
14
14
|
import { generatedServerName } from '../src/mcp-config.mjs';
|
|
15
15
|
import { redactText } from '../src/redact.mjs';
|
|
@@ -232,7 +232,7 @@ async function announceSetup(result) {
|
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
if (result?.reason === 'non-tty') {
|
|
235
|
-
await announce('Destination selection was skipped because npm did not provide an interactive terminal.\nMCP config was not changed.\nRun sap-ai-dev --setup from an interactive
|
|
235
|
+
await announce('Destination selection was skipped because npm did not provide an interactive terminal.\nMCP config was not changed.\nRun sap-ai-dev --setup from an interactive terminal, or run npx --yes --ignore-scripts sap-ai-dev-toolkit --setup from any terminal.', 'warning');
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
238
238
|
if (result?.reason === 'no-destinations') {
|
|
@@ -304,11 +304,15 @@ async function main() {
|
|
|
304
304
|
if (brandedEnvValue(runtimeEnv, 'BINARY')) {
|
|
305
305
|
await announce('Using the SAP_AI_DEV_TOOLKIT_BINARY override.', 'info');
|
|
306
306
|
} else {
|
|
307
|
-
|
|
307
|
+
// The bundled binary resolves in milliseconds; the animation only
|
|
308
|
+
// appears when provisioning actually takes time (download fallback).
|
|
309
|
+
const stopBinaryProgress = startProgress('Preparing the pinned VSP runtime for this platform', { output: process.stderr, env: runtimeEnv });
|
|
308
310
|
try {
|
|
309
311
|
await installBinary(pkg, { env: runtimeEnv });
|
|
312
|
+
stopBinaryProgress();
|
|
310
313
|
await announce('Pinned VSP binary installed and ready.', 'success');
|
|
311
314
|
} catch (error) {
|
|
315
|
+
stopBinaryProgress();
|
|
312
316
|
throw new Error(`VSP binary provisioning failed: ${error.message}`);
|
|
313
317
|
}
|
|
314
318
|
}
|
package/src/launcher.mjs
CHANGED
|
@@ -7,13 +7,14 @@ import { discoverDestinations, remediation, slugifyDestination, statusRows } fro
|
|
|
7
7
|
import { discoverSapGuiSystems } from './local-sap-gui.mjs';
|
|
8
8
|
import { findBinary } from './binary.mjs';
|
|
9
9
|
import { MCPProxy } from './mcp-proxy.mjs';
|
|
10
|
-
import { installMcpConfig, repairManagedMcpConfig } from './mcp-config.mjs';
|
|
10
|
+
import { installMcpConfig, npxMcpLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
|
|
11
11
|
import { runSetup } from './setup.mjs';
|
|
12
12
|
import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
|
|
13
13
|
import { createWindowsSsoAdtProxy } from './windows-sso-adt-proxy.mjs';
|
|
14
14
|
import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
|
|
15
15
|
import { withBrandedEnvironment } from './branding.mjs';
|
|
16
16
|
import { redactText } from './redact.mjs';
|
|
17
|
+
import { startProgress } from './terminal-ui.mjs';
|
|
17
18
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
19
|
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
19
20
|
const runtimeEnv = withBrandedEnvironment(process.env);
|
|
@@ -53,7 +54,9 @@ function doctorRow(name, stage, status, detail) {
|
|
|
53
54
|
|
|
54
55
|
async function runDoctor(destinations) {
|
|
55
56
|
const checks = [];
|
|
57
|
+
const stopProgress = startProgress(destinations.length === 1 ? `Checking ${destinations[0]?.name || 'destination'}` : `Checking ${destinations.length} destinations`, { output: process.stderr, label: 'Doctor' });
|
|
56
58
|
if (!destinations.length) {
|
|
59
|
+
stopProgress();
|
|
57
60
|
return { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', 'No destinations were found. Check H2O_URL or the configured Cloud Foundry destination.')] };
|
|
58
61
|
}
|
|
59
62
|
let binary;
|
|
@@ -76,6 +79,7 @@ async function runDoctor(destinations) {
|
|
|
76
79
|
checks.push(doctorRow('-', 'MCP config repair', 'failed', redactText(error.message || error).slice(0, 300)));
|
|
77
80
|
}
|
|
78
81
|
for (const destination of destinations) {
|
|
82
|
+
stopProgress.update(`Checking ${destination.name}: ADT probe, VSP startup, tools/list`);
|
|
79
83
|
const probe = destination.probe;
|
|
80
84
|
const probeStatus = !probe || probe.status === 'skipped' ? 'skipped' : (probe.available === true ? 'passed' : 'failed');
|
|
81
85
|
checks.push(doctorRow(destination.name, 'ADT probe', probeStatus, probe?.status || 'No BAS ADT probe was supplied; GetSystemInfo will check the route.'));
|
|
@@ -126,6 +130,7 @@ async function runDoctor(destinations) {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
const required = checks.filter(check => check.status !== 'skipped');
|
|
133
|
+
stopProgress();
|
|
129
134
|
return { ok: required.every(check => check.status === 'passed'), destinations: destinations.length, checks };
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -253,11 +258,17 @@ async function main() {
|
|
|
253
258
|
if (doctor) {
|
|
254
259
|
let report;
|
|
255
260
|
try {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
const stopDiscoveryProgress = startProgress('Discovering SAP destinations', { output: process.stderr, env: runtimeEnv, label: 'Doctor' });
|
|
262
|
+
let destinations;
|
|
263
|
+
try {
|
|
264
|
+
destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
|
|
265
|
+
? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
|
|
266
|
+
: (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local'
|
|
267
|
+
? [await configuredLocalSapGuiDestination(runtimeEnv)]
|
|
268
|
+
: await discoverForCommand());
|
|
269
|
+
} finally {
|
|
270
|
+
stopDiscoveryProgress();
|
|
271
|
+
}
|
|
261
272
|
report = await runDoctor(destinations);
|
|
262
273
|
} catch (error) {
|
|
263
274
|
report = { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', redactText(error.message || error).slice(0, 300))] };
|
|
@@ -272,17 +283,26 @@ async function main() {
|
|
|
272
283
|
includeSapDevelopmentToolsPrompt: hasFlag('--tools') || hasFlag('--companion-tools')
|
|
273
284
|
};
|
|
274
285
|
if (hasFlag('--npx')) {
|
|
286
|
+
// Windows has no npx.exe, so generated entries there launch through
|
|
287
|
+
// `cmd /c npx`; other platforms use npx directly.
|
|
288
|
+
const launcher = npxMcpLauncher();
|
|
275
289
|
setupOptions.install = (selected, options) => installMcpConfig(selected, {
|
|
276
290
|
...options,
|
|
277
|
-
command:
|
|
278
|
-
args: ['--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit@${pkg.version}`, 'sap-ai-dev']
|
|
291
|
+
command: launcher.command,
|
|
292
|
+
args: [...launcher.prefixArgs, '--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit@${pkg.version}`, 'sap-ai-dev']
|
|
279
293
|
});
|
|
280
294
|
}
|
|
281
295
|
await runSetup(setupOptions);
|
|
282
296
|
return;
|
|
283
297
|
}
|
|
284
298
|
if (check || list) {
|
|
285
|
-
const
|
|
299
|
+
const stopProgress = startProgress('Discovering SAP destinations and probing ADT endpoints', { output: process.stderr, env: runtimeEnv });
|
|
300
|
+
let destinations;
|
|
301
|
+
try {
|
|
302
|
+
destinations = await discoverForCommand();
|
|
303
|
+
} finally {
|
|
304
|
+
stopProgress();
|
|
305
|
+
}
|
|
286
306
|
const statuses = statusRows(destinations);
|
|
287
307
|
if (list && hasFlag('--json')) json(statuses);
|
|
288
308
|
else for (const row of statuses) console.error(`${row.name}: client=${row.client} authentication=${row.authentication} probe=${row.probe}`);
|
|
@@ -319,11 +339,15 @@ async function main() {
|
|
|
319
339
|
return;
|
|
320
340
|
}
|
|
321
341
|
const binary = await binaryOrError();
|
|
322
|
-
// Windows cannot exec
|
|
342
|
+
// Windows cannot exec .js/.mjs files directly; route JavaScript entries
|
|
323
343
|
// (test fixtures) through the current Node binary like MCPProxy does.
|
|
344
|
+
// Windows .cmd/.bat shims (e.g. a SAP_AI_DEV_TOOLKIT_BINARY override)
|
|
345
|
+
// must go through cmd /c; Node refuses to spawn batch files directly.
|
|
324
346
|
const spawnTarget = /\.(?:mjs|cjs|js)$/i.test(binary)
|
|
325
347
|
? { command: process.execPath, prefixArgs: [binary] }
|
|
326
|
-
:
|
|
348
|
+
: (/\.(?:cmd|bat)$/i.test(binary)
|
|
349
|
+
? { command: 'cmd', prefixArgs: ['/c', binary] }
|
|
350
|
+
: { command: binary, prefixArgs: [] });
|
|
327
351
|
const child = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...process.argv.slice(2)], { env: runtimeEnv, stdio: 'inherit' });
|
|
328
352
|
process.once('SIGINT', () => child.kill('SIGINT'));
|
|
329
353
|
process.once('SIGTERM', () => child.kill('SIGTERM'));
|
|
@@ -336,7 +360,13 @@ async function main() {
|
|
|
336
360
|
return;
|
|
337
361
|
}
|
|
338
362
|
|
|
339
|
-
const
|
|
363
|
+
const stopProgress = startProgress('Discovering BAS destinations and probing ADT endpoints', { output: process.stderr, env: runtimeEnv });
|
|
364
|
+
let discovered;
|
|
365
|
+
try {
|
|
366
|
+
discovered = await discoverDestinations({ env: runtimeEnv });
|
|
367
|
+
} finally {
|
|
368
|
+
stopProgress();
|
|
369
|
+
}
|
|
340
370
|
for (const destination of discovered) logLine(probeDiagnostic(destination));
|
|
341
371
|
const destinations = discovered;
|
|
342
372
|
if (!destinations.length) {
|
package/src/mcp-config.mjs
CHANGED
|
@@ -98,6 +98,44 @@ export async function resolveMcpServerCommand(env = process.env) {
|
|
|
98
98
|
return 'sap-ai-dev';
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Windows ships npx as npx.cmd, not npx.exe: MCP hosts spawn the configured
|
|
102
|
+
// command directly without shell PATH resolution, so npx-launched servers
|
|
103
|
+
// must route through `cmd /c npx` there (the form VS Code, Claude Code, and
|
|
104
|
+
// Cursor document for Windows). Other platforms run npx directly.
|
|
105
|
+
export function npxMcpLauncher(platform = process.platform) {
|
|
106
|
+
return platform === 'win32'
|
|
107
|
+
? { command: 'cmd', prefixArgs: ['/c', 'npx'] }
|
|
108
|
+
: { command: 'npx', prefixArgs: [] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Normalizes the argument list of npx-launched entries, whether written as
|
|
112
|
+
// `npx ...` or the Windows `cmd /c npx ...` form; returns null when the entry
|
|
113
|
+
// is not npx-launched.
|
|
114
|
+
function npxLaunchArgs(entry) {
|
|
115
|
+
const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
|
|
116
|
+
if (entry?.command === 'npx') return args;
|
|
117
|
+
const commandName = typeof entry?.command === 'string'
|
|
118
|
+
? entry.command.slice(Math.max(entry.command.lastIndexOf('/'), entry.command.lastIndexOf('\\')) + 1)
|
|
119
|
+
: '';
|
|
120
|
+
if (/^cmd(?:\.exe)?$/i.test(commandName) && args[0]?.toLowerCase() === '/c' && /^npx(?:\.cmd)?$/i.test(args[1] || '')) {
|
|
121
|
+
return args.slice(2);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Resolves how generated destination entries launch the toolkit: the global
|
|
127
|
+
// sap-ai-dev command when it is on PATH, otherwise the pinned package
|
|
128
|
+
// through npx (routed through cmd /c on Windows) so entries written by a
|
|
129
|
+
// pure `npx sap-ai-dev-toolkit --setup` run still start on every host.
|
|
130
|
+
async function resolveLauncherInstallCommand(env) {
|
|
131
|
+
const resolved = await resolveMcpServerCommand(env);
|
|
132
|
+
if (resolved !== 'sap-ai-dev') return { command: resolved, args: null };
|
|
133
|
+
const { command, prefixArgs } = npxMcpLauncher();
|
|
134
|
+
const selfVersion = await runningPackageVersion();
|
|
135
|
+
const packageSpec = selfVersion ? `sap-ai-dev-toolkit@${selfVersion}` : 'sap-ai-dev-toolkit';
|
|
136
|
+
return { command, args: [...prefixArgs, '--yes', '--ignore-scripts', `--package=${packageSpec}`, 'sap-ai-dev'] };
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
export async function resolveMcpConfigPath(env = process.env) {
|
|
102
140
|
const configuredPath = [env.SAP_AI_DEV_MCP_CONFIG, brandedEnvValue(env, 'MCP_CONFIG')]
|
|
103
141
|
.find(value => typeof value === 'string' && value.length > 0);
|
|
@@ -138,16 +176,17 @@ export function generatedDestinationServerName(destination) {
|
|
|
138
176
|
return generatedServerName(destination?.source === 'cloud-foundry' ? destination.serverName : destination?.name);
|
|
139
177
|
}
|
|
140
178
|
|
|
141
|
-
export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SERVERS.map(server => server.id), { packageVersions = {}, packageManager = 'npx' } = {}) {
|
|
179
|
+
export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SERVERS.map(server => server.id), { packageVersions = {}, packageManager = 'npx', platform = process.platform } = {}) {
|
|
142
180
|
const selected = new Set(serverIds);
|
|
143
181
|
const entries = Object.create(null);
|
|
182
|
+
const launcher = packageManager === 'npx' ? npxMcpLauncher(platform) : { command: packageManager, prefixArgs: [] };
|
|
144
183
|
for (const server of SAP_DEVELOPMENT_MCP_SERVERS) {
|
|
145
184
|
if (!selected.has(server.id)) continue;
|
|
146
185
|
const packageSpec = packageVersions[server.id] || packageVersions[server.packageName] || server.packageName;
|
|
147
186
|
entries[server.id] = {
|
|
148
187
|
type: 'stdio',
|
|
149
|
-
command:
|
|
150
|
-
args: ['--yes', ...(server.ignoreScripts ? ['--ignore-scripts'] : []), `--package=${packageSpec}`, server.bin],
|
|
188
|
+
command: launcher.command,
|
|
189
|
+
args: [...launcher.prefixArgs, '--yes', ...(server.ignoreScripts ? ['--ignore-scripts'] : []), `--package=${packageSpec}`, server.bin],
|
|
151
190
|
BAS_EXT: 'true',
|
|
152
191
|
BAS_EXT_KIND: COMPANION_SERVER_KIND,
|
|
153
192
|
displayName: server.name,
|
|
@@ -232,16 +271,14 @@ function isPackageLauncher(entry) {
|
|
|
232
271
|
const legacyCommand = legacyCommands.has(entry?.command) || legacyCommands.has(commandName);
|
|
233
272
|
const currentPackages = [/^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/];
|
|
234
273
|
const legacyPackages = [/^--package=bas-mcp-addon(?:@[^/]+)?$/];
|
|
235
|
-
const hasPackage = patterns =>
|
|
236
|
-
|
|
237
|
-
const currentNpxLauncher =
|
|
238
|
-
&&
|
|
239
|
-
&&
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
&&
|
|
243
|
-
&& entry.args.some(argument => legacyCommands.has(argument))
|
|
244
|
-
&& hasPackage(legacyPackages);
|
|
274
|
+
const hasPackage = (arguments_, patterns) => arguments_.some(argument => patterns.some(pattern => pattern.test(argument)));
|
|
275
|
+
const npxArgs = npxLaunchArgs(entry);
|
|
276
|
+
const currentNpxLauncher = npxArgs !== null
|
|
277
|
+
&& npxArgs.some(argument => currentCommands.has(argument))
|
|
278
|
+
&& hasPackage(npxArgs, currentPackages);
|
|
279
|
+
const legacyNpxLauncher = npxArgs !== null
|
|
280
|
+
&& npxArgs.some(argument => legacyCommands.has(argument))
|
|
281
|
+
&& hasPackage(npxArgs, legacyPackages);
|
|
245
282
|
// Current launchers are treated as managed only when tagged by this add-on.
|
|
246
283
|
// Some early bas-mcp-addon entries were not tagged with BAS_EXT, though, and
|
|
247
284
|
// leaving them behind keeps advertising the old full VSP surface (~159 tools).
|
|
@@ -264,10 +301,10 @@ function isManagedBasDestinationEntry(entry) {
|
|
|
264
301
|
? entry.command.slice(Math.max(entry.command.lastIndexOf('/'), entry.command.lastIndexOf('\\')) + 1).replace(/\.(?:cmd|exe)$/i, '')
|
|
265
302
|
: '';
|
|
266
303
|
const currentCommand = currentCommands.has(entry?.command) || currentCommands.has(commandName);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
&&
|
|
270
|
-
&&
|
|
304
|
+
const npxArgs = npxLaunchArgs(entry);
|
|
305
|
+
const currentNpxLauncher = npxArgs !== null
|
|
306
|
+
&& npxArgs.some(argument => currentCommands.has(argument))
|
|
307
|
+
&& npxArgs.some(argument => /^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/.test(argument));
|
|
271
308
|
return entry?.BAS_EXT === 'true'
|
|
272
309
|
&& (currentCommand || currentNpxLauncher)
|
|
273
310
|
&& typeof destinationValue(entry?.env) === 'string'
|
|
@@ -356,12 +393,18 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
|
|
|
356
393
|
if (input?.id) generatedInputs.set(input.id, input);
|
|
357
394
|
}
|
|
358
395
|
}
|
|
359
|
-
|
|
396
|
+
let launcherCommand = options.command || null;
|
|
397
|
+
let launcherArgs = options.command ? options.args : null;
|
|
398
|
+
if (!launcherCommand && destinations.length) {
|
|
399
|
+
const resolved = await resolveLauncherInstallCommand(env);
|
|
400
|
+
launcherCommand = resolved.command;
|
|
401
|
+
launcherArgs = resolved.args;
|
|
402
|
+
}
|
|
360
403
|
if (launcherCommand) {
|
|
361
404
|
for (const entry of Object.values(generated)) {
|
|
362
405
|
if (isCompanionServer(entry)) continue;
|
|
363
406
|
entry.command = launcherCommand;
|
|
364
|
-
if (
|
|
407
|
+
if (launcherArgs?.length) entry.args = [...launcherArgs];
|
|
365
408
|
}
|
|
366
409
|
}
|
|
367
410
|
const servers = Object.create(null);
|
|
@@ -406,7 +449,7 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
|
|
|
406
449
|
const config = await readConfig(configPath);
|
|
407
450
|
const managedEntries = Object.values(config.servers).filter(isManagedBasDestinationEntry).length;
|
|
408
451
|
if (!basDestinations.size) return { path: configPath, changed: false, repaired: 0, managedEntries };
|
|
409
|
-
const
|
|
452
|
+
const localLauncher = await resolveLauncherInstallCommand(env);
|
|
410
453
|
let repaired = 0;
|
|
411
454
|
for (const [serverName, entry] of Object.entries(config.servers)) {
|
|
412
455
|
if (!isManagedBasDestinationEntry(entry)) continue;
|
|
@@ -430,14 +473,16 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
|
|
|
430
473
|
delete nextEnvironment.BAS_VSP_DESTINATION;
|
|
431
474
|
delete nextEnvironment.BAS_VSP_DESTINATION_SOURCE;
|
|
432
475
|
const nextEntry = { ...entry, type: 'stdio', env: nextEnvironment, BAS_EXT: 'true' };
|
|
433
|
-
if (entry
|
|
476
|
+
if (npxLaunchArgs(entry) !== null) {
|
|
434
477
|
if (packageVersion && Array.isArray(entry.args)) {
|
|
435
478
|
nextEntry.args = entry.args.map(argument => typeof argument === 'string' && /^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/.test(argument)
|
|
436
479
|
? `--package=sap-ai-dev-toolkit@${packageVersion}`
|
|
437
480
|
: argument);
|
|
438
481
|
}
|
|
439
482
|
} else {
|
|
440
|
-
nextEntry.command =
|
|
483
|
+
nextEntry.command = localLauncher.command;
|
|
484
|
+
if (localLauncher.args) nextEntry.args = [...localLauncher.args];
|
|
485
|
+
else delete nextEntry.args;
|
|
441
486
|
}
|
|
442
487
|
if (expectedName === serverName) {
|
|
443
488
|
if (JSON.stringify(nextEntry) === JSON.stringify(entry)) continue;
|
package/src/mcp-proxy.mjs
CHANGED
|
@@ -200,9 +200,12 @@ export function childArguments(destination, env = process.env) {
|
|
|
200
200
|
|
|
201
201
|
// Windows cannot exec a .js/.mjs file directly (no shebang support), so test
|
|
202
202
|
// fixtures written in JavaScript are launched through the current Node binary.
|
|
203
|
-
// The production VSP binary is a native executable and is unaffected
|
|
203
|
+
// The production VSP binary is a native executable and is unaffected; a
|
|
204
|
+
// Windows .cmd/.bat override needs cmd /c because Node refuses to spawn
|
|
205
|
+
// batch files directly (EINVAL since the CVE-2024-27980 hardening).
|
|
204
206
|
function spawnCommand(binary) {
|
|
205
207
|
if (/\.(?:mjs|cjs|js)$/i.test(binary)) return [process.execPath, binary];
|
|
208
|
+
if (/\.(?:cmd|bat)$/i.test(binary) && process.platform === 'win32') return ['cmd', '/c', binary];
|
|
206
209
|
return [binary];
|
|
207
210
|
}
|
|
208
211
|
|
package/src/setup.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
import { isIP } from 'node:net';
|
|
5
|
-
import { checkboxPrompt, colorText, formatStatus, selectPrompt, textPrompt } from './terminal-ui.mjs';
|
|
5
|
+
import { checkboxPrompt, colorText, formatStatus, selectPrompt, startProgress, textPrompt } from './terminal-ui.mjs';
|
|
6
6
|
import { discoverDestinations, remediation } from './bas-discovery.mjs';
|
|
7
7
|
import { defaultAdtUrl, discoverSapGuiSystems } from './local-sap-gui.mjs';
|
|
8
8
|
import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
|
|
@@ -237,15 +237,21 @@ export async function runSetup({
|
|
|
237
237
|
return { skipped: true, reason: 'non-tty' };
|
|
238
238
|
}
|
|
239
239
|
print(output, '');
|
|
240
|
-
print(output, formatStatus(
|
|
240
|
+
if (!isBas) print(output, formatStatus('Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
|
|
241
241
|
|
|
242
242
|
let destinations = [];
|
|
243
243
|
if (isBas) {
|
|
244
244
|
let basDestinations;
|
|
245
|
+
// BAS discovery probes every destination's ADT endpoint (up to a 5s
|
|
246
|
+
// timeout each); the animated progress line keeps the terminal visibly
|
|
247
|
+
// alive from the first moment instead of looking stuck.
|
|
248
|
+
const stopDiscoveryProgress = startProgress('Contacting BAS to discover destinations', { output, env, label: 'Setup' });
|
|
245
249
|
try {
|
|
246
250
|
basDestinations = await discover({ env: { ...env, SAP_AI_DEV_TOOLKIT_DESTINATION: '' } });
|
|
247
251
|
} catch (error) {
|
|
248
252
|
throw new Error(`BAS discovery failed: ${error.message}`);
|
|
253
|
+
} finally {
|
|
254
|
+
stopDiscoveryProgress();
|
|
249
255
|
}
|
|
250
256
|
destinations = basDestinations.map(safeBasDestination);
|
|
251
257
|
} else {
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -401,3 +401,43 @@ export async function textPrompt({ message, secret = false, required = true, pla
|
|
|
401
401
|
}
|
|
402
402
|
});
|
|
403
403
|
}
|
|
404
|
+
|
|
405
|
+
// Animated progress line for long-running steps (destination discovery,
|
|
406
|
+
// ADT probes, VSP binary provisioning). TTY outputs get spinner frames on an
|
|
407
|
+
// unref'd timer that starts after a short delay, so steps that finish quickly
|
|
408
|
+
// never flash anything. Non-TTY outputs (pipes, MCP hosts, --json callers,
|
|
409
|
+
// CI) stay completely silent so machine consumers never see progress noise.
|
|
410
|
+
// Returns stop(); on a TTY, stop() clears the line so it never pollutes
|
|
411
|
+
// scrollback. stop.update(message) retargets the label while running (for
|
|
412
|
+
// example per destination during --doctor).
|
|
413
|
+
export function startProgress(initialMessage, { output = process.stdout, env = process.env, label = 'sap-ai-dev-toolkit' } = {}) {
|
|
414
|
+
const noop = Object.assign(() => {}, { update: () => {} });
|
|
415
|
+
if (!output || typeof output.write !== 'function') return noop;
|
|
416
|
+
const animationDisabled = env.TERM === 'dumb' || env.CI || env.SAP_AI_DEV_TOOLKIT_DISABLE_SCAN_ANIMATION === 'true';
|
|
417
|
+
if (!output.isTTY || !Number.isFinite(output.columns) || animationDisabled) return noop;
|
|
418
|
+
let message = String(initialMessage);
|
|
419
|
+
let index = 0;
|
|
420
|
+
let active = false;
|
|
421
|
+
let interval;
|
|
422
|
+
const render = () => {
|
|
423
|
+
active = true;
|
|
424
|
+
index += 1;
|
|
425
|
+
const columns = Math.max(20, output.columns || 80);
|
|
426
|
+
const dots = `.${'.'.repeat(index % 3)}${' '.repeat(2 - index % 3)}`;
|
|
427
|
+
const line = `${label}: ${message} ${dots}`.slice(0, Math.max(1, columns - 1));
|
|
428
|
+
output.write(`\r\u001b[2K${line}`);
|
|
429
|
+
};
|
|
430
|
+
const delay = setTimeout(() => {
|
|
431
|
+
render();
|
|
432
|
+
interval = setInterval(render, 120);
|
|
433
|
+
if (typeof interval.unref === 'function') interval.unref();
|
|
434
|
+
}, 150);
|
|
435
|
+
if (typeof delay.unref === 'function') delay.unref();
|
|
436
|
+
const stop = () => {
|
|
437
|
+
clearTimeout(delay);
|
|
438
|
+
if (interval) clearInterval(interval);
|
|
439
|
+
if (active) output.write('\r\u001b[2K');
|
|
440
|
+
};
|
|
441
|
+
stop.update = next => { message = String(next); };
|
|
442
|
+
return stop;
|
|
443
|
+
}
|