sap-ai-dev-toolkit 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,15 +41,17 @@ SAP AI Dev Toolkit connects your AI coding assistant to the SAP development tool
41
41
 
42
42
  ## ⚡ Quick start
43
43
 
44
+ The default installation is a single npx command — no global install, nothing left behind except the generated MCP configuration:
45
+
44
46
  ```sh
45
- npm install --global sap-ai-dev-toolkit
46
- sap-ai-dev --setup
47
+ npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
47
48
  ```
48
49
 
49
- Or run the whole toolkit once with npx — no global install, nothing left behind except the generated MCP configuration:
50
+ Prefer a persistent global install instead?
50
51
 
51
52
  ```sh
52
- npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
53
+ npm install --global sap-ai-dev-toolkit
54
+ sap-ai-dev --setup
53
55
  ```
54
56
 
55
57
  To also add optional full-stack SAP companion MCP servers for Fiori, UI5, CAP, and browser validation, run:
@@ -248,17 +250,23 @@ Once the destination server is enabled in your chat's tools picker, ask for outc
248
250
 
249
251
  ## 📦 Installation
250
252
 
251
- Install globally from SAP Business Application Studio or from a local workstation with SAP GUI installed, then run setup explicitly for your environment:
253
+ The default installation is a single npx command that runs the guided setup directly through npm — no global install required. It works in SAP Business Application Studio and on local workstations alike:
254
+
255
+ ```sh
256
+ npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
257
+ ```
258
+
259
+ Prefer a persistent global install? Install globally from SAP Business Application Studio or from a local workstation with SAP GUI installed, then run setup explicitly for your environment:
252
260
 
253
261
  | Environment / device | What to run | What setup does |
254
262
  | --- | --- | --- |
