sap-ai-dev-toolkit 0.9.9904 → 0.9.9908
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/README.md +12 -6
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/package.json +2 -2
- package/patches/vsp-bas-proxy-auth.patch +162 -5
- package/scripts/postinstall.mjs +5 -193
- package/src/abaplint.mjs +229 -24
- package/src/harness-mcp-config.mjs +9 -1
- package/src/harness-setup.mjs +202 -0
- package/src/harnesses.mjs +21 -4
- package/src/launcher.mjs +6 -3
- package/src/mcp-proxy.mjs +7 -1
- package/src/setup.mjs +54 -10
- package/src/sso-browsers.mjs +154 -4
- package/tools.md +3 -2
package/src/setup.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { adtBaseUrl, adtPort, defaultAdtUrl, discoverSapGuiSystems, hasExplicitP
|
|
|
8
8
|
import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
|
|
9
9
|
import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
|
|
10
10
|
import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
|
|
11
|
+
import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
|
|
11
12
|
|
|
12
13
|
const SETUP_COMMAND = 'sap-ai-dev --setup';
|
|
13
14
|
|
|
@@ -66,6 +67,45 @@ async function chooseLocalAuthMode(destination, { input = stdin, output = stdout
|
|
|
66
67
|
}, { input, output });
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env }) {
|
|
71
|
+
let selectedPath;
|
|
72
|
+
let selectionComplete = false;
|
|
73
|
+
return async destination => {
|
|
74
|
+
if (selectionComplete) return selectedPath;
|
|
75
|
+
selectionComplete = true;
|
|
76
|
+
print(output, formatStatus('Checking installed browsers for remote DevTools access.', 'progress', output, 'SSO'));
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await discoverSsoBrowsers({ env });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
|
|
82
|
+
print(output, formatStatus(`Browser control check failed (${reason}). Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { browsers = [], unavailable = [] } = result || {};
|
|
86
|
+
if (!browsers.length) {
|
|
87
|
+
const details = unavailable.map(browser => `${browser.name}: ${browser.reason}`).join('; ').slice(0, 300);
|
|
88
|
+
print(output, formatStatus(`No remotely controllable SSO browser was found${details ? ` (${details})` : ''}. Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (browsers.length === 1) {
|
|
92
|
+
selectedPath = browsers[0].path;
|
|
93
|
+
print(output, formatStatus(`Using ${browsers[0].name}; it responded to a remote DevTools command.`, 'success', output, 'SSO browser'));
|
|
94
|
+
return selectedPath;
|
|
95
|
+
}
|
|
96
|
+
selectedPath = await selectPrompt({
|
|
97
|
+
message: `Browser for SSO (${destination.name})`,
|
|
98
|
+
defaultValue: browsers[0].path,
|
|
99
|
+
choices: browsers.map((browser, index) => ({
|
|
100
|
+
name: `${browser.name} (${browser.path})`,
|
|
101
|
+
value: browser.path,
|
|
102
|
+
shortcut: String(index + 1)
|
|
103
|
+
}))
|
|
104
|
+
}, { input, output });
|
|
105
|
+
return selectedPath;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
async function confirmCloudFoundryImport({ input = stdin, output = stdout } = {}) {
|
|
70
110
|
const readline = createInterface({ input, output });
|
|
71
111
|
try {
|
|
@@ -238,7 +278,7 @@ async function confirmPrompt(message, { input = stdin, output = stdout } = {}) {
|
|
|
238
278
|
return !/^n(?:o)?$/i.test(String(answer || '').trim());
|
|
239
279
|
}
|
|
240
280
|
|
|
241
|
-
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false } = {}) {
|
|
281
|
+
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false, browserPath } = {}) {
|
|
242
282
|
const name = localClientDestinationName(destination, client, { multipleClients, duplicateSystemName });
|
|
243
283
|
const localDestination = { ...destination, name, serverName: name, client };
|
|
244
284
|
const userInputId = `sap-ai-dev-${localDestination.serverName || localDestination.name}-user`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
@@ -248,7 +288,7 @@ function buildLocalClientDestination(destination, client, authMode, { multipleCl
|
|
|
248
288
|
{ id: passwordInputId, type: 'promptString', description: `SAP password for ${localDestination.name}`, password: true }
|
|
249
289
|
];
|
|
250
290
|
if (authMode === 'sso') {
|
|
251
|
-
localDestination.childEnv = { SAP_AUTH_MODE: 'sso' };
|
|
291
|
+
localDestination.childEnv = { SAP_AUTH_MODE: 'sso', ...(browserPath ? { SAP_BROWSER_EXEC: browserPath } : {}) };
|
|
252
292
|
localDestination.inputs = [];
|
|
253
293
|
} else {
|
|
254
294
|
localDestination.childEnv = {
|
|
@@ -285,7 +325,7 @@ async function maybeConfigureTlsSelfHeal(destination, promptLabel, { input = std
|
|
|
285
325
|
// collects the minimal ADT configuration directly from the user and produces
|
|
286
326
|
// the same local destination shape as the SAP GUI flow, so installation,
|
|
287
327
|
// credential inputs, and TLS handling stay identical.
|
|
288
|
-
async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl } = {}) {
|
|
328
|
+
async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl, selectSsoBrowser } = {}) {
|
|
289
329
|
print(output, '');
|
|
290
330
|
print(output, colorText('✍️ Manual system entry — skip discovery, add your SAP system directly', 'magenta', output));
|
|
291
331
|
print(output, colorText(' The ADT URL is the base URL that Eclipse ADT or VS Code ADT tools use, for example https://myhost.example:44300.', 'cyan', output));
|
|
@@ -338,10 +378,11 @@ async function runManualEntryWizard({ env = process.env, input = stdin, output =
|
|
|
338
378
|
} else {
|
|
339
379
|
if (authMode === 'sso') warnSsoIpHost(destination, name, output);
|
|
340
380
|
else await maybeConfigureTlsSelfHeal(destination, name, { input, output });
|
|
381
|
+
const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
|
|
341
382
|
destination.authentication = localAuthenticationLabel(authMode);
|
|
342
383
|
destination.authMode = authMode;
|
|
343
384
|
for (const client of clients) {
|
|
344
|
-
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1 }));
|
|
385
|
+
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, browserPath }));
|
|
345
386
|
}
|
|
346
387
|
print(output, formatStatus(`Added ${name} (client${clients.length === 1 ? '' : 's'} ${clients.join(', ')}).`, 'success', output, 'Manual entry'));
|
|
347
388
|
}
|
|
@@ -469,7 +510,7 @@ function printConnectionInstructions(output, result) {
|
|
|
469
510
|
|
|
470
511
|
// Checkbox selection over the discovered destinations plus, outside BAS, the
|
|
471
512
|
// per-system confirmation prompts (clients, ADT URL, TLS self-heal, login).
|
|
472
|
-
async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map() } = {}) {
|
|
513
|
+
async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map(), selectSsoBrowser } = {}) {
|
|
473
514
|
const orderedDestinations = [...destinations].sort((left, right) => {
|
|
474
515
|
const leftRank = left.disabledReason ? 2 : (left.probe?.available === false ? 1 : 0);
|
|
475
516
|
const rightRank = right.disabledReason ? 2 : (right.probe?.available === false ? 1 : 0);
|
|
@@ -495,7 +536,7 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
|
|
|
495
536
|
print(output, colorText(' Space = select/deselect · a = toggle all · m = add a system manually · Enter = confirm.', 'cyan', output));
|
|
496
537
|
print(output, colorText(' 💡 No system listed, or need an extra one? Press m to skip discovery and add any SAP system manually.', 'magenta', output));
|
|
497
538
|
print(output, '');
|
|
498
|
-
const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl })).map(destination => ({
|
|
539
|
+
const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser })).map(destination => ({
|
|
499
540
|
value: destination,
|
|
500
541
|
name: `${destination.name} (manual, client ${destination.client})`
|
|
501
542
|
}));
|
|
@@ -549,10 +590,11 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
|
|
|
549
590
|
}
|
|
550
591
|
if (authMode === 'sso') warnSsoIpHost(destination, promptLabel, output);
|
|
551
592
|
else await maybeConfigureTlsSelfHeal(destination, promptLabel, { input, output });
|
|
593
|
+
const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
|
|
552
594
|
destination.authentication = localAuthenticationLabel(authMode);
|
|
553
595
|
destination.authMode = authMode;
|
|
554
596
|
for (const client of clients) {
|
|
555
|
-
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName }));
|
|
597
|
+
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName, browserPath }));
|
|
556
598
|
}
|
|
557
599
|
}
|
|
558
600
|
selected = expanded;
|
|
@@ -570,7 +612,8 @@ export async function runSetup({
|
|
|
570
612
|
confirmCfImport = confirmCloudFoundryImport,
|
|
571
613
|
install = installMcpConfig,
|
|
572
614
|
includeSapDevelopmentToolsPrompt = false,
|
|
573
|
-
detectAdtUrl
|
|
615
|
+
detectAdtUrl,
|
|
616
|
+
discoverSsoBrowsers = findRemoteControllableSsoBrowsers
|
|
574
617
|
} = {}) {
|
|
575
618
|
const isBas = Boolean(env.H2O_URL);
|
|
576
619
|
if (!isBas && (!input.isTTY || !output.isTTY)) {
|
|
@@ -581,6 +624,7 @@ export async function runSetup({
|
|
|
581
624
|
print(output, formatStatus(`Interactive setup needs a terminal. Rerun ${SETUP_COMMAND} from a terminal.`, 'warning', output, 'SAP AI Dev Toolkit'));
|
|
582
625
|
return { skipped: true, reason: 'non-tty' };
|
|
583
626
|
}
|
|
627
|
+
const selectSsoBrowser = createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env });
|
|
584
628
|
print(output, '');
|
|
585
629
|
print(output, colorText('💡 Tip: you can skip auto-discovery and add a system manually — press m in the destination picker, or take the wizard offer when nothing is found.', 'magenta', output));
|
|
586
630
|
if (!isBas) print(output, formatStatus('Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
|
|
@@ -721,11 +765,11 @@ export async function runSetup({
|
|
|
721
765
|
]
|
|
722
766
|
}, { input, output });
|
|
723
767
|
if (choice !== 'manual') return await exitWithoutDestinations();
|
|
724
|
-
selected = await runManualEntryWizard({ env, input, output, detectAdtUrl });
|
|
768
|
+
selected = await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser });
|
|
725
769
|
additiveSetup = true;
|
|
726
770
|
if (!selected.length) return await exitWithoutDestinations();
|
|
727
771
|
} else {
|
|
728
|
-
selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls });
|
|
772
|
+
selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls, selectSsoBrowser });
|
|
729
773
|
additiveSetup = selected.length > 0;
|
|
730
774
|
}
|
|
731
775
|
|
package/src/sso-browsers.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { access, constants } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { posix, win32 } from 'node:path';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { access, constants, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { join, posix, win32 } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { WebSocket } from 'undici';
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
|
|
@@ -59,6 +60,155 @@ async function fileExists(path) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function remoteDebuggingEndpoint(output) {
|
|
64
|
+
return /DevTools listening on (ws:\/\/[^\s]+)/.exec(output)?.[1] || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function waitForRemoteDebuggingEndpoint(child, timeoutMs) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let output = '';
|
|
70
|
+
let settled = false;
|
|
71
|
+
const finish = (error, endpoint) => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
child.stdout?.off('data', onData);
|
|
76
|
+
child.stderr?.off('data', onData);
|
|
77
|
+
child.off('error', onError);
|
|
78
|
+
child.off('close', onClose);
|
|
79
|
+
if (error) reject(error);
|
|
80
|
+
else resolve(endpoint);
|
|
81
|
+
};
|
|
82
|
+
const onData = chunk => {
|
|
83
|
+
output = `${output}${chunk}`.slice(-8192);
|
|
84
|
+
const endpoint = remoteDebuggingEndpoint(output);
|
|
85
|
+
if (endpoint) finish(null, endpoint);
|
|
86
|
+
};
|
|
87
|
+
const onError = error => finish(error);
|
|
88
|
+
const onClose = (code, signal) => finish(new Error(`browser exited before exposing DevTools (code ${code ?? 'unknown'}, signal ${signal || 'none'})`));
|
|
89
|
+
const timer = setTimeout(() => finish(new Error(`browser did not expose DevTools within ${timeoutMs}ms`)), timeoutMs);
|
|
90
|
+
child.stdout?.on('data', onData);
|
|
91
|
+
child.stderr?.on('data', onData);
|
|
92
|
+
child.once('error', onError);
|
|
93
|
+
child.once('close', onClose);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function controlBrowser(endpoint, timeoutMs) {
|
|
98
|
+
const url = new URL(endpoint);
|
|
99
|
+
if (url.protocol !== 'ws:' || !['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname)) {
|
|
100
|
+
throw new Error('browser exposed a non-local DevTools endpoint');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
let settled = false;
|
|
105
|
+
const socket = new WebSocket(url.href);
|
|
106
|
+
const finish = error => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
try { socket.close(); } catch {}
|
|
111
|
+
if (error) reject(error);
|
|
112
|
+
else resolve();
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(() => finish(new Error(`DevTools command timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
115
|
+
socket.addEventListener('open', () => socket.send(JSON.stringify({ id: 1, method: 'Browser.getVersion' })));
|
|
116
|
+
socket.addEventListener('message', event => {
|
|
117
|
+
let response;
|
|
118
|
+
try { response = JSON.parse(event.data); }
|
|
119
|
+
catch { return finish(new Error('DevTools returned an invalid response')); }
|
|
120
|
+
if (response.id !== 1) return;
|
|
121
|
+
if (response.error) return finish(new Error(response.error.message || 'DevTools rejected Browser.getVersion'));
|
|
122
|
+
if (!response.result?.product) return finish(new Error('DevTools returned no browser version'));
|
|
123
|
+
finish();
|
|
124
|
+
});
|
|
125
|
+
socket.addEventListener('error', () => finish(new Error('could not connect to the browser DevTools endpoint')));
|
|
126
|
+
socket.addEventListener('close', () => {
|
|
127
|
+
if (!settled) finish(new Error('browser closed the DevTools connection'));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForClose(child, timeoutMs) {
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve(true);
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
child.off('close', onClose);
|
|
137
|
+
resolve(false);
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
const onClose = () => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve(true);
|
|
142
|
+
};
|
|
143
|
+
child.once('close', onClose);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function signalBrowserTree(child, platform, signal) {
|
|
148
|
+
if (platform === 'win32' && child.pid) {
|
|
149
|
+
try {
|
|
150
|
+
await execFileAsync('taskkill', [
|
|
151
|
+
'/pid', String(child.pid), '/t',
|
|
152
|
+
...(signal === 'SIGKILL' ? ['/f'] : [])
|
|
153
|
+
], { windowsHide: true, timeout: 1000 });
|
|
154
|
+
return;
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
|
|
159
|
+
else child.kill(signal);
|
|
160
|
+
} catch {
|
|
161
|
+
try { child.kill(signal); } catch {}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function stopBrowser(child, platform) {
|
|
166
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
167
|
+
await signalBrowserTree(child, platform, 'SIGTERM');
|
|
168
|
+
if (await waitForClose(child, 1000)) return;
|
|
169
|
+
await signalBrowserTree(child, platform, 'SIGKILL');
|
|
170
|
+
await waitForClose(child, 1000);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function probeSsoBrowser(browser, {
|
|
174
|
+
platform = process.platform,
|
|
175
|
+
timeoutMs = 5000,
|
|
176
|
+
spawnBrowser = spawn,
|
|
177
|
+
controlRemote = controlBrowser
|
|
178
|
+
} = {}) {
|
|
179
|
+
let profile;
|
|
180
|
+
let child;
|
|
181
|
+
try {
|
|
182
|
+
profile = await mkdtemp(join(tmpdir(), 'sap-ai-dev-sso-probe-'));
|
|
183
|
+
child = spawnBrowser(browser.path, [
|
|
184
|
+
`--user-data-dir=${profile}`,
|
|
185
|
+
'--remote-debugging-port=0',
|
|
186
|
+
'--headless=new',
|
|
187
|
+
'--no-first-run',
|
|
188
|
+
'--no-default-browser-check',
|
|
189
|
+
'about:blank'
|
|
190
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: platform !== 'win32' });
|
|
191
|
+
const endpoint = await waitForRemoteDebuggingEndpoint(child, timeoutMs);
|
|
192
|
+
await controlRemote(endpoint, timeoutMs);
|
|
193
|
+
return { usable: true };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
|
|
196
|
+
return { usable: false, reason };
|
|
197
|
+
} finally {
|
|
198
|
+
await stopBrowser(child, platform);
|
|
199
|
+
if (profile) await rm(profile, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function findRemoteControllableSsoBrowsers({ probe = probeSsoBrowser, ...options } = {}) {
|
|
204
|
+
const candidates = await findSsoBrowsers(options);
|
|
205
|
+
const results = await Promise.all(candidates.map(async browser => ({ browser, ...await probe(browser) })));
|
|
206
|
+
return {
|
|
207
|
+
browsers: results.filter(result => result.usable).map(result => result.browser),
|
|
208
|
+
unavailable: results.filter(result => !result.usable).map(({ browser, reason }) => ({ ...browser, reason }))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
62
212
|
// Every Chromium-based browser the runtime can drive, best first; SAP_BROWSER_EXEC pins one ahead of the rest.
|
|
63
213
|
export async function findSsoBrowsers({ env = process.env, platform = process.platform, exists = fileExists, appPaths = registeredAppPaths } = {}) {
|
|
64
214
|
const candidates = [];
|
package/tools.md
CHANGED
|
@@ -72,11 +72,12 @@ Hidden upstream tools are not directly callable through the proxy. Local workflo
|
|
|
72
72
|
|
|
73
73
|
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. UI5 repository writes are hidden too: the ADT filestore only reads UI5 apps, so deploy them with SAP Fiori tools instead.
|
|
74
74
|
|
|
75
|
-
##
|
|
75
|
+
## Local and destination-scoped workflow tools
|
|
76
76
|
|
|
77
|
-
The proxy adds local
|
|
77
|
+
The proxy adds local tools to the MCP surface. Some are workspace-wide; others are added to each destination when their required upstream capabilities are available. Like the upstream tools above, they are exposed publicly in lowercase snake_case (`lint_abap`, `lint_abap_project`, `get_application_log`, and so on):
|
|
78
78
|
|
|
79
79
|
- `LintABAP` — local in-memory ABAP linting.
|
|
80
|
+
- `LintABAPProject` — scan serialized ABAP objects in the configured workspace root (SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT, defaulting to the MCP server working directory) using the project abaplint config and available local dependency folders. This is static analysis only; it does not contact SAP or activate objects.
|
|
80
81
|
- `GetApplicationLog` — bounded convenience mapping to the upstream SAP application-log route when available.
|
|
81
82
|
- `PrepareABAPChangeSet` and `ApplyABAPChangeSet` — review full-source diffs and recheck source before explicit writes.
|
|
82
83
|
- `CheckTransportReadiness` — collect a whitelisted bundle of transport, dependency, inactive-object, ABAP Unit, and ATC results.
|