sap-ai-dev-toolkit 0.3.4 → 0.4.0

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.
Files changed (35) hide show
  1. package/.github/agents/abap-developer.agent.md +5 -5
  2. package/.github/agents/abap-runtime-debugger.agent.md +10 -10
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +22 -0
  4. package/.github/agents/rap-service-developer.agent.md +10 -10
  5. package/.github/agents/sap-solution-architect.agent.md +7 -6
  6. package/.github/skills/abap-debugging/SKILL.md +2 -2
  7. package/.github/skills/abap-runtime-analysis/SKILL.md +4 -4
  8. package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
  9. package/.github/skills/hana-cloud-inspection/SKILL.md +35 -0
  10. package/.github/skills/hana-cloud-native-development/SKILL.md +26 -0
  11. package/.github/skills/hana-cloud-validation/SKILL.md +26 -0
  12. package/.github/skills/rap-development/SKILL.md +2 -2
  13. package/.github/skills/rap-service-delivery/SKILL.md +3 -3
  14. package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
  15. package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
  16. package/.github/skills/sap-transport-release/SKILL.md +1 -1
  17. package/README.md +46 -22
  18. package/package.json +9 -4
  19. package/scripts/postinstall.mjs +3 -3
  20. package/src/hana-config.mjs +170 -0
  21. package/src/hana-database.mjs +150 -0
  22. package/src/hana-inspector.mjs +114 -0
  23. package/src/hana-tools.mjs +277 -0
  24. package/src/mcp-config.mjs +10 -1
  25. package/src/mcp-proxy.mjs +61 -0
  26. package/src/setup.mjs +3 -2
  27. package/test/copilot-content.test.mjs +48 -0
  28. package/test/hana-config.test.mjs +168 -0
  29. package/test/hana-inspector-stdio.test.mjs +44 -0
  30. package/test/hana-tools.test.mjs +206 -0
  31. package/test/live-s4h.test.mjs +202 -0
  32. package/test/mcp-config-cf.test.mjs +40 -0
  33. package/test/mcp-proxy.test.mjs +17 -31
  34. package/test/setup.test.mjs +10 -6
  35. package/tools.md +58 -50
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { resolveHanaReadOnlyConfig } from './hana-config.mjs';
7
+ import { connectReadOnlyHana } from './hana-database.mjs';
8
+ import { registerHanaTools } from './hana-tools.mjs';
9
+
10
+ const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
11
+
12
+ function publicConnectionSummary(config) {
13
+ return Object.freeze({
14
+ source: config.source,
15
+ serviceName: config.serviceName,
16
+ bindingName: config.bindingName,
17
+ host: config.host,
18
+ port: config.port,
19
+ schema: config.schema
20
+ });
21
+ }
22
+
23
+ /** Create an injectable server runtime for tests and the stdio executable. */
24
+ export function createHanaInspector({ env = process.env, driver, databaseFactory = connectReadOnlyHana } = {}) {
25
+ const config = resolveHanaReadOnlyConfig(env);
26
+ const summary = publicConnectionSummary(config);
27
+ let databasePromise;
28
+ let database;
29
+
30
+ const getDatabase = async () => {
31
+ if (!databasePromise) {
32
+ databasePromise = databaseFactory(config, { driver })
33
+ .then(connected => {
34
+ database = connected;
35
+ return connected;
36
+ })
37
+ .catch(error => {
38
+ databasePromise = undefined;
39
+ throw error;
40
+ });
41
+ }
42
+ return databasePromise;
43
+ };
44
+
45
+ const server = new McpServer({
46
+ name: 'sap-ai-hana-inspector',
47
+ version: packageMetadata.version
48
+ }, { capabilities: { tools: {} } });
49
+ registerHanaTools(server, getDatabase, { connectionSummary: summary });
50
+
51
+ let closing;
52
+ const close = () => {
53
+ if (!closing) {
54
+ closing = (async () => {
55
+ await server.close().catch(() => {});
56
+ if (!database && databasePromise) database = await databasePromise.catch(() => undefined);
57
+ if (database) await database.close().catch(() => {});
58
+ })();
59
+ }
60
+ return closing;
61
+ };
62
+
63
+ return { server, close, connectionSummary: summary };
64
+ }
65
+
66
+ export async function runHanaInspector({ env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, driver } = {}) {
67
+ if (process.argv.slice(2).some(argument => argument === '--help' || argument === '-h')) {
68
+ stdout.write([
69
+ 'Usage: sap-ai-hana',
70
+ '',
71
+ 'Starts a read-only stdio MCP server for the HANA service selected by HANA_RO_* or VCAP_SERVICES.',
72
+ 'Required explicit variables: HANA_RO_HOST, HANA_RO_PORT, HANA_RO_USER, HANA_RO_PASSWORD, HANA_RO_SCHEMA.',
73
+ 'VCAP selection: HANA_RO_VCAP_SERVICE and HANA_RO_BINDING are optional when exactly one HANA binding exists.',
74
+ 'TLS certificate validation is always enabled. Deployment credentials are never used as a fallback.',
75
+ ''
76
+ ].join('\n'));
77
+ return;
78
+ }
79
+ let runtime;
80
+ try {
81
+ runtime = createHanaInspector({ env, driver });
82
+ } catch (error) {
83
+ stderr.write(`[sap-ai-hana] ${error.message}\n`);
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize: 1024 * 1024 });
89
+ let closing;
90
+ const shutdown = () => {
91
+ if (!closing) closing = runtime.close().catch(() => {});
92
+ return closing;
93
+ };
94
+ const onStdinEnd = () => { void shutdown(); };
95
+ const onSignal = signal => {
96
+ void shutdown().finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
97
+ };
98
+ stdin.once('end', onStdinEnd);
99
+ process.once('SIGINT', onSignal);
100
+ process.once('SIGTERM', onSignal);
101
+ transport.onerror = () => stderr.write('[sap-ai-hana] MCP stdio transport failed.\n');
102
+
103
+ try {
104
+ await runtime.server.connect(transport);
105
+ } catch {
106
+ stderr.write('[sap-ai-hana] MCP server startup failed.\n');
107
+ await shutdown();
108
+ process.exitCode = 1;
109
+ }
110
+ }
111
+
112
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
113
+ await runHanaInspector();
114
+ }
@@ -0,0 +1,277 @@
1
+ import { z } from 'zod';
2
+ import { quoteHanaIdentifier } from './hana-config.mjs';
3
+
4
+ export const HANA_MAX_ROWS = 200;
5
+ export const HANA_MAX_OUTPUT_BYTES = 96 * 1024;
6
+ const MAX_COLUMNS = 40;
7
+ const MAX_FILTERS = 20;
8
+ const MAX_SELECTED_COLUMN_WIDTH = 8192;
9
+ const UNBOUNDED_COLUMN_TYPES = new Set(['BLOB', 'CLOB', 'NCLOB', 'BINARY', 'VARBINARY', 'ST_GEOMETRY', 'ST_POINT']);
10
+ const SENSITIVE_COLUMN_NAME = /(?:PASS(?:WORD|WD)|SECRET|(?:ACCESS|REFRESH|AUTH)?_?TOKEN|API_?KEY|PRIVATE_?KEY|CREDENTIAL)/i;
11
+ const SAFE_INSPECTION_ERRORS = new Set([
12
+ 'HANA result is too large to return safely; request fewer rows or columns.',
13
+ 'The requested object was not found in the bound HDI schema.',
14
+ 'A requested column is not present on the requested HDI object.',
15
+ 'Credential-like columns are not returned by the HANA inspector.',
16
+ 'LOB and binary columns are not supported by the bounded row-read tool.',
17
+ 'The selected columns exceed the safe row-width limit; select fewer or narrower columns.'
18
+ ]);
19
+
20
+ const READ_ONLY_ANNOTATIONS = Object.freeze({
21
+ readOnlyHint: true,
22
+ destructiveHint: false,
23
+ idempotentHint: true,
24
+ openWorldHint: false
25
+ });
26
+
27
+ const OPERATOR_SQL = Object.freeze({
28
+ eq: '=',
29
+ ne: '<>',
30
+ lt: '<',
31
+ lte: '<=',
32
+ gt: '>',
33
+ gte: '>='
34
+ });
35
+
36
+ function jsonSafe(value, seen = new WeakSet()) {
37
+ if (typeof value === 'bigint') return value.toString();
38
+ if (value instanceof Date) return value.toISOString();
39
+ if (Buffer.isBuffer(value)) return { type: 'binary', bytes: value.byteLength };
40
+ if (Array.isArray(value)) return value.map(entry => jsonSafe(entry, seen));
41
+ if (value && typeof value === 'object') {
42
+ if (seen.has(value)) return '[circular]';
43
+ seen.add(value);
44
+ const result = Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsonSafe(entry, seen)]));
45
+ seen.delete(value);
46
+ return result;
47
+ }
48
+ if (typeof value === 'number' && !Number.isFinite(value)) return String(value);
49
+ return value;
50
+ }
51
+
52
+ function jsonText(value) {
53
+ const text = JSON.stringify(jsonSafe(value), null, 2);
54
+ if (Buffer.byteLength(text, 'utf8') > HANA_MAX_OUTPUT_BYTES) {
55
+ throw new Error('HANA result is too large to return safely; request fewer rows or columns.');
56
+ }
57
+ return text;
58
+ }
59
+
60
+ function toolError(error) {
61
+ let message = 'HANA inspection failed. Check the selected read-only binding, target schema, and read permissions.';
62
+ if (error?.name === 'HanaDatabaseError' || error?.name === 'HanaQueryTimeoutError') {
63
+ message = error.message;
64
+ } else if (SAFE_INSPECTION_ERRORS.has(error?.message)) {
65
+ message = error.message;
66
+ } else if (typeof error?.message === 'string' && /^(?:Set HANA_RO_|Explicit HANA |VCAP_SERVICES|The selected HANA |No HANA VCAP|Multiple HANA VCAP|The configured HANA |HANA_RO_HOST|HANA_RO_PORT)/.test(error.message)) {
67
+ message = error.message;
68
+ }
69
+ return { isError: true, content: [{ type: 'text', text: message }] };
70
+ }
71
+
72
+ async function resultOf(operation) {
73
+ try {
74
+ return { content: [{ type: 'text', text: jsonText(await operation()) }] };
75
+ } catch (error) {
76
+ return toolError(error);
77
+ }
78
+ }
79
+
80
+ function rowsOf(value) {
81
+ return Array.isArray(value) ? value : [];
82
+ }
83
+
84
+ async function listObjects(database, schema, objectType, maxResults) {
85
+ const entries = [];
86
+ if (objectType !== 'VIEW') {
87
+ const tables = rowsOf(await database.query(
88
+ `SELECT TABLE_NAME AS OBJECT_NAME FROM SYS.TABLES WHERE SCHEMA_NAME = ? ORDER BY TABLE_NAME LIMIT ${maxResults}`,
89
+ [schema]
90
+ ));
91
+ entries.push(...tables.map(row => ({ objectName: row.OBJECT_NAME, objectType: 'TABLE' })));
92
+ }
93
+ if (objectType !== 'TABLE') {
94
+ const views = rowsOf(await database.query(
95
+ `SELECT VIEW_NAME AS OBJECT_NAME FROM SYS.VIEWS WHERE SCHEMA_NAME = ? ORDER BY VIEW_NAME LIMIT ${maxResults}`,
96
+ [schema]
97
+ ));
98
+ entries.push(...views.map(row => ({ objectName: row.OBJECT_NAME, objectType: 'VIEW' })));
99
+ }
100
+ entries.sort((left, right) => String(left.objectName).localeCompare(String(right.objectName)) || left.objectType.localeCompare(right.objectType));
101
+ return {
102
+ schema,
103
+ objectType,
104
+ objects: entries.slice(0, maxResults),
105
+ maxResults,
106
+ truncated: entries.length >= maxResults
107
+ };
108
+ }
109
+
110
+ async function findObject(database, schema, objectName) {
111
+ const tables = rowsOf(await database.query(
112
+ 'SELECT TABLE_NAME AS OBJECT_NAME FROM SYS.TABLES WHERE SCHEMA_NAME = ? AND TABLE_NAME = ?',
113
+ [schema, objectName]
114
+ ));
115
+ if (tables.length) return { objectName, objectType: 'TABLE' };
116
+ const views = rowsOf(await database.query(
117
+ 'SELECT VIEW_NAME AS OBJECT_NAME FROM SYS.VIEWS WHERE SCHEMA_NAME = ? AND VIEW_NAME = ?',
118
+ [schema, objectName]
119
+ ));
120
+ if (views.length) return { objectName, objectType: 'VIEW' };
121
+ throw new Error('The requested object was not found in the bound HDI schema.');
122
+ }
123
+
124
+ async function describeObject(database, schema, objectName) {
125
+ const target = await findObject(database, schema, objectName);
126
+ const sql = target.objectType === 'TABLE'
127
+ ? 'SELECT COLUMN_NAME, POSITION, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION'
128
+ : 'SELECT COLUMN_NAME, POSITION, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE FROM SYS.VIEW_COLUMNS WHERE SCHEMA_NAME = ? AND VIEW_NAME = ? ORDER BY POSITION';
129
+ const columns = rowsOf(await database.query(sql, [schema, objectName]));
130
+ return { ...target, schema, columns };
131
+ }
132
+
133
+ function assertRequestedColumns(columns, metadataColumns) {
134
+ const available = new Set(metadataColumns.map(column => column.COLUMN_NAME));
135
+ for (const name of columns) {
136
+ if (!available.has(name)) throw new Error('A requested column is not present on the requested HDI object.');
137
+ }
138
+ }
139
+
140
+ function assertBoundedColumns(columns, metadataColumns) {
141
+ let estimatedWidth = 0;
142
+ for (const name of columns) {
143
+ const column = metadataColumns.find(entry => entry.COLUMN_NAME === name);
144
+ const type = String(column.DATA_TYPE_NAME || '').toUpperCase();
145
+ if (UNBOUNDED_COLUMN_TYPES.has(type)) {
146
+ throw new Error('LOB and binary columns are not supported by the bounded row-read tool.');
147
+ }
148
+ const length = Number(column.LENGTH);
149
+ estimatedWidth += Number.isFinite(length) && length > 0 ? length : 32;
150
+ if (estimatedWidth > MAX_SELECTED_COLUMN_WIDTH) {
151
+ throw new Error('The selected columns exceed the safe row-width limit; select fewer or narrower columns.');
152
+ }
153
+ }
154
+ }
155
+
156
+ function assertNonSensitiveColumns(columns) {
157
+ if (columns.some(name => SENSITIVE_COLUMN_NAME.test(name))) {
158
+ throw new Error('Credential-like columns are not returned by the HANA inspector.');
159
+ }
160
+ }
161
+
162
+ async function readRows(database, schema, args) {
163
+ const target = await findObject(database, schema, args.objectName);
164
+ const metadata = await describeObject(database, schema, args.objectName);
165
+ assertRequestedColumns(args.columns, metadata.columns);
166
+ assertNonSensitiveColumns([
167
+ ...args.columns,
168
+ ...args.filters.map(filter => filter.column),
169
+ ...(args.orderBy ? [args.orderBy.column] : [])
170
+ ]);
171
+ assertBoundedColumns(args.columns, metadata.columns);
172
+
173
+ for (const filter of args.filters) assertRequestedColumns([filter.column], metadata.columns);
174
+ if (args.orderBy) assertRequestedColumns([args.orderBy.column], metadata.columns);
175
+
176
+ const selected = args.columns.map(quoteHanaIdentifier).join(', ');
177
+ const conditions = args.filters.map(filter => `${quoteHanaIdentifier(filter.column)} ${OPERATOR_SQL[filter.operator]} ?`);
178
+ const order = args.orderBy
179
+ ? ` ORDER BY ${quoteHanaIdentifier(args.orderBy.column)} ${args.orderBy.direction}`
180
+ : '';
181
+ const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
182
+ const sql = `SELECT ${selected} FROM ${quoteHanaIdentifier(schema)}.${quoteHanaIdentifier(args.objectName)}${where}${order} LIMIT ${args.limit}`;
183
+ const values = args.filters.map(filter => filter.value);
184
+ const rows = rowsOf(await database.query(sql, values));
185
+ return {
186
+ schema,
187
+ ...target,
188
+ columns: args.columns,
189
+ limit: args.limit,
190
+ rowCount: rows.length,
191
+ rows,
192
+ possiblyTruncated: rows.length === args.limit
193
+ };
194
+ }
195
+
196
+ /** Register read-only inspection tools. No handler accepts arbitrary SQL or a caller-selected schema. */
197
+ export function registerHanaTools(server, getDatabase, { connectionSummary } = {}) {
198
+ const schemaName = connectionSummary?.schema;
199
+ if (typeof schemaName !== 'string' || !schemaName.trim()) {
200
+ throw new Error('A configured HDI schema is required to register HANA tools.');
201
+ }
202
+
203
+ server.registerTool('hana_connection_info', {
204
+ title: 'HANA connection info',
205
+ description: 'Return the configured HANA Cloud endpoint and verified HDI schema/current user. Uses the dedicated read-only identity; never returns credentials.',
206
+ inputSchema: {},
207
+ annotations: READ_ONLY_ANNOTATIONS
208
+ }, async () => resultOf(async () => {
209
+ const database = await getDatabase();
210
+ const rows = rowsOf(await database.query('SELECT CURRENT_SCHEMA AS CURRENT_SCHEMA, CURRENT_USER AS CURRENT_USER FROM DUMMY'));
211
+ if (!rows.length) throw new Error('HANA did not return its current connection identity.');
212
+ const currentSchema = rows[0].CURRENT_SCHEMA;
213
+ if (currentSchema !== schemaName) throw new Error('The connected HANA schema does not match the configured HDI schema.');
214
+ return {
215
+ source: connectionSummary.source,
216
+ serviceName: connectionSummary.serviceName,
217
+ bindingName: connectionSummary.bindingName,
218
+ host: connectionSummary.host,
219
+ port: connectionSummary.port,
220
+ configuredSchema: schemaName,
221
+ currentSchema,
222
+ databaseUser: rows[0].CURRENT_USER,
223
+ tls: 'enabled; certificate verification required'
224
+ };
225
+ }));
226
+
227
+ server.registerTool('hana_list_objects', {
228
+ title: 'List HANA objects',
229
+ description: 'List tables and/or views from the configured HDI schema only. Results are capped at 200 objects.',
230
+ inputSchema: {
231
+ objectType: z.enum(['ALL', 'TABLE', 'VIEW']).default('ALL'),
232
+ maxResults: z.number().int().min(1).max(HANA_MAX_ROWS).default(100)
233
+ },
234
+ annotations: READ_ONLY_ANNOTATIONS
235
+ }, async ({ objectType, maxResults }) => resultOf(async () => {
236
+ return listObjects(await getDatabase(), schemaName, objectType, maxResults);
237
+ }));
238
+
239
+ server.registerTool('hana_describe_object', {
240
+ title: 'Describe HANA object',
241
+ description: 'Return column metadata for a table or view in the configured HDI schema only.',
242
+ inputSchema: { objectName: z.string().trim().min(1).max(127) },
243
+ annotations: READ_ONLY_ANNOTATIONS
244
+ }, async ({ objectName }) => resultOf(async () => {
245
+ return describeObject(await getDatabase(), schemaName, objectName);
246
+ }));
247
+
248
+ const valueSchema = z.union([z.string().max(2048), z.number().finite(), z.boolean()]);
249
+ const filterSchema = z.object({
250
+ column: z.string().trim().min(1).max(127),
251
+ operator: z.enum(['eq', 'ne', 'lt', 'lte', 'gt', 'gte']),
252
+ value: valueSchema
253
+ }).strict();
254
+
255
+ server.registerTool('hana_read_rows', {
256
+ title: 'Read HANA rows',
257
+ description: `Run a bounded, parameterized read from one catalog-listed table/view in the configured HDI schema. Select 1–${MAX_COLUMNS} known columns; no free-form SQL, DDL, DML, procedure calls, or schema selection is accepted. Maximum ${HANA_MAX_ROWS} rows.`,
258
+ inputSchema: {
259
+ objectName: z.string().trim().min(1).max(127),
260
+ columns: z.array(z.string().trim().min(1).max(127)).min(1).max(MAX_COLUMNS),
261
+ filters: z.array(filterSchema).max(MAX_FILTERS).default([]),
262
+ orderBy: z.object({
263
+ column: z.string().trim().min(1).max(127),
264
+ direction: z.enum(['ASC', 'DESC']).default('ASC')
265
+ }).strict().optional(),
266
+ limit: z.number().int().min(1).max(HANA_MAX_ROWS).default(50)
267
+ },
268
+ annotations: READ_ONLY_ANNOTATIONS
269
+ }, async args => resultOf(async () => readRows(await getDatabase(), schemaName, args)));
270
+ }
271
+
272
+ export const HANA_TOOL_NAMES = Object.freeze([
273
+ 'hana_connection_info',
274
+ 'hana_list_objects',
275
+ 'hana_describe_object',
276
+ 'hana_read_rows'
277
+ ]);
@@ -39,6 +39,15 @@ export const SAP_DEVELOPMENT_MCP_SERVERS = Object.freeze([
39
39
  packageName: '@playwright/mcp',
40
40
  bin: 'playwright-mcp',
41
41
  priority: 'recommended'
42
+ }),
43
+ Object.freeze({
44
+ id: 'hana-cloud-inspector',
45
+ name: 'HANA Cloud inspector',
46
+ description: 'Read-only HANA Cloud/HDI metadata and bounded row inspection using host-provided HANA_RO_* or VCAP_SERVICES credentials.',
47
+ packageName: 'sap-ai-dev-toolkit',
48
+ bin: 'sap-ai-hana',
49
+ ignoreScripts: true,
50
+ priority: 'optional'
42
51
  })