255
- | **SAP Business Application Studio dev space** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers BAS destinations, optionally imports Cloud Foundry Destination service records, and writes VS Code/BAS MCP entries. |
256
- | **Windows workstation with SAP GUI** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries. Windows SSO can be selected if the ADT endpoint supports Negotiate/SPNEGO. |
257
- | **macOS workstation with SAP GUI** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries with secure login prompts. |
263
+ | **Any environment (default — no global install)** | `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` | Runs the guided setup through npm and writes generated entries that launch the pinned package through `npx` (`cmd /c npx` on Windows). Add `--npx` to force the npx launcher even when a global `sap-ai-dev` command exists. |
264
+ | **SAP Business Application Studio dev space (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers BAS destinations, optionally imports Cloud Foundry Destination service records, and writes VS Code/BAS MCP entries. |
265
+ | **Windows workstation with SAP GUI (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries. Windows SSO can be selected if the ADT endpoint supports Negotiate/SPNEGO. |
266
+ | **macOS workstation with SAP GUI (global install)** | `npm install --global sap-ai-dev-toolkit`<br>`sap-ai-dev --setup` | Discovers SAP GUI landscape entries, asks for SAP client(s) and ADT URL, then writes local VS Code MCP entries with secure login prompts. |
258
267
  | **Any local interactive terminal, already installed** | `sap-ai-dev --setup` | Re-runs the wizard to add, update, or remove generated MCP entries. Plain `sap-ai-dev` with no arguments also starts local setup when no BAS environment is detected. |
259
268
  | **npm install hook did not show a wizard** | `sap-ai-dev --setup` | This is expected on some npm/Windows combinations because lifecycle scripts may run without an interactive console. The explicit setup command is the reliable path. |
260
269
  | **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. |
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
270
 
263
271
  ### 🪟 Windows notes
264
272
 
@@ -267,6 +275,7 @@ Everything works natively on Windows — PowerShell, cmd, and Windows Terminal a
267
275
  - `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
276
  - The pinned VSP runtime ships inside the package as a native `vsp-win32-x64.exe`; no Go toolchain, WSL, or extra downloads are required.
269
277
  - MCP entries that launch through npm are written as `cmd /c npx ...` on Windows — the form VS Code, Claude Code, and Cursor can spawn there. Entries written on Linux/macOS use plain `npx`.
278
+ - Setup reads `mcp.json` even when Windows tools saved it with a UTF-8/UTF-16 BOM (PowerShell `>`/`Set-Content`, Notepad "UTF-8 with BOM") and rewrites it as plain UTF-8. If the existing file cannot be used as-is, setup self-heals before falling back to a reset: VS Code-style comments and trailing commas (JSONC) are recovered in place, torn reads from a concurrent editor save are re-read once, and broken `servers`/`inputs` fields are repaired individually. Every lossy change first backs up the original next to the real file as `mcp.json.healed|repaired|invalid-<timestamp>.bak` (exclusive create, so reruns never overwrite an earlier backup; symlinked configs keep their link). `sap-ai-dev --doctor` stays strict and reports such files instead of rewriting them — run `sap-ai-dev --setup` to heal.
270
279
  - 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.
271
280
 
272
281
  If you previously installed the old package, remove it first because the legacy `bas-vsp-mcp` command is no longer shipped:
@@ -848,6 +857,8 @@ Runtime diagnostics take the stderr lane. In the MCP server's Output view, check
848
857
 
849
858
  If `GetSystemInfo` or `RunQuery` reports a CSRF-token fetch failure, VSP tries `/sap/bc/adt/core/discovery` first and falls back to `/sap/bc/adt/discovery` when the core endpoint returns HTTP 404 or 5xx without a token. If both endpoints fail, the error includes their HTTP statuses; check BAS destination routing and the SAP ADT service. `GetInstalledComponents` is a separate GET request and a failure there is not evidence of a CSRF problem.
850
859
 
860
+ **`MCP config ... contains invalid JSON`** usually means one of three things, all healed automatically by setup: the file starts with a byte-order mark (Windows PowerShell `>` redirection, `Set-Content`, or Notepad's "UTF-8 with BOM" mode — invisible in editors and terminals), it contains comments or trailing commas (VS Code reads `mcp.json` as JSONC), or setup raced an editor save and read a torn file (retried once before healing). Unrecoverable content is backed up as `mcp.json.<kind>-<timestamp>.bak` and replaced; the backup path is printed in the setup warning. On older versions, re-save the file as UTF-8 without BOM (VS Code: "Change File Encoding → Save with Encoding → UTF-8") and rerun `sap-ai-dev --setup`.
861
+
851
862
  If automatic Go or VSP provisioning fails, check network access and the package's supported platform. You can install Go manually or set `GO_BINARY` as an explicit fallback. A trusted prebuilt VSP can be supplied with `SAP_AI_DEV_TOOLKIT_BINARY`.
852
863
 
853
864
  ## 🧑‍💻 Development
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
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",
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { harnessById, harnessRoot } from './harnesses.mjs';
6
+ import { decodeJsonFileBuffer } from './mcp-config.mjs';
6
7
 
7
8
  const MANAGED_ENV = 'SAP_AI_DEV_TOOLKIT_MANAGED';
8
9
 
@@ -24,13 +25,15 @@ function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = p
24
25
  async function readJsonConfig(path) {
25
26
  let raw;
26
27
  try {
27
- raw = await readFile(path, 'utf8');
28
+ raw = await readFile(path);
28
29
  } catch (error) {
29
30
  if (error.code === 'ENOENT') return {};
30
31
  throw error;
31
32
  }
32
33
  try {
33
- const parsed = JSON.parse(raw);
34
+ // decodeJsonFileBuffer tolerates the UTF-8 BOM / UTF-16 encodings that
35
+ // Windows tools commonly write (see mcp-config.mjs).
36
+ const parsed = JSON.parse(decodeJsonFileBuffer(raw));
34
37
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('top-level value is not an object');
35
38
  return parsed;
36
39
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
- import { access, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { setTimeout as sleep } from 'node:timers/promises';
4
5
  import { homedir, platform } from 'node:os';
5
6
  import { basename, delimiter, dirname, join, resolve } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
@@ -79,6 +80,26 @@ async function exists(path) {
79
80
  }
80
81
  }
81
82
 
83
+ // Windows tools (PowerShell `>` redirection and Set-Content, Notepad "UTF-8
84
+ // with BOM", several editors) write mcp.json with a byte-order mark or even
85
+ // UTF-16. JSON.parse rejects a leading BOM (rendering invisibly as
86
+ // "Unexpected token ''"), so decode defensively before parsing.
87
+ export function decodeJsonFileBuffer(buffer) {
88
+ if (!Buffer.isBuffer(buffer)) return String(buffer ?? '').replace(/^/, '');
89
+ if (buffer.length >= 2) {
90
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) return buffer.subarray(2).toString('utf16le');
91
+ if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
92
+ let body = buffer.subarray(2);
93
+ if (body.length % 2) body = body.subarray(0, body.length - 1);
94
+ return Buffer.from(body).swap16().toString('utf16le');
95
+ }
96
+ }
97
+ const utf8 = (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF)
98
+ ? buffer.subarray(3).toString('utf8')
99
+ : buffer.toString('utf8');
100
+ return utf8.replace(/^\uFEFF/, '');
101
+ }
102
+
82
103
  export async function resolveMcpServerCommand(env = process.env) {
83
104
  const executableNames = process.platform === 'win32'
84
105
  ? ['sap-ai-dev.cmd', 'sap-ai-dev.exe', 'sap-ai-dev']
@@ -336,36 +357,186 @@ export function collectCloudFoundryKeyReferencesFromAllEntries(config) {
336
357
  return collectCloudFoundryKeyReferences(config, false);
337
358
  }
338
359
 
339
- async function readConfig(path) {
340
- let raw;
360
+ function freshConfig() {
361
+ return { servers: {}, inputs: [] };
362
+ }
363
+
364
+ // VS Code parses mcp.json as JSONC: // and /* */ comments plus trailing
365
+ // commas are accepted there but rejected by strict JSON.parse. Strip them in
366
+ // one string-aware pass (string literals are copied verbatim, so "//", "/*",
367
+ // and "," inside values survive) so self-healing can recover hand-written
368
+ // content instead of resetting the file.
369
+ export function loosenJsonText(text) {
370
+ let out = '';
371
+ let i = 0;
372
+ const n = text.length;
373
+ while (i < n) {
374
+ const ch = text[i];
375
+ if (ch === '"') {
376
+ let j = i + 1;
377
+ while (j < n && text[j] !== '"') j += text[j] === '\\' ? 2 : 1;
378
+ out += text.slice(i, Math.min(j + 1, n));
379
+ i = Math.min(j + 1, n);
380
+ continue;
381
+ }
382
+ if (ch === '/' && text[i + 1] === '/') {
383
+ i += 2;
384
+ while (i < n && text[i] !== '\n' && text[i] !== '\r') i += 1;
385
+ continue;
386
+ }
387
+ if (ch === '/' && text[i + 1] === '*') {
388
+ i += 2;
389
+ while (i < n && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
390
+ i = Math.min(i + 2, n);
391
+ continue;
392
+ }
393
+ if (ch === ',') {
394
+ // A trailing comma is dropped only when nothing but whitespace and
395
+ // comments separate it from a closing bracket.
396
+ let k = i + 1;
397
+ while (k < n) {
398
+ const lookahead = text[k];
399
+ if (lookahead === ' ' || lookahead === '\t' || lookahead === '\n' || lookahead === '\r') { k += 1; continue; }
400
+ if (lookahead === '/' && text[k + 1] === '/') { k += 2; while (k < n && text[k] !== '\n' && text[k] !== '\r') k += 1; continue; }
401
+ if (lookahead === '/' && text[k + 1] === '*') { k += 2; while (k < n && !(text[k] === '*' && text[k + 1] === '/')) k += 1; k = Math.min(k + 2, n); continue; }
402
+ break;
403
+ }
404
+ if (text[k] === '}' || text[k] === ']') { i += 1; continue; }
405
+ }
406
+ out += ch;
407
+ i += 1;
408
+ }
409
+ return out;
410
+ }
411
+
412
+ export function backupPathCandidates(path, kind, now = new Date()) {
413
+ const stamp = new Date(now).toISOString().replace(/[:.]/g, '-');
414
+ return ['', '-2', '-3', '-4', '-5'].map(suffix => `${path}.${kind}-${stamp}${suffix}.bak`);
415
+ }
416
+
417
+ // Backups use exclusive creation (wx) so a repeated setup can never clobber
418
+ // an earlier backup; identical timestamps walk a numeric suffix instead.
419
+ // Failing to write the backup is fatal on purpose: healing must never risk
420
+ // losing the only copy of the original file.
421
+ export async function writeConfigBackup(path, buffer, { kind, now } = {}) {
422
+ let lastError;
423
+ for (const candidate of backupPathCandidates(path, kind, now)) {
424
+ try {
425
+ await writeFile(candidate, buffer, { flag: 'wx', mode: 0o600 });
426
+ return candidate;
427
+ } catch (error) {
428
+ if (error?.code !== 'EEXIST') {
429
+ throw new Error(`Could not back up ${path} before healing (${error.message}); the original file was left untouched.`);
430
+ }
431
+ lastError = error;
432
+ }
433
+ }
434
+ throw new Error(`Could not back up ${path}: no unique backup name was available (${lastError?.message}).`);
435
+ }
436
+
437
+ function shapeFailures(config) {
438
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
439
+ return [{ field: null, message: 'must contain a top-level JSON object' }];
440
+ }
441
+ const failures = [];
442
+ if (config.servers !== undefined && (!config.servers || typeof config.servers !== 'object' || Array.isArray(config.servers))) {
443
+ failures.push({ field: 'servers', message: 'has a non-object servers value' });
444
+ }
445
+ if (config.inputs !== undefined && config.inputs !== null && !Array.isArray(config.inputs)) {
446
+ failures.push({ field: 'inputs', message: 'has a non-array inputs value' });
447
+ }
448
+ return failures;
449
+ }
450
+
451
+ // Healing ladder (install/setup only; strict readers such as --doctor keep
452
+ // rejecting): BOM/UTF-16 decode -> torn-write re-read -> JSONC recovery ->
453
+ // field-level repair -> full reset. Every lossy step backs up the original
454
+ // first; a backup failure aborts rather than risking data loss.
455
+ async function readConfig(path, options = {}) {
456
+ const { recoverInvalid = false, warnings, retryDelayMs = 250, readFileImpl = readFile } = options;
457
+ let buffer;
341
458
  try {
342
- raw = await readFile(path, 'utf8');
459
+ buffer = await readFileImpl(path);
343
460
  } catch (error) {
344
- if (error?.code === 'ENOENT') return { servers: {}, inputs: [] };
461
+ if (error?.code === 'ENOENT') return freshConfig();
345
462
  throw new Error(`MCP config ${path} could not be read: ${error.message}`);
346
463
  }
347
- let config;
348
- try {
349
- config = JSON.parse(raw);
350
- } catch (error) {
351
- throw new Error(`MCP config ${path} contains invalid JSON: ${error.message}`);
464
+ let text = decodeJsonFileBuffer(buffer);
465
+ if (!text.trim()) return freshConfig();
466
+ const tryParse = candidate => {
467
+ try { return { config: JSON.parse(candidate) }; } catch (error) { return { error }; }
468
+ };
469
+ let parsed = tryParse(text);
470
+ if (parsed.error && retryDelayMs > 0) {
471
+ // A torn read — setup racing an editor's non-atomic save — looks exactly
472
+ // like corruption; re-read once before healing so a transient state is
473
+ // never backed up and replaced.
474
+ await sleep(retryDelayMs);
475
+ const second = await readFileImpl(path).catch(() => null);
476
+ if (second && !second.equals(buffer)) {
477
+ buffer = second;
478
+ text = decodeJsonFileBuffer(second);
479
+ if (!text.trim()) return freshConfig();
480
+ parsed = tryParse(text);
481
+ }
352
482
  }
353
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
354
- throw new Error(`MCP config ${path} must contain a top-level JSON object`);
483
+ let loosened = false;
484
+ if (parsed.error && recoverInvalid) {
485
+ const recovered = tryParse(loosenJsonText(text));
486
+ if (!recovered.error) {
487
+ parsed = recovered;
488
+ loosened = true;
489
+ }
355
490
  }
356
- if (config.servers !== undefined && (!config.servers || typeof config.servers !== 'object' || Array.isArray(config.servers))) {
357
- throw new Error(`MCP config ${path} has a non-object servers value`);
491
+ const failures = parsed.error ? [{ field: null, message: `contains invalid JSON: ${parsed.error.message}` }] : shapeFailures(parsed.config);
492
+ if (failures.some(failure => failure.field === null)) {
493
+ const detail = failures[0].message;
494
+ if (!recoverInvalid) throw new Error(`MCP config ${path} ${detail}`);
495
+ return recoverConfig(path, buffer, warnings, {
496
+ kind: 'invalid',
497
+ notes: [`was not usable as JSON (${detail.replace(/^contains invalid JSON: /, '')}) and was replaced with a fresh config`],
498
+ config: freshConfig()
499
+ });
358
500
  }
359
- return { ...config, servers: config.servers || {} };
501
+ const blocking = failures.find(failure => failure.field !== 'inputs');
502
+ if (!recoverInvalid) {
503
+ if (blocking) throw new Error(`MCP config ${path} ${blocking.message}`);
504
+ return { ...parsed.config, servers: parsed.config.servers || {} };
505
+ }
506
+ const notes = [];
507
+ if (loosened) notes.push('contained comments or trailing commas (accepted by VS Code, not strict JSON)');
508
+ const healed = { ...parsed.config };
509
+ for (const failure of failures) {
510
+ if (failure.field === 'servers') { healed.servers = {}; notes.push('had a non-object "servers" value, which was reset'); }
511
+ if (failure.field === 'inputs') { delete healed.inputs; notes.push('had a non-array "inputs" value, which was removed'); }
512
+ }
513
+ if (!notes.length) return { ...parsed.config, servers: parsed.config.servers || {} };
514
+ return recoverConfig(path, buffer, warnings, {
515
+ kind: loosened ? 'healed' : 'repaired',
516
+ notes,
517
+ config: { ...healed, servers: healed.servers || {} }
518
+ });
519
+ }
520
+
521
+ async function recoverConfig(path, buffer, warnings, { kind, notes, config }) {
522
+ // Back up beside the real file (following symlinks) so a linked mcp.json
523
+ // keeps its link and the backup sits next to the actual target.
524
+ const backupBase = await realpath(path).catch(() => path);
525
+ const backup = await writeConfigBackup(backupBase, buffer, { kind });
526
+ warnings?.push(`The existing MCP config ${notes.join(' and ')}; the original was backed up to ${backup}.`);
527
+ return config;
360
528
  }
361
529
 
362
530
  async function writeConfig(path, config) {
363
- const directory = dirname(path);
531
+ // Follow symlinks so a linked mcp.json (dotfiles managers) keeps its link
532
+ // while the real target is replaced atomically.
533
+ const target = await realpath(path).catch(() => path);
534
+ const directory = dirname(target);
364
535
  await mkdir(directory, { recursive: true });
365
- const temporary = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
536
+ const temporary = join(directory, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
366
537
  try {
367
538
  await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
368
- await rename(temporary, path);
539
+ await rename(temporary, target);
369
540
  } catch (error) {
370
541
  await rm(temporary, { force: true }).catch(() => {});
371
542
  throw new Error(`MCP config ${path} could not be written: ${error.message}`);
@@ -380,7 +551,11 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
380
551
  }
381
552
  const env = options.env || process.env;
382
553
  const path = options.path || await resolveMcpConfigPath(env);
383
- const config = await readConfig(path);
554
+ const installWarnings = [];
555
+ // recoverInvalid: BOM/encoding damage, VS Code JSONC comments, torn writes
556
+ // racing an editor save, and corrupt configs are healed (with a backup)
557
+ // instead of failing the whole setup (self-healing).
558
+ const config = await readConfig(path, { recoverInvalid: true, warnings: installWarnings });
384
559
  const selfVersion = await runningPackageVersion();
385
560
  const companionVersions = selfVersion ? { 'sap-ai-dev-toolkit': selfVersion, 'hana-cloud-inspector': selfVersion } : {};
386
561
  const generated = {
@@ -434,7 +609,7 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
434
609
  config.inputs = [...existingInputs, ...generatedInputs.values()];
435
610
  }
436
611
  await writeConfig(path, config);
437
- return { path, servers: generated };
612
+ return { path, servers: generated, warnings: installWarnings };
438
613
  }
439
614
 
440
615
  export async function repairManagedMcpConfig(discoveredDestinations, { env = process.env, path, discoveryComplete = false, packageVersion } = {}) {
@@ -499,6 +674,6 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
499
674
  return { path: configPath, changed: true, repaired, managedEntries };
500
675
  }
501
676
 
502
- export async function readMcpConfig(path) {
503
- return readConfig(path);
677
+ export async function readMcpConfig(path, options = {}) {
678
+ return readConfig(path, options);
504
679
  }
package/src/setup.mjs CHANGED
@@ -531,6 +531,7 @@ export async function runSetup({
531
531
  const configuredCount = selected.length + sapDevelopmentServers.length;
532
532
  print(output, formatStatus(`Configured ${configuredCount} MCP server${configuredCount === 1 ? '' : 's'}${location}.`, 'success', output, isBas ? 'BAS setup' : 'Local setup'));
533
533
  printConnectionInstructions(output, result);
534
+ warnings.push(...(Array.isArray(result?.warnings) ? result.warnings.filter(message => typeof message === 'string') : []));
534
535
  let cleanupWarnings = [];
535
536
  const finalPath = result?.path || configPath;
536
537
  if (isBas) {