sap-ai-dev-toolkit 0.2.7 → 0.3.1

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/src/launcher.mjs CHANGED
@@ -9,8 +9,10 @@ import { MCPProxy } from './mcp-proxy.mjs';
9
9
  import { installMcpConfig } from './mcp-config.mjs';
10
10
  import { runSetup } from './setup.mjs';
11
11
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
12
+ import { withBrandedEnvironment } from './branding.mjs';
12
13
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
13
14
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
15
+ const runtimeEnv = withBrandedEnvironment(process.env);
14
16
 
15
17
  function hasFlag(name) { return process.argv.slice(2).includes(name); }
16
18
  function json(value) { process.stdout.write(`${JSON.stringify(value)}\n`); }
@@ -19,7 +21,7 @@ function usage() {
19
21
  return [
20
22
  'Usage: sap-ai-dev [options]',
21
23
  '',
22
- 'With H2O_URL set and no command, starts the BAS MCP stdio server.',
24
+ 'With H2O_URL set and no command, starts the SAP AI Dev Toolkit stdio server for selected BAS destinations.',
23
25
  'Options:',
24
26
  ' --setup Configure generated BAS MCP servers',
25
27
  ' --tools Also offer full-stack SAP companion MCP servers (use with --setup)',
@@ -27,6 +29,9 @@ function usage() {
27
29
  ' --list-destinations List discovered BAS destinations',
28
30
  ' --list-destinations --json Print redacted JSON status',
29
31
  ' --check Probe destination availability',
32
+ ' --doctor Check destination, VSP, SAP, and MCP connectivity',
33
+ ' --doctor --json Print redacted JSON diagnostics',
34
+ ' --demo Start an isolated, offline SAP MCP playground',
30
35
  ' --help, -h Show this help and exit',
31
36
  '',
32
37
  'Use an MCP client to call server tools; the BAS runtime uses stdio for MCP.',
@@ -34,6 +39,86 @@ function usage() {
34
39
  ].join('\n');
35
40
  }
36
41
 
42
+ function doctorRow(name, stage, status, detail) {
43
+ return { name, stage, status, ...(detail ? { detail } : {}) };
44
+ }
45
+
46
+ function redactText(value) {
47
+ return String(value).replace(/(authorization|cookie|password|secret|token)\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]');
48
+ }
49
+
50
+ async function runDoctor(destinations) {
51
+ const checks = [];
52
+ if (!destinations.length) {
53
+ return { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', 'No destinations were found. Check H2O_URL or the configured Cloud Foundry destination.')] };
54
+ }
55
+ let binary;
56
+ let binaryError;
57
+ try { binary = await binaryOrError(); }
58
+ catch (error) { binaryError = error.message; }
59
+
60
+ for (const destination of destinations) {
61
+ const probe = destination.probe;
62
+ const probeStatus = !probe || probe.status === 'skipped' ? 'skipped' : (probe.available === true ? 'passed' : 'failed');
63
+ checks.push(doctorRow(destination.name, 'ADT probe', probeStatus, probe?.status || 'No BAS ADT probe was supplied; GetSystemInfo will check the route.'));
64
+ checks.push(doctorRow(destination.name, 'VSP binary', binary ? 'passed' : 'failed', binary ? 'installed' : binaryError));
65
+ if (!binary) {
66
+ checks.push(doctorRow(destination.name, 'VSP MCP startup', 'skipped', 'The VSP binary is unavailable.'));
67
+ checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'VSP MCP startup could not run.'));
68
+ checks.push(doctorRow(destination.name, 'MCP tools/list', 'skipped', 'VSP MCP startup could not run.'));
69
+ try { await destination.close?.(); } catch {}
70
+ continue;
71
+ }
72
+ const proxy = new MCPProxy({ binary, destinations: [destination], env: runtimeEnv, log: message => console.error(message) });
73
+ try {
74
+ proxy.start();
75
+ const initialized = await proxy.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } });
76
+ if (initialized?.error) throw new Error(initialized.error.message || 'MCP initialization failed');
77
+ checks.push(doctorRow(destination.name, 'VSP MCP startup', 'passed', 'initialized'));
78
+ const listed = await proxy.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
79
+ if (listed?.error) throw new Error(listed.error.message || 'MCP tools/list failed');
80
+ const tools = listed.result?.tools || [];
81
+ const localNames = new Set(['LintABAP', 'GetApplicationLog', 'PrepareABAPChangeSet', 'ApplyABAPChangeSet', 'CheckTransportReadiness', 'PlanABAPCloudMigration', 'GenerateRAPRegressionSuite', 'RunRAPRegressionSuite']);
82
+ const upstreamCount = tools.filter(tool => !localNames.has(tool.name.slice(tool.name.indexOf('__') + 2))).length;
83
+ checks.push(doctorRow(destination.name, 'MCP tools/list', upstreamCount ? 'passed' : 'failed', `${upstreamCount} VSP tools available`));
84
+ const systemInfo = tools.find(tool => tool.name.endsWith('__GetSystemInfo'));
85
+ if (!systemInfo) {
86
+ checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'GetSystemInfo is not exposed by this VSP mode.'));
87
+ } else {
88
+ const inspected = await proxy.handle({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: systemInfo.name, arguments: {} } });
89
+ const failed = inspected?.error || inspected?.result?.isError;
90
+ checks.push(doctorRow(destination.name, 'SAP system check', failed ? 'failed' : 'passed', failed ? redactText(inspected?.error?.message || inspected?.result?.content?.[0]?.text || 'GetSystemInfo failed') : 'GetSystemInfo returned successfully'));
91
+ }
92
+ } catch (error) {
93
+ const detail = redactText(error.message || error).slice(0, 300);
94
+ const initialized = checks.some(row => row.name === destination.name && row.stage === 'VSP MCP startup' && row.status === 'passed');
95
+ if (!initialized) checks.push(doctorRow(destination.name, 'VSP MCP startup', 'failed', detail));
96
+ else checks.push(doctorRow(destination.name, 'MCP tools/list', 'failed', detail));
97
+ checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'An earlier diagnostic step failed.'));
98
+ } finally {
99
+ await proxy.close();
100
+ }
101
+ }
102
+ const required = checks.filter(check => check.status !== 'skipped');
103
+ return { ok: required.every(check => check.status === 'passed'), destinations: destinations.length, checks };
104
+ }
105
+
106
+ async function runOfflineDemo() {
107
+ const demoPath = join(root, 'src', 'demo-vsp.mjs');
108
+ const proxy = new MCPProxy({
109
+ binary: process.execPath,
110
+ childArgs: () => [demoPath],
111
+ destinations: [{ name: 'demo', url: 'http://demo.dest', client: '001', demo: true }],
112
+ env: { ...runtimeEnv, SAP_AI_DEV_TOOLKIT_DESTINATION: 'demo' },
113
+ log: message => console.error(message)
114
+ });
115
+ console.error('[sap-ai-dev-toolkit] offline demo active; all SAP changes are simulated in memory');
116
+ const shutdown = signal => { void proxy.close().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143)); };
117
+ process.once('SIGINT', () => shutdown('SIGINT'));
118
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
119
+ await proxy.serve(process.stdin);
120
+ }
121
+
37
122
  function probeDiagnostic(destination) {
38
123
  const probe = destination.probe || {};
39
124
  const details = [];
@@ -49,14 +134,14 @@ function probeDiagnostic(destination) {
49
134
  }
50
135
 
51
136
  async function binaryOrError() {
52
- const path = await findBinary(pkg);
137
+ const path = await findBinary(pkg, { env: runtimeEnv });
53
138
  if (path) return path;
54
- throw new Error('VSP binary is not installed. Reinstall with scripts enabled, set BAS_VSP_BINARY to a trusted patched binary, or run npm run build:vsp.');
139
+ throw new Error('VSP binary is not installed. Reinstall with scripts enabled, set SAP_AI_DEV_TOOLKIT_BINARY to a trusted patched binary, or run npm run build:vsp.');
55
140
  }
56
141
 
57
142
  async function discoverForCommand() {
58
- const env = { ...process.env };
59
- if (env.BAS_VSP_DESTINATION_SOURCE === 'cloud-foundry') env.BAS_VSP_DESTINATION = '';
143
+ const env = { ...runtimeEnv };
144
+ if (env.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry') env.SAP_AI_DEV_TOOLKIT_DESTINATION = '';
60
145
  return discoverDestinations({ env });
61
146
  }
62
147
 
@@ -64,10 +149,32 @@ async function main() {
64
149
  const setup = hasFlag('--setup');
65
150
  const check = hasFlag('--check');
66
151
  const list = hasFlag('--list-destinations');
152
+ const doctor = hasFlag('--doctor');
153
+ const demo = hasFlag('--demo');
67
154
  if (hasFlag('--help') || hasFlag('-h')) {
68
155
  process.stdout.write(usage());
69
156
  return;
70
157
  }
158
+ if (doctor && demo) throw new Error('Use either --doctor or --demo, not both.');
159
+ if (demo) {
160
+ await runOfflineDemo();
161
+ return;
162
+ }
163
+ if (doctor) {
164
+ let report;
165
+ try {
166
+ const destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
167
+ ? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
168
+ : await discoverForCommand();
169
+ report = await runDoctor(destinations);
170
+ } catch (error) {
171
+ report = { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', redactText(error.message || error).slice(0, 300))] };
172
+ }
173
+ if (hasFlag('--json')) json(report);
174
+ else for (const row of report.checks) console.log(`[${row.status.toUpperCase()}] ${row.name} — ${row.stage}: ${row.detail || ''}`);
175
+ if (!report.ok) process.exitCode = 1;
176
+ return;
177
+ }
71
178
  if (setup) {
72
179
  const setupOptions = {
73
180
  includeSapDevelopmentToolsPrompt: hasFlag('--tools') || hasFlag('--companion-tools')
@@ -93,12 +200,12 @@ async function main() {
93
200
  return;
94
201
  }
95
202
 
96
- if (process.env.BAS_VSP_DESTINATION_SOURCE === 'cloud-foundry') {
97
- const destination = await resolveConfiguredCloudFoundryDestination({ env: process.env });
203
+ if (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry') {
204
+ const destination = await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv });
98
205
  let proxy;
99
206
  try {
100
207
  const binary = await binaryOrError();
101
- proxy = new MCPProxy({ binary, destinations: [destination], env: process.env, log: message => console.error(message) });
208
+ proxy = new MCPProxy({ binary, destinations: [destination], env: runtimeEnv, log: message => console.error(message) });
102
209
  const shutdown = signal => { void proxy.close().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143)); };
103
210
  process.once('SIGINT', () => shutdown('SIGINT'));
104
211
  process.once('SIGTERM', () => shutdown('SIGTERM'));
@@ -111,9 +218,9 @@ async function main() {
111
218
  return;
112
219
  }
113
220
 
114
- if (!process.env.H2O_URL) {
221
+ if (!runtimeEnv.H2O_URL) {
115
222
  const binary = await binaryOrError();
116
- const child = spawn(binary, process.argv.slice(2), { env: process.env, stdio: 'inherit' });
223
+ const child = spawn(binary, process.argv.slice(2), { env: runtimeEnv, stdio: 'inherit' });
117
224
  process.once('SIGINT', () => child.kill('SIGINT'));
118
225
  process.once('SIGTERM', () => child.kill('SIGTERM'));
119
226
  const [code, signal] = await new Promise(resolve => child.once('exit', (exitCode, exitSignal) => resolve([exitCode, exitSignal])));
@@ -122,7 +229,7 @@ async function main() {
122
229
  return;
123
230
  }
124
231
 
125
- const discovered = await discoverDestinations({ env: process.env });
232
+ const discovered = await discoverDestinations({ env: runtimeEnv });
126
233
  for (const destination of discovered) console.error(probeDiagnostic(destination));
127
234
  const destinations = discovered;
128
235
  if (!destinations.length) {
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, dirname, join } from 'node:path';
5
+ import { brandedEnvValue } from './branding.mjs';
5
6
 
6
7
  const LEGACY_MCP_SERVER_PREFIXES = ['sapAiDev_', 'basVspMcp_'];
7
8
  const COMPANION_SERVER_KIND = 'sap-development-companion';
@@ -52,8 +53,8 @@ async function exists(path) {
52
53
  }
53
54
 
54
55
  export async function resolveMcpConfigPath(env = process.env) {
55
- if (env.SAP_AI_DEV_MCP_CONFIG) return env.SAP_AI_DEV_MCP_CONFIG;
56
- if (env.BAS_VSP_MCP_CONFIG) return env.BAS_VSP_MCP_CONFIG;
56
+ const configuredPath = brandedEnvValue(env, 'MCP_CONFIG');
57
+ if (configuredPath) return configuredPath;
57
58
  const home = env.HOME || homedir();
58
59
  const candidates = [
59
60
  join(home, '.vscode', 'data', 'User', 'mcp.json'),
@@ -91,7 +92,7 @@ export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SE
91
92
 
92
93
  export function buildMcpEntries(destinations, env = process.env) {
93
94
  const h2oUrl = env.H2O_URL;
94
- if (!h2oUrl) throw new Error('H2O_URL is required to write BAS MCP configuration');
95
+ if (!h2oUrl) throw new Error('H2O_URL is required to write SAP AI Dev Toolkit configuration');
95
96
  const entries = Object.create(null);
96
97
  const selected = [...destinations].sort((a, b) => String(a.serverName || a.name).localeCompare(String(b.serverName || b.name)));
97
98
  for (const destination of selected) {
@@ -106,8 +107,8 @@ export function buildMcpEntries(destinations, env = process.env) {
106
107
  const cf = destination.cf;
107
108
  if (!cf) throw new Error(`Cloud Foundry destination "${destination.name}" is missing service references`);
108
109
  Object.assign(entryEnv, {
109
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
110
- BAS_VSP_DESTINATION: String(destination.name),
110
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
111
+ SAP_AI_DEV_TOOLKIT_DESTINATION: String(destination.name),
111
112
  BAS_CF_SPACE_GUID: String(cf.spaceGuid),
112
113
  BAS_CF_DESTINATION_INSTANCE_GUID: String(cf.destinationInstanceGuid),
113
114
  BAS_CF_DESTINATION_INSTANCE: String(cf.destinationInstanceName),
@@ -122,7 +123,7 @@ export function buildMcpEntries(destinations, env = process.env) {
122
123
  });
123
124
  }
124
125
  } else {
125
- entryEnv.BAS_VSP_DESTINATION = String(destination.name);
126
+ entryEnv.SAP_AI_DEV_TOOLKIT_DESTINATION = String(destination.name);
126
127
  }
127
128
  entries[name] = {
128
129
  type: 'stdio',
@@ -139,7 +140,7 @@ function isCompanionServer(entry) {
139
140
  }
140
141
 
141
142
  function isPackageLauncher(entry) {
142
- const commands = new Set(['sap-ai-dev', 'bas-vsp-mcp']);
143
+ const commands = new Set(['sap-ai-dev', 'sap-ai-dev-toolkit', 'bas-vsp-mcp']);
143
144
  const packages = [/^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/, /^--package=bas-mcp-addon(?:@[^/]+)?$/];
144
145
  const npxLauncher = entry?.command === 'npx'
145
146
  && Array.isArray(entry.args)
@@ -148,11 +149,19 @@ function isPackageLauncher(entry) {
148
149
  return entry?.BAS_EXT === 'true' && (commands.has(entry?.command) || npxLauncher);
149
150
  }
150
151
 
152
+ function destinationValue(env) {
153
+ return env?.SAP_AI_DEV_TOOLKIT_DESTINATION ?? env?.BAS_VSP_DESTINATION;
154
+ }
155
+
156
+ function destinationSource(env) {
157
+ return env?.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE ?? env?.BAS_VSP_DESTINATION_SOURCE;
158
+ }
159
+
151
160
  function collectCloudFoundryKeyReferences(config, managedOnly) {
152
161
  const refs = new Map();
153
162
  for (const entry of Object.values(config?.servers || {})) {
154
163
  const e = entry?.env;
155
- if (!e || (managedOnly && (e.BAS_VSP_DESTINATION_SOURCE !== 'cloud-foundry' || !isPackageLauncher(entry)))) continue;
164
+ if (!e || (managedOnly && (destinationSource(e) !== 'cloud-foundry' || !isPackageLauncher(entry)))) continue;
156
165
  const candidates = [
157
166
  { kind: 'destination', spaceGuid: e.BAS_CF_SPACE_GUID, instanceGuid: e.BAS_CF_DESTINATION_INSTANCE_GUID, instanceName: e.BAS_CF_DESTINATION_INSTANCE, keyName: e.BAS_CF_DESTINATION_KEY },
158
167
  { kind: 'connectivity', spaceGuid: e.BAS_CF_SPACE_GUID, instanceGuid: e.BAS_CF_CONNECTIVITY_INSTANCE_GUID, instanceName: e.BAS_CF_CONNECTIVITY_INSTANCE, keyName: e.BAS_CF_CONNECTIVITY_KEY }
@@ -232,8 +241,8 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
232
241
  const servers = Object.create(null);
233
242
  for (const [name, entry] of Object.entries(config.servers)) {
234
243
  const managed = isPackageLauncher(entry)
235
- && typeof entry?.env?.BAS_VSP_DESTINATION === 'string'
236
- && (entry.env.BAS_VSP_DESTINATION_SOURCE === 'cloud-foundry'
244
+ && typeof destinationValue(entry?.env) === 'string'
245
+ && (destinationSource(entry.env) === 'cloud-foundry'
237
246
  ? typeof entry.env.BAS_CF_DESTINATION_KEY === 'string'
238
247
  : entry.env.BAS_VSP_DESTINATION_SOURCE === undefined);
239
248
  if (!LEGACY_MCP_SERVER_PREFIXES.some(prefix => name.startsWith(prefix)) && !managed && !isCompanionServer(entry)) servers[name] = entry;
package/src/mcp-proxy.mjs CHANGED
@@ -2,6 +2,8 @@ import { spawn as nodeSpawn } from 'node:child_process';
2
2
  import { once } from 'node:events';
3
3
  import { sanitizeChildEnv, slugifyDestination } from './bas-discovery.mjs';
4
4
  import { ABAP_LINT_TOOL, runABAPLint } from './abaplint.mjs';
5
+ import { createEngineeringTools } from './engineering-tools.mjs';
6
+ import { brandedEnvValue } from './branding.mjs';
5
7
 
6
8
  const JSONRPC = '2.0';
7
9
  const FORWARDED_METHODS = new Set(['ping', 'resources/list', 'resources/read', 'resources/templates/list', 'prompts/list', 'completion/complete', 'logging/setLevel']);
@@ -64,7 +66,7 @@ function childEnvironment(destination, env) {
64
66
  export function childArguments(destination, env = process.env) {
65
67
  // The proxy allowlist keeps transport mutations to CreateTransport.
66
68
  // Transportable source edits are controlled by SAP_ALLOW_TRANSPORTABLE_EDITS.
67
- const mode = env.BAS_VSP_MODE || 'expert';
69
+ const mode = brandedEnvValue(env, 'MODE') || 'expert';
68
70
  const args = ['--url', destination.url, '--client', destination.client || '001', '--mode', mode];
69
71
  if (destination.source !== 'cloud-foundry' || destination.authentication === 'PrincipalPropagation') args.push('--proxy-auth');
70
72
  args.push('--enable-transports');
@@ -186,11 +188,12 @@ class Child {
186
188
  }
187
189
 
188
190
  export class MCPProxy {
189
- constructor({ binary, destinations, env = process.env, spawn = nodeSpawn, log = message => console.error(message), output = line => process.stdout.write(`${line}\n`) }) {
191
+ constructor({ binary, destinations, env = process.env, spawn = nodeSpawn, childArgs, log = message => console.error(message), output = line => process.stdout.write(`${line}\n`) }) {
190
192
  this.binary = binary;
191
193
  this.destinations = destinations;
192
194
  this.env = env;
193
195
  this.spawn = spawn;
196
+ this.childArgs = childArgs;
194
197
  this.log = log;
195
198
  this.output = output;
196
199
  this.children = [];
@@ -214,6 +217,7 @@ export class MCPProxy {
214
217
  child: new Child(this.binary, destination, {
215
218
  env: this.env,
216
219
  spawn: this.spawn,
220
+ args: this.childArgs?.(destination),
217
221
  log: this.log,
218
222
  onNotification: notification => {
219
223
  if (this.clientInitialized) this.output(JSON.stringify(notification));
@@ -264,7 +268,8 @@ export class MCPProxy {
264
268
  this.namespace.set(lintName, { handler: runABAPLint });
265
269
  merged.push({ ...ABAP_LINT_TOOL, name: lintName });
266
270
  try {
267
- for (const tool of await entry.child.listTools()) {
271
+ const upstreamTools = await entry.child.listTools();
272
+ for (const tool of upstreamTools) {
268
273
  if (tool.name === 'SAP') {
269
274
  const applicationLogName = `${slug}__GetApplicationLog`;
270
275
  this.namespace.set(applicationLogName, {
@@ -284,6 +289,15 @@ export class MCPProxy {
284
289
  this.namespace.set(name, { entry, upstream: tool.name });
285
290
  merged.push({ ...tool, name, description: `${tool.description || tool.name} [destination: ${entry.destination.name}]` });
286
291
  }
292
+ for (const localTool of createEngineeringTools(entry, upstreamTools, { env: this.env, log: this.log })) {
293
+ const name = `${slug}__${localTool.definition.name}`;
294
+ this.namespace.set(name, { handler: localTool.handler });
295
+ merged.push({
296
+ ...localTool.definition,
297
+ name,
298
+ description: `${localTool.definition.description} [destination: ${entry.destination.name}]`
299
+ });
300
+ }
287
301
  } catch (error) {
288
302
  this.log(`[${entry.destination.name}] tools/list failed: ${redactText(error.message)}`);
289
303
  }
@@ -301,7 +315,7 @@ export class MCPProxy {
301
315
  try {
302
316
  if (message.method === 'initialize') {
303
317
  await this.initializeChildren(message.params || {});
304
- return rpcResult(message.id, { protocolVersion: message.params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: this.env.BAS_VSP_DESTINATION || this.children[0].destination.name, version: '0.1.0' } });
318
+ return rpcResult(message.id, { protocolVersion: message.params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: brandedEnvValue(this.env, 'DESTINATION') || this.children[0].destination.name, version: '0.1.0' } });
305
319
  }
306
320
  if (!this.initialized) return rpcError(message.id, -32002, 'MCP proxy is not initialized');
307
321
  if (message.method === 'tools/list') return rpcResult(message.id, { tools: await this.mergedTools() });
package/src/setup.mjs CHANGED
@@ -72,7 +72,7 @@ function printConnectionInstructions(output, result) {
72
72
  const servers = Object.entries(result?.servers || {});
73
73
  if (!servers.length) {
74
74
  print(output, '');
75
- print(output, formatStatus('No destinations were selected; no MCP servers were added.', 'info', output, 'BAS setup'));
75
+ print(output, formatStatus('No destinations were selected; no MCP servers were added.', 'info', output, 'SAP AI Dev Toolkit'));
76
76
  return;
77
77
  }
78
78
  print(output, '');
@@ -99,11 +99,11 @@ export async function runSetup({
99
99
  includeSapDevelopmentToolsPrompt = false
100
100
  } = {}) {
101
101
  if (!env.H2O_URL) {
102
- print(output, formatStatus(`Setup skipped: H2O_URL is not set. Run ${SETUP_COMMAND} in a BAS dev space.`, 'info', output, 'BAS setup'));
102
+ print(output, formatStatus(`Setup skipped: H2O_URL is not set. Run ${SETUP_COMMAND} in a BAS dev space.`, 'info', output, 'SAP AI Dev Toolkit'));
103
103
  return { skipped: true, reason: 'non-bas' };
104
104
  }
105
105
  if (!input.isTTY || !output.isTTY) {
106
- print(output, formatStatus(`Interactive setup needs a terminal. Rerun ${SETUP_COMMAND} from a BAS terminal.`, 'warning', output, 'BAS setup'));
106
+ print(output, formatStatus(`Interactive setup needs a terminal. Rerun ${SETUP_COMMAND} from a BAS terminal.`, 'warning', output, 'SAP AI Dev Toolkit'));
107
107
  return { skipped: true, reason: 'non-tty' };
108
108
  }
109
109
  print(output, '');
@@ -111,7 +111,7 @@ export async function runSetup({
111
111
 
112
112
  let basDestinations;
113
113
  try {
114
- basDestinations = await discover({ env: { ...env, BAS_VSP_DESTINATION: '' } });
114
+ basDestinations = await discover({ env: { ...env, SAP_AI_DEV_TOOLKIT_DESTINATION: '' } });
115
115
  } catch (error) {
116
116
  throw new Error(`BAS discovery failed: ${error.message}`);
117
117
  }
@@ -146,7 +146,7 @@ export async function runSetup({
146
146
  destinations.push(...cfDestinations.map(safeCloudFoundryDestination));
147
147
  warnings.push(...(Array.isArray(result?.warnings) ? result.warnings.filter(value => typeof value === 'string') : []));
148
148
  } catch {
149
- warnings.push('Cloud Foundry import failed; BAS setup can continue.');
149
+ warnings.push('Cloud Foundry import failed; SAP AI Dev Toolkit setup can continue.');
150
150
  }
151
151
  } else if (!warnings.some(message => message.includes('Cloud Foundry import prompt failed'))) {
152
152
  warnings.push('Cloud Foundry destinations were skipped because import was not confirmed.');
@@ -269,4 +269,3 @@ export async function runSetup({
269
269
  throw new Error(`MCP config writing failed: ${error.message}`);
270
270
  }
271
271
  }
272
-
@@ -273,8 +273,8 @@ test('resolves a CF Basic destination only after matching the active space', asy
273
273
  const cf = mockCf({ origin });
274
274
  const env = {
275
275
  ...envForLocal(),
276
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
277
- BAS_VSP_DESTINATION: 'runtime-basic',
276
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
277
+ SAP_AI_DEV_TOOLKIT_DESTINATION: 'runtime-basic',
278
278
  BAS_CF_SPACE_GUID: SPACE_GUID,
279
279
  BAS_CF_DESTINATION_INSTANCE_GUID: DESTINATION_GUID,
280
280
  BAS_CF_DESTINATION_INSTANCE: DESTINATION_INSTANCE,
@@ -129,9 +129,9 @@ test('launches a CF Basic destination with per-child credentials and no proxy-au
129
129
  const env = {
130
130
  ...process.env,
131
131
  ...cfEnv,
132
- BAS_VSP_BINARY: fakeVsp,
133
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
134
- BAS_VSP_DESTINATION: 'cf-basic',
132
+ SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
133
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
134
+ SAP_AI_DEV_TOOLKIT_DESTINATION: 'cf-basic',
135
135
  BAS_CF_SPACE_GUID: SPACE_GUID,
136
136
  BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
137
137
  BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
@@ -268,9 +268,9 @@ test('routes a PrincipalPropagation MCP call through CF Connectivity to the SAP
268
268
  const result = await runLauncher({
269
269
  ...process.env,
270
270
  ...cfEnv,
271
- BAS_VSP_BINARY: fakeVsp,
272
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
273
- BAS_VSP_DESTINATION: 'pp-target',
271
+ SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
272
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
273
+ SAP_AI_DEV_TOOLKIT_DESTINATION: 'pp-target',
274
274
  BAS_CF_SPACE_GUID: SPACE_GUID,
275
275
  BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
276
276
  BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
@@ -420,9 +420,9 @@ test('routes a CF Basic OnPremise MCP call through the HTTP Connectivity proxy',
420
420
  const result = await runLauncher({
421
421
  ...process.env,
422
422
  ...cfEnv,
423
- BAS_VSP_BINARY: fakeVsp,
424
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
425
- BAS_VSP_DESTINATION: 'basic-onprem',
423
+ SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
424
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
425
+ SAP_AI_DEV_TOOLKIT_DESTINATION: 'basic-onprem',
426
426
  BAS_CF_SPACE_GUID: SPACE_GUID,
427
427
  BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
428
428
  BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
@@ -491,13 +491,13 @@ test('CF list-destinations clears the CF name filter and remains BAS-only', asyn
491
491
  const result = await runLauncher({
492
492
  ...process.env,
493
493
  ...cfEnv,
494
- BAS_VSP_DESTINATION_SOURCE: 'cloud-foundry',
495
- BAS_VSP_DESTINATION: 'cf-name-not-in-bas',
494
+ SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
495
+ SAP_AI_DEV_TOOLKIT_DESTINATION: 'cf-name-not-in-bas',
496
496
  H2O_URL: origin,
497
497
  HTTP_PROXY: '',
498
498
  HTTPS_PROXY: '',
499
499
  NO_PROXY: '127.0.0.1,localhost',
500
- BAS_VSP_SKIP_PROBE: 'true'
500
+ SAP_AI_DEV_TOOLKIT_SKIP_PROBE: 'true'
501
501
  }, ['--list-destinations', '--json']);
502
502
  assert.equal(result.code, 0, result.stderr);
503
503
  assert.deepEqual(JSON.parse(result.stdout), [{ name: 'bas-only', client: '101', authentication: 'Unknown', probe: 'skipped' }]);
@@ -1,6 +1,7 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { createHash } from 'node:crypto';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { dirname, join } from 'node:path';
6
7
  import { installUserCopilotAssets } from '../scripts/install-user-copilot-assets.mjs';
@@ -35,3 +36,28 @@ test('updates unchanged global assets and preserves user-edited customizations',
35
36
  assert.equal(await readFile(join(home, '.copilot', 'agents', 'abap-developer.agent.md'), 'utf8'), 'user customization');
36
37
  assert.equal(await readFile(join(home, '.copilot', 'skills', 'example', 'SKILL.md'), 'utf8'), 'skill revision two');
37
38
  });
39
+
40
+ test('migrates the previous managed-file manifest without losing update ownership', async t => {
41
+ const directory = await mkdtemp(join(tmpdir(), 'sap-ai-toolkit-assets-migration-'));
42
+ t.after(() => rm(directory, { recursive: true, force: true }));
43
+ const root = join(directory, 'package');
44
+ const home = join(directory, 'home');
45
+ const target = join(home, '.copilot', 'agents', 'abap-developer.agent.md');
46
+ const oldManifest = join(home, '.copilot', '.bas-mcp-addon-assets.json');
47
+ await writeAsset(root, 'agents/abap-developer.agent.md', 'managed revision one');
48
+ await writeAsset(root, 'skills/example/SKILL.md', 'skill revision one');
49
+ const first = await installUserCopilotAssets({ home, root });
50
+ assert.equal(first.installed, 2);
51
+
52
+ const managedContent = await readFile(target);
53
+ const managedHash = createHash('sha256').update(managedContent).digest('hex');
54
+ await rm(join(home, '.copilot', '.sap-ai-dev-toolkit-assets.json'));
55
+ await writeFile(oldManifest, JSON.stringify({ version: 1, files: { [join('agents', 'abap-developer.agent.md')]: managedHash } }));
56
+ await writeAsset(root, 'agents/abap-developer.agent.md', 'managed revision two');
57
+
58
+ const migrated = await installUserCopilotAssets({ home, root });
59
+ assert.equal(migrated.updated, 1);
60
+ assert.equal(await readFile(target, 'utf8'), 'managed revision two');
61
+ await assert.rejects(readFile(oldManifest), { code: 'ENOENT' });
62
+ assert.equal(JSON.parse(await readFile(join(home, '.copilot', '.sap-ai-dev-toolkit-assets.json'), 'utf8')).version, 1);
63
+ });
@@ -23,7 +23,7 @@ test('returns every named destination regardless of ADT probe and preserves allo
23
23
  const probeImpl = async url => ({ status: url.includes('a-system') ? 403 : url.includes('b-system') ? 200 : 404 });
24
24
  const allowlisted = await discoverDestinations({
25
25
  body,
26
- env: { BAS_VSP_DESTINATION: 'a-system,b-system' },
26
+ env: { SAP_AI_DEV_TOOLKIT_DESTINATION: 'a-system,b-system' },
27
27
  probeImpl
28
28
  });
29
29
  assert.deepEqual(allowlisted.map(item => [item.name, item.client, item.probe.status]), [
@@ -38,6 +38,15 @@ test('returns every named destination regardless of ADT probe and preserves allo
38
38
  ['z-system', 'not-found']
39
39
  ]);
40
40
  });
41
+
42
+ test('accepts the previous destination environment variable during upgrades', async () => {
43
+ const destinations = await discoverDestinations({
44
+ body: [{ Name: 'legacy-system' }, { Name: 'other-system' }],
45
+ env: { BAS_VSP_DESTINATION: 'legacy-system' },
46
+ skipProbe: true
47
+ });
48
+ assert.deepEqual(destinations.map(destination => destination.name), ['legacy-system']);
49
+ });
41
50
  test('parses sample-style destination records and enables every ADT heartbeat response', async () => {
42
51
  const body = [
43
52
  {