Generic, reusable TypeScript SDK over SAP's headless adt-ls — the
adt-lsclanguage server shipped inside the officialsapse.adt-vscodeextension. It hides the painful setup (discovery, JVM, named-pipe + LSP handshake, reentrance logon, TLS/truststore, session resilience) so that driving adt-ls is a few lines of code.
Status: published on npm — functionally complete & live-proven. The full SAP authoring
lifecycle (search → create → update → read → activate → run-tests → delete), plus code
intelligence, quality gates, ABAP formatting, transport, and OData service info, all run
end-to-end through createAdtLs() against a real S/4HANA system (adt-ls
1.0.1.202606111342). Runs under Node ≥ 20 and Bun (both verified live).
npm install @arc-mcp/adt-ls
You bring adt-ls (SAP Developer License — not redistributable): install the
sapse.adt-vscode extension (VS Code / Cursor) and the library auto-discovers it, or
vendor the per-platform VSIX for CI. New here → docs/setup.md:
which platform build to download, CI vendoring, and connecting with auth.
This release requires adt-ls >= 1.0.1 and is verified against 1.0.1.202606111342.
import { createAdtLs, basic } from '@arc-mcp/adt-ls';
const adt = await createAdtLs({
connection: { systemUrl: 'https://my-s4:50001', selfSigned: true, client: '001' },
auth: basic('DEVELOPER', process.env.SAP_PW!), // or bearer(token) / interactive({ openUrl }) / clientCert({ cert, key })
});
const hits = await adt.repository.search('CL_ABAP*', { types: ['CLAS/OC'] });
const src = await adt.source.read({ name: 'ZCL_FOO', objectType: 'CLAS/OC' });
await adt.lifecycle.create({ objectType: 'CLAS/OC', name: 'ZCL_BAR', packageName: '$TMP', description: 'demo' });
await adt.lifecycle.activate({ name: 'ZCL_BAR', objectType: 'CLAS/OC' });
await adt.dispose();
The on-the-wire logon is always a reentrance ticket; auth supplies the credential:
basic(user, password) — headless user/password (on-prem fixed user).bearer(token | getToken) — headless OAuth bearer (BTP ABAP / Steampunk).interactive({ openUrl }) — the consumer opens the SSO URL; a human signs in (the lib
ships no browser/TTY).clientCert({ cert, key }) — passwordless X.509 mutual TLS, no browser. The reverse
proxy presents the client cert on every upstream hop, so the backend authenticates the TLS
connection itself and the reentrance handler runs with no credential. Requires
connection.selfSigned. Server-side this is the standard AS ABAP
X.509 client-certificate logon
import { createAdtLs, clientCert } from '@arc-mcp/adt-ls';
import { readFileSync } from 'node:fs';
const adt = await createAdtLs({
connection: { systemUrl: 'https://my-s4:50001', selfSigned: true, client: '001' },
auth: clientCert({ cert: readFileSync('client.crt'), key: readFileSync('client.key') }),
});
Consumers that drive adt-ls themselves — e.g. proxying its MCP endpoint to external
agents — can skip createAdtLs() and use the primitives directly (this is what
abapify/openadt adopts):
import { resolveAdtLsPath, AdtLsDriver, startMcpServer } from '@arc-mcp/adt-ls';
const driver = new AdtLsDriver(resolveAdtLsPath(), {
extraArgs: ['-consoleLog', `-Djco.middleware.snc_lib=${sncLib}`], // SNC/JCo JVM flags
});
await driver.start(); // discovery + spawn + LSP initialize (short pipe; macOS-safe)
// register your own logon handlers: driver.setRequestHandler('adtLs/destinations/requestBrowserBasedLogon', …)
const { port, token } = await startMcpServer(driver, { port: 2240, token: myToken });
// → proxy http://localhost:${port}/mcp (Authorization: Bearer ${token}) however you like
await driver.dispose();
One namespaced client over both adt-ls channels (LSP + its own MCP) — the split is hidden:
repository — object search, file read/write/delete, inactive-object list, name→URI resolver.source / lifecycle — read; create, update, activate (native — per-phase diagnostics, forceActivation), run unit tests, delete; RAP generators; creatable-type catalog + creation-form (legal values per field) + validation.navigation — document symbols, definition/declaration, references, type hierarchy, hover, completion (with resolve → method signatures + ABAP-Doc), syntax check, semantic tokens, and ABAP Pretty-Printer formatting.quality — ATC static analysis + ABAP Unit code coverage.services — run a console app, service-binding details/publish, and live OData service info (URL + entity sets).transport — find / create / assign / list, lock status, and the transport decision oracle (check).raw — escape hatches to any adt-ls MCP tool or LSP method.What maps to which adt-ls call: the capability matrix. What's reachable headless vs. not (with live evidence): the capability survey.
main.npm run docs:api).Two first-party projects already drive headless adt-ls and reimplement the same
fragile, reverse-engineered plumbing: abapify/openadt (its
@openadt/sap-adt-mcp-launcher) and arc-1-lsp (src/adt-ls/*). Both encode the
identical landmines (the userAgentInfos NPE, HTTPS-only + hostname verification,
silent session death, the reentrance-ticket dance). The fragmentation that actually
hurt here is first-party duplication, and the cure is one shared library.
The thesis: adt-ls is the correct path (SAP-maintained CSRF/locking/activation/XML), but its setup is so much harder than calling ADT REST directly that people avoid it. This library makes adt-ls as easy to use as a plain API, so the easy choice is also the right one.
adt-ls ONLY. No direct ADT/SAP HTTP, no SAP ADT SDK sidecar, no MCP server, no Cloud-Connector/BTP bridge inside the library. What adt-ls can't do headless is out of scope. See ADR-0001.
1.0.1.202606111342)Proven hands-on against the freshly-downloaded 1.0.1 VSIX and the live a4h system:
initialize (with the userAgentInfos workaround) → ADTLS 1.0.1.202606111342.keytool.create → update → read → activate → run-tests → delete GREEN against a4h — exercising auth (reentrance + TLS proxy), the LSP channel, the MCP channel, and the resilience layer end-to-end.→ No design or protocol blockers remain.
src/adt-ls/* entirely; keeps its own MCP server, BTP/Cloud-Connector
bridge (via the connection.forwardProxy hook), authz, and write-safety as thin wrappers over the lib.