Metadata-Version: 2.4
Name: sap-mcp-config
Version: 1.1.0
Summary: Shared configuration models for MCP servers that connect to SAP systems
Project-URL: Changelog, https://github.com/Hochfrequenz/sap-mcp-config/releases
Project-URL: Homepage, https://github.com/Hochfrequenz/sap-mcp-config
Author-email: Hochfrequenz Unternehmensberatung GmbH <info+github@hochfrequenz.de>
License: MIT
License-File: LICENSE
Keywords: configuration,mcp,sap
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: pydantic>=2
Requires-Dist: python-dotenv>=1
Requires-Dist: pyyaml>=6
Description-Content-Type: text/markdown

# sap-mcp-config

![Go Tests](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Go%20Tests/badge.svg)
![Go Coverage](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Go%20Coverage/badge.svg)
![Go Lint](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Go%20Lint/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/Hochfrequenz/sap-mcp-config.svg)](https://pkg.go.dev/github.com/Hochfrequenz/sap-mcp-config)
[![Go Report Card](https://goreportcard.com/badge/github.com/Hochfrequenz/sap-mcp-config)](https://goreportcard.com/report/github.com/Hochfrequenz/sap-mcp-config)
![Python Tests](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Python%20Tests/badge.svg)
![Python Coverage](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Python%20Coverage/badge.svg)
![Python Lint](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Python%20Lint/badge.svg)
![Python Formatting](https://github.com/Hochfrequenz/sap-mcp-config/workflows/Python%20Formatting/badge.svg)
![Python Versions](https://img.shields.io/pypi/pyversions/sap-mcp-config.svg)
![PyPI](https://img.shields.io/pypi/v/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](https://github.com/Hochfrequenz/mcp-server-abap) (Go) and [sapgui.mcp](https://github.com/Hochfrequenz/sapgui.mcp) (Python) use this package.

The default config path (`~/.config/sap-mcp/systems.json`) follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/).

### 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, so `systems.json` holds 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
- **`.env` file support** - `SAP_CONFIG_FILE` can be set in a `.env` file
- **Easy to extend** - subclass `SAPSystem` in 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 (`env` block), 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 - like [`sapgui.mcp`](https://github.com/Hochfrequenz/sapgui.mcp) and [`mcp-server-abap`](https://github.com/Hochfrequenz/mcp-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

```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

```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_FILE` environment variable:
> ```bash
> export SAP_CONFIG_FILE=/path/to/my/config.yaml
> ```
> This also works from a `.env` file 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](#finding-your-connection_name-in-sap-logon) 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_system` must reference an existing system key
- `host` is required and must start with `http://` or `https://`
- `client`, if set, must be exactly 3 digits
- `language`, if set, must be `"DE"` or `"EN"`
- Either both `user` and `password` must 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:

```json
{
  "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}"
    }
  }
}
```

```bash
export SAP_DEV_USER=DEV_USER
export SAP_DEV_PASSWORD=...
```

**Rules:**

- Only the exact form `${env:NAME}` is a placeholder, where `NAME` is 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 `export` cannot 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 via `load_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 as `got the value taken from the environment` instead.
>
> This includes the exception itself: because interpolation happens before validation, the document handed to the validator holds resolved secrets, so `load()`, `parse()` and `parse_yaml()` raise a `ValidationError` with its `input` withheld. 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...       |

![SAP Logon pad showing Description/Name, System ID, Instance Number, and Message Server columns](saplogon.png)

> [!IMPORTANT]
> Copy the Description/Name text exactly as it appears - spaces, slashes, and capitalisation all matter. If the value in `connection_name` doesn't match exactly, the server will return _"SAP Logon connection entry not found"_.

> [!NOTE]
> `connection_name` is only used by the **Desktop backend** (SAP GUI desktop client). The WebGUI backend connects directly to `host` and does not use SAP Logon, so you can leave `connection_name` empty 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:

```json
{
  "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:

```json
{
  "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

```bash
pip install sap-mcp-config
```

#### Usage

```python
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:

```python
from pydantic import ConfigDict
from sap_mcp_config import SAPSystem

class MySAPSystem(SAPSystem):
    model_config = ConfigDict()  # unfreeze for subclass

    custom_timeout: int = 30
```

#### Development

```bash
uv sync --group dev
uv run pytest unittests
```

Or run individual checks:

```bash
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

```bash
go get github.com/Hochfrequenz/sap-mcp-config
```

#### Usage

```go
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:

```go
type MySAPSystem struct {
    sapmcpconfig.SAPSystem
    CustomTimeout int `json:"custom_timeout"`
}
```

#### Development

```bash
go test ./...
```

### Error Messages (Python and Go)

Both implementations validate eagerly and return **all** errors at once. A misconfigured file like this:

```json
{
  "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)
```

