Skip to main content
Glama

SAP-MCP

MCP server for SAP on-premise via ADT: a single process running both the MCP endpoint and web admin, connecting to multiple SAP systems simultaneously, with debugger and runtime diagnostics tools. Tool names follow the PascalCase convention of vibing-steampunk, plus multi-system model and admin page.

No ABAP objects need to be installed on SAP to use — the only exception is RunReport, and the server installs it for you (see Group D section).

Installation

Windows — double-click install.bat, or run in terminal:

install.bat

It checks for Python 3.10+, creates .venv, installs dependencies, and creates systems.json from the template file. Then open systems.json and fill in the real SAP system URL / user / password, then run run.bat.

Other platforms:

python -m venv .venv && . .venv/bin/activate
pip install -e .
cp systems.example.json systems.json   # sửa URL, user, password
python -m sap_mcp

systems.json contains passwords and is already in .gitignore — don't commit it.

Running the server

Windows — double-click run.bat, or:

run.bat              :: cổng 8765, chế độ focused (50 tool)
run.bat 8766         :: đổi cổng
run.bat 8766 expert  :: đổi cổng + bật đủ 66 tool

run.bat automatically sets the console to UTF-8 (logs contain Vietnamese, cp1252 console would crash Python), automatically creates systems.json from the template if missing, and clearly reports which PID is holding the port instead of letting uvicorn throw a raw socket error.

Open http://127.0.0.1:8765 to add/edit/test systems. MCP endpoint is at /mcp.

Connecting to an MCP client

Copy mcp.example.json to .mcp.json in the project directory, or merge the mcpServers section into the client's existing config file:

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

Adjust the port if you run run.bat with a different port. The server must be running before the client connects — this is a streamable-http transport, the client doesn't start the process itself like with stdio.

System configuration (systems.json)

Field

Default

Description

url

https://host:port of the SAP system

client

100

sap-client

language

EN

sap-language

username / password

Basic auth

verify_ssl

true

Set false for self-signed certs

ca_bundle

Path to custom CA (replaces verify_ssl)

timeout

30

Regular HTTP ceiling, seconds

allow_write

false

Write access only when enabled

write_packages

["Z*","Y*","$TMP"]

Packages allowed for writing

write_objects

Additional limits by object name

require_transport

true

Transportable package requires TR

allow_debug

false

Group D (debugger + code execution) only when enabled

debug_timeout

1800

How long to stay at breakpoint, seconds

debug_listen_seconds

300

DebuggerListen default wait, seconds

Objects in the standard SAP namespace are always rejected, not configurable off.

debug_timeout is not just a number for show. Code stopped at a breakpoint holds the HTTP request that ran it, so the regular timeout (30s) cuts in mid-inspection while you're looking at variables: the background thread dies and the report result is lost, DebuggerDetach returns The read operation timed out instead of data. This ceiling is only raised while debugging (when there's a listener, or when stopped at a debuggee) — raising it for every run means a hung report holds an SAP work process for half an hour with no one watching.

Environment variables

Variable

Default

Description

SAP_MCP_SYSTEMS

./systems.json

Config file path

SAP_MCP_PORT

8765

HTTP port

SAP_MCP_MODE

focused

focused (50 tools) or expert (66 tools)

SAP_MCP_DISABLED_GROUPS

Disable feature groups, e.g. C or C,D

Group codes: C transport requests, D debugger, P runtime diagnostics (dumps, traces) — see Tools section below. Core tools belong to no group and are always enabled. Disabling both (SAP_MCP_DISABLED_GROUPS=D,P) brings focused/expert back to 30/45 tools.

Tools

Administration ListSystems GetConnectionInfo GetSystemInfo

Read GetSource GetObjectStructure GetClassInfo GetPackage GetFunctionGroup SyntaxCheck · expert: GetProgram GetClass GetInterface GetInclude GetFunction GetClassInclude

