Bridging SAP and Modern AI: Supercharging ABAP Development with MCP & Antigravity CLI
Generative AI coding assistants have transformed software development across mainstream languages like TypeScript, Python, and Go. However, enterprise development environments—specifically SAP ABAP—often remain isolated behind proprietary protocols, specialized tooling, and legacy workflows.
By combining the open Model Context Protocol (MCP) with Antigravity CLI in Visual Studio Code, you can connect autonomous AI agents directly to your SAP backend. This guide walks through setting up an enterprise-ready environment that empowers AI agents to inspect ABAP objects, modify code safely under Transport Requests, and execute ABAP Unit tests with coverage reports.
Architecture Overview
Connecting an external AI coding assistant to SAP relies on a two-tier MCP architecture:
-
Official SAP ADT Extension Server: Runs an HTTP-based MCP server providing baseline language features.
-
Custom ADT MCP Server: A local Node.js service that interfaces with the underlying language server client via IPC/named pipes. This bridges critical operational gaps: reading concatenated includes (local definitions, test classes), locking files under specific Transport Requests, and retrieving structured code coverage reports.
1. Prerequisites & Base Tooling
Ensure your local development workstation has the following installed:
-
Node.js: v18.0 or higher.
-
Visual Studio Code: Latest stable release.
-
SAP Network Access: Active connection to your corporate SAP network or enterprise VPN.
-
Antigravity CLI: Installed and accessible in your workspace terminal environment.
-
ABAP Development Tools (ADT) Extension for VS Code:
-
Open VS Code and open the Extensions view (Ctrl+Shift+X).
-
Search for and install ABAP Development Tools by SAP SE (sapse.adt-vscode).
-
Open VS Code Settings (Ctrl+,), search for ADT MCP, and enable the setting Adt > Mcp Server: Enabled.
-
Note the default port (2236) and copy the generated bearer token.
-
2. Configuring the SAP System Connection
Configure your SAP destination within VS Code to establish an authenticated RFC/SSO session:
-
Open the VS Code Command Palette (Ctrl+Shift+P).
-
Select ABAP: New Destination… and choose your connection type (e.g., RFC: on-prem system).
-
Enter your system ID, client, username, and logon language.
-
Open the Command Palette again and run ABAP: Add destination as folder in workspace, selecting the destination created above.
-
In the Explorer pane, right-click the destination folder and choose Logon to verify connectivity.
3. The Custom ADT MCP Bridge Server
While the official extension provides high-level language server integration, AI workflows require programmatic hooks for automated code generation, refactoring, and test verification.
Create a working directory on your system (e.g., C:\tools\sap-adt-mcp\ on Windows or ~/.saptools/ on Linux/macOS) and save the following Node.js bridge as sap-adt-mcp.js:
const { spawn } = require('child_process');
const fs = require('fs');
const net = require('net');
const path = require('path');
const os = require('os');
// Dynamic Path Resolutions
const userHome = os.homedir();
const appData = process.env.APPDATA || path.join(userHome, 'AppData', 'Roaming');
// 1. Locate VS Code ADT language server executable
const extensionsDir = path.join(userHome, '.vscode', 'extensions');
let exePath = '';
if (fs.existsSync(extensionsDir)) {
const folders = fs.readdirSync(extensionsDir);
const adtExtFolder = folders.find(f => f.startsWith('sapse.adt-vscode-'));
if (adtExtFolder) {
exePath = path.join(extensionsDir, adtExtFolder, 'adt-ls', 'win32', 'win32', 'x86_64', 'adt-lsc.exe');
}
}
// 2. Resolve destination and workspace storage
const storePath = path.join(userHome, '.adtls', 'destinations.json');
const storageRoot = path.join(appData, 'Code', 'User', 'workspaceStorage');
let workspaceDir = '';
if (fs.existsSync(storageRoot)) {
const folders = fs.readdirSync(storageRoot);
for (const f of folders) {
const candidate = path.join(storageRoot, f, 'SAPSE.adt-vscode', 'adtWorkspace');
if (fs.existsSync(candidate)) {
workspaceDir = candidate;
break;
}
}
}
// MCP Stdio Message Handler
let buffer = '';
process.stdin.on('data', chunk => {
buffer += chunk.toString('utf8');
processMessages();
});
function processMessages() {
while (true) {
const newlineIndex = buffer.indexOf('\n');
if (newlineIndex === -1) break;
const line = buffer.substring(0, newlineIndex).trim();
buffer = buffer.substring(newlineIndex + 1);
if (line) {
try {
serverInfo: { name: 'sap-adt-mcp-server', version: '1.0.0' }
handleMcpRequest(msg);
} catch (e) {
sendMcpError(null, -32700, 'Parse error: ' + e.message);
}
}
}
}
function sendMcpResponse(id, result) {
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
}
function sendMcpError(id, code, message) {
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');
}
async function handleMcpRequest(msg) {
if (msg.method === 'initialize') {
if (!exePath || !fs.existsSync(exePath)) {
return sendMcpError(msg.id, -32603, 'SAP ADT language server executable not found.');
}
if (!workspaceDir || !fs.existsSync(workspaceDir)) {
return sendMcpError(msg.id, -32603, 'Active VS Code ADT workspace storage not found.');
}
sendMcpResponse(msg.id, {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'sap-adt-mcp-agent', version: '1.0.0' }
});
} else if (msg.method === 'tools/list') {
sendMcpResponse(msg.id, {
tools: [
{
name: 'read_abap_object',
description: 'Reads complete ABAP source including local classes, definitions, and test classes.',
inputSchema: {
type: 'object',
properties: {
destinationId: { type: 'string', description: 'Destination ID (e.g., DEV_100_USER_EN)' },
adtUri: { type: 'string', description: 'ADT URI (e.g., /sap/bc/adt/oo/classes/zcl_order_manager)' }
},
required: ['destinationId', 'adtUri']
}
},
{
name: 'write_abap_object',
description: 'Saves edits to an ABAP object locked under a specified Transport Request.',
inputSchema: {
type: 'object',
properties: {
destinationId: { type: 'string' },
adtUri: { type: 'string' },
content: { type: 'string' },
transportId: { type: 'string', description: 'Transport Request (e.g., DEVK900001)' }
},
required: ['destinationId', 'adtUri', 'content']
}
},
{
name: 'run_abap_unit_tests',
description: 'Executes ABAP Unit tests and calculates code coverage.',
inputSchema: {
type: 'object',
properties: {
destinationId: { type: 'string' },
adtUri: { type: 'string' },
measurement: { type: 'string', enum: ['COVERAGE', 'NONE'], default: 'COVERAGE' }
},
required: ['destinationId', 'adtUri']
}
}
]
});
} else if (msg.method === 'tools/call') {
try {
const { name, arguments: args } = msg.params;
let result;
if (name === 'read_abap_object') {
result = await executeReadAbapObject(args.destinationId, args.adtUri);
} else if (name === 'write_abap_object') {
result = await executeWriteAbapObject(args.destinationId, args.adtUri, args.content, args.transportId);
} else if (name === 'run_abap_unit_tests') {
result = await executeRunAbapUnitTests(args.destinationId, args.adtUri, args.measurement || 'COVERAGE');
} else {
return sendMcpError(msg.id, -32601, 'Method not found');
}
sendMcpResponse(msg.id, {
content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
});
} catch (e) {
sendMcpResponse(msg.id, { isError: true, content: [{ type: 'text', text: e.message }] });
}
}
}
function runAdtLsSession(actionFn) {
return new Promise((resolve, reject) => {
const pipeName = '\\\\.\\pipe\\adt-ls-bridge-' + Date.now();
let child = null, socket = null, lspBuffer = Buffer.alloc(0), requestId = 1, pendingResolver = null, isFinished = false, serverRequestHandler = null;
const server = net.createServer(sock => {
socket = sock;
socket.on('data', chunk => {
lspBuffer = Buffer.concat([lspBuffer, chunk]);
processLspBuffer();
});
socket.on('error', cleanup);
startSequence();
});
server.listen(pipeName, () => {
child = spawn(exePath, ['-Djco.trace_path=' + workspaceDir, '-data', workspaceDir, '--pipe=' + pipeName], { stdio: 'ignore' });
child.on('error', cleanup);
child.on('exit', code => {
if (code !== 0 && !isFinished) cleanup(new Error('adt-lsc.exe exited with code ' + code));
});
});
function cleanup(err) {
if (isFinished) return;
isFinished = true;
if (socket) socket.destroy();
if (child) child.kill();
server.close();
if (err) reject(err);
}
function processLspBuffer() {
while (true) {
const str = lspBuffer.toString('utf8');
const headerEnd = str.indexOf('\r\n\r\n');
if (headerEnd === -1) break;
let len = 0;
for (const line of str.substring(0, headerEnd).split('\r\n')) {
if (line.startsWith('Content-Length:')) len = parseInt(line.substring(15).trim());
}
const total = headerEnd + 4 + len;
if (lspBuffer.length < total) break;
const body = str.substring(headerEnd + 4, total);
lspBuffer = lspBuffer.subarray(total);
try {
const res = JSON.parse(body);
if (res.id && pendingResolver && pendingResolver.id === res.id) {
const r = pendingResolver.resolve;
pendingResolver = null;
r(res);
} else if (res.method && res.id && serverRequestHandler) {
serverRequestHandler(res, out => {
const payload = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: res.id, result: out }), 'utf8');
socket.write(`Content-Length: ${payload.length}\r\n\r\n`);
socket.write(payload);
});
}
} catch (e) {
cleanup(e);
}
}
}
function callRequest(method, params) {
return new Promise(res => {
const id = requestId++;
pendingResolver = { id, resolve: res };
const payload = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id, method, params }), 'utf8');
socket.write(`Content-Length: ${payload.length}\r\n\r\n`);
socket.write(payload);
});
}
async function startSequence() {
try {
await callRequest('initialize', {
processId: process.pid,
rootUri: 'file:///' + workspaceDir.replace(/\\/g, '/'),
capabilities: {},
initializationOptions: { userAgentInfos: [{ name: 'ADTVSCode', version: '1.0.1' }] }
});
await callRequest('adtLs/destinations/initializeService', {
destinationsStorePath: storePath,
workspaceFolderUris: ['file:///' + workspaceDir.replace(/\\/g, '/')],
fileUris: []
});
const out = await actionFn(callRequest, h => (serverRequestHandler = h));
cleanup();
resolve(out);
} catch (e) {
cleanup(e);
}
}
});
}
async function executeReadAbapObject(destinationId, adtUri) {
return runAdtLsSession(async callRequest => {
await callRequest('adtLs/destinations/ensureLoggedOn', destinationId);
const uriRes = await callRequest('adtLs/repository/getLsUri', { destination: destinationId, adtUri });
const mainUri = uriRes.result.uri;
const fileRes = await callRequest('adtLs/fileSystem/readFile', { uri: mainUri });
let content = fileRes.result.content;
if (mainUri.endsWith('.clas.abap')) {
const baseUri = mainUri.slice(0, -10);
const includes = [
{ suffix: '.clas.locals_def.abap', label: 'Local Definitions' },
{ suffix: '.clas.implementations.abap', label: 'Local Implementations' },
{ suffix: '.clas.testclasses.abap', label: 'Test Classes' }
];
for (const inc of includes) {
try {
const incRes = await callRequest('adtLs/fileSystem/readFile', { uri: baseUri + inc.suffix });
if (incRes.result?.content?.trim()) {
content += `\n\n"* ==========================================\n"* ${inc.label}\n"* ==========================================\n` + incRes.result.content;
}
} catch (_) {}
}
}
return content;
});
}
async function executeWriteAbapObject(destinationId, adtUri, content, transportId) {
return runAdtLsSession(async (callRequest, setRequestHandler) => {
await callRequest('adtLs/destinations/ensureLoggedOn', destinationId);
const uriRes = await callRequest('adtLs/repository/getLsUri', { destination: destinationId, adtUri });
const lsUri = uriRes.result.uri;
setRequestHandler((req, respond) => {
if (req.method === 'window/showMessageRequest') {
const actions = req.params.actions || [];
if (transportId) {
const matched = actions.find(a => a.title.includes(transportId));
if (matched) return respond(matched);
}
if (actions.length === 1) return respond(actions[0]);
throw new Error(`Multiple open transports found: [${actions.map(a => a.title).join(', ')}]. Specify transportId.`);
}
respond(null);
});
await callRequest('adtLs/fileSystem/lockFile', { uri: lsUri });
try {
await callRequest('adtLs/fileSystem/writeFile', { uri: lsUri, content, options: { create: false, overwrite: true } });
return `SUCCESS: Saved updates to ${adtUri} under transport lock.`;
} finally {
await callRequest('adtLs/fileSystem/unlockFile', { uri: lsUri });
}
});
}
async function executeRunAbapUnitTests(destinationId, adtUri, measurement) {
return runAdtLsSession(async callRequest => {
await callRequest('adtLs/destinations/ensureLoggedOn', destinationId);
const uriRes = await callRequest('adtLs/repository/getLsUri', { destination: destinationId, adtUri });
const testRes = await callRequest('adtLs/abapUnit/runTests', {
measurement,
lsUris: [uriRes.result.uri],
outputFormatParams: { format: 'ANSI_ESCAPED', summaryOnly: false, lineDelimiter: '\r\n' }
});
const output = { overallStatus: testRes.result?.overallStatus || 'PASSED' };
if (measurement === 'COVERAGE' && testRes.result?.coverageParams) {
const cov = await callRequest('adtLs/coverage/getCoverage', testRes.result.coverageParams);
if (cov.result) output.coverage = cov.result;
}
return output;
});
}
4. Registering MCP Servers in Antigravity CLI
Antigravity CLI and MCP-compatible agents negotiate capabilities using standard JSON configuration files (e.g., mcp_config.json or Antigravity settings). Add both the official HTTP endpoint and the custom stdio server:
{
"mcpServers": {
"sap-adt-official": {
"serverUrl": "http://localhost:2236/mcp",
"headers": {
"Authorization": "Bearer <<YOUR_ADT_BEARER_TOKEN>>",
"User-Agent": "adt-ls-client-vscode"
}
},
"sap-adt-mcp-server": {
"command": "node",
"args": ["C:\\tools\\sap-adt-mcp\\sap-adt-mcp.js"]
}
}
}
When Antigravity CLI starts, it initializes connection with both tools:
-
sap-adt-official provides repository exploration and language diagnostics via HTTP.
-
sap-adt-mcp-server exposes read_abap_object, write_abap_object, and run_abap_unit_tests over stdio.
5. Enforcing Modern ABAP Style Guidelines
To keep generated code aligned with modern ABAP paradigms (such as the Clean ABAP style guide), configure rules in your workspace configuration or system instruction prompt file (e.g., AGENTS.md or .antigravity/rules):
6. Remote & Cloud Development (SSH Reverse Tunneling)
If your AI coding agent runs on a remote Linux development server or cloud VM while your SAP GUI/ADT session lives on a local Windows machine, forward the local ADT port to the remote instance:
Bash
ssh -R 2236:localhost:2236 user@remote-dev-box.company.internal
In your remote environment, set the Antigravity MCP configuration to target http://localhost:2236/mcp. Commands issued by Antigravity CLI will route back across the secure SSH tunnel directly to your authenticated local ADT session.
Unlocking Autonomous ABAP Workflows
By standardizing SAP ADT access through the Model Context Protocol, ABAP teams can move beyond generic, out-of-context code completion. AI agents can autonomously inspect existing business logic, apply refactoring patterns under proper change control, and validate changes immediately against ABAP Unit test suites.