43
52
  ]);
44
53
 
@@ -78,7 +87,7 @@ export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SE
78
87
  entries[server.id] = {
79
88
  type: 'stdio',
80
89
  command: packageManager,
81
- args: ['--yes', `--package=${packageSpec}`, server.bin],
90
+ args: ['--yes', ...(server.ignoreScripts ? ['--ignore-scripts'] : []), `--package=${packageSpec}`, server.bin],
82
91
  BAS_EXT: 'true',
83
92
  BAS_EXT_KIND: COMPANION_SERVER_KIND,
84
93
  displayName: server.name,
package/src/mcp-proxy.mjs CHANGED
@@ -34,6 +34,66 @@ const APPLICATION_LOG_SCHEMA = {
34
34
  };
35
35
  const APPLICATION_LOG_DESCRIPTION = 'Read SAP application log (SLG1) entries. Results are newest first and limited to 100 by default; set messages=true to include log message details.';
36
36
 
37
+ // Keep the default public VSP surface small enough for developer-lifecycle use.
38
+ // Hidden upstream tools can still be used by local workflow tools when needed.
39
+ export const PUBLIC_VSP_TOOLS = new Set([
40
+ 'Activate',
41
+ 'ActivateMultiple',
42
+ 'CompareSource',
43
+ 'CreateTransport',
44
+ 'DebuggerAttach',
45
+ 'DebuggerDetach',
46
+ 'DebuggerGetStack',
47
+ 'DebuggerGetVariables',
48
+ 'DebuggerStep',
49
+ 'EditSource',
50
+ 'FindDefinition',
51
+ 'FindReferences',
52
+ 'GetAPIReleaseState',
53
+ 'GetBreakpoints',
54
+ 'GetCDSDependencies',
55
+ 'GetCDSElementInfo',
56
+ 'GetCDSImpactAnalysis',
57
+ 'GetClass',
58
+ 'GetClassComponents',
59
+ 'GetClassInclude',
60
+ 'GetClassInfo',
61
+ 'GetConnectionInfo',
62
+ 'GetContext',
63
+ 'GetFeatures',
64
+ 'GetFunction',
65
+ 'GetFunctionGroup',
66
+ 'GetInactiveObjects',
67
+ 'GetInclude',
68
+ 'GetInstalledComponents',
69
+ 'GetInterface',
70
+ 'GetPackage',
71
+ 'GetProgram',
72
+ 'GetSource',
73
+ 'GetSystemInfo',
74
+ 'GetTable',
75
+ 'GetTableContents',
76
+ 'GetTransport',
77
+ 'GetTransportInfo',
78
+ 'GetUserTransports',
79
+ 'GrepObjects',
80
+ 'GrepPackages',
81
+ 'ListDependencies',
82
+ 'ListTransports',
83
+ 'PrettyPrint',
84
+ 'RunATCCheck',
85
+ 'RunQuery',
86
+ 'RunUnitTests',
87
+ 'SearchObject',
88
+ 'SetBreakpoint',
89
+ 'SyntaxCheck',
90
+ 'WriteSource'
91
+ ]);
92
+
93
+ function exposeVspTool(tool) {
94
+ return PUBLIC_VSP_TOOLS.has(tool?.name);
95
+ }
96
+
37
97
  function applicationLogArguments(arguments_ = {}) {
38
98
  const filters = arguments_ && typeof arguments_ === 'object' && !Array.isArray(arguments_) ? arguments_ : {};
39
99
  const params = { type: 'application_log' };
@@ -358,6 +418,7 @@ export class MCPProxy {
358
418
  });
359
419
  }
