Skip to main content

MCP Server

erpl-adt doubles as a Model Context Protocol server. Every CLI command is exposed as an MCP tool over JSON-RPC 2.0 on stdin/stdout. Plug it into any MCP-compatible agent — Claude Code, Cursor, Gemini CLI — and the agent decides which tools to call and in what order.

Start the server directly:

erpl-adt mcp --host sap.example.com --port 44300 --https

The process talks JSON-RPC on stdio, so you don't run this yourself; the MCP client launches it for you.


Client configuration

Claude Code

Add an entry to your ~/.claude/mcp.json (or per-project .claude/mcp.json):

{
"mcpServers": {
"sap": {
"command": "erpl-adt",
"args": ["mcp", "--host", "sap.example.com", "--port", "44300", "--https"],
"env": { "SAP_PASSWORD": "..." }
}
}
}

Restart Claude Code and the sap tools appear in the tool list.

Cursor

Settings → MCP → + Add new MCP server and paste the same shape as above.

Gemini CLI

Add to ~/.config/gemini/config.yaml under mcp_servers::

mcp_servers:
sap:
command: erpl-adt
args: ["mcp", "--host", "sap.example.com", "--port", "44300", "--https"]
env:
SAP_PASSWORD: "${SAP_PASSWORD}"

Tools the agent gets

The MCP tool names are derived from the CLI command paths. The most-used ones:

Tool names are verb-first: adt_read_source, not adt_source_read.

MCP toolCLI equivalentWhat it does
adt_searchsearchSearch ABAP objects by pattern and type
adt_read_objectobject readRead an object's metadata (URI, includes, source URIs)
adt_read_sourcesource readRead source for a class, program, or include
adt_write_sourcesource writeWrite source (auto-lock, transport, optional activate)
adt_activateactivateActivate an inactive object
adt_run_teststest runRun ABAP Unit tests
adt_run_atccheck runRun ATC quality checks
adt_check_syntaxsource checkSyntax-check an object
adt_run_classobject runExecute a class implementing IF_OO_ADT_CLASSRUN
adt_create_transport / adt_list_transports / adt_release_transporttransport …Transport lifecycle
adt_read_tableddic tableInspect a transparent table with check tables and ABAP types resolved
adt_read_cdsddic cdsRead a CDS view's source
adt_list_package / adt_package_tree / adt_package_existspackage …Browse package contents
adt_lock / adt_unlockobject lock / object unlockExplicit lock handling for multi-step edits
adt_create_object / adt_delete_objectobject create / object deleteObject lifecycle
adt_discoverdiscoverDiscover available ADT services
bw_search, bw_read_adso, bw_lineage_graph, bw_read_dataflow, …bw …Full BW/4HANA toolkit, 43 tools (see BW page)
catalog_search, catalog_get, catalog_lineage, catalog_where_used, …catalog …Metadata-catalog tools, 12 in total — available when started with --catalog-db (see Catalog page)

77 tools are registered in total — 22 adt_*, 43 bw_* and 12 catalog_*. Call the standard MCP tools/list method from your client to print the current set, with each tool's title, annotations and output schema. Pass --tools adt,bw (or any subset) to register only the families you want, which keeps the agent's prompt smaller.

The catalog_* family is only exposed when you start the server against a pre-built catalog file:

erpl-adt mcp --catalog-db catalog.duckdb --host sap.example.com --port 44300 --https

These answer from the DuckDB cache in milliseconds without touching SAP. See the Catalog guide for building the file.


Example agent loop

A typical session inside Claude Code looks like this — three CLI commands the agent picks itself, from one natural-language question.

You: What flight-related classes exist in this SAP system?

Claude calls adt_search with pattern ZCL_FLIGHT*:

Found 4 matching objects:

Name Type Package Description
──────────────── ──────── ──────────── ──────────────────────────
ZCL_FLIGHT_CTRL CLAS/OC ZFLIGHT_APP Flight booking controller
ZCL_FLIGHT_MODEL CLAS/OC ZFLIGHT_APP Flight data model
ZCL_FLIGHT_TEST CLAS/OC ZFLIGHT_APP Flight module unit tests
ZCL_FLIGHT_API CLAS/OC ZFLIGHT_APP REST API wrapper

