guniweb-sap-mcp 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +74 -1
  2. package/dist/index.js +80 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -67,7 +67,7 @@ graph TB
67
67
  - **Three Transport Modes** -- stdio, Streamable HTTP, SSE
68
68
  - **API Key Protection** -- Secure HTTP transport with Bearer token
69
69
  - **Docker Ready** -- Docker Compose for n8n + SAP MCP sidecar
70
- - **1143 Tests** -- Unit, integration, E2E with CI/CD via GitHub Actions
70
+ - **1267 Tests** -- Unit, integration, E2E with CI/CD via GitHub Actions
71
71
 
72
72
  ## Quick Start
73
73
 
@@ -159,6 +159,79 @@ Clients that *require* a session id — they read `mcp-session-id` from the init
159
159
 
160
160
  The legacy `--transport sse` mode is different: it keeps a per-connection session and expects a `?sessionId=` query parameter on `/messages`. Prefer `http` for new setups.
161
161
 
162
+ #### Several SAP systems: `destinations.json`
163
+
164
+ The `SAP_*` variables describe **one** SAP system. To run one server against several — a production system next to a test client, or several customers' systems behind one n8n — put the connections into a `destinations.json` and point the server at it:
165
+
166
+ ```bash
167
+ guniweb-sap-mcp --transport http --port 8808 --destinations /config/destinations.json
168
+ # or: SAP_MCP_DESTINATIONS=/config/destinations.json
169
+ ```
170
+
171
+ ```json
172
+ {
173
+ "version": 1,
174
+ "destinations": {
175
+ "default": {
176
+ "baseUrl": "https://s4prod.example.com",
177
+ "authType": "basic",
178
+ "username": "MCP_USER",
179
+ "password": "${S4PROD_PASSWORD}",
180
+ "sapClient": "100"
181
+ },
182
+ "s4test": {
183
+ "baseUrl": "https://s4test.example.com",
184
+ "authType": "basic",
185
+ "username": "MCP_USER",
186
+ "password": "${S4TEST_PASSWORD}",
187
+ "sapClient": "200"
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ - **One entry = one connection.** Every entry accepts exactly the fields the `SAP_*` variables accept (`authType` and its credentials, `sapClient`, `baseUrl`) — all seven authentication types work per destination.
194
+ - **Secrets stay out of the file.** Any string may contain `${VARIABLE}`; it is resolved from the environment at load time (Docker secrets, `.env`). A placeholder that cannot be resolved makes the file invalid — an empty password would otherwise surface as a misleading `401` from SAP.
195
+ - **Hot reload.** The file is watched; a change is picked up on the next request, no restart. `kill -HUP <pid>` forces a reload. An invalid file **never** replaces the running configuration — the error is logged and the last valid one stays in force.
196
+ - **Backwards compatible.** Without `--destinations` nothing changes: the `SAP_*` variables are the single destination `default`. With the file, `SAP_*` (if set) is added as `default` unless the file defines one — so an installation can migrate without a gap.
197
+ - **Which destination answers?** Without a token: the one named `default` (or the only one). With a token: the token's destination — see below.
198
+
199
+ **Tokens: one Bearer value per n8n credential selects the system.** The n8n MCP Client sends exactly one `Authorization` header, so a token does two jobs at once — it authenticates the request and picks the destination. SAP passwords never leave the server; n8n only holds the token.
200
+
201
+ ```json
202
+ {
203
+ "version": 1,
204
+ "destinations": { "s4prod": { "...": "..." }, "s4test": { "...": "..." } },
205
+ "tokens": [
206
+ { "hash": "sha256:9f2c…", "destination": "s4prod", "label": "n8n prod workflows" },
207
+ { "hash": "sha256:1a7e…", "destination": "s4test", "label": "n8n dev", "revoked": false }
208
+ ]
209
+ }
210
+ ```
211
+
212
+ - The file holds only the **SHA-256 hash**; the plain token (`gsm_<destination>_<random>`) is printed **once** by `tokens issue` and cannot be recovered afterwards. Put it into an n8n **Bearer Auth** credential on the MCP Client node. As soon as at least one token exists, requests without a valid token get `401` — `--api-key` keeps working alongside and serves the `default` destination.
213
+ - Lookup is constant-time over all entries; `"revoked": true` disables a token on the next request (hot reload, no restart). After 10 failed attempts within a minute the client address gets `429` with `Retry-After` — also for a valid token — as a brake on guessing (a reverse proxy with a real rate limit is still the right place for that). Tokens are never written to the log; rejections are logged with address and reason.
214
+ **Managing the file — no hand-editing needed.** The same binary carries the admin commands; every command validates the result exactly like the server does before writing (never leaves an unloadable file), writes atomically, and a running server picks the change up on the next request:
215
+
216
+ ```bash
217
+ export SAP_MCP_DESTINATIONS=/config/destinations.json # or --destinations <path> on each call
218
+
219
+ guniweb-sap-mcp destinations add s4prod --base-url https://s4prod.example.com --auth-type basic \
220
+ --username MCP_USER --password '${S4PROD_PASSWORD}' --sap-client 100
221
+ guniweb-sap-mcp destinations add legacy --from-env # copy the current SAP_* environment (migration)
222
+ guniweb-sap-mcp destinations list # names, URLs, auth type, client, token count — never secrets
223
+
224
+ guniweb-sap-mcp tokens issue s4prod --label "n8n prod" --read-only --tiers core,odata
225
+ # → prints the plain token ONCE on stdout (everything else goes to stderr, so TOKEN=$(…) works)
226
+ guniweb-sap-mcp tokens list # label, destination, hash prefix, policy, status
227
+ guniweb-sap-mcp tokens revoke "n8n prod" # by label or ≥ 8 hex chars of the hash
228
+ guniweb-sap-mcp destinations remove s4test [--force] # refuses while tokens point at it, unless --force
229
+ ```
230
+
231
+ `--set field=value` covers any auth type (`--auth-type oauth2 --set clientId=… --set clientSecret='${BTP_SECRET}' --set tokenServiceUrl=…`); `${ENV_VAR}` values are checked against the current environment (`--allow-missing-env` to skip, e.g. when the variable only exists inside the container). In Docker, run the commands inside the container so they see the same file and environment: `docker compose exec sap-mcp guniweb-sap-mcp tokens issue s4prod`.
232
+
233
+ - **Per-token permissions** — `"policy": { "readOnly": true, "tiers": ["core", "odata"] }`. A policy only ever *restricts*: `readOnly: true` hides the write tools for that token even when the server runs with `--allow-write` (`readOnly: false` cannot open a read-only server), and `tiers` is a ceiling — tiers outside it are invisible for that token and `sap_enable_tools` cannot switch them on. `tools/list` is filtered per request, so an agent on a read-only token never sees `sap_create` in the first place.
234
+
162
235
  ### 4. Use with n8n
163
236
 
164
237
  1. Add an **AI Agent** node with an **MCP Client Tool** sub-node