muave-sapmcp
v0.2.0
Published
Enterprise MCP server connecting Claude Code to SAP S/4HANA Cloud Public Edition OData (V2 + V4) services dynamically.
Maintainers
Readme
muave-sapmcp
An enterprise-grade MCP server that connects Claude Code (and any MCP client) to SAP S/4HANA Cloud Public Edition OData services — dynamically.
Point it at an OData service path; it fetches and parses $metadata, caches it
locally, and exposes generic tools to discover, read, and (opt-in) write any
entity set — for both OData V2 and V4.
- 🔐 Pluggable auth — OAuth 2.0 client-credentials (recommended for cloud) and HTTP Basic, behind one abstraction (X.509/mTLS on the roadmap).
- 🧭 Dynamic & metadata-driven — no per-service code; register a path and the tools adapt to its entities, keys, and navigations.
- 🛟 Handles what breaks naive clients — CSRF (V2 and V4), draft-enabled RAP/Fiori entities, ETag/
If-Matchconcurrency,Retry-Afterthrottling, and server-driven pagination. - 🛡️ Safe by default — read-only unless you opt in per system; write tools are dry-run-first; entity allowlists; secrets never become tool arguments and are redacted from logs.
Status:
0.1.0— initial release. See CHANGELOG.md and the Roadmap.
Install
npm install -g muave-sapmcp
# or run from source:
npm install && npm run buildRequires Node.js ≥ 20.12.
Configure
1. systems.json (no secrets — env-var names only)
Copy systems.json.example to systems.json (in the cwd,
./.muave-sapmcp/, or wherever MUAVE_SYSTEMS_FILE points):
{
"schemaVersion": 1,
"defaultSystem": "EXAMPLE",
"systems": [
{
"key": "EXAMPLE",
"baseUrl": "https://my123456-api.s4hana.cloud.sap",
"sapClient": "100",
"authType": "OAUTH2",
"tokenUrl": "https://my123456-api.s4hana.cloud.sap/sap/bc/sec/oauth2/token?sap-client=100",
"clientIdEnvVar": "EXAMPLE_OAUTH_CLIENT_ID",
"clientSecretEnvVar": "EXAMPLE_OAUTH_CLIENT_SECRET",
"readOnly": true
}
]
}Per-system fields: key, baseUrl, authType (OAUTH2 | BASIC | X509),
optional sapClient, readOnly (default true), allowedEntities,
timeoutMs, maxConcurrency.
- OAUTH2:
tokenUrl(read it from the Communication Arrangement's "OAuth 2.0 Confidential Client Token Service URL" — do not hardcode the host),clientIdEnvVar,clientSecretEnvVar. The flow isgrant_type=client_credentialswith the client id/secret in theAuthorization: Basicheader; there is no scope param and no refresh token (the token is cached and re-fetched on expiry/401). - BASIC:
userEnvVar+passwordEnvVar, or apreEncodedEnvVar(pre-base64'duser:pass).
2. Credentials (environment)
Secrets are resolved from the environment by the names referenced above — supply
them via your MCP client's env block, a process manager, or a secret manager.
See .env.example. Never put secret values in systems.json.
Where the catalog cache lives
Registered services and parsed metadata persist in catalog.json (entries are
namespaced per system as "<systemKey>:<serviceId>", so multiple S/4HANA systems
share one cache file without mixing). Location precedence:
MUAVE_CACHE_DIRenv varcacheDirinsystems.json(relative paths resolve against the systems file's directory)MUAVE_HOMEenv var- Default: a
.muave-sapmcp/directory next to yoursystems.json— predictable even when the server is spawned with an arbitrary working directory (e.g. by Claude Desktop) <cwd>/.muave-sapmcp/(no systems file found yet)
3. Register with Claude Code (.mcp.json)
{
"mcpServers": {
"muave-sapmcp": {
"command": "muave-sapmcp",
"env": {
"MUAVE_SYSTEMS_FILE": "/abs/path/systems.json",
"EXAMPLE_OAUTH_CLIENT_ID": "…",
"EXAMPLE_OAUTH_CLIENT_SECRET": "…"
}
}
}
}(From source, use "command": "node", "args": ["/abs/path/dist/index.js"].)
4. Use with Claude Desktop
Claude Desktop reads the same mcpServers shape from its own config file:
| OS | Config file |
|---|---|
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Windows (Microsoft Store app) | %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\claude_desktop_config.json |
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
(Or open it via Claude Desktop → Settings → Developer → Edit Config.)
{
"mcpServers": {
"muave-sapmcp": {
"command": "node",
"args": ["C:\\path\\to\\node_modules\\muave-sapmcp\\dist\\index.js"],
"env": {
"MUAVE_SYSTEMS_FILE": "C:\\path\\to\\systems.json",
"MUAVE_ENV_FILE": "C:\\path\\to\\.env.local",
"LOG_LEVEL": "info"
}
}
}
}Desktop-specific notes:
- Use
nodewith an absolute path todist/index.js— GUI apps don't reliably inherit your shellPATH, andnpx/.cmdshims often fail on Windows. Afternpm install -g muave-sapmcp, find the install dir withnpm root -g. - Keep secrets out of the config: point
MUAVE_ENV_FILEat a local env file (e.g..env.local) containing the credential variables yoursystems.jsonreferences, instead of inlining them inenv. - Catalog cache: from 0.1.1 the cache defaults to a
.muave-sapmcp/dir next to yoursystems.json, so Claude Desktop and Claude Code share registrations automatically. SetMUAVE_HOME(orcacheDirinsystems.json) only if you want it elsewhere. - Fully quit and relaunch after editing the config (system tray → Quit on Windows; ⌘Q on macOS) — closing the window isn't enough.
- Verify: the tools icon in the chat box lists the
muave-sapmcptools; try "List my SAP systems." If the server shows as failed, check the MCP logs next to the config file (logs/mcp-server-muave-sapmcp.log).
Tools
| Tool | Purpose |
|---|---|
| list_systems | Configured systems (no secrets) + default. |
| discover_catalog | Best-effort catalog enumeration; degrades gracefully when gated. |
| register_service | Fetch + parse a service's $metadata by path and cache it. |
| list_services / refresh_metadata | List / re-fetch registered services. |
| describe_service / describe_entity | Entity sets, keys, draft status; full property/nav detail + draft action FQNs. |
| query_entities | Paged collection read ($filter/$select/$expand/$orderby) with an opaque nextCursor. |
| get_entity | Fetch one entity by key; returns its ETag. |
| create_entity / update_entity / delete_entity | Draft-aware writes; CSRF + ETag handled; dry-run-first. |
| activate_draft | Drive the draft lifecycle (Prepare+Activate / Discard). |
| odata_request | Low-level passthrough for $batch, function imports, etc. |
Registered services and metadata are also exposed as read-only MCP resources
(services://, metadata://{system}/{serviceId}[/{entitySet}]), and a starter
prompt explore_sap_service teaches the safe workflow.
Read-only by default; enabling writes
Every system is readOnly: true until you opt in. Set "readOnly": false (and
optionally "allowedEntities": [...]) on a system to enable writes there. Write
tools are only advertised when some system allows writes, and every write is
re-checked at call time. Writes are dry-run-first: call with confirm: false
(default) to get a preview of the resolved request, then confirm: true to execute.
SAP specifics it handles
- CSRF — fetches the token + session cookie and replays both on every write (V2 and V4); auto-retries once on a
403 X-CSRF-Token: Required. - Draft entities — detects draft-enabled RAP/Fiori entities (IsActiveEntity key, DraftAdministrativeData, DraftRoot/Node) and resolves bound action FQNs;
create_entitycan drive create-draft → Activate;activate_drafthandles recovery. - ETag —
update_entity/delete_entityuse optimistic concurrency and retry once on412after re-reading. - Throttling — honors
Retry-Afteron429, with bounded per-system concurrency. - Pagination — never materializes whole collections; follows
__next/@odata.nextLink, else$top/$skipwith a deterministic$orderby.
Catalog discovery caveat
On S/4HANA Cloud Public Edition the OData catalog services are frequently gated
(KBA 3657717; they need a catalog communication scenario such as SAP_COM_0449).
discover_catalog therefore degrades gracefully (returns available: false with
guidance) — manual register_service is the reliable path. Find service
paths on the SAP Business Accelerator Hub and in your
Communication Arrangement's Inbound Services.
Troubleshooting
[auth/401]— check the system's OAuth client id/secret or Basic user/password env vars, and that the Communication Arrangement authorizes the service.[csrf/403]— usually an expired session; retry (token+cookie are automatic).[etag/412]— the entity changed concurrently; re-read and retry.[throttle/429]— back off;retryAfterSecondsis surfaced.- All diagnostics go to stderr (JSON via
pino); setLOG_LEVEL=debugfor detail.
Development
npm run dev # tsx watch
npm run typecheck
npm run lint
npm test # vitest (unit + mocked-fetch integration + in-memory MCP roundtrip)
npm run coverage
npm run buildRoadmap
X.509/mTLS auth provider (trusting the SAP Cloud Root CA — 2026 CA migration);
remote Streamable-HTTP transport with the MCP OAuth 2.1 resource-server framework;
secret-manager credential backends (Vault / AWS / Azure / GCP); $batch as a
first-class tool; SOAP/REST adapters; metadata-driven per-entity input schemas.
