sap-ai-dev-toolkit 0.3.3 → 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.
Files changed (36) hide show
  1. package/.github/agents/abap-developer.agent.md +3 -1
  2. package/.github/agents/abap-runtime-debugger.agent.md +3 -1
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +22 -0
  4. package/.github/agents/rap-service-developer.agent.md +3 -1
  5. package/.github/agents/sap-solution-architect.agent.md +6 -3
  6. package/.github/skills/hana-cloud-inspection/SKILL.md +35 -0
  7. package/.github/skills/hana-cloud-native-development/SKILL.md +26 -0
  8. package/.github/skills/hana-cloud-validation/SKILL.md +26 -0
  9. package/README.md +50 -22
  10. package/package.json +9 -4
  11. package/scripts/postinstall.mjs +3 -3
  12. package/src/bas-destination-relay.mjs +24 -13
  13. package/src/credential-overrides.mjs +75 -0
  14. package/src/credentials-store.mjs +14 -5
  15. package/src/hana-config.mjs +170 -0
  16. package/src/hana-database.mjs +150 -0
  17. package/src/hana-inspector.mjs +114 -0
  18. package/src/hana-tools.mjs +277 -0
  19. package/src/launcher.mjs +10 -32
  20. package/src/mcp-config.mjs +10 -1
  21. package/src/mcp-proxy.mjs +61 -0
  22. package/src/setup.mjs +87 -61
  23. package/test/bas-destination-relay.test.mjs +50 -0
  24. package/test/cf-runtime.test.mjs +27 -5
  25. package/test/copilot-content.test.mjs +63 -0
  26. package/test/credentials-store.test.mjs +31 -0
  27. package/test/hana-config.test.mjs +168 -0
  28. package/test/hana-inspector-stdio.test.mjs +44 -0
  29. package/test/hana-tools.test.mjs +206 -0
  30. package/test/launcher.test.mjs +52 -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-cf.test.mjs +131 -4
  35. package/test/setup.test.mjs +10 -6
  36. package/tools.md +58 -50
@@ -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
+ ]);
package/src/launcher.mjs CHANGED
@@ -10,36 +10,11 @@ import { installMcpConfig } from './mcp-config.mjs';
10
10
  import { runSetup } from './setup.mjs';
11
11
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
12
12
  import { withBrandedEnvironment } from './branding.mjs';
13
- import { readCredentials, resolveCredentialsPath } from './credentials-store.mjs';
13
+ import { enrichWithStoredCredentials } from './credential-overrides.mjs';
14
14
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
15
15
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
16
16
  const runtimeEnv = withBrandedEnvironment(process.env);
17
17
 
18
- // Attach stored direct-connect credentials (setup prompts for Internet
19
- // destinations) to matching discovered destinations. Credentials stay in
20
- // memory only; nothing is logged or written to the MCP config.
21
- async function enrichWithStoredCredentials(destinations, env = runtimeEnv) {
22
- const needingCredentials = destinations.filter(destination =>
23
- destination.source !== 'cloud-foundry'
24
- && destination.url
25
- && String(destination.url).includes('.dest'));
26
- if (!needingCredentials.length) return destinations;
27
- let stored;
28
- try {
29
- const path = await resolveCredentialsPath(env);
30
- stored = await readCredentials(path);
31
- } catch (error) {
32
- console.error(`[sap-ai-dev] stored credentials could not be read: ${String(error.message).slice(0, 200)}`);
33
- return destinations;
34
- }
35
- return destinations.map(destination => {
36
- const entry = stored.destinations?.[destination.name];
37
- if (!entry?.host || !entry.user || !entry.password) return destination;
38
- if (!destination.backendUrl) return destination;
39
- return { ...destination, credentials: { host: entry.host, user: entry.user, password: entry.password } };
40
- });
41
- }
42
-
43
18
  function hasFlag(name) { return process.argv.slice(2).includes(name); }
44
19
  function json(value) { process.stdout.write(`${JSON.stringify(value)}\n`); }
45
20
 
@@ -111,7 +86,9 @@ async function runDoctor(destinations) {
111
86
  } else {
112
87
  checks.push(doctorRow(destination.name, 'BAS destination relay', 'skipped', 'Not applicable for this destination.'));
113
88
  }
114
- if (relayedDestination?.credentials) {
89
+ if (relayedDestination?.credentials?.mode === 'bas-tunnel') {
90
+ checks.push(doctorRow(destination.name, 'OnPremise credential override', 'passed', 'Basic auth is routed through a BAS proxy tunnel; Cloud Connector routing is retained'));
91
+ } else if (relayedDestination?.credentials?.host) {
115
92
  checks.push(doctorRow(destination.name, 'Direct connect', 'passed', `active (${new URL(relayedDestination.credentials.host).origin}); ADT writes use stored credentials`));
116
93
  }
117
94
  const listed = await proxy.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
@@ -203,8 +180,8 @@ async function main() {
203
180
  let report;
204
181
  try {
205
182
  const destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
206
- ? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
207
- : await enrichWithStoredCredentials(await discoverForCommand());
183
+ ? await enrichWithStoredCredentials([await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })], runtimeEnv)
184
+ : await enrichWithStoredCredentials(await discoverForCommand(), runtimeEnv);
208
185
  report = await runDoctor(destinations);
209
186
  } catch (error) {
210
187
  report = { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', redactText(error.message || error).slice(0, 300))] };
@@ -240,7 +217,7 @@ async function main() {
240
217
  }
241
218
 
242
219
  if (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry') {
243
- const destination = await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv });
220
+ const destination = (await enrichWithStoredCredentials([await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })], runtimeEnv))[0];
244
221
  let proxy;
245
222
  try {
246
223
  const binary = await binaryOrError();
@@ -270,14 +247,15 @@ async function main() {
270
247
 
271
248
  const discovered = await discoverDestinations({ env: runtimeEnv });
272
249
  for (const destination of discovered) console.error(probeDiagnostic(destination));
273
- const destinations = await enrichWithStoredCredentials(discovered);
250
+ const destinations = await enrichWithStoredCredentials(discovered, runtimeEnv);
274
251
  if (!destinations.length) {
275
252
  console.error('[sap-ai-dev] destination discovery returned no named BAS destinations');
276
253
  throw new Error(remediation);
277
254
  }
278
255
  const binary = await binaryOrError();
279
256
  for (const destination of destinations) {
280
- if (destination.credentials) console.error(`[sap-ai-dev] ${destination.name}: direct connect enabled through stored credentials`);
257
+ if (destination.credentials?.mode === 'bas-tunnel') console.error(`[sap-ai-dev] ${destination.name}: Basic credentials enabled through the BAS proxy tunnel`);
258
+ else if (destination.credentials?.host) console.error(`[sap-ai-dev] ${destination.name}: direct connect enabled through stored credentials`);
281
259
  }
282
260
  console.error(`[sap-ai-dev] starting MCP proxy for ${destinations.map(destination => `${destination.name} (client=${destination.client})`).join(', ')}`);
283
261
  const proxy = new MCPProxy({ binary, destinations, env: process.env, log: message => console.error(message) });
@@ -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}]` });