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.
- package/.github/agents/hana-cloud-hdi-specialist.agent.md +22 -0
- package/.github/agents/sap-solution-architect.agent.md +3 -2
- package/.github/skills/hana-cloud-inspection/SKILL.md +35 -0
- package/.github/skills/hana-cloud-native-development/SKILL.md +26 -0
- package/.github/skills/hana-cloud-validation/SKILL.md +26 -0
- package/README.md +40 -17
- package/package.json +9 -4
- package/scripts/postinstall.mjs +3 -3
- package/src/hana-config.mjs +170 -0
- package/src/hana-database.mjs +150 -0
- package/src/hana-inspector.mjs +114 -0
- package/src/hana-tools.mjs +277 -0
- package/src/mcp-config.mjs +10 -1
- package/src/mcp-proxy.mjs +61 -0
- package/src/setup.mjs +3 -2
- package/test/copilot-content.test.mjs +48 -0
- package/test/hana-config.test.mjs +168 -0
- package/test/hana-inspector-stdio.test.mjs +44 -0
- package/test/hana-tools.test.mjs +206 -0
- package/test/live-s4h.test.mjs +202 -0
- package/test/mcp-config-cf.test.mjs +40 -0
- package/test/mcp-proxy.test.mjs +17 -31
- package/test/setup.test.mjs +10 -6
- package/tools.md +58 -50
|
@@ -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');
|
package/test/mcp-proxy.test.mjs
CHANGED
|
@@ -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
|
|
13
|
-
'AnalyzeABAPCode', '
|
|
14
|
-
'
|
|
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
|
|
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
|
|
174
|
-
|
|
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
|
|
182
|
-
assert.
|
|
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({
|
package/test/setup.test.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
301
|
-
assert.match(acceptLogs, /Installed
|
|
302
|
-
assert.ok(acceptLogs.indexOf('Installation configuration summary') > acceptLogs.indexOf('Installed
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
7
|
+
## Public VSP tools
|
|
7
8
|
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
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
|
-
- `
|
|
18
|
+
- `EditSource`
|
|
19
|
+
- `FindDefinition`
|
|
20
|
+
- `FindReferences`
|
|
21
|
+
- `GetAPIReleaseState`
|
|
30
22
|
- `GetBreakpoints`
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
- `
|
|
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
|
-
- `
|
|
54
|
-
- `
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
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
|
-
- `
|
|
68
|
-
- `
|
|
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.
|