You: Show me the booking controller and tell me what's broken.

Claude calls adt_read_objectadt_read_sourceadt_run_tests and writes back a diagnosis. The end-to-end narrative is in Your AI Coding Agent Just Learned ABAP and the follow-ups Real-Time SAP for AI, Part 1 and Part 2.


HTTP transport

Most clients speak stdio, which is the default and needs none of this. --http serves the same tool contract as JSON-RPC over HTTP at POST /mcp — one implementation, two transports — for clients that cannot spawn a process.

erpl-adt mcp --http                                     # 127.0.0.1:8383
erpl-adt mcp --http --mcp-host 0.0.0.0 --mcp-port 9000
FlagEffect
--mcp-host <addr>Address to bind (default: 127.0.0.1)
--mcp-port <n>Port to bind (default: 8383)
--cors-origin <list>Extra browser origins allowed to call /mcp. * allows every origin
--allowed-hosts <list>Host header values this server answers to. * allows every host
--auth-token <tok>Require Authorization: Bearer <tok>; requests without it get 401 and run nothing
--auth-token-env <var>Read that token from an environment variable
-c, --config <path>YAML file whose http: block supplies any of the above

Access control

The tools behind this endpoint write to a live SAP system — adt_write_source, adt_delete_object, adt_activate, adt_release_transport — so who may call it is checked, and binding to 127.0.0.1 is not by itself a boundary: your browser runs inside it.

Origin. Requests with no Origin header (curl, native MCP clients) are allowed, as are same-origin and loopback origins. Any other browser origin is refused with 403 unless named with --cors-origin. That stops a page you happen to visit from posting writes to your SAP system.

Host. Origin checking alone cannot see DNS rebinding: a page that points its own hostname at 127.0.0.1 arrives with a Host it controls and an Origin to match, so both sides of the same-origin comparison belong to the attacker. The Host header is the half they cannot launder, so it is checked separately. Loopback names, IP literals and the address bound are always served; a browser asking for any other host is refused with 403.

# a browser reaching this server by a DNS name needs it allowed
erpl-adt mcp --http --mcp-host 0.0.0.0 --allowed-hosts mcp.internal.example

# or, for containers and unit files, the same thing as an env var
export ERPL_ADT_ALLOWED_HOSTS=mcp.internal.example
erpl-adt mcp --http --mcp-host 0.0.0.0
# erpl.yaml, passed with -c — also read by `catalog webui`
http:
allowed_hosts: [mcp.internal.example, buildbox.corp]
cors_origin: [https://catalog.example]
# the variable's name — a raw auth_token key is deliberately not read
auth_token_env: ERPL_ADT_MCP_TOKEN

An IP literal is always allowed — an IP address has no DNS name to rebind — so --mcp-host 0.0.0.0 reached at http://192.168.1.5:8383 needs no configuration. Non-browser clients (no Origin, no Sec-Fetch-*) are unaffected whatever hostname they use, because rebinding is a browser attack by construction.

Authentication is off unless a token is configured. Binding beyond loopback without one warns on stderr; /healthz never requires the token, so liveness probes keep working.

Each server prints its posture at startup, so what is enforced is visible where the refusals appear:

erpl-adt MCP HTTP server listening on http://127.0.0.1:8383/mcp
hosts: loopback, IP literals (add with --allowed-hosts)
origins: same-origin, loopback (add with --cors-origin)
auth: none (--auth-token to require a bearer token)

Authentication

Credentials are resolved in this order: CLI flags → --password-env env var → ~/.adt.creds (saved by erpl-adt login) → the SAP_PASSWORD environment variable.

For MCP usage, the env block in the client config is the cleanest path — the agent never sees the password, the password never appears in shell history, and you can rotate it without touching the client config.

This is SAP authentication. Authenticating callers of the HTTP transport is a separate control — see Access control above.


Where to next