GetSource can read a section instead of the whole object: around="SELECT" gets the window around the first occurrence outside comments, or from_line/to_line gets the exact line range. Each section starts with a comment line noting it's a section — only the full version can be used with UpdateSource; overwriting with a window deletes the rest. GetPackage has a max_objects ceiling and says so when truncated.

Search SearchObject GrepObjects GrepPackages · expert: GrepObject GrepPackage

Data RunQuery GetTableContents

RunQuery runs Open SQL SELECT and returns the result table; GetTableContents builds the SELECT for you. No allow_write needed because SAP itself rejects write commands at this endpoint:

DELETE FROM t001 …  → 400 Invalid query string. Only SELECT statement is allowed.

The barrier for READING is the SAP user's permissions in systems.json — any table that user can read, the agent can read too, including HR tables. Don't configure a superuser.

Source navigation FindDefinition FindReferences

FindDefinition(system, 'CLAS', 'ZCL_X', symbol='cl_salv_bs_runtime_info') — the server finds the symbol in the source (ignoring comments) then resolves it there, returning the type, name, and component list. It doesn't use navigation/target even though the name sounds more appropriate: it returns the input uri itself when it can't resolve, i.e., a false positive. The working path is abapsource/codecompletion/elementinfo, and it requires the entire source in the body.

FindReferences returns where-used. SAP returns a tree mixing three node types; only indices with gradeDirect are real usages. gradeComponent is a component of the object being looked up — counting it would make an uncalled class appear to have 6 usages.

Write WriteSource EditSource Activate ActivatePackage CreatePackage GetInactiveObjects LockObject UnlockObject · expert: CreateObject UpdateSource DeleteObject

Miscellaneous CompareSource CloneObject PrettyPrint ImportFromFile ExportToFile

Group C ListTransports · expert: GetTransport CreateTransport ReleaseTransport DeleteTransport

Group D — debugger SetBreakpoint DeleteBreakpoint DebuggerListen DebuggerPoll DebuggerStopListener DebuggerAttach DebuggerDetach DebuggerGetStack DebuggerGetVariables DebuggerStep RunClass RunReport RunUnitTests

Requires allow_debug: true. Usage flow:

  1. SetBreakpoint — the line must be an executable statement, not a declaration. No need to count lines: statement="SELECT" lets the server find it (it skips comments so it won't land on a non-executable line) and reports the line number.

  2. DebuggerListen — returns immediately, listener runs in the background

  3. RunClass / RunReport / RunUnitTests — runs the code

  4. If the breakpoint fires, step 3 returns Stopped at breakpoint … immediately (not data). DebuggerPoll reports status at any time.

  5. DebuggerAttachDebuggerGetStack / DebuggerGetVariables / DebuggerStep

  6. DebuggerDetach — releases the debuggee; the code finishes and the result of step 3 is returned here (or at DebuggerPoll if it runs long)

If no breakpoint fires, step 3 returns the result directly like a normal tool.

Why the three code-running tools run in the background. When code stops at a breakpoint, SAP holds the HTTP request that ran it — the call only returns after the debuggee is released. Calling synchronously would hang the tool itself and the agent could never call DebuggerAttach to release it: a self-deadlock. These three tools therefore run on a separate session in a background thread and respond as soon as the listener catches the debuggee.

Each system uses three separate HTTP sessions when debugging: one for the listener + debug session (stateful, held for tens of seconds), one to run code (can be blocked until the debuggee is released), one to set/delete breakpoints. Without separation they'd block each other: running code on the listener's session can only squeeze into the gap between two long-polls — exactly when SAP has no listener registered, so breakpoints never fire.

Debugging reports with selection screens. External breakpoints don't catch dialog sessions — pressing F8 in SE38 means the debugger sees nothing (tested on a real system). Use RunReport instead of RunClass in step 3: it runs the report in an external session, so breakpoints fire.

RunReport intercepts ALV display while still getting the data (cl_salv_bs_runtime_info), so a report ending in ALV doesn't dump mid-way. Accepts both PARAMETERS and SELECT-OPTIONS (parameter names starting with S_) and variants.

RunReport writes to SAP, so it needs both allow_write and allow_debug, not just allow_debug like the other debugger tools. The server installs two objects into $TMP itself, no action needed:

  • ZCL_MCP_RUNNER — intermediary class, generic and never modified. It does SUBMIT (mv_report) WITH SELECTION-TABLE mt_sel, i.e., the report name and the entire selection screen are runtime data.

  • ZMCP_RUNNER_ARGS — a program consisting of a single comment line, rewritten before each run. The class reads it at runtime with READ REPORT.

*@MCP TOKEN 24b8bff8dfb8477b
*@MCP REPORT ZPG_DEMO
*@MCP MAX 100
*@MCP SEL S_BUKRS S I BT
*@MCP LOW 1000
*@MCP HIGH 2000

Why it still has to write: IF_OO_ADT_CLASSRUN~MAIN( out ) accepts no parameters — no query params, no body. An object's source is the only parameter-passing channel ADT REST exposes.

The most important consequence is safety: nothing agent-supplied becomes ABAP code anymore. The previous version embedded filter values into ABAP literals, so a single stray quote could inject arbitrary commands into the SAP system — that had to be escaped to be safe. Now the value sits on a comment line and reaches SAP via the RSPARAMS table, so there's no syntax left to break. Only the newline character is forbidden (it would create a fake parameter line), and values longer than 45 characters are rejected because RSPARAMS-LOW is CHAR45 — SAP would silently truncate, meaning wrong filters with no one knowing.

Each run carries a token; the class returns that token and the server cross-checks. If a parameter write fails but execution continues, the report runs with old parameters and the result gets labeled as the new run — the token is what prevents that kind of silent error.

Compared to vibing-steampunk (requires the ZADT_VSP plugin: 1 interface, 3 classes, WebSocket handler), RunReport needs less and doesn't require manual SAPC + SICF configuration:

vsp (ZADT_VSP)

SAP-MCP (RunReport)

ABAP objects to install

4

2

SAPC + SICF configuration

needs basis admin

no

Server self-installs

no

yes

Class modified on each run

no

no

SELECT-OPTIONS

no (hardcoded kind='P')

yes

Writes to SAP on each run

no

yes (one comment file)

The last row is the price of not needing an admin to install anything: vsp passes parameters via WebSocket so it never touches the system, RunReport passes them via source because ADT REST doesn't expose any other channel. In exchange, the object written is a file of only comments — there's no syntax to break, and the class containing the logic stays untouched.

Group P — runtime diagnostics ListDumps GetDump StartTrace ListTraces GetTrace DeleteTrace GetSQLTraceState · expert: DeleteTraceRequest

Short dump (ST22). ListDumps filters by user/error/program/since; GetDump returns summary (what happened, error analysis, abort location, call stack), source (the source code at the crash point), full, or meta.

Performance measurement (SAT/ATRA). StartTrace('ZPG_X', 'report') → run the code → ListTracesGetTrace. GetTrace by default returns a per-call time profile, sorted in descending order; view='db' returns database access by table — number of calls, buffer reads, time. RunReport(..., trace=True) compacts the whole flow: it automatically sets a trace request limited to that exact report.

   NET µs      %  GROSS µs   LẦN  GỌI TỪ                     VIỆC
     3800   51.2      3800     1  CL_HTTP_SERVER_NET=======C DB: Exec Static
      368    5.0      4185     1  SAPLHTTP_RUNTIME           Call M. …SEND_RESPONSE

Three things measured on NW 758, contradicting what vibing-steampunk assumed — each would silently break the tool if followed blindly:

vsp does

measured on NW 758

Feed dump Accept type

application/atom+xml

406 — must be …;type=feed

Dump filtering

sends $filter FQL

SAP ignores it, returns the whole list

TREX trace/directory

reads it as a feed

returns a Fiori URL, no records

So ListDumps filters on the MCP server side, and the SQL query is taken from dbAccesses of the ABAP trace instead of from ST05. GetSQLTraceState is still useful for detecting a trace left enabled — it slows down the whole system while being invisible from the outside.

StartTrace requires an object name. An unlimited trace request immediately picks up the very HTTP call that started it: the measurement is all ICFSERVICE/HTTP_HEADER_REG — it measures the ADT machinery, not your code — and yet the trace still looks like a real data set. With a limiting constraint, the measurement lands on the next run (T001, DDFTX, VARID, …).

StartTrace, DeleteTrace, and DeleteTraceRequest need allow_debug: they change the behavior of the system, and a leftover request silently measures a later run. The three read tools (ListDumps, GetDump, GetSQLTraceState) need nothing.

Status

The core tool set, the D group (debugger, including RunReport), and the P group (dump + trace) are complete — 50 tools in focused mode / 66 in expert mode, running on NetWeaver 758. The DDIC/i18n, abapGit, and ABAP helper parts are not yet implemented.

Known limitations

1. ImportFromFile / ExportToFile have no path restriction. These two tools accept any path the model gives them. import_from_file only checks os.path.isfile; export_to_file only checks os.path.isdir — no allowlist, no restriction to a workspace directory, no blocking of .. or absolute paths. This means any agent — even one influenced by slight changes to what it reads from SAP — can read any file readable by the process and push it into SAP, or write any SAP source file to any writable path. The current mitigation: run the server only on a host you control, under an account that can access only the files you want the agent to have.

2. The REST admin route has no authentication. / and /api/systems* are protected only by binding to 127.0.0.1. Anything that reaches loopback on that port can list, add, modify, or delete system configurations and trigger connection tests. Do not expose this port outside your local machine, and do not run it on shared hosts.

3. The debugger can read every variable in memory. DebuggerGetVariables returns the actual value at a breakpoint, including sensitive data sitting in variables — passwords, keys, personal data. This is the nature of debugging, not a flaw. In addition, RunClass executes arbitrary ABAP. Therefore, allow_debug is off by default and should only be enabled on development systems.

4. RunReport uses a single shared parameter file. ZMCP_RUNNER_ARGS in $TMP is overwritten before each run. In a single server, runs are already serialized (one execution channel per system), but two servers, or two users sharing a system, still overwrite each other’s parameters. A token in the output detects this and turns it into an error, instead of returning wrong data. vibing-stames, on the other hand, avoids the issue entirely with a dedicated per-session internal WebSocket, at the cost of having to configure the SICF manually.

Architecture

transport/ (HTTP, auth, CSRF) → adt/ (code) → tools/ (formatting + MCP registration). The adt/uri.py file is the single source of URI construction. The D group also uses a separate AdtSession per system (transport/debug_pool.py, grouped by channel) because the listener runs in the background, debug sessions must maintain state across multiple calls, and code already running can stop at a breakpoint — none of those sessions should be taken from the shared SessionPool.

Tools run on a worker thread, not on the event loop. FastMCP calls synchronous endpoints directly on the event loop, so if left alone, one SAP call would block the whole server: the agent would not call DebuggerPoll while RunReport was waiting, two different systems would block each other, and the admin UI would hang. _internal/registration.py wraps every tool with anyio.to_thread.run_sync before registration. The per-system serialization remains and is intentional — it exists in SessionPool, because a lock handle for SAP is only valid on one connection per system.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Hosted Amazon Seller and Vendor MCP server for Claude, ChatGPT, Cursor, Codex, Gemini, Copilot.

  • Official Microsoft MCP Server to query Microsoft Entra data using natural language

  • GibsonAI MCP server: manage your databases with natural language

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NhatPD-VNEXT/SAP-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server