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.
- package/lib/s4hClient.js +44 -44
- 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
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async function
|
|
31
|
-
log('debug', '
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
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",
|