sap-ai-dev-toolkit 0.3.4 → 0.3.7

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.
@@ -0,0 +1,202 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { once } from 'node:events';
5
+ import { readFile } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const launcher = fileURLToPath(new URL('../src/launcher.mjs', import.meta.url));
11
+ const LIVE = /^(1|true|yes)$/iu.test(process.env.SAP_AI_DEV_LIVE_S4H || '');
12
+ const DESTINATION = process.env.SAP_AI_DEV_LIVE_DESTINATION || 'S4H';
13
+ const SLUG = process.env.SAP_AI_DEV_LIVE_DESTINATION_SLUG || DESTINATION.toLowerCase().replace(/[^a-z0-9_-]+/gu, '-');
14
+ const REQUEST_TIMEOUT_MS = Number(process.env.SAP_AI_DEV_LIVE_TIMEOUT_MS || 45_000);
15
+
16
+ async function readConfiguredServer() {
17
+ if (/^(0|false|no)$/iu.test(process.env.SAP_AI_DEV_LIVE_USE_MCP_CONFIG || '')) return null;
18
+ const candidates = [
19
+ process.env.SAP_AI_DEV_MCP_CONFIG,
20
+ process.env.BAS_VSP_MCP_CONFIG,
21
+ join(homedir(), '.vscode', 'data', 'User', 'mcp.json'),
22
+ join(homedir(), '.vscode-server', 'data', 'User', 'mcp.json'),
23
+ join(homedir(), '.code-server', 'data', 'User', 'mcp.json')
24
+ ].filter(Boolean);
25
+ for (const path of candidates) {
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(await readFile(path, 'utf8'));
29
+ } catch {
30
+ continue;
31
+ }
32
+ const server = parsed?.servers?.[DESTINATION];
33
+ if (server?.command) return { path, server };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ class StdioMcpClient {
39
+ constructor(command, args, env) {
40
+ this.child = spawn(command, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
41
+ this.nextId = 1;
42
+ this.pending = new Map();
43
+ this.stdout = '';
44
+ this.stderr = '';
45
+ this.notifications = [];
46
+ this.child.stdout.setEncoding('utf8');
47
+ this.child.stderr.setEncoding('utf8');
48
+ this.child.stdout.on('data', chunk => this.#onStdout(chunk));
49
+ this.child.stderr.on('data', chunk => { this.stderr += chunk; });
50
+ this.child.on('exit', (code, signal) => {
51
+ const error = new Error(`MCP process exited (${code ?? signal})\n${this.stderr.slice(-4000)}`);
52
+ for (const pending of this.pending.values()) pending.reject(error);
53
+ this.pending.clear();
54
+ });
55
+ }
56
+
57
+ #onStdout(chunk) {
58
+ this.stdout += chunk;
59
+ let newline;
60
+ while ((newline = this.stdout.indexOf('\n')) >= 0) {
61
+ const line = this.stdout.slice(0, newline).trim();
62
+ this.stdout = this.stdout.slice(newline + 1);
63
+ if (!line) continue;
64
+ let message;
65
+ try {
66
+ message = JSON.parse(line);
67
+ } catch (error) {
68
+ for (const pending of this.pending.values()) pending.reject(new Error(`Invalid JSON-RPC line: ${line}`));
69
+ this.pending.clear();
70
+ return;
71
+ }
72
+ if (message.id === undefined || message.id === null) {
73
+ this.notifications.push(message);
74
+ continue;
75
+ }
76
+ const pending = this.pending.get(message.id);
77
+ if (!pending) continue;
78
+ this.pending.delete(message.id);
79
+ clearTimeout(pending.timeout);
80
+ if (message.error) pending.reject(Object.assign(new Error(message.error.message), { response: message }));
81
+ else pending.resolve(message.result);
82
+ }
83
+ }
84
+
85
+ request(method, params) {
86
+ const id = this.nextId++;
87
+ const payload = { jsonrpc: '2.0', id, method, ...(params === undefined ? {} : { params }) };
88
+ return new Promise((resolve, reject) => {
89
+ const timeout = setTimeout(() => {
90
+ this.pending.delete(id);
91
+ reject(new Error(`Timed out waiting for ${method}\n${this.stderr.slice(-4000)}`));
92
+ }, REQUEST_TIMEOUT_MS);
93
+ this.pending.set(id, { resolve, reject, timeout });
94
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
95
+ });
96
+ }
97
+
98
+ notify(method, params) {
99
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, ...(params === undefined ? {} : { params }) })}\n`);
100
+ }
101
+
102
+ async close() {
103
+ this.child.stdin.end();
104
+ this.child.kill('SIGTERM');
105
+ await Promise.race([
106
+ once(this.child, 'exit').catch(() => {}),
107
+ new Promise(resolve => setTimeout(resolve, 1500))
108
+ ]);
109
+ if (!this.child.killed) this.child.kill('SIGKILL');
110
+ }
111
+ }
112
+
113
+ function textPayload(result) {
114
+ return (result?.content || []).filter(item => item.type === 'text').map(item => item.text).join('\n');
115
+ }
116
+
117
+ async function withLiveClient(t) {
118
+ const configured = await readConfiguredServer();
119
+ const server = configured?.server;
120
+ const command = process.env.SAP_AI_DEV_LIVE_COMMAND || server?.command || process.execPath;
121
+ const args = process.env.SAP_AI_DEV_LIVE_COMMAND
122
+ ? (process.env.SAP_AI_DEV_LIVE_ARGS ? JSON.parse(process.env.SAP_AI_DEV_LIVE_ARGS) : [])
123
+ : (server?.command ? (server.args || []) : [launcher]);
124
+ const client = new StdioMcpClient(command, args, {
125
+ ...process.env,
126
+ ...(server?.env || {}),
127
+ SAP_AI_DEV_TOOLKIT_DESTINATION: server?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION || DESTINATION,
128
+ SAP_AI_DEV_TOOLKIT_MODE: process.env.SAP_AI_DEV_TOOLKIT_MODE || server?.env?.SAP_AI_DEV_TOOLKIT_MODE || 'expert',
129
+ SAP_ALLOW_TRANSPORTABLE_EDITS: process.env.SAP_ALLOW_TRANSPORTABLE_EDITS || server?.env?.SAP_ALLOW_TRANSPORTABLE_EDITS || 'true'
130
+ });
131
+ t.after(() => client.close());
132
+ const initialized = await client.request('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'sap-ai-dev-live-test', version: '1.0.0' } });
133
+ client.notify('notifications/initialized');
134
+ assert.equal(initialized.serverInfo.name, DESTINATION);
135
+ return client;
136
+ }
137
+
138
+ test('live S4H MCP exposes a curated ABAP lifecycle surface under 60 tools', { skip: LIVE ? false : 'set SAP_AI_DEV_LIVE_S4H=1 to run against a live BAS/S4H destination' }, async t => {
139
+ const client = await withLiveClient(t);
140
+ const listed = await client.request('tools/list', {});
141
+ const tools = listed.tools || [];
142
+ const names = new Set(tools.map(tool => tool.name));
143
+ const prefix = `${SLUG}__`;
144
+
145
+ assert.ok(tools.length > 25, `expected a useful ABAP lifecycle surface, got ${tools.length}`);
146
+ assert.ok(tools.length < 60, `curated destination tool count must stay below 60, got ${tools.length}`);
147
+ assert.ok(tools.every(tool => tool.name.startsWith(prefix)), 'every tool is destination-prefixed for the selected S4H server');
148
+
149
+ for (const required of [
150
+ 'LintABAP', 'GetSystemInfo', 'GetConnectionInfo', 'GetFeatures',
151
+ 'SearchObject', 'GrepObjects', 'GrepPackages', 'GetSource', 'WriteSource', 'EditSource', 'CompareSource',
152
+ 'SyntaxCheck', 'PrettyPrint', 'Activate', 'RunUnitTests', 'RunATCCheck', 'GetInactiveObjects',
153
+ 'GetClass', 'GetProgram', 'GetPackage', 'GetTable', 'GetTableContents', 'RunQuery',
154
+ 'GetTransport', 'GetTransportInfo', 'ListTransports', 'CreateTransport',
155
+ 'GetApplicationLog', 'PrepareABAPChangeSet', 'ApplyABAPChangeSet', 'CheckTransportReadiness', 'PlanABAPCloudMigration'
156
+ ]) {
157
+ assert.ok(names.has(`${prefix}${required}`), `expected curated lifecycle tool ${required}`);
158
+ }
159
+
160
+ for (const hidden of [
161
+ 'SAP', 'ReleaseTransport', 'DeleteTransport', 'DeleteObject', 'ExecuteABAP', 'CallRFC',
162
+ 'ListSQLTraces', 'AnalyzeABAPCode', 'CreateObject', 'CreateClassWithTests', 'WriteClass', 'UpdateSource'
163
+ ]) {
164
+ assert.equal(names.has(`${prefix}${hidden}`), false, `broad/destructive tool ${hidden} must remain hidden`);
165
+ }
166
+ });
167
+
168
+ test('live S4H MCP handles safe local and read-only SAP calls', { skip: LIVE ? false : 'set SAP_AI_DEV_LIVE_S4H=1 to run against a live BAS/S4H destination' }, async t => {
169
+ const client = await withLiveClient(t);
170
+ const prefix = `${SLUG}__`;
171
+ await client.request('tools/list', {});
172
+
173
+ const lint = await client.request('tools/call', {
174
+ name: `${prefix}LintABAP`,
175
+ arguments: { files: [{ filename: 'zlive_probe.prog.abap', source: "REPORT zlive_probe.\nWRITE / 'ok'.\n" }] }
176
+ });
177
+ const lintJson = JSON.parse(textPayload(lint));
178
+ assert.equal(lintJson.filesChecked, 1);
179
+
180
+ for (const [tool, args] of [
181
+ ['GetSystemInfo', {}],
182
+ ['GetConnectionInfo', {}],
183
+ ['GetFeatures', {}],
184
+ ['GetInstalledComponents', {}]
185
+ ]) {
186
+ const result = await client.request('tools/call', { name: `${prefix}${tool}`, arguments: args });
187
+ assert.notEqual(result.isError, true, `${tool} returned an MCP tool error: ${textPayload(result)}`);
188
+ }
189
+
190
+ const query = await client.request('tools/call', {
191
+ name: `${prefix}RunQuery`,
192
+ arguments: { sql_query: 'SELECT * FROM T000', max_rows: 1 }
193
+ });
194
+ assert.notEqual(query.isError, true, `RunQuery returned an MCP tool error: ${textPayload(query)}`);
195
+ assert.match(textPayload(query), /T000|MANDT|client|rows|\[/iu);
196
+
197
+ const appLog = await client.request('tools/call', {
198
+ name: `${prefix}GetApplicationLog`,
199
+ arguments: { max_results: 1 }
200
+ });
201
+ assert.notEqual(appLog.isError, true, `GetApplicationLog returned an MCP tool error: ${textPayload(appLog)}`);
202
+ });
@@ -100,6 +100,17 @@ test('builds SAP development companion MCP entries', () => {
100
100
  assert.throws(() => buildSapDevelopmentMcpEntries(['missing-tool']), /Unknown SAP development MCP server id/);
101
101
  });
102
102
 
103
+ test('configures the HANA inspector without persisting credentials', () => {
104
+ const entry = buildSapDevelopmentMcpEntries(['hana-cloud-inspector'])['hana-cloud-inspector'];
105
+ assert.equal(entry.command, 'npx');
106
+ assert.deepEqual(entry.args, ['--yes', '--ignore-scripts', '--package=sap-ai-dev-toolkit', 'sap-ai-hana']);
107
+ assert.equal(entry.BAS_EXT_KIND, 'sap-development-companion');
108
+ assert.equal(entry.displayName, 'HANA Cloud inspector');
109
+ assert.equal(entry.env, undefined);
110
+ assert.doesNotMatch(JSON.stringify(entry), /"(?:HANA_RO_PASSWORD|VCAP_SERVICES)"\s*:/);
111
+ assert.doesNotMatch(JSON.stringify(entry), /"(?:password|pwd)"\s*:/i);
112
+ });
113
+
103
114
  test('installs and removes SAP development companion entries', async t => {
104
115
  const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-tools-config-'));
105
116
  const path = join(directory, 'mcp.json');
@@ -120,6 +131,35 @@ test('installs and removes SAP development companion entries', async t => {
120
131
  assert.deepEqual(Object.keys(cleared.servers), ['userServer']);
121
132
  });
122
133
 
134
+ test('installs and removes the HANA inspector without changing unrelated MCP servers', async t => {
135
+ const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-hana-config-'));
136
+ const path = join(directory, 'mcp.json');
137
+ const initial = {
138
+ custom: true,
139
+ servers: { userServer: { type: 'stdio', command: 'custom-server' } }
140
+ };
141
+ await writeFile(path, JSON.stringify(initial));
142
+ t.after(() => rm(directory, { recursive: true, force: true }));
143
+
144
+ const installed = await installMcpConfig([], {
145
+ env: { H2O_URL: 'http://h2o.example' },
146
+ path,
147
+ sapDevelopmentServers: ['hana-cloud-inspector']
148
+ });
149
+ assert.deepEqual(Object.keys(installed.servers), ['hana-cloud-inspector']);
150
+ const configured = await readMcpConfig(path);
151
+ assert.deepEqual(Object.keys(configured.servers).sort(), ['hana-cloud-inspector', 'userServer']);
152
+ assert.equal(configured.custom, true);
153
+ assert.equal(configured.servers['hana-cloud-inspector'].env, undefined);
154
+ assert.doesNotMatch(JSON.stringify(configured), /"(?:HANA_RO_PASSWORD|VCAP_SERVICES)"\s*:/);
155
+ assert.doesNotMatch(JSON.stringify(configured), /"(?:password|pwd)"\s*:/i);
156
+
157
+ await installMcpConfig([], { env: { H2O_URL: 'http://h2o.example' }, path });
158
+ const removed = await readMcpConfig(path);
159
+ assert.deepEqual(removed.servers, initial.servers);
160
+ assert.equal(removed.custom, true);
161
+ });
162
+
123
163
  test('installs and removes CF entries without deleting unrelated MCP servers', async t => {
124
164
  const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-cf-config-'));
125
165
  const path = join(directory, 'mcp.json');
@@ -5,24 +5,13 @@ import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { once } from 'node:events';
8
- import { childArguments, MCPProxy } from '../src/mcp-proxy.mjs';
8
+ import { childArguments, MCPProxy, PUBLIC_VSP_TOOLS } from '../src/mcp-proxy.mjs';
9
9
  import { PassThrough } from 'node:stream';
10
10
 
11
11
  const fixture = fileURLToPath(new URL('./fixtures/fake-vsp.mjs', import.meta.url));
12
- const REQUESTED_TOOLS = [
13
- 'AnalyzeABAPCode', 'AnalyzeCallGraph', 'CodeCompletion', 'GetAbapHelp',
14
- 'GetCallGraph', 'GetCalleesOf', 'GetCallersOf', 'GetCodeCoverage',
15
- 'GetConnectionInfo', 'GetObjectStructure', 'GetTypeHierarchy', 'GetTypeInfo',
16
- 'GrepObject', 'GrepPackage', 'CallRFC', 'DebuggerAttach', 'DebuggerDetach',
17
- 'DebuggerGetStack', 'DebuggerGetVariables', 'DebuggerListen', 'DebuggerStep',
18
- 'DeleteBreakpoint', 'GetBreakpoints', 'GetDump', 'GetSQLTraceState', 'GetTrace',
19
- 'ListDumps', 'SetBreakpoint', 'CloneObject', 'CreateAndActivateProgram',
20
- 'CreateClassWithTests', 'CreateObject', 'CreateTestInclude', 'DeleteObject',
21
- 'ExecuteABAP', 'GetClass', 'GetClassComponents', 'GetClassInclude', 'GetFunction',
22
- 'GetInclude', 'GetInterface', 'GetProgram', 'GetStructure', 'GetTransaction',
23
- 'LockObject', 'MoveObject', 'RecoverFailedCreate', 'RenameObject', 'SaveToFile',
24
- 'UnlockObject', 'UpdateClassInclude', 'UpdateSource', 'WriteClass', 'WriteProgram',
25
- 'CreateTransport', 'ListDependencies', 'PublishServiceBinding', 'UnpublishServiceBinding'
12
+ const HIDDEN_VSP_TOOLS = [
13
+ 'AnalyzeABAPCode', 'DebuggerListen', 'DeleteBreakpoint', 'DeleteObject', 'ExecuteABAP', 'ReleaseTransport',
14
+ 'DeleteTransport', 'SAP', 'ListSQLTraces', 'PublishServiceBinding', 'UnpublishServiceBinding'
26
15
  ];
27
16
 
28
17
 
@@ -166,22 +155,23 @@ test('merges paged tools and routes calls to the selected child', async t => {
166
155
  assert.deepEqual(lintTool.inputSchema.required, ['files']);
167
156
  assert.deepEqual(lintTool.inputSchema.properties.files.items.required, ['filename', 'source']);
168
157
 
158
+ const localWorkflowTools = [
159
+ 'LintABAP', 'GetApplicationLog', 'PrepareABAPChangeSet', 'ApplyABAPChangeSet',
160
+ 'CheckTransportReadiness', 'PlanABAPCloudMigration', 'GenerateRAPRegressionSuite', 'RunRAPRegressionSuite'
161
+ ];
169
162
  for (const destination of ['alpha', 'beta']) {
170
- for (const toolName of REQUESTED_TOOLS) {
171
- assert.ok(names.has(`${destination}__${toolName}`), `${destination} exposes ${toolName}`);
163
+ for (const toolName of PUBLIC_VSP_TOOLS) {
164
+ assert.ok(names.has(`${destination}__${toolName}`), `${destination} exposes curated ${toolName}`);
172
165
  }
173
- for (const coreTool of [
174
- 'GetSource', 'RunQuery', 'GetSystemInfo', 'GetInstalledComponents',
175
- 'GetFeatures', 'GetAPIReleaseState', 'PrettyPrint', 'ListTransports',
176
- 'GetTransport', 'GetUserTransports', 'GetTransportInfo',
177
- 'GetApplicationLog', 'ActivateMultiple'
178
- ]) {
179
- assert.ok(names.has(`${destination}__${coreTool}`), `${destination} exposes ${coreTool}`);
166
+ for (const toolName of localWorkflowTools) {
167
+ assert.ok(names.has(`${destination}__${toolName}`), `${destination} exposes local ${toolName}`);
180
168
  }
181
- for (const formerlyExcludedTool of ['ListSQLTraces', 'ReleaseTransport', 'DeleteTransport', 'SAP']) {
182
- assert.ok(names.has(`${destination}__${formerlyExcludedTool}`), `${destination} exposes ${formerlyExcludedTool}`);
169
+ for (const hiddenTool of HIDDEN_VSP_TOOLS) {
170
+ assert.equal(names.has(`${destination}__${hiddenTool}`), false, `${destination} hides ${hiddenTool}`);
183
171
  }
184
172
  }
173
+ assert.equal(listed.result.tools.filter(tool => tool.name.startsWith('alpha__')).length, PUBLIC_VSP_TOOLS.size + localWorkflowTools.length);
174
+ assert.equal(PUBLIC_VSP_TOOLS.size + localWorkflowTools.length, 59);
185
175
 
186
176
  const requiredArguments = {
187
177
  GetFeatures: [],
@@ -234,11 +224,7 @@ test('merges paged tools and routes calls to the selected child', async t => {
234
224
  params: { type: 'delete' }
235
225
  }],
236
226
  ['ActivateMultiple', { objects: ['PROG ZDEMO'] }],
237
- ['CreateTransport', {}],
238
- ['ReleaseTransport', {}],
239
- ['DeleteTransport', {}],
240
- ['ListSQLTraces', {}],
241
- ['SAP', { action: 'analyze', params: { type: 'application_log' } }]
227
+ ['CreateTransport', {}]
242
228
  ];
243
229
  for (const [index, [name, arguments_]] of additionalCalls.entries()) {
244
230
  const response = await proxy.handle({
@@ -70,7 +70,7 @@ function runPostinstallInPty(env, keys, assetsAnswer = '\r') {
70
70
  selectionSent = true;
71
71
  child.stdin.write(keys);
72
72
  }
73
- if (!assetsAnswerSent && stdout.includes('Install the bundled agents and eleven skills')) {
73
+ if (!assetsAnswerSent && stdout.includes('Install the bundled agents and all skills')) {
74
74
  assetsAnswerSent = true;
75
75
  child.stdin.write(assetsAnswer);
76
76
  }
@@ -90,6 +90,7 @@ function runPostinstallInPty(env, keys, assetsAnswer = '\r') {
90
90
  async function assertUserCopilotAssets(home) {
91
91
  const copilotRoot = join(home, '.copilot');
92
92
  assert.equal((await stat(join(copilotRoot, 'agents', 'abap-developer.agent.md'))).isFile(), true);
93
+ assert.equal((await stat(join(copilotRoot, 'agents', 'hana-cloud-hdi-specialist.agent.md'))).isFile(), true);
93
94
  const skillNames = (await readdir(join(copilotRoot, 'skills'))).sort();
94
95
  assert.deepEqual(skillNames, [
95
96
  'abap-debugging',
@@ -98,6 +99,9 @@ async function assertUserCopilotAssets(home) {
98
99
  'abap-testing-quality',
99
100
  'cds-development',
100
101
  'clean-core-extensibility',
102
+ 'hana-cloud-inspection',
103
+ 'hana-cloud-native-development',
104
+ 'hana-cloud-validation',
101
105
  'rap-development',
102
106
  'rap-service-delivery',
103
107
  'sap-sdlc-orchestration',
@@ -279,13 +283,13 @@ test('global postinstall completes BAS selection before optional Copilot assets'
279
283
  const declineLogs = `${declined.stdout}\n${declined.stderr}`;
280
284
  assert.match(declineLogs, /Configured 0 MCP servers/);
281
285
  assert.match(declineLogs, /\[Y\/n\]/);
282
- assert.match(declineLogs, /Press Enter to install the bundled agents and eleven skills in the path shown below; type n then press Enter to skip/);
286
+ assert.match(declineLogs, /Press Enter to install the bundled agents and all skills in the path shown below; type n then press Enter to skip/);
283
287
  assert.match(declineLogs, /🤖 sap-ai-dev-toolkit/);
284
288
  assert.match(declineLogs, /\u001b\[1;35m/);
285
289
  assert.match(declineLogs, /Optional Copilot setup is waiting for your choice/);
286
290
  assert.match(declineLogs, /Copilot agent and skills were skipped\. Your files were not changed/);
287
291
  assert.ok(declineLogs.indexOf('Optional Copilot setup is waiting for your choice') > declineLogs.indexOf('Configured 0 MCP servers'), declineLogs);
288
- assert.ok(declineLogs.indexOf('Install the bundled agents and eleven skills') > declineLogs.indexOf('Optional Copilot setup is waiting for your choice'), declineLogs);
292
+ assert.ok(declineLogs.indexOf('Install the bundled agents and all skills') > declineLogs.indexOf('Optional Copilot setup is waiting for your choice'), declineLogs);
289
293
  assert.ok(declineLogs.indexOf('No MCP server entries are configured for this add-on.') > declineLogs.indexOf('Copilot agent and skills were skipped'), declineLogs);
290
294
  assert.ok(declineLogs.includes(`MCP config file: ${config}`), declineLogs);
291
295
  const configAfterDecline = JSON.parse(await readFile(config, 'utf8'));
@@ -297,9 +301,9 @@ test('global postinstall completes BAS selection before optional Copilot assets'
297
301
  assert.equal(accepted.selectionSent, true, accepted.stdout);
298
302
  assert.equal(accepted.assetsAnswerSent, true, accepted.stdout);
299
303
  const acceptLogs = `${accepted.stdout}\n${accepted.stderr}`;
300
- assert.ok(acceptLogs.indexOf('Install the bundled agents and eleven skills') > acceptLogs.indexOf('Configured 1 MCP server'), acceptLogs);
301
- assert.match(acceptLogs, /Installed 15 Copilot files/);
302
- assert.ok(acceptLogs.indexOf('Installation configuration summary') > acceptLogs.indexOf('Installed 15 Copilot files'), acceptLogs);
304
+ assert.ok(acceptLogs.indexOf('Install the bundled agents and all skills') > acceptLogs.indexOf('Configured 1 MCP server'), acceptLogs);
305
+ assert.match(acceptLogs, /Installed 19 Copilot files/);
306
+ assert.ok(acceptLogs.indexOf('Installation configuration summary') > acceptLogs.indexOf('Installed 19 Copilot files'), acceptLogs);
303
307
  assert.ok(acceptLogs.includes(`MCP config file: ${config}`), acceptLogs);
304
308
  assert.ok(acceptLogs.includes('Destination: BAS · alpha-system · client 100 · Basic'), acceptLogs);
305
309
  assert.ok(acceptLogs.includes('Launch: stdio · sap-ai-dev'), acceptLogs);
package/tools.md CHANGED
@@ -1,79 +1,87 @@
1
+ # Curated VSP tools exposed through the BAS proxy
1
2
 
2
- # Additional VSP tools exposed through the BAS proxy
3
+ The proxy no longer advertises every VSP tool. To keep the developer-lifecycle MCP surface manageable, each generated one-destination server exposes a cherry-picked set of VSP tools plus local workflow tools (currently 59 tools when all curated VSP capabilities are registered).
3
4
 
4
- The proxy forwards these tools when the installed VSP mode registers them. They extend its existing curated tool set.
5
+ Hidden upstream VSP tools are not directly callable through the proxy. Local workflow tools may still call hidden or non-advertised upstream operations internally when they are required for a bounded workflow.
5
6
 
6
- ## Analysis, metadata, debugging, and trace
7
+ ## Public VSP tools
7
8
 
8
- - `AnalyzeABAPCode`
9
- - `AnalyzeCallGraph`
10
- - `CodeCompletion`
11
- - `GetAbapHelp`
12
- - `GetCallGraph`
13
- - `GetCalleesOf`
14
- - `GetCallersOf`
15
- - `GetCodeCoverage`
16
- - `GetConnectionInfo`
17
- - `GetObjectStructure`
18
- - `GetTypeHierarchy`
19
- - `GetTypeInfo`
20
- - `GrepObject`
21
- - `GrepPackage`
22
- - `CallRFC`
9
+ - `Activate`
10
+ - `ActivateMultiple`
11
+ - `CompareSource`
12
+ - `CreateTransport`
23
13
  - `DebuggerAttach`
24
14
  - `DebuggerDetach`
25
15
  - `DebuggerGetStack`
26
16
  - `DebuggerGetVariables`
27
- - `DebuggerListen`
28
17
  - `DebuggerStep`
29
- - `DeleteBreakpoint`
18
+ - `EditSource`
19
+ - `FindDefinition`
20
+ - `FindReferences`
21
+ - `GetAPIReleaseState`
30
22
  - `GetBreakpoints`
31
- - `GetDump`
32
- - `GetSQLTraceState`
33
- - `GetTrace`
34
- - `ListDumps`
35
- - `SetBreakpoint`
36
-
37
- ## Object, source, transport, and service operations
38
-
39
- - `CloneObject`
40
- - `CreateAndActivateProgram`
41
- - `CreateClassWithTests`
42
- - `CreateObject`
43
- - `CreateTestInclude`
44
- - `DeleteObject`
45
- - `ExecuteABAP`
23
+ - `GetCDSDependencies`
24
+ - `GetCDSElementInfo`
25
+ - `GetCDSImpactAnalysis`
46
26
  - `GetClass`
47
27
  - `GetClassComponents`
48
28
  - `GetClassInclude`
29
+ - `GetClassInfo`
30
+ - `GetConnectionInfo`
31
+ - `GetContext`
32
+ - `GetFeatures`
49
33
  - `GetFunction`
34
+ - `GetFunctionGroup`
35
+ - `GetInactiveObjects`
50
36
  - `GetInclude`
37
+ - `GetInstalledComponents`
51
38
  - `GetInterface`
39
+ - `GetPackage`
52
40
  - `GetProgram`
53
- - `GetStructure`
54
- - `GetTransaction`
55
- - `LockObject`
56
- - `MoveObject`
57
- - `RecoverFailedCreate`
58
- - `RenameObject`
59
- - `SaveToFile`
60
- - `UnlockObject`
61
- - `UpdateClassInclude`
62
- - `UpdateSource`
63
- - `WriteClass`
64
- - `WriteProgram`
65
- - `CreateTransport`
41
+ - `GetSource`
42
+ - `GetSystemInfo`
43
+ - `GetTable`
44
+ - `GetTableContents`
45
+ - `GetTransport`
46
+ - `GetTransportInfo`
47
+ - `GetUserTransports`
48
+ - `GrepObjects`
49
+ - `GrepPackages`
66
50
  - `ListDependencies`
67
- - `PublishServiceBinding`
68
- - `UnpublishServiceBinding`
51
+ - `ListTransports`
52
+ - `PrettyPrint`
53
+ - `RunATCCheck`
54
+ - `RunQuery`
55
+ - `RunUnitTests`
56
+ - `SearchObject`
57
+ - `SetBreakpoint`
58
+ - `SyntaxCheck`
59
+ - `WriteSource`
60
+
61
+ ## Hidden from direct proxy calls
62
+
63
+ Broad/destructive surfaces such as object deletion, SQL trace, arbitrary SAP router calls, transport release/delete, RFC execution, debugger listen/delete-breakpoint helpers, and one-off generator/update helpers are intentionally not advertised by the BAS proxy.
69
64
 
70
65
  ## Destination-scoped workflow tools
71
66
 
72
67
  The proxy adds local multi-step tools to each destination when their required VSP capabilities are available:
73
68
 
69
+ - `LintABAP` — local in-memory ABAP linting.
70
+ - `GetApplicationLog` — bounded convenience mapping to the upstream SAP application-log route when available.
74
71
  - `PrepareABAPChangeSet` and `ApplyABAPChangeSet` — review full-source diffs and recheck source before explicit writes.
75
72
  - `CheckTransportReadiness` — collect a whitelisted bundle of transport, dependency, inactive-object, ABAP Unit, and ATC results.
76
73
  - `PlanABAPCloudMigration` — batch SAP API release-state checks and prioritize recognized unreleased results.
77
74
  - `GenerateRAPRegressionSuite` and `RunRAPRegressionSuite` — create and run reusable OData GET-only checks under the selected service root.
78
75
 
79
76
  `sap-ai-dev-toolkit --doctor` checks destination probing, VSP startup, `GetSystemInfo`, and MCP tool listing. `sap-ai-dev-toolkit --demo` runs the sample tools and OData fixtures without contacting SAP; demo writes and transport creation remain in memory until that process exits.
77
+
78
+ ## Separate optional HANA Cloud inspector
79
+
80
+ The optional `hana-cloud-inspector` companion starts `sap-ai-hana` as a separate stdio MCP server. It does not use the VSP proxy, BAS destination prefix, or ABAP SQL `RunQuery`. When selected and configured with a dedicated read-only identity, it exposes:
81
+
82
+ - `hana_connection_info` — verify the configured endpoint, service/binding, current user, schema, and TLS state without returning credentials.
83
+ - `hana_list_objects` — list tables/views in the bound HDI schema, with a 200-object maximum.
84
+ - `hana_describe_object` — inspect columns for a catalog-verified object in that schema.
85
+ - `hana_read_rows` — perform parameterized reads of selected columns, capped at 200 rows; credential-like columns are blocked and arbitrary SQL is not accepted.
86
+
87
+ Connection secrets are read from the MCP host environment via explicit `HANA_RO_*` variables or a selected `VCAP_SERVICES` binding and are not written to `mcp.json`. TLS certificate verification is mandatory. No HANA write, DDL/DML, deploy/undeploy, grants, or service-key tools are exposed; deployment remains a user-run project workflow.