sap-support-mcp
by arief135
README.md
# sap-support-mcp
An MCP server over **Streamable HTTP** that exposes two SAP help-desk operations to an
AI agent:
| Tool | What it does |
| --- | --- |
| `sap_reset_password` | Sets a user's password to a generated temporary value via `BAPI_USER_CHANGE`. The password is stored as *initial*, so SAP forces the user to choose a new one at next logon. |
| `sap_unlock_users` | Clears the administrator lock and resets the failed-logon counter for 1–25 users via `BAPI_USER_UNLOCK`. |
The two are deliberately separate, because SAP treats them separately: unlocking does not
change a password, and resetting a password does not clear a lock. A user who is locked
*and* has forgotten their password needs both. `sap_reset_password` detects this case and
says so in its response.
## How it talks to SAP
Calls go over plain HTTP to the ABAP system's SOAP RFC service
(`/sap/bc/soap/rfc`) with basic authentication — no SAP NetWeaver RFC SDK, no native
binaries, so the server runs anywhere Node does.
```
MCP client ──Streamable HTTP──▶ sap-support-mcp ──SOAP/HTTPS──▶ /sap/bc/soap/rfc ──▶ BAPI_USER_*
```
Three function modules are used: `BAPI_USER_GET_DETAIL` (existence + lock state),
`BAPI_USER_CHANGE` (password), `BAPI_USER_UNLOCK` (locks).
Each SOAP RFC call is its own stateless session, so a follow-up
`BAPI_TRANSACTION_COMMIT` would land in a different LUW and do nothing. That is fine
here: the `BAPI_USER_*` modules commit internally. Do not extend this server with BAPIs
that require an explicit commit without first moving to a stateful transport.
## Setup
```bash
npm install
cp .env.example .env
```
Generate the bearer token clients will use:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
```
Fill in `.env`, then:
```bash
npm run dev
```
For production:
```bash
npm run build && npm start
```
### SAP-side prerequisites
1. The SOAP RFC service must be active in `SICF` at `/sap/bc/soap/rfc`.
2. Create a dedicated technical user (type *Communications*) for `SAP_USER`. It needs:
- `S_RFC` — activity 16, function groups `SUSR` and `SU_USER` (or the specific
modules `BAPI_USER_GET_DETAIL`, `BAPI_USER_CHANGE`, `BAPI_USER_UNLOCK`).
- `S_USER_GRP` — `ACTVT` 02 (change) and 05 (lock/unlock), with `CLASS` restricted to
the user groups the help desk is allowed to serve. **This is the strongest control
available** — the allow/deny lists in this server are defence in depth on top of it,
not a replacement for it.
3. Do **not** give the technical user `S_USER_GRP` over the `SUPER` group.
## Configuration
Every setting is an environment variable; see [.env.example](.env.example) for the full
list with comments. The ones that matter most:
| Variable | Purpose |
| --- | --- |
| `MCP_AUTH_TOKEN` | Bearer token required on every MCP request. No unauthenticated mode exists. Minimum 16 characters. |
| `SAP_BASE_URL`, `SAP_CLIENT`, `SAP_USER`, `SAP_PASSWORD` | The target system and the technical user. |
| `USER_ALLOW_LIST` | If set, **only** matching users can be targeted. Supports `*`, e.g. `SUPPORT_*,BUSINESS_*`. |
| `USER_DENY_LIST` | Merged on top of the built-in deny list. Deny always beats allow. |
| `AUDIT_LOG_FILE` | JSONL audit trail. Defaults to `audit/sap-support-mcp.audit.jsonl`. |
| `SAP_REJECT_UNAUTHORIZED` | Leave `true`. Set `false` only for a sandbox with a self-signed certificate. |
## Guardrails
**Bearer auth.** Every request to `MCP_PATH` needs `Authorization: Bearer <token>`,
compared with a timing-safe equality check. `/healthz` is the only unauthenticated route
and reveals nothing about SAP.
**Allow / deny lists.** `SAP*`, `DDIC`, `SAPCPIC`, `EARLYWATCH`, `TMSADM`, `SAPSUPPORT`,
`ADS_AGENT` and `ADSUSER` are denied unconditionally — adding `*` to the allow list
cannot expose them. Patterns are anchored, so `SUPPORT_*` does not match `XSUPPORT_1`.
A denied user is rejected *before* any call reaches SAP.
**Dry run.** Both tools take `dryRun: true`, which checks the policy and confirms the
user exists and reports its current lock state, without changing anything.
**Audit trail.** Append-only JSONL. Each operation writes an `attempt` record before
touching SAP and a terminal record (`success` / `failed` / `denied` / `dry_run`)
afterwards, sharing one `correlationId`, so a crash mid-call still leaves evidence:
```json
{"ts":"2026-07-31T08:18:49.261Z","correlationId":"ed66d93a…","operation":"unlock_user","outcome":"attempt","targetUser":"JDOE","sapClient":"100","reason":"INC-SMOKE-1","sessionId":"9b85a10a…","clientIp":"127.0.0.1"}
```
Every tool call requires a `reason` (ticket number or justification) which is recorded.
Generated passwords are never written to the audit trail — `AuditLog` has no field that
could carry one, and a test asserts the password does not appear in the file.
## Connecting a client
```json
{
"mcpServers": {
"sap-support": {
"type": "http",
"url": "http://127.0.0.1:3000/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN_HERE" }
}
}
}
```
Sessions are stateful: the server issues an `Mcp-Session-Id` on initialize, and each
session gets its own `McpServer` instance so caller identity stays isolated in the audit
trail.
## Security notes worth reading before you deploy this
- **The temporary password passes through the model's context.** `sap_reset_password`
returns it in the tool result, so it is visible to the LLM and to whatever transcript
store sits behind it. That is inherent to handing a password back through MCP. If your
threat model cannot accept it, change `resetPassword.ts` to deliver the password out of
band (email the user, push to a vault) and return only a confirmation.
- **`HOST` defaults to `127.0.0.1`.** Binding to `0.0.0.0` puts SAP password resets on
your network; put it behind TLS and a reverse proxy if you do.
- **Everything the caller supplies is XML-escaped** before it enters the SOAP envelope,
and usernames are additionally validated against SAP's character rules, so a crafted
username cannot inject extra RFC parameters.
- The deny list protects against a *confused* agent, not a *hostile* one holding the
bearer token. `S_USER_GRP` on the SAP side is the real boundary.
## Tests
```bash
npm test
```
43 tests, no SAP system required. `test/mockSap.ts` is a stand-in for `/sap/bc/soap/rfc`
that reproduces SAP's real response shapes — including message class `01`/`124` for an
unknown user, HTTP 401 for bad credentials, SOAP faults, and the fact that
`BAPI_USER_UNLOCK` clears an administrator lock but *not* a CUA global lock. The
end-to-end tests drive the real Express app through a real MCP client over Streamable
HTTP.
## Layout
```
src/
index.ts entry point: config, wiring, graceful shutdown
config.ts env schema + built-in deny list
http/
server.ts Express app, Streamable HTTP transport, session registry
auth.ts bearer token middleware
mcp/
server.ts McpServer construction + instructions
tools/
resetPassword.ts sap_reset_password
unlockUsers.ts sap_unlock_users
sap/
soap.ts SOAP envelope building, XML escaping, fault handling
userService.ts the three BAPIs + RETURN-table interpretation
security/
policy.ts allow/deny evaluation
audit.ts append-only JSONL audit trail
util/
password.ts temporary password generation
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues