S/4HANA MCP Server
README.md
# S/4HANA MCP Server
An [MCP](https://modelcontextprotocol.io) server exposing 38 tools for SAP S/4HANA (Cloud Public Edition and on-premise Private Edition) over OData V2, OData V4, and SOAP: business partners, products, sales pricing, warehouses, purchase orders, purchasing info records, supplier invoices (including posting and cancellation), service entry sheets, journal entries, cost centers, and finance org structure.
Write tools (`create_*`, `cancel_supplier_invoice`, `check_invoice_posting_readiness`) and `get_finance_structure` return their result inline as JSON. Every other (read) tool writes its result — CSV for collections, Markdown for single records — to an [E2B](https://e2b.dev) sandbox and returns the file path(s), instead of inlining potentially large SAP result sets (hundreds of business partners, full journal-entry item detail, etc.) directly into the calling agent's context. **This server only connects to an E2B sandbox your MCP client already owns — it never creates one.** Your client needs its own E2B integration (the E2B SDK, or a Claude environment with matching code-execution access) to read the files back; see [E2B sandbox setup](#e2b-sandbox-setup).
## Quick start
```bash
git clone https://github.com/ilia-inovaflow/s4hana-mcp-server.git
cd s4hana-mcp-server
npm install
npm run build
cp .env.example .env # fill in your E2B sandbox + SAP connection details
npm start # stdio transport (Claude Desktop, MCP Inspector, etc.)
```
For local development without building first: `npm run dev`.
## E2B sandbox setup
Register for a free API key at [e2b.dev](https://e2b.dev), then start (or reuse) a sandbox your MCP client can read files from, and set:
```bash
E2B_API_KEY=<your E2B API key>
SANDBOX_ID=<an already-running sandbox ID> # stdio only — HTTP takes this per-session
```
Over the HTTP transport, `SANDBOX_ID` is supplied per session instead, via the `sandbox-id` request header — useful when one running server instance handles multiple callers, each with their own sandbox.
This is unrelated to SAP's own `sandbox.api.sap.com` trial host below — "sandbox" means two different things in this README.
### Try it with no SAP tenant of your own
SAP publishes a free, public read-only API sandbox at `sandbox.api.sap.com` that this server talks to out of the box. Register for a free API key at [api.sap.com](https://api.sap.com), then set:
```bash
SAP_HOST=https://sandbox.api.sap.com
SAP_CLIENT_ID=<your sandbox API key>
```
No username, password, or OAuth setup needed — the sandbox authenticates with a single `APIKey` header. Business partner, product, and pricing tools work immediately against sandbox demo data (you still need the E2B sandbox above to receive read-tool results).
## Connecting a real SAP tenant
Set `SAP_HOST` to your tenant URL and pick one of three auth modes via `SAP_AUTH_MODE`:
| Mode | Use case | Required variables |
|---|---|---|
| `basic` | Quick testing, on-prem systems without OAuth configured | `SAP_USERNAME`, `SAP_PASSWORD` |
| `cc` | S/4HANA Cloud Public Edition, service-to-service (OAuth 2.0 Client Credentials) | `SAP_TOKEN_URL`, `SAP_CLIENT_ID`, `SAP_CLIENT_SECRET` |
| `oauth` | User-delegated OAuth Authorization Code — HTTP transport only, caller supplies a fresh token per request via the `sap-access-token` header | none (token comes from the MCP client) |
`SAP_CLIENT` (the SAP client/mandant number, e.g. `100`) defaults to `100` if unset.
Two API families need basic or X.509 authentication by SAP design and reject OAuth: journal entry posting (`create_journal_entry`, SOAP `SAP_COM_0002`) and the accounting-document detail views (`get_journal_entry`, `search_journal_entries` with `document_level=true`). Everything else works under any of the three modes.
See `.env.example` for the full variable list, including the optional `SAP_JE_SOAP_PATH` override some on-premise SOAMANAGER configurations need.
## Transports
**stdio** (default, `npm start` / `npm run dev`) — one SAP connection and one E2B sandbox per process, configured entirely from `.env`. This is what Claude Desktop and most local MCP clients expect:
```json
{
"mcpServers": {
"s4hana": {
"command": "node",
"args": ["/absolute/path/to/s4hana-mcp-server/dist/index.js"],
"env": {
"E2B_API_KEY": "...", "SANDBOX_ID": "...",
"SAP_HOST": "...", "SAP_AUTH_MODE": "basic", "SAP_USERNAME": "...", "SAP_PASSWORD": "..."
}
}
}
}
```
**Streamable HTTP** (`npm run start:http` / `npm run dev:http`) — for remote or multi-tenant deployment. Each session's SAP connection is configured either from `.env` defaults or per-request headers (`sap-host`, `sap-auth-mode`, `sap-username`, `sap-password`, `sap-token-url`, `sap-client-id`, `sap-client-secret`, `sap-client`, `sap-je-soap-path`), and its E2B sandbox from the `sandbox-id` header (or `SANDBOX_ID` env as a fallback) — letting one running server field requests against different tenants and sandboxes. `E2B_API_KEY` is always read from the server's own environment. `PORT` (default `3000`) and `HOST` (default `localhost`) configure the listener; the MCP endpoint is `POST /mcp`, with `GET /health` for liveness checks.
If you deploy the HTTP transport somewhere reachable over the network, put it behind your own authentication/network boundary — this server does not itself gate access to the `/mcp` endpoint, only to the downstream SAP connection.
## Tools
<details>
<summary><strong>Business Partners</strong> (3 tools)</summary>
- `search_business_partners` — search customers, vendors, persons, organizations
- `get_business_partner` — full detail for one Business Partner
- `query_business_partner_odata` — generic query against any `API_BUSINESS_PARTNER` entity set
</details>
<details>
<summary><strong>Products</strong> (3 tools)</summary>
- `search_products` — search materials, finished goods, services, raw materials
- `get_product` — full detail for one product
- `query_product_odata` — generic query against any `API_PRODUCT_SRV` entity set
</details>
<details>
<summary><strong>Sales Pricing</strong> (4 tools)</summary>
- `get_product_pricing` — combined price lookup for a material (start here for "what does X cost")
- `search_pricing_conditions` — browse condition records by type/rate
- `get_pricing_condition` — full detail for one condition record
- `query_pricing_odata` — generic query against any pricing entity set
</details>
<details>
<summary><strong>Warehouses</strong> (2 tools)</summary>
- `list_warehouses`
- `get_warehouse`
</details>
<details>
<summary><strong>Purchase Orders</strong> (4 tools)</summary>
- `search_purchase_orders`
- `get_purchase_order`
- `query_purchase_order_odata` — generic query, including item/schedule-line level
- `get_purchase_order_confirmations` — supplier order/shipping/GR confirmations, bulk lookup
</details>
<details>
<summary><strong>Purchasing Info Records</strong> (3 tools)</summary>
- `search_purchasing_info_records`
- `get_purchasing_info_record`
- `query_purchasing_info_record_odata`
</details>
<details>
<summary><strong>Supplier Invoices</strong> (8 tools)</summary>
- `search_supplier_invoices`
- `get_supplier_invoice`
- `query_supplier_invoice_odata`
- `create_supplier_invoice_po` ⚠️ write — PO-referenced 3-way-match invoice
- `create_supplier_invoice_gl` ⚠️ write — non-PO expense invoice, direct G/L lines
- `create_supplier_credit_memo` ⚠️ write — PO-referenced credit memo
- `cancel_supplier_invoice` ⚠️ write — reverses a posted invoice or credit memo
- `check_invoice_posting_readiness` — read-only pre-flight guard check
</details>
<details>
<summary><strong>Service Entry Sheets</strong> (3 tools)</summary>
- `search_service_entry_sheets`
- `get_service_entry_sheet`
- `query_service_entry_sheet_odata`
</details>
<details>
<summary><strong>Journal Entries & FI-CO</strong> (8 tools)</summary>
- `create_journal_entry` ⚠️ write — posts a G/L document (SOAP, basic/X.509 auth only)
- `search_journal_entries` — analytical (aggregated) or accounting-document (line-level) view
- `get_journal_entry` — one accounting document by number
- `search_gl_accounts`
- `search_open_po_items` — a supplier's still-invoiceable PO lines, in one call
- `search_cost_centers`
- `get_cost_center_hierarchy` — the standard cost center hierarchy tree
- `get_finance_structure` — company codes, ledgers, charts of accounts, segments, business areas
</details>
Every write tool runs server-side guard checks (existence, blocking flags, duplicate detection, balance validation) before posting and reports a structured reason list — with nothing sent to SAP — if any guard fails. Deterministic SAP rejections (closed posting periods, missing tax jurisdiction, etc.) are translated into actionable messages rather than raw error codes.
## Architecture
```
src/
├── server.ts # McpServer factory — tool registration, schemas, annotations
├── client.ts # SapClient — OData V2/V4 requests, CSRF-protected writes, SOAP posts
├── auth.ts # Auth strategies: Basic, OAuth Client Credentials, OAuth Authorization Code
├── config.ts # Connection config + SAP public sandbox detection
├── index.ts # stdio entry point
├── http.ts # Streamable HTTP entry point (Express, per-session auth)
└── tools/
├── shared.ts # OData/CSV/error-parsing + E2B sandbox file I/O, shared by every tool
└── *.ts # one file per tool (schema + handler)
```
Writes to OData V2 endpoints follow SAP's CSRF token protocol: fetch a token via `GET` with `X-CSRF-Token: Fetch`, resend it with the session cookie on the `POST`, retry once on a `403`. Journal entry posting instead builds and sends a raw SOAP envelope, since that API has no OData equivalent.
## Development
```bash
npm run dev # stdio, hot reload via tsx
npm run dev:http # HTTP, hot reload via tsx
npm run typecheck # tsc --noEmit
npm run build # compile to dist/
```
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing