Skip to main content
Glama

ADT MCP (Python)

Multi-system MCP server for reading and writing ABAP source via SAP ADT, with a local web admin to configure systems. One process serves both the MCP endpoint (/mcp) and the web admin (/).

Install (Windows)

git clone https://github.com/NhatPD-VNEXT/ADT-MCP adt-mcp
cd adt-mcp
install.bat        REM creates .venv, installs everything, verifies the import
run.bat            REM starts the server and opens the web admin

Only prerequisite: Python 3.10+ on the machine. install.bat finds it via py -3 or python and skips the Microsoft Store stub; it creates a project .venv so nothing global is touched. Flags:

Flag

Effect

--no-venv

Install into the Python already on PATH instead of .venv

--no-browser

Skip Playwright (only fine if every system uses basic auth)

Playwright is installed by default because cookie systems log in through a browser. It drives the machine's own Chrome/Edge, so there is no browser download; if neither is installed, run .venv\Scripts\python -m playwright install chromium.

Configure the new machine

systems.json and cookies/ are gitignored — they hold credentials and are never in the repo. After install.bat, pick one:

  • Add the system in the web admin (run.bat → http://127.0.0.1:8765) and log in once with the browser flow. Nothing secret has to be copied.

  • **Copy systems.json + cookies\ by hand** from a working machine (USB, password manager, internal share — not email/chat). Fix the absolute cookie_file paths afterwards, or re-login from the admin.

Until that is done the server runs and the admin opens, but list_systems reports no systems.

Related MCP server: dassian-adt

Install (manual / non-Windows)

cd adt-mcp
python -m pip install -e ".[refresh]"   # -e matters: config, web/ and cookies/
                                        # are read from the checkout.
                                        # [refresh] = Playwright cookie login

Run

python -m adt_mcp        # or: adt-mcp
# → http://127.0.0.1:8765  (MCP at /mcp, admin at /)

Environment variables (all optional):

Variable

Default

Purpose

ADT_MCP_PORT

8765

Port for MCP + web admin (run.bat follows it)

ADT_MCP_HOME

the checkout

Folder holding systems.json, web/, cookies/

ADT_MCP_SYSTEMS

<home>/systems.json

Explicit path to the systems config

ADT_MCP_TOOLS

full

core exposes only the essential tools

ADT_MCP_BROWSER

chrome

Browser channel for cookie login (msedge/chromium)

ADT_MCP_CDP

http://127.0.0.1:9222

Chrome DevTools endpoint for mode: "cdp"

Set ADT_MCP_HOME when the package is installed non-editable or run as a service from another working directory.

Open http://127.0.0.1:8765 to add SAP systems (URL, client, language, auth). Config is stored in systems.json (gitignored). See systems.example.json. Cookie systems can be (re)authenticated from the web admin via a browser login.

Connect Claude Code

Add to .mcp.json:

{
  "mcpServers": {
    "sap-adt": { "type": "http", "url": "http://127.0.0.1:8765/mcp" }
  }
}

Tools

Read / navigate:

  • list_systems, list_package, search_objects, get_object_info (package, active state, last change; no source)

  • get_source, get_source_by_uri, get_class_method_source, get_class_include, get_object_structure, get_package_source (get_source with object_type=MSAG returns message class texts)

  • grep_package (also scans class local includes), find_references (where-used), cds_dependencies

  • get_context (object + compressed dependencies: CDS/BDEF/CLAS)

  • get_revisions, get_revision_source, compare_source

  • syntax_check, run_unit_tests (ABAP Unit; filter by test class/method, own timeout), data_preview (CDS/SQL data), data_preview_batch (many previews in one call, results written as evidence files)

  • trace_start, trace_list, trace_analyze (ABAP profiler: CPU hotspots + DB accesses)

  • list_dumps, get_dump (ST22 runtime dumps: liệt kê + đọc chi tiết để phân tích lỗi)

Write (gated by safety, see below):

  • update_source, update_class_include, activate

  • create_object (CLAS / INTF / DDLS / DDLX / BDEF / SRVD / SRVB / TABL)

  • clone_package (clone toàn bộ object của một package sang package đích, thêm suffix _VN + sửa tham chiếu chéo trong source; dry-run mặc định)

  • push_local (đẩy một thư mục source local lên SAP theo phase: sync / check / activate / status, xem mục dưới)

Debug (gated by allow_debug, see below):

  • debug_set_breakpoint, debug_delete_breakpoint, debug_clear_breakpoints

  • debug_listen, debug_poll, debug_stop_listener

  • debug_attach, debug_detach

  • debug_stack, debug_variables, debug_step

  • run_class (IF_OO_ADT_CLASSRUN — Eclipse's F9)

Cookie maintenance: refresh_cookies_for.

Reading large classes

Do not read a large class in full. Get the outline first, then fetch only the part you need:

get_source(system, object_type, name, to_file="C:\case\abap\ZCL_X.clas.abap")
  → writes the source to the file and returns only file names + outline.
    A CLAS also writes its non-empty includes next to it, named like
    abapGit: ZCL_X.locals_def / .locals_imp / .macros / .testclasses.abap.
    Then Grep / Read only the part you need from the file.
    A sidecar ZCL_X.clas.abap.json records system, object, time and per
    file the SAP hash and change time. On the next download, a file named
    in the old sidecar that is no longer produced (an include that became
    empty) is deleted; a file not in it is never deleted (WARNING instead).
    The path must not go through a symlink or junction.

get_source(system, "CLAS" | "INTF", name, section="public")
  → only the public declarations (class header + PUBLIC SECTION, or the
    whole interface), comments removed. For method signatures of a
    standard class/interface.

get_object_structure(system, class_name)
  → per part (main / local include): CLASS blocks and methods with line ranges
    implementations (421 lines)
      lhc_zi_mi902_01 DEFINITION 1-70
      lhc_zi_mi902_01 IMPLEMENTATION 72-421
        process 77-190
        get_data 282-342

get_class_method_source(system, class_name, method, local_class?)
  → one METHOD…ENDMETHOD block, also from local classes (RAP lhc_*/lsc_*)
    * lhc_zi_mi902_01=>get_data  implementations:282-342
get_class_method_source(system, class_name, methods=["get_data", "send"])
  → several blocks from one fetch, separated by a blank line

get_class_include(system, class_name, include, start_line?, end_line?)
get_source(system, object_type, name, ..., start_line?, end_line?)
  → only that line range (1-based, inclusive), e.g. a DEFINITION block
  • The outline skips includes that only hold the SAP template comment.

  • A method implemented by several local classes (e.g. validate in lhc_a and lhc_b) returns the candidates; call again with local_class.

  • Interface methods are named as in the source: if_rap_query_provider~select.

  • Line numbers are per part, so they match get_class_include and the header of get_class_method_source.

Measured on HAX: ZBP_I_MI902_01 is 16,728 characters in full (main + 4 includes); its outline is 351 characters and one method (get_data) is 1,604. Measured on VNEXT with section="public": IF_BALI_LOG 8,112 → 2,933 characters, CL_BOTD_MOCKEMLAPI_BO_TEST_ENV 10,295 → 495.

Object info and unit test filters

get_object_info(system, object_type, name)
  CLAS ZCL_MCP_DEBUG_DEMO  ZCL_MCP_DEBUG_DEMO
  package:  ZVNEXT_003
  status:   active            (or: inactive (a version is waiting for activation))
  changed:  2026-10-02 10:24:37 +07:00 by CB9980000020
  created:  2026-08-13 by CB9980000020
  includes: (CLAS only) change time and user per include, INACTIVE flag

get_object_info(..., hashes=True)
  → also reads the source and adds hash=<source_hash> per include (or a
    hash: line), the same hash as the to_file sidecar and push_local.

run_unit_tests(system, "CLAS", name, test_class="ltc_job_logic", method="t26_*", timeout=180)
  • test_class / method (CLAS only) take names, * ? patterns or comma lists. Exact names go straight to SAP; patterns, or a method without a test class, are resolved from the testclasses include first. setup, teardown, class_setup, class_teardown never match a pattern.

  • A filter that matches nothing lists the test methods that exist.

  • timeout is in seconds (default 300, 10–3600). A run that times out on our side may still finish on SAP.

Data preview in batches

data_preview_batch runs up to 200 data_preview queries in one call (4 in parallel; one by one with stop_on_error=true). It only reads SAP; the only files it writes are inside out_dir.

data_preview_batch(system, out_dir?, queries=[...] | file="C:\\...\\q.json",
                   output="table" | "json" | "both", overwrite=False,
                   stop_on_error=False)

queries: [{"id": "find-open", "sql": "SELECT ... FROM I_SalesDocumentItem WHERE ...",
           "max_rows": 5, "return_rows": 5},           # rows shown in the reply
          {"id": "1-1/01", "sql": "SELECT ... FROM zv907t_vn WHERE ...",
           "out": "1-1/01.txt"}]                       # written under out_dir
  • sql is a SELECT or an entity name, with the same limits as data_preview (one table/CDS, no JOIN). max_rows defaults to 5.

  • file holds {"queries": [...]}. Passing queries inline saves the agent one tool call.

  • Input is checked before any SAP call: absolute file/out_dir, unique id, out inside out_dir (no .., no absolute path, no two queries on one file, case-insensitive), return_rows <= max_rows, at most 200.

  • overwrite=False: a query whose out file exists is not run and its file is kept.

  • All queries run before any file is written. An expired SAP session writes nothing; the server logs in again and reruns the whole batch.

Evidence file (UTF-8, LF; the format of the sap-evidence skill):

[data_preview] system VNEXT | 2026-10-02 12:02
SELECT SalesDocument FROM I_SalesDocumentItem
SALESDOCUMENT
0000000004
0000000005
(2 of 209 rows)
  • Column names are as SAP returns them: upper case.

  • Values are SAP's internal format: leading zeros kept, dates 20250422. Tabs and line breaks in a value become spaces, trailing blanks are dropped, values over 200 characters are cut with ….

  • Last line: (1 row), (N rows), or (N of M rows) when max_rows cut the result. A COUNT(*) (aggregates without GROUP BY) is always (1 row).

  • A failing query with out still gets its file, with ERROR: <SAP message> as line 3.

Pushing a local case folder

push_local writes a folder of source files to SAP, one phase per call. The folder holds manifest.json and the files it names:

{"case": "VI907_VN", "system": "VNEXT", "package": "ZVNEXT_003",
 "objects": [
  {"type": "DDLS", "name": "ZI_VI907_01_VN", "action": "create", "phase": 1,
   "description": "...", "files": {"main": "DDLS/ZI_VI907_01_VN.ddls"}},
  {"type": "CLAS", "name": "ZBP_I_VI907_01_VN", "action": "create", "phase": 3,
   "files": {"main": "CLAS/ZBP_I_VI907_01_VN.main.abap",
             "implementations": "CLAS/ZBP_I_VI907_01_VN.implementations.abap"}},
  {"type": "SRVB", "name": "ZSB_U4_VI907_01_VN", "action": "create", "phase": 5,
   "service_definition": "ZSD_VI907_01_VN", "binding_version": "V4", "files": {}}
 ]}
  • Types: TABL, DDLS, DDLX, DCLS, BDEF, SRVD, CLAS, INTF, PROG (source) and SRVB (created only, no source). CLAS roles: main, definitions, implementations, macros, testclasses.

  • action: create creates the object when it is missing; action: edit never creates. An existing object must be in the manifest's package.

  • purpose: "ut" (optional) marks a unit test manifest; see activate.

  • Objects run in manifest order. Pass exactly one of phase or objects: ["TYPE:NAME", ...] (status also accepts neither = all objects; accept_sap needs objects).

Modes:

  • sync — create missing objects, write each file that differs from SAP as inactive, then check the object with the local sources. Never activates. Per object it refuses, writing nothing:

    • an existing object with action: create that this case did not create (no created_by_this_manifest mark for this system, package and case in the state): change the manifest to edit;

    • a part whose SAP source changed since its baseline (sap_changed), e.g. an edit in Eclipse: merge it into the local file, or, after the user agrees, run accept_sap for that object;

    • a part of an existing object with no baseline yet: run status first.

    Every part is checked before any is written, so a class is never half written. The state is saved right after each part is written and read back. RESULT_JSON lists written_roles per object.

  • check — check local files of objects already on SAP; writes nothing. SAP does not check an object that does not exist.

  • activate — activate the scope in one call. Refused, with nothing sent to SAP, unless the last sync checked clean and neither the files nor the SAP source changed since (e.g. an edit in Eclipse). Activating a class also activates its parts the manifest did not write, so it is also refused when a class part not in the manifest has an inactive version that differs from the active one (comment-only differences such as SAP's template do not count). With purpose: "ut", a listed part other than testclasses may only differ in friend declarations (CLASS ltc DEFINITION DEFERRED [FOR TESTING], ... LOCAL FRIENDS ltc, a FRIENDS ltc clause); other changes are listed in the refusal.

  • status — compare each local file with SAP's current source by hash; writes nothing, prints no source. Per file: same, local_changed, sap_changed, both_changed (from .push-state.json), differs (never synced, so unknown which side), missing, and extra_on_sap for a class include that has code on SAP but is not in the manifest (an include with only SAP's template comment does not count). Per object: active or not, package, last change time and user. If the object is inactive, it also says whether the active version still differs. An object is ok only when every entry is same; then the local files hold all of its code and can be read instead of reading SAP again. status writes nothing to SAP, but records SAP's current hash as the baseline of each file that has none yet. It never moves an existing baseline, so an edit made on SAP after it stays visible as sap_changed. Run it right after reading the reference sources, before generating code.

  • accept_sap — take SAP's current source as the new baseline of the named objects, so the next sync writes the local files over it. Writes nothing to SAP. Use it only after the user agreed to overwrite.

output (all modes): both (default) prints the table, SUMMARY and RESULT_JSON; table leaves out RESULT_JSON; json prints only the RESULT_JSON line.

Typical flow, per phase: sync → fix files until clean → activate → next phase. A phase that uses objects of an earlier phase must wait until those are active: SAP checks against active versions ("does not exist or is not active").

Output is one line per object plus SUMMARY and a one-line RESULT_JSON; errors point at the local file, e.g. DDLS/ZI_ZZPL_T01.ddls:7:6 E: The column NoSuchFieldZz is unknown. State is kept in <dir>/.push-state.json; a rerun after a failure repeats nothing that already landed.

Debugging

The tenant must expose the ADT debugger; check with

GET /sap/bc/adt/compatibility/graph   → COM.SAP.ADT.DEBUGGER :: userRequestDebugging

Enable it per system in systems.json — it is off by default, separately from allow_write, because run_class executes arbitrary ABAP:

"allow_debug": true,
"debug_timeout": 600,
"debug_listen_seconds": 120

The flow, in order:

debug_set_breakpoint  → statement="…" lets the server find the executable line
debug_listen          → returns at once; the listener runs in the background
run_class             → run the code WHILE the listener waits
debug_poll            → state: listening → caught
debug_attach
debug_stack / debug_variables / debug_step
debug_detach          → releases the debuggee and returns the run's output

debug_variables needs explicit names — read the source first. There is no working "list what is in scope" call on this platform: getChildVariables answers with ME alone whatever parent it is asked for.

Two things follow from how SAP works, not from choices made here:

  • Run the code while the listener is waiting. A debuggee is only trapped by a listener that was already registered. debug_listen therefore returns immediately instead of blocking.

  • A breakpoint freezes the HTTP request that started the code. run_class runs in the background for that reason, and the console output only arrives at debug_detach (or a later debug_poll).

Debugging a Fiori app (RAP action, determination, validation)

Nothing here runs the code — you do, by clicking in the browser. An external breakpoint is keyed on the ABAP user, so it traps any session of that user, including the OData request the Fiori UI sends.

debug_set_breakpoint  VNEXT CLAS ZBP_I_FLIGHT_013 \
                      include="implementations" statement="READ ENTITIES"
debug_listen          VNEXT seconds=600      ← wide enough to switch to the browser
      … now trigger the action in the Fiori app …
debug_poll            VNEXT                  → state: caught
debug_attach / debug_stack / debug_variables / debug_step
debug_detach          VNEXT                  → the OData request completes

run_class plays no part in this flow.

Two things decide whether it works:

  • include="implementations". RAP handler code (lhc_… classes, where actions, determinations and validations live) is in the class's Local Types include. A behavior pool's /source/main is an empty shell — a breakpoint there never fires. The returned id shows where it really landed (…INCLUDE=…CCIMP.LINE_NR=131).

  • Same ABAP user. The Fiori session must run as the user in /sap/bc/adt/core/http/systeminformation. A different login is a different user and is never trapped.

While you sit at the breakpoint the OData request is held open, so the Fiori UI spins and can hit its own gateway timeout — detach promptly. Leaving a breakpoint armed with no listener is not dangerous, though: measured on the tenant, the code simply runs straight through.

Cost on cloud

The ABAP session is the SAP_SESSIONID cookie, so the debugger's isolated channels each need their own login. The first debug_listen and the first run_class on a system therefore take ~20–25s while a headless login runs; afterwards the sessions are reused. Channel cookies land in cookies/<system>.debug.txt / .exec.txt and are gitignored like the rest.

Breakpoints set here are external breakpoints keyed on the ABAP user, so leaving one behind would pop the debugger open on a real session later. The server deletes everything it set when it shuts down; debug_clear_breakpoints does it on demand. The ideId is adt-mcp, distinct from Eclipse's, so your IDE breakpoints are never touched.

Write safety

Writes are off by default. Per system in systems.json:

  • allow_write: true — required to enable any create/update.

  • write_packages: ["Z*", "$TMP"] — target package must match (default).

Delete is intentionally not supported.

Token economy

Tool schemas are sent to the model on every turn. Set ADT_MCP_TOOLS=core to expose only the essential ~16 tools (smaller schema); default full exposes all 29. Descriptions are kept terse.

ADT_MCP_TOOLS=core python -m adt_mcp

Security

  • systems.json, cookies/, *-cookies.txt hold session secrets and are gitignored — never commit them.

  • The server binds 127.0.0.1 only.

  • Stored passwords are plaintext. A username/password is only kept to enable headless cookie refresh (refresh_cookies_for). For real systems prefer the cookie flows that store no password:

    • mode: "browser" — log in once in a visible browser; only session cookies are saved (the persistent profile keeps SSO so re-login is rare).

    • mode: "cdp" — attach to your already-authenticated Chrome. If you must keep a password, store it in an OS keyring / secrets manager and inject it into systems.json at deploy time rather than committing it.

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    Not graded
    maintenance
    An MCP server that facilitates seamless interaction with SAP ABAP systems to manage development objects, transport requests, and source code. It provides a comprehensive suite of tools for performing syntax checks, object searches, and code modifications via the ADT API.
    100
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Multi-system MCP server for reading and writing ABAP source via SAP ADT, with write safety controls and a local web admin.
    2
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for SAP on-premise via ADT, enabling multi-system connection, code navigation, data queries, and debugging through natural language. Includes a web admin and runtime diagnostics.
    MIT