360
420
 
421
+ if (!exposeVspTool(tool)) continue;
361
422
  const name = `${slug}__${tool.name}`;
362
423
  this.namespace.set(name, { entry, upstream: tool.name });
363
424
  merged.push({ ...tool, name, description: `${tool.description || tool.name} [destination: ${entry.destination.name}]` });
package/src/setup.mjs CHANGED
@@ -306,8 +306,9 @@ export async function runSetup({
306
306
  let sapDevelopmentServers = [];
307
307
  if (includeSapDevelopmentToolsPrompt) {
308
308
  print(output, '');
309
- print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development.', 'step', output, 'Tools'));
310
- print(output, ' Recommended for RAP/Fiori/CAP/UI validation work. Leave empty to only configure ABAP/ADT destination servers.');
309
+ print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development and HANA inspection.', 'step', output, 'Tools'));
310
+ print(output, ' HANA Cloud inspector is read-only and uses HANA_RO_* or VCAP_SERVICES already present in the MCP host environment; credentials are not written to mcp.json.');
311
+ print(output, ' Leave empty to only configure ABAP/ADT destination servers.');
311
312
  print(output, '');
312
313
  sapDevelopmentServers = await checkboxPrompt({
313
314
  message: colorText('🧰 Select companion tools', 'cyan', output),
@@ -10,6 +10,7 @@ const skillsRoot = join(root, '.github', 'skills');
10
10
  const expectedAgents = [
11
11
  'abap-developer.agent.md',
12
12
  'abap-runtime-debugger.agent.md',
13
+ 'hana-cloud-hdi-specialist.agent.md',
13
14
  'rap-service-developer.agent.md',
14
15
  'sap-solution-architect.agent.md'
15
16
  ];
@@ -20,6 +21,9 @@ const expectedSkills = [
20
21
  'abap-testing-quality',
21
22
  'cds-development',
22
23
  'clean-core-extensibility',
24
+ 'hana-cloud-inspection',
25
+ 'hana-cloud-native-development',
26
+ 'hana-cloud-validation',
23
27
  'rap-development',
24
28
  'rap-service-delivery',
25
29
  'sap-sdlc-orchestration',
@@ -136,6 +140,7 @@ test('SAP Solution Architect agent has valid metadata and planning/handoff polic
136
140
  assert.match(markdown, /ABAP Developer/i);
137
141
  assert.match(markdown, /RAP Service Developer/i);
138
142
  assert.match(markdown, /ABAP Runtime Debugger/i);
143
+ assert.match(markdown, /HANA Cloud\/HDI Specialist/i);
139
144
  assert.match(markdown, /SDLC|lifecycle/i);
140
145
  assertPlanFirstPolicy(markdown, path);
141
146
  assertLocalFirstPolicy(markdown, path);
@@ -158,6 +163,27 @@ test('RAP Service Developer agent has valid metadata and plan-first local-first
158
163
  assertNoUnsafeTransportClaims(markdown, path);
159
164
  });
160
165
 
166
+ test('HANA Cloud/HDI Specialist agent has a read-only target-bound workflow', async () => {
167
+ const path = join(agentsRoot, 'hana-cloud-hdi-specialist.agent.md');
168
+ const markdown = await readMarkdown(path);
169
+ const metadata = parseFrontmatter(markdown, path);
170
+ assert.deepEqual(Object.keys(metadata).sort(), ['description', 'name', 'target', 'user-invocable']);
171
+ assert.equal(metadata.name, 'HANA Cloud/HDI Specialist');
172
+ assert.equal(metadata.target, 'vscode');
173
+ assert.equal(metadata['user-invocable'], 'true');
174
+ assert.match(metadata.description, /HANA Cloud/i);
175
+ assertDirectMcpUsage(markdown, path);
176
+ assertMentionsLiveToolDiscovery(markdown, path);
177
+ for (const tool of ['hana_connection_info', 'hana_list_objects', 'hana_describe_object', 'hana_read_rows']) {
178
+ assert.match(markdown, new RegExp(tool));
179
+ }
180
+ assert.match(markdown, /read-only/i);
181
+ assert.match(markdown, /HANA_RO_\*/);
182
+ assert.match(markdown, /cds build --for hana/);
183
+ assert.match(markdown, /user-run deployment|deployment is performed by the user/i);
184
+ assert.match(markdown, /never execute HANA DDL\/DML|Never execute HANA DDL\/DML/i);
185
+ });
186
+
161
187
  test('each skill has valid metadata, matching name, and enforceable live-tool validation policy', async () => {
162
188
  for (const skill of expectedSkills) {
163
189
  const path = join(skillsRoot, skill, 'SKILL.md');
@@ -199,3 +225,25 @@ test('debugging skill requires evidence before fixes and bounded debugger use',
199
225
  assert.match(markdown, /Avoid attaching to another user's session|do not.*another user's session/i);
200
226
  assert.match(markdown, /before changing code|before proposing a cause/i);
201
227
  });
228
+
229
+ test('HANA skills describe the read-only tool surface and safe CAP/HDI generation', async () => {
230
+ const inspection = await readMarkdown(join(skillsRoot, 'hana-cloud-inspection', 'SKILL.md'));
231
+ for (const tool of ['hana_connection_info', 'hana_list_objects', 'hana_describe_object', 'hana_read_rows']) {
232
+ assert.match(inspection, new RegExp(tool));
233
+ }
234
+ assert.match(inspection, /200-row limit/);
235
+ assert.match(inspection, /Do not launch `sap-ai-dev` or `sap-ai-hana` for MCP operations/);
236
+ assert.match(inspection, /schema-bound/);
237
+ assert.match(inspection, /credential-like columns/);
238
+
239
+ const development = await readMarkdown(join(skillsRoot, 'hana-cloud-native-development', 'SKILL.md'));
240
+ assert.match(development, /cds build --for hana/);
241
+ assert.match(development, /CAP-owned database models/);
242
+ assert.match(development, /Never execute `cds deploy` against HANA/);
243
+ assert.match(development, /HANA DDL\/DML/);
244
+
245
+ const validation = await readMarkdown(join(skillsRoot, 'hana-cloud-validation', 'SKILL.md'));
246
+ assert.match(validation, /deployment handoff/);
247
+ assert.match(validation, /HANA tools are metadata and bounded read-only inspection only/);
248
+ assert.match(validation, /Never claim HANA activation\/deployment/);
249
+ });