sap-mcp-config
The standard way to manage SAP credentials for MCP servers.
General
If you're building an MCP server that connects to SAP, use this package. It gives you validated, type-safe configuration in both Go and Python with a single shared config file. No more reinventing credential loading, no more inconsistent formats between projects.
Both mcp-server-abap (Go) and sapgui.mcp (Python) use this package.
The default config path (~/.config/sap-mcp/systems.json) follows the XDG Base Directory Specification.
Features
- One config file, two languages - Go and Python read the same config, guaranteed by shared test fixtures
- JSON and YAML - use whichever format you prefer (auto-detected by file extension)
- Validates eagerly - reports all errors at once so users fix everything in one pass
- Secrets can stay out of the file -
${env:VAR}placeholders pull values from the environment, sosystems.jsonholds structure only and becomes safe to commit and share - Passwords never leak in print/log output - masked in
str()/repr()/fmt.Println()/fmt.Sprintf("%+v")(Go:fmt.Formatter; Python:pydantic.SecretStr) - Immutable after loading - frozen Pydantic models in Python; in Go, use the returned structs as read-only
.envfile support -SAP_CONFIG_FILEcan be set in a.envfile- Easy to extend - subclass
SAPSystemin Python or embed the struct in Go to add project-specific fields
License
MIT
Users
This section covers everything you need to connect an MCP server to your SAP system. No programming knowledge required.
[!NOTE] Why a separate file for credentials? Most MCP tools put credentials directly in each project's MCP config (
envblock), which means re-entering them for every tool you install. This package separates SAP credentials into a single shared file (systems.json) so that multiple MCP servers - likesapgui.mcpandmcp-server-abap- can all read the same credentials without duplication.
Configuration File
Create ~/.config/sap-mcp/systems.json (or systems.yaml - format is auto-detected by file extension):
[!NOTE] On Windows,
~resolves to%USERPROFILE%, so the default path is%USERPROFILE%\.config\sap-mcp\systems.json.
JSON
{
"default_system": "dev",
"systems": {
"dev": {
"connection_name": "DEV - ERP Development",
"host": "https://your-sap-system:44300",
"client": "100",
"user": "YOUR_USER",
"password": "YOUR_PASSWORD",
"language": "DE"
},
"prod": {
"connection_name": "PROD - ERP Production",
"host": "https://prod-sap:44300",
"client": "200",
"user": "PROD_USER",
"password": "PROD_PASSWORD",
"language": "EN"
}
}
}
YAML
default_system: dev
systems:
dev:
connection_name: "DEV - ERP Development"
host: "https://your-sap-system:44300"
client: "100"
user: YOUR_USER
password: YOUR_PASSWORD
language: DE
prod:
connection_name: "PROD - ERP Production"
host: "https://prod-sap:44300"
client: "200"
user: PROD_USER
password: PROD_PASSWORD
language: EN
[!TIP] Override the config file location via the
SAP_CONFIG_FILEenvironment variable:export SAP_CONFIG_FILE=/path/to/my/config.yamlThis also works from a
.envfile in the current directory.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
connection_name |
string | no | "" |
SAP Logon connection entry name - must match the bold description text shown in the SAP Logon pad, not the System ID (SID). Used by desktop backends (e.g. SAP GUI) to open the correct connection. (See Finding your connection_name below.) |
host |
string | yes | SAP system base URL (must start with http:// or https://) |
|
client |
string | no | "" |
SAP client/mandant, must be exactly 3 digits (e.g. "100") |
user |
string | conditional | "" |
SAP username (omit for OAuth2) |
password |
string | conditional | "" |
SAP password (omit for OAuth2) |
language |
string | no | "EN" |
Login language: "DE" or "EN" |
tls_skip_verify |
bool | no | false |
Skip TLS certificate verification |
oauth2_client_id |
string | no | "" |
OAuth2 client ID for token-based auth |
Validation rules:
- At least one system must be defined
default_systemmust reference an existing system keyhostis required and must start withhttp://orhttps://client, if set, must be exactly 3 digitslanguage, if set, must be"DE"or"EN"- Either both
userandpasswordmust be set, or neither (for OAuth2)
Keeping secrets out of the config file
The string fields of a system - connection_name, host, client, user, password, language and oauth2_client_id - plus the top-level default_system may contain an ${env:VAR} placeholder, which is replaced with that environment variable's value when the config is loaded. This lets you split the file into two parts: the structure - which systems exist, their hosts, clients and connection names - stays in systems.json, while the credentials come from your environment, CI secret store, or password manager.
The result is a systems.json you can commit to a repository and share with your team:
{
"default_system": "dev",
"systems": {
"dev": {
"connection_name": "DEV - ERP Development",
"host": "https://dev-sap.example.com:44300",
"client": "100",
"user": "${env:SAP_DEV_USER}",
"password": "${env:SAP_DEV_PASSWORD}"
},
"prod": {
"connection_name": "PROD - ERP Production",
"host": "https://prod-sap.example.com:44300",
"client": "200",
"user": "${env:SAP_PROD_USER}",
"password": "${env:SAP_PROD_PASSWORD}"
}
}
}
export SAP_DEV_USER=DEV_USER
export SAP_DEV_PASSWORD=...
Rules:
- Only the exact form
${env:NAME}is a placeholder, whereNAMEis a plain identifier: letters, digits and underscores, not starting with a digit. Anything that does not match that form is left alone as literal text - see the table below. - A placeholder can be the whole value or embedded in a larger string, and a string may contain several:
"host": "https://${env:SAP_HOSTNAME}:${env:SAP_PORT}".
What counts as a placeholder:
| In your config | Result |
|---|---|
${env:SAP_DEV_PASSWORD} |
Replaced. If the variable is unset, loading fails with an error |
https://${env:HOST}:${env:PORT} |
Both replaced |
${env:not an identifier} |
Literal text - spaces are not allowed in a name |
${env:2FA_TOKEN} |
Literal text - a name cannot start with a digit |
${SAP_PASSWORD} |
Literal text - missing the env: prefix |
$env:SAP_PASSWORD |
Literal text - missing the braces |
The important half of this table is the bottom: text that looks like a placeholder but does not match the exact form is used verbatim, so a near-miss such as ${SAP_PASSWORD} becomes your literal password rather than an error. A genuine placeholder whose variable is unset always fails loudly, so the two cases can never be confused.
- An unset variable is an error, reported alongside every other validation problem. It never resolves to an empty string - so a forgotten
exportcannot silently turn a user/password system into an OAuth2 one. - A variable that is set but empty is also an error.
SAP_PASSWORD=is the shape an unpopulated CI secret takes, and accepting it would strip the credential just as silently. - Substitution runs once. A value pulled from the environment is not scanned again, so a secret that happens to contain
${env:...}is kept as literal text rather than triggering a further lookup - and it does not matter whether that inner name refers to a real variable. - Placeholders are resolved in the fields listed above, in JSON and YAML alike. They are not resolved in
tls_skip_verify(which is a boolean, not a string) nor in the system names themselves. ${env:VAR}is resolved in.env-provided variables too, when you load viaload_default()/LoadDefault().
A missing variable is reported like any other error:
invalid configuration:
- system "dev": password references ${env:SAP_DEV_PASSWORD}, which is not set in the environment
- system "prod": user references ${env:SAP_PROD_USER}, which is set but empty
[!NOTE] Values taken from the environment are not echoed back in error messages. Where a literal from the file would be quoted -
host must start with http:// or https://, got "ftp://x"- an env-supplied value is reported asgot the value taken from the environmentinstead.This includes the exception itself: because interpolation happens before validation, the document handed to the validator holds resolved secrets, so
load(),parse()andparse_yaml()raise aValidationErrorwith itsinputwithheld. Printing the full exception is safe.
Finding your connection_name in SAP Logon
Open the SAP Logon pad - your systems appear in a table. The connection_name is the text in the Description or Name column (the leftmost column with the bold/display name). It is not the short System ID (SID):
Description or Name - use this as connection_name |
System ID (SID) | Instance Number | Message Server |
|---|---|---|---|
| Production S/4HANA | PRD | 00 | prd-ms... |
| DEV - ERP Development | DEV | 00 | dev-ms... |
| QA System | QAS | 01 | qa-ms... |
[!IMPORTANT] Copy the Description/Name text exactly as it appears - spaces, slashes, and capitalisation all matter. If the value in
connection_namedoesn't match exactly, the server will return "SAP Logon connection entry not found".
[!NOTE]
connection_nameis only used by the Desktop backend (SAP GUI desktop client). The WebGUI backend connects directly tohostand does not use SAP Logon, so you can leaveconnection_nameempty or omit it.
Multiple entries for the same SAP system
The dictionary key (e.g. "dev", "prod") is only used to look up systems in the config. It has no connection to the SAP system itself. The connection_name field is what identifies the SAP Logon entry for desktop backends. This distinction allows you to configure multiple entries for the same SAP system with different clients or credentials:
{
"default_system": "dev-100",
"systems": {
"dev-100": {
"connection_name": "DEV - ERP Development",
"host": "https://dev-sap.example.com:44300",
"client": "100",
"user": "DEV_USER",
"password": "DEV_PASSWORD"
},
"dev-200": {
"connection_name": "DEV - ERP Development",
"host": "https://dev-sap.example.com:44300",
"client": "200",
"user": "QA_USER",
"password": "QA_PASSWORD"
}
}
}
Both entries share the same connection_name (same SAP Logon entry) but use different clients and credentials.
MCP JSON Configuration
Once your systems.json is ready, whoever configured the MCP server needs to point it at the credentials file via SAP_CONFIG_FILE. Both servers below read the same file - this is the key advantage over putting credentials in each server's env block separately:
{
"mcpServers": {
"sap-abap": {
"command": "mcp-server-abap",
"env": {
"SAP_CONFIG_FILE": "/home/user/.config/sap-mcp/systems.json"
}
},
"sap-webgui": {
"command": "run-sapgui-mcp-server",
"env": {
"SAP_CONFIG_FILE": "/home/user/.config/sap-mcp/systems.json"
}
}
}
}
[!TIP] Both servers read the same config file with the same credentials - no duplication needed.
Developers
This section is for developers building or extending MCP servers that use this package.
Python
Installation
pip install sap-mcp-config
Usage
import sys
from pydantic import ValidationError
from sap_mcp_config import load_default
try:
# Load from SAP_CONFIG_FILE env var or ~/.config/sap-mcp/systems.json
cfg = load_default()
except FileNotFoundError:
print("Config file not found. Create ~/.config/sap-mcp/systems.json")
sys.exit(1)
except ValidationError as e:
print(f"Configuration error:\n{e}")
sys.exit(1)
# Access the default system
dev = cfg.get_default()
print(dev.host, dev.client, dev.user)
# Access a specific system
prod = cfg.systems["prod"]
print(prod.host, prod.client, prod.user)
# Password is a SecretStr - it won't leak in print/logs
print(dev) # password=SecretStr('**********')
# Access the actual password value when needed
password = dev.password.get_secret_value()
Extending the Configuration
Subclass SAPSystem to add your own fields:
from pydantic import ConfigDict
from sap_mcp_config import SAPSystem
class MySAPSystem(SAPSystem):
model_config = ConfigDict() # unfreeze for subclass
custom_timeout: int = 30
Development
uv sync --group dev
uv run pytest unittests
Or run individual checks:
uv run --group tests pytest unittests # unit tests
uv run --group linting ruff check src/sap_mcp_config # ruff
uv run --group type_check mypy --strict src/sap_mcp_config # mypy --strict
uv run --group coverage coverage run -m pytest unittests # coverage
Go
Installation
go get github.com/Hochfrequenz/sap-mcp-config
Usage
package main
import (
"fmt"
"os"
sapmcpconfig "github.com/Hochfrequenz/sap-mcp-config"
)
func main() {
// Load from SAP_CONFIG_FILE env var or ~/.config/sap-mcp/systems.json
cfg, err := sapmcpconfig.LoadDefault()
if err != nil {
fmt.Fprintf(os.Stderr, "Configuration error:\n%s\n", err)
os.Exit(1)
}
// Access the default system
dev := cfg.GetDefault()
fmt.Println(dev.Host, dev.Client, dev.User)
// Access a specific system
prod := cfg.Systems["prod"]
fmt.Println(prod.Host, prod.Client, prod.User)
// Password is safe to print - it won't leak
fmt.Println(dev) // Output: SAPSystem{ConnectionName:DEV - ERP Development Host:https://... Client:100 User:DEV_USER Password:*** Language:DE}
}
Extending the Configuration
Embed SAPSystem in your own struct:
type MySAPSystem struct {
sapmcpconfig.SAPSystem
CustomTimeout int `json:"custom_timeout"`
}
Development
go test ./...
Error Messages (Python and Go)
Both implementations validate eagerly and return all errors at once. A misconfigured file like this:
{
"default_system": "missing",
"systems": {
"dev": { "host": "ftp://wrong", "client": "1", "user": "u" }
}
}
...will report all problems in a single error:
invalid configuration:
- default_system "missing" not found in systems
- system "dev": host must start with http:// or https://, got "ftp://wrong"
- system "dev": client must be a 3-digit string (e.g. "100"), got "1"
- system "dev": must have both user and password, or neither (for OAuth2)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sap_mcp_config-1.1.0.tar.gz.
File metadata
- Download URL: sap_mcp_config-1.1.0.tar.gz
- Upload date:
- Size: 107.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
65f35b0a9700f10e37ceb75aa8065fb4b9c582b9a0d5af1ea6d5cd2da754b3c0
|
|
| MD5 |
322b155540b3308d5881cddc50467756
|
|
| BLAKE2b-256 |
6410ad189f41c7d2b5f7c3900417324fe510465aec8ce59e2a53401833d63c98
|
Provenance
The following attestation bundles were made for sap_mcp_config-1.1.0.tar.gz:
Publisher:
python-publish.yml on Hochfrequenz/sap-mcp-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sap_mcp_config-1.1.0.tar.gz -
Subject digest:
65f35b0a9700f10e37ceb75aa8065fb4b9c582b9a0d5af1ea6d5cd2da754b3c0 - Sigstore transparency entry: 2409169877
- Sigstore integration time:
-
Permalink:
Hochfrequenz/sap-mcp-config@6673899cf289ab000be374a8039fe214e431851b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Hochfrequenz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@6673899cf289ab000be374a8039fe214e431851b -
Trigger Event:
release
-
Statement type:
File details
Details for the file sap_mcp_config-1.1.0-py3-none-any.whl.
File metadata
- Download URL: sap_mcp_config-1.1.0-py3-none-any.whl
- Upload date:
- Size: 15.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8be51e4642ba7760d9212dc90a06a5cef534f0e3c5ad25c4e414a35dc0434dd7
|
|
| MD5 |
cfb43dd54f4e5609a35e9affc1e344d7
|
|
| BLAKE2b-256 |
d1351a0603a57b47e4f93001b13d5accb598409d5514d87e5c6b236424f77feb
|
Provenance
The following attestation bundles were made for sap_mcp_config-1.1.0-py3-none-any.whl:
Publisher:
python-publish.yml on Hochfrequenz/sap-mcp-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sap_mcp_config-1.1.0-py3-none-any.whl -
Subject digest:
8be51e4642ba7760d9212dc90a06a5cef534f0e3c5ad25c4e414a35dc0434dd7 - Sigstore transparency entry: 2409170387
- Sigstore integration time:
-
Permalink:
Hochfrequenz/sap-mcp-config@6673899cf289ab000be374a8039fe214e431851b -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Hochfrequenz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@6673899cf289ab000be374a8039fe214e431851b -
Trigger Event:
release
-
Statement type: