sap-wm-mcp 0.3.2 → 0.3.4

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 (2) hide show
  1. package/lib/s4hClient.js +44 -44
  2. package/package.json +1 -1
package/lib/s4hClient.js CHANGED
@@ -21,19 +21,20 @@ function log(level, msg, meta = {}) {
21
21
  console.error(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...meta }));
22
22
  }
23
23
 
24
- // ── CSRF token cache ──────────────────────────────────────────────────────────
25
- // Tokens are valid for the HTTP session. We cache the token + session cookie so
26
- // each write operation needs only one round trip instead of two.
27
- let _csrfToken = null;
28
- let _cookieHeader = null;
29
-
30
- async function refreshCsrf() {
31
- log('debug', 'csrf_refresh');
24
+ // ── CSRF token fetch ──────────────────────────────────────────────────────────
25
+ // Always fetches a fresh token + session cookie before each POST.
26
+ // No caching: avoids session-expiry races and token-echo bugs (some SAP gateway
27
+ // configurations return 'Required' or 'Fetch' on the service root; those are
28
+ // invalid tokens that look valid but cause 403 on every write).
29
+ // The extra round-trip cost is negligible — writes are rare in WM operations.
30
+ async function fetchCsrf() {
31
+ log('debug', 'csrf_fetch');
32
32
  const res = await fetch(`${BASE_URL}${BASE_PATH}`, {
33
33
  method: 'GET',
34
34
  headers: {
35
35
  'Authorization': `Basic ${AUTH}`,
36
- 'x-csrf-token': 'fetch',
36
+ 'Accept': 'application/json',
37
+ 'x-csrf-token': 'Fetch',
37
38
  'sap-client': CLIENT
38
39
  },
39
40
  agent,
@@ -41,16 +42,26 @@ async function refreshCsrf() {
41
42
  });
42
43
 
43
44
  const token = res.headers.get('x-csrf-token');
44
- if (!token) throw new Error('CSRF token fetch failed no token in response headers');
45
+ // SAP may echo 'Fetch' or 'Required' back when the endpoint doesn't support
46
+ // CSRF token issuance — treat those as failures so the error is clear.
47
+ if (!token || token === 'Fetch' || token === 'Required') {
48
+ throw new Error(`CSRF token fetch failed — SAP returned: ${token ?? '(no header)'}`);
49
+ }
45
50
 
46
- const raw = res.headers.raw?.()['set-cookie'] ?? res.headers.get('set-cookie');
47
- const cookie = Array.isArray(raw)
48
- ? raw.map(c => c.split(';')[0]).join('; ')
49
- : (raw ?? '').split(',').map(c => c.trim().split(';')[0]).join('; ');
51
+ // node-fetch v3: getSetCookie() is WHATWG spec (Node 18+); forEach fallback for older envs.
52
+ let cookieParts;
53
+ if (typeof res.headers.getSetCookie === 'function') {
54
+ cookieParts = res.headers.getSetCookie().map(c => c.split(';')[0].trim());
55
+ } else {
56
+ cookieParts = [];
57
+ res.headers.forEach((value, key) => {
58
+ if (key.toLowerCase() === 'set-cookie') cookieParts.push(value.split(';')[0].trim());
59
+ });
60
+ }
50
61
 
51
- _csrfToken = token;
52
- _cookieHeader = cookie;
53
- log('debug', 'csrf_refreshed');
62
+ const cookieHeader = cookieParts.join('; ');
63
+ log('debug', 'csrf_fetch_ok', { hasCookie: !!cookieHeader });
64
+ return { token, cookieHeader };
54
65
  }
55
66
 
56
67
  // ── GET ───────────────────────────────────────────────────────────────────────
@@ -93,36 +104,25 @@ export async function s4hPost(path, body) {
93
104
  const start = Date.now();
94
105
  log('debug', 'odata_post', { url });
95
106
 
96
- // Obtain CSRF token (use cache; refresh if missing or if SAP returns 403)
97
- if (!_csrfToken) await refreshCsrf();
98
-
99
- const doPost = async () => fetch(url, {
100
- method: 'POST',
101
- headers: {
102
- 'Authorization': `Basic ${AUTH}`,
103
- 'Content-Type': 'application/json',
104
- 'Accept': 'application/json',
105
- 'x-csrf-token': _csrfToken,
106
- 'sap-client': CLIENT,
107
- ...(_cookieHeader ? { 'Cookie': _cookieHeader } : {})
108
- },
109
- body: JSON.stringify(body),
110
- agent,
111
- signal: AbortSignal.timeout(TIMEOUT_MS)
112
- });
107
+ // Always fetch a fresh CSRF token + session cookie before the POST.
108
+ const { token, cookieHeader } = await fetchCsrf();
113
109
 
114
110
  let response;
115
111
  try {
116
- response = await doPost();
117
-
118
- // CSRF token expired — refresh once and retry
119
- if (response.status === 403) {
120
- log('warn', 'csrf_expired_retry', { url });
121
- _csrfToken = null;
122
- _cookieHeader = null;
123
- await refreshCsrf();
124
- response = await doPost();
125
- }
112
+ response = await fetch(url, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Authorization': `Basic ${AUTH}`,
116
+ 'Content-Type': 'application/json',
117
+ 'Accept': 'application/json',
118
+ 'x-csrf-token': token,
119
+ 'sap-client': CLIENT,
120
+ ...(cookieHeader ? { 'Cookie': cookieHeader } : {})
121
+ },
122
+ body: JSON.stringify(body),
123
+ agent,
124
+ signal: AbortSignal.timeout(TIMEOUT_MS)
125
+ });
126
126
  } catch (err) {
127
127
  log('error', 'odata_post_failed', { url, err: err.message, ms: Date.now() - start });
128
128
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-wm-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "MCP server for SAP Classic Warehouse Management — connects AI agents to S/4HANA WM via a custom RAP OData V4 service. For systems where EWM is not active.",
5
5
  "type": "module",
6
6
  "main": "index.js",