@arc-mcp/arc-1-lsp
v0.3.0
Published
ARC-1 edition that delegates all ABAP/ADT work to SAP's embedded adt-ls language server.
Maintainers
Readme
arc-1-lsp
An edition of ARC-1 — a Model Context Protocol (MCP) server for SAP ABAP
development — that delegates all ABAP/ADT interaction to SAP's own embedded
adt-ls language server instead of a hand-rolled ADT HTTP client. arc-1-lsp
owns the MCP front-end, auth/scopes, write-safety, and orchestration; adt-ls
owns CSRF, locking, XML, activation, transport — everything system-specific.
Status: working — connects headless to a SAP system, exposes 39 MCP tools (reads + LSP code-intelligence incl. hover · ATC + ABAP-Unit coverage · a full create→edit→activate→test→delete authoring loop · RAP generation · run-application · service-binding publish · native transport), runs locally over stdio or as a Docker app on SAP BTP Cloud Foundry. Single-tenant / one technical user today; per-user principal propagation is on the roadmap.
Where adt-ls comes from
adt-ls is SAP's language server: the headless core of the Eclipse-based
ABAP Development Tools (ADT), shipped inside the official
ABAP Development Tools for VS Code
extension (sapse.adt-vscode). It exposes three surfaces, and arc-1-lsp drives
all of them:
- standard LSP code-intelligence (
textDocument/*— document symbols, go-to- definition, references/where-used, type hierarchy, diagnostics/syntax check, completion) — it is a language server; - a private LSP namespace (
adtLs/*— destinations, logon, filesystem, activation, transport, unit tests); - an embedded experimental MCP server (object creation, activation, RAP generators, transports, …).
arc-1-lsp does not reimplement any of that — it discovers, spawns, and drives
the developer-provided adt-ls headless (no Eclipse, no VS Code, no browser). The
adt-ls binary is under SAP's Developer License and is never bundled or
redistributed — you bring your own (see Prerequisites and
ADR-0002).
arc-1-lsp vs. main ARC-1 — which should I use?
Both are MCP servers for SAP ABAP and share the same tool shape. They differ in how they talk to SAP, and therefore in what they can do.
| | ARC-1 (main) | arc-1-lsp (this repo) |
|---|---|---|
| ADT protocol | Hand-rolled (CSRF, locking, XML, version quirks) | Delegated to SAP's adt-ls |
| System-specific code to maintain | ~29 ADT modules | ~zero (it's SAP's job) |
| Object-type coverage | All — classic and modern (programs, tables, function groups, domains, CDS, classes, RAP, …) | Modern ABAP-Cloud types only (class, interface, CDS, behavior def, service def/binding, …) |
| Free SQL / data preview | ✅ | ❌ (absent in adt-ls) |
| Navigation / where-used / type hierarchy | ✅ | ✅ (via adt-ls's standard LSP — textDocument/*) |
| Syntax check / ATC | ✅ | ✅ syntax check (check_syntax) + ATC (run_atc, system-default variant) |
| Git (gCTS / abapGit) | ✅ | ❌ (absent in adt-ls) |
| Maturity | Production, multi-user, write-capable | Working; reads + authoring loop; single technical user |
Use main ARC-1 for the broadest coverage (classic objects, free SQL, git, transport release/delete) and production multi-user deployments. Use arc-1-lsp when you want SAP itself to own the ADT protocol — less code to maintain, and behavior that tracks ADT exactly (including its standard LSP code-intelligence) — and your work is on modern ABAP-Cloud objects.
The honest, line-by-line map of what is and isn't wired (and why) lives in
docs/arc-1-feature-parity.md; the live-verified
capability boundary of adt-ls itself is in
docs/adt-ls-reference.md.
What works today
27 MCP tools. Reads work read-only; mutating tools are gated behind
ARC1_ALLOW_WRITES + a package allowlist (transport creation additionally needs
ARC1_ALLOW_TRANSPORT_WRITES).
- Reads (14):
health,list_destinations,list_creatable_objects,search_objects,list_inactive_objects,list_users,list_generators,get_generator_schema,get_object_type_details,get_service_binding,get_service_details,read_source,validate_object,find_transport. - Code intelligence (9, LSP):
document_symbols(outline),go_to_definition,go_to_declaration,find_references,type_hierarchy(super/subtypes + implementations),hover(signature + ABAP-Doc),document_highlight,check_syntax(the ABAP syntax check, no activation needed),completion. adt-ls is a language server — these proxy its standardtextDocument/*APIs; target a declaredsymbolby name or a 1-basedline+character. - Quality & test (3):
run_atc(ABAP Test Cockpit static analysis — security/ performance/cloud-readiness, system-default variant),list_atc_variants,run_unit_tests_with_coverage(test result + statement/branch/procedure coverage). - Runtime & business services (3):
run_application(run anif_oo_adt_classrunclass or program, capture console output),service_binding_details,publish_service_binding(publish/unpublish a SRVB → live OData; write-gated). - Authoring loop (5, write-gated):
create_object,update_source,activate_object,run_unit_tests,delete_object— a full create → edit → activate → test → delete cycle, by object name, for modern ABAP-Cloud types.activate_objectreturns ranged syntax diagnostics so an agent can self-correct. - Generation + transport (5, gated):
generate_objectsruns a RAP generator (scaffolds a full table/CDS/behavior/service set);create_transportopens a CTS request;assign_transportpins an existing TR to an object (returns a structured{assigned, object, transport});list_transports(capped tolimit, default 100, with an optionalqueryfilter — the system can hold thousands of requests) +get_lock_status(reads). For transportable (non-$TMP) packages the flow isvalidate_object→find_transport→ (create_transport) →create_object/generate_objects(pass the TR astransport).
Out of scope here (use main ARC-1): classic object types (program/table/
function group/domain/…), free SQL, transport release/delete, and git. These are
honest limits of adt-ls's headless surface, not missing features — details in
docs/arc-1-feature-parity.md.
The SAP session behind adt-ls self-heals: if it expires (idle timeout →
"logged off"), arc-1-lsp transparently re-logs on and retries the call once.
Architecture
agent (Claude / Copilot / Cursor / …)
│ MCP (stdio | http-streamable)
▼
arc-1-lsp (Node/TS — discovers, spawns & supervises adt-ls; auth + scopes; owns SAP logon)
├─ LSP over pipe ───────────▶ adt-ls (headless, BYO) ← bootstrap, destinations, logon, filesystem, activation
└─ HTTP localhost ─────────▶ adt-ls's own /mcp ← federated tools
│ HTTPS
▼
TLS reverse proxy (CN=localhost, in arc-1-lsp)
│ DIRECT ───────────────▶ SAP ABAP (internet-reachable)
└ CC ─▶ connectivity bridge ─▶ BTP Connectivity ─▶ Cloud Connector ─▶ SAP ABAPadt-ls requires an HTTPS backend and validates its hostname; SAP's default
self-signed cert (CN=*.dummy.nodomain) fails that. So arc-1-lsp runs a local
TLS-terminating reverse proxy (cert CN=localhost, trusted via a truststore
built from adt-ls's own JRE) and re-originates to the real backend — directly,
or through the connectivity bridge on BTP. Logon is headless reentrance-ticket
emulation (no browser). Full recipe + decisions:
docs/adt-ls-headless-notes.md +
docs/adr/.
Prerequisites
- Node.js 22+.
- A developer-provided
adt-ls(BYO — never redistributed). Install the official ABAP Development Tools for VS Code extension (sapse.adt-vscode, which accepts SAP's Developer License) and arc-1-lsp finds itsadt-lsautomatically. Discovery order:ARC1_ADT_LS_PATH(explicit path)vendor/adt-ls/<platform>/…(build-time injection, for containers)- the newest installed
sapse.adt-vscode-*VS Code extension
- A reachable SAP ABAP system to connect to (optional — the server also
starts disconnected and still serves
health/tools). Runtime cert/proxy deps:openssl+keytool(the latter ships insideadt-ls's JRE). Seedocs/native-deps.md.
Compatibility: arc-1-lsp drives
adt-ls's privateadtLs/*protocol, which can change between releases. This build is verified againstsapse.adt-vscode1.0.0 / adt-ls 1.0.0.202605281240; on a different version arc-1-lsp logs a startup warning and you should re-verify againstdocs/adt-ls-reference.md.
Install & run
From source (stdio)
npm install
npm run build
node dist/index.js # or: npm run dev (tsx, no build)Point an MCP client at the process over stdio. With no SAP vars set it starts
disconnected (handy for inspecting the tool list); set ARC1_SAP_* to auto-connect
(see Connect a SAP system).
As a CLI (npm)
npm install -g @arc-mcp/arc-1-lsp
arc1-lsp # stdio MCP server (honors the same env/flags)The npm package ships the Node wrapper only — it still discovers your BYO adt-ls
(it does not contain any SAP binary).
Docker / http-streamable
The container bundles a build-time-injected linux adt-ls and serves MCP over
http-streamable behind an API key — this is the artifact deployed to BTP CF.
# stage the linux adt-ls (admin provides the licensed VSIX → vendor/)
node scripts/extract-adt-ls.mjs
# build the linux/amd64 image (host-builds dist; only prod deps + adt-ls are amd64)
IMAGE=arc-1-lsp:dev bash scripts/docker-build.sh
# run
docker run -e ARC1_API_KEYS=devkey -p 8080:8080 arc-1-lsp:devGET /healthz (no auth) for health checks; POST /mcp with
Authorization: Bearer <key> for MCP.
Connect a SAP system
Set the ARC1_SAP_* vars (or --sap-* flags) and arc-1-lsp logs on at startup.
ARC1_SAP_HOST=a4h.example.com ARC1_SAP_PORT=50001 \
ARC1_SAP_USER=DEVELOPER ARC1_SAP_PASSWORD=… ARC1_SAP_DESTINATION=A4H \
node dist/index.jshealth then reports connectedDestination, and list_creatable_objects returns
the system's object catalog. Two connection modes:
- DIRECT (default) — the reverse proxy connects straight to an
internet-reachable backend. All four of
HOST/PORT/USER/PASSWORDmust be set. - CC (on-prem via Cloud Connector, on BTP) — bind the
connectivity+destinationservices and set onlyARC1_SAP_DESTINATION <btp-destination-name>; the engine resolves it and routes through the connectivity bridge automatically.
Auth (DIRECT mode): basic (user + password, default), sso (interactive browser
logon), or clientcert (passwordless X.509 mutual TLS — no browser, no password, silent
re-auth; set ARC1_SAP_CLIENT_CERT/_KEY). The client-cert path has its own guide covering
the AS ABAP server setup and how to obtain/store certificates (incl. enterprise / SAP Secure
Login Service): docs/client-cert-auth-setup.md.
Connect an MCP client
// Claude Code (HTTP):
// claude mcp add --transport http arc1lsp https://<host>/mcp \
// --header "Authorization: Bearer <api-key>"
//
// Cursor / Claude Desktop / VS Code — mcp.json:
{
"mcpServers": {
"arc1lsp": {
"url": "https://<host>/mcp",
"headers": { "Authorization": "Bearer <api-key>" }
}
}
}For local stdio, point the client at the node dist/index.js (or arc1-lsp)
process instead of a URL. GUI inspector: npx @modelcontextprotocol/inspector.
Configuration (precedence: CLI flag > env var > default)
Env prefix is
ARC1_*, notSAP_*. The main ARC-1 server usesSAP_*vars; arc-1-lsp usesARC1_*(e.g.ARC1_ALLOW_TRANSPORT_WRITES,ARC1_SAP_USER). ASAP_*var is ignored — arc-1-lsp logs a startup warning naming theARC1_*twin it expected so a migrated config doesn't silently lose a setting.
| Env / flag | Default | Meaning |
|------------|---------|---------|
| ARC1_ADT_LS_PATH / --adt-ls-path | (discovered) | explicit adt-ls binary |
| ARC1_ADT_LS_MCP_PORT / --adt-ls-mcp-port | 2240 | port for adt-ls's own MCP server |
| ARC1_ADT_LS_MCP_TOKEN / --adt-ls-mcp-token | (generated) | bearer for adt-ls's MCP server |
| ARC1_TRANSPORT / --transport | stdio | stdio | http-streamable |
| ARC1_PORT / --port | 8080 | HTTP port (http-streamable; CF $PORT honored) |
| ARC1_API_KEYS / --api-keys | (none) | edge auth: key[:label-or-profile][,key2…]; empty disables auth (local only). A profile suffix :viewer/:developer/:admin assigns scopes (per-tool enforcement arrives with the XSUAA edge — see ADR-0007); any other suffix is a free label (defaults to developer) |
| ARC1_ALLOW_WRITES / --allow-writes | false | enable mutating tools (create/update/activate/delete/generate) |
| ARC1_ALLOW_TRANSPORT_WRITES / --allow-transport-writes | false | enable CTS transport creation (create_transport) — also requires ARC1_ALLOW_WRITES |
| ARC1_ALLOWED_PACKAGES / --allowed-packages | $TMP | packages writes may target — exact / PREFIX* / * |
| ARC1_LOG_LEVEL | info | debug|info|warn|error (stderr only) |
| SAP connection — DIRECT mode (internet-reachable backend) | | |
| ARC1_SAP_HOST / --sap-host | — | backend host |
| ARC1_SAP_PORT / --sap-port | — | backend HTTPS port |
| ARC1_SAP_AUTH / --sap-auth | basic | basic (headless user+password), sso (interactive browser logon — local desktop only; no password, but startup blocks until you sign in and re-auth re-opens the browser on your next call), or clientcert (passwordless X.509 mutual TLS — fully headless, no browser, silent re-auth; needs ARC1_SAP_CLIENT_CERT/_KEY). |
| ARC1_SAP_USER / --sap-user | — | SAP user (basic: the reentrance ticket is fetched with these creds; sso / clientcert: an optional destination hint — the real user comes from the cert mapping) |
| ARC1_SAP_PASSWORD / --sap-password | — | SAP password (basic mode; set via env / cf set-env, never committed). Not used in sso / clientcert mode. |
| ARC1_SAP_CLIENT_CERT / --sap-client-cert | — | PEM client-certificate file path (clientcert mode); the subject must map to a SAP user via CERTRULE. Where to get it — and why your everyday "it just works" SSO cert usually can't be used (short-lived / non-exportable): see Obtaining a client certificate. |
| ARC1_SAP_CLIENT_KEY / --sap-client-key | — | PEM private-key file path (clientcert mode). |
| ARC1_SAP_DESTINATION / --sap-destination | SAP | adt-ls destination id (DIRECT) or BTP destination name (CC) |
| ARC1_SAP_CLIENT / --sap-client | 001 | SAP client |
| ARC1_SAP_LANGUAGE / --sap-language | EN | SAP logon language |
| ARC1_SAP_INSECURE / --sap-insecure | true | accept the backend's self-signed cert (the proxy's own TLS is trusted separately) |
| SAP connection — CC mode (on-prem via Cloud Connector) | | |
| ARC1_SAP_DESTINATION | — | BTP Destination Service name; resolved when connectivity is bound |
Config is read from CLI flags and the process environment only (no
.envauto-loading) — export the vars in your shell or set them viacf set-env.
Deploy to BTP Cloud Foundry
The image deploys to CF as a docker app (see manifest.yml). Secrets stay out of
git — the API key, SAP creds, and the registry pull token are passed at deploy time:
docker push ghcr.io/<owner>/arc-1-lsp:0.1.0
# secrets via cf set-env (never committed):
cf set-env arc-1-lsp ARC1_API_KEYS "$(openssl rand -hex 16)"
cf set-env arc-1-lsp ARC1_SAP_HOST <host>
cf set-env arc-1-lsp ARC1_SAP_PORT 50001
cf set-env arc-1-lsp ARC1_SAP_USER <user>
cf set-env arc-1-lsp ARC1_SAP_PASSWORD <secret>
cf set-env arc-1-lsp ARC1_SAP_DESTINATION <id>
cf set-env arc-1-lsp ARC1_ALLOW_WRITES true # optional, to enable the authoring loop
cf set-env arc-1-lsp ARC1_ALLOWED_PACKAGES '$TMP' # scope writes
# re-push (stop first if the org memory quota is tight — avoids a transient 2×2G):
cf stop arc-1-lsp
CF_DOCKER_PASSWORD=$(gh auth token) cf push arc-1-lsp -f manifest.ymlcf logs shows engine: connected destination …; the MCP health tool reports
connectedDestination. cf push preserves cf set-env vars not listed in the
manifest. If the ghcr image is private, CF pulls it with CF_DOCKER_PASSWORD;
make the package public to drop that.
Test a running instance (local or CF)
The /mcp endpoint is stateless StreamableHTTP — a bare tools/call works, no
session handshake.
ARC1_URL=https://<host>/mcp ARC1_KEY=<api-key> bash scripts/smoke-remote.sh
# → /healthz ok · health {connectedDestination} · tools/list · list_creatable_objectsDocumentation
| Doc | What it covers |
|-----|----------------|
| docs/adt-ls-reference.md | The authoritative, live-verified adt-ls capability map — URI model, the getLsUri name→URI resolver, the method/tool matrix, the object-type boundary, the proven lifecycle, session self-heal, gotchas |
| docs/arc-1-feature-parity.md | arc-1 vs arc-1-lsp coverage, per-capability "implemented? why / why not" |
| docs/research/adt-ls-capability-map.md | The complete DECOMPILED adt-ls surface (ground truth — CFR-decompiled com.sap.adt.ls): all 23 adtLs/* segments / ~92 methods with DTO shapes + per-capability usefulness triage + wiring gap. Corrects hover/ATC/formatting verdicts; documents the embedded MCP server's dynamic tool collection |
| docs/research/whats-left-on-sap.md | The earlier extension-front-end inventory (superseded in part by the capability map above) — strategic "what's reachable-but-unwired vs blocked-on-SAP" framing |
| docs/adt-ls-headless-notes.md | The reverse-engineered headless connection recipe (initialize, reentrance-ticket logon, TLS/truststore) |
| docs/adr/ | Architecture Decision Records — each decision, its context, and when to revisit it |
| docs/assumptions-and-future-changes.md | The watch-list: what to re-verify against new adt-ls releases, and what would let us delete complexity |
| docs/native-deps.md | System libraries adt-ls needs in a slim container |
| docs/journey.md | The chronological story, including dead-ends (so they aren't re-walked) |
Contributing? See CONTRIBUTING.md (setup, tests, conventions)
and SECURITY.md (reporting vulnerabilities). Working with Claude
Code? CLAUDE.md is the design + codebase map. Releases are
automated from Conventional Commits via release-please.
Status & roadmap
✅ foundation → ✅ containerize → ✅ deploy to BTP CF → ✅ headless connect (DIRECT) → ✅ read + authoring-loop + generation/transport tools → ✅ session self-heal → ✅ LSP code-intelligence + ATC + coverage + run + native transport (39 tools).
Next: CC-mode deploy (code ready; needs a running Cloud Connector + bound
connectivity/destination), then per-user principal propagation (one
adt-ls session per user via the BTP Destination Service). Roadmap detail in
docs/plans/ and docs/assumptions-and-future-changes.md.
License & credits
MIT © 2026 Marian Zeis and contributors.
Built on the shell of ARC-1 (MIT,
© Alice Vinogradova and contributors) — arc-1-lsp reuses its MCP server,
configuration, authorization model, audit, and logging patterns. SAP's adt-ls
is not included or redistributed (SAP Developer License) — bring your own; see
ADR-0002.
