sap-wm-mcp 0.2.7 → 0.2.8
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/README.md +5 -3
- package/index.js +23 -1
- package/package.json +1 -1
- package/tools/interimZoneAnomalies.js +82 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Connect AI agents — Claude, Copilot, or any MCP-compatible client — directly
|
|
|
13
13
|
>
|
|
14
14
|
> This project ships a custom RAP OData V4 service that exposes classic WM operations as a proper API — and wraps it in an MCP server so AI agents can drive it. Large portions of the SAP install base are still on classic WM. This fills the gap.
|
|
15
15
|
|
|
16
|
-
The **npm package** ships
|
|
16
|
+
The **npm package** ships 22 tools covering core operations, analytics, shift management, anomaly detection, audit history, proactive replenishment, and interim zone reconciliation. This repository contains the 9 open-sourced tool implementations — the ABAP RAP service source and additional tool source are available separately (see [ABAP Service Installation](#abap-service-installation)).
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -374,7 +374,7 @@ For write operations:
|
|
|
374
374
|
|
|
375
375
|
## Tools Reference
|
|
376
376
|
|
|
377
|
-
The npm package ships **
|
|
377
|
+
The npm package ships **22 tools** across five capability areas. The 9 tools below are open-sourced in this repository. Analytics, shift management, anomaly detection, and operations tools are available in the published package.
|
|
378
378
|
|
|
379
379
|
---
|
|
380
380
|
|
|
@@ -536,6 +536,7 @@ The following tools are available in the published npm package and fully functio
|
|
|
536
536
|
| `get_inventory_anomalies` | Bins stuck in mid-inventory state — empty bins with locks, open count docs, orphaned lock codes |
|
|
537
537
|
| `get_transfer_order_history` | Full TO history — creator, executor (resolved from LTAP.QNAME), and item detail; filterable by date range, status, movement type, material, `createdBy`, or `executedBy` |
|
|
538
538
|
| `get_replenishment_needs` | Find forward-pick bins at or below a stock threshold — `defaultReplenishQty` param (default 50) used as fallback when no bin max qty is configured; flags bins with an open replenishment TO to avoid duplicate moves |
|
|
539
|
+
| `get_interim_zone_anomalies` | Detect positive stock stranded in interim/staging zones (types 999, 998, 902) — surfaces same-day, overnight, and multi-day strandings with likely cause per zone; use `minDaysStranded` to filter noise during active shifts |
|
|
539
540
|
|
|
540
541
|
---
|
|
541
542
|
|
|
@@ -677,7 +678,8 @@ sap-wm-mcp/
|
|
|
677
678
|
│ ├── confirmTransferOrderSU.js ← confirm_transfer_order_su
|
|
678
679
|
│ ├── cancelTransferOrder.js ← cancel_transfer_order
|
|
679
680
|
│ ├── transferOrderHistory.js ← get_transfer_order_history
|
|
680
|
-
│
|
|
681
|
+
│ ├── replenishmentNeeds.js ← get_replenishment_needs
|
|
682
|
+
│ └── interimZoneAnomalies.js ← get_interim_zone_anomalies
|
|
681
683
|
├── .env.example
|
|
682
684
|
└── package.json
|
|
683
685
|
```
|
package/index.js
CHANGED
|
@@ -28,8 +28,9 @@ import { getInventoryAnomalies } from './tools/inventoryAnomalies.js';
|
|
|
28
28
|
import { getTransferOrderHistory } from './tools/transferOrderHistory.js';
|
|
29
29
|
import { cancelTransferOrder } from './tools/cancelTransferOrder.js';
|
|
30
30
|
import { getReplenishmentNeeds } from './tools/replenishmentNeeds.js';
|
|
31
|
+
import { getInterimZoneAnomalies } from './tools/interimZoneAnomalies.js';
|
|
31
32
|
|
|
32
|
-
const server = new McpServer({ name: 'sap-wm-mcp', version: '0.2.
|
|
33
|
+
const server = new McpServer({ name: 'sap-wm-mcp', version: '0.2.8' });
|
|
33
34
|
|
|
34
35
|
// Tool 1 — get_bin_status
|
|
35
36
|
server.tool(
|
|
@@ -457,6 +458,27 @@ server.tool(
|
|
|
457
458
|
}
|
|
458
459
|
);
|
|
459
460
|
|
|
461
|
+
// Tool 22 — get_interim_zone_anomalies
|
|
462
|
+
server.tool(
|
|
463
|
+
'get_interim_zone_anomalies',
|
|
464
|
+
'Detect positive stock stranded in interim/staging zones (types 999, 998, 902) where stock should only pass through briefly. Surfaces same-day, overnight, and multi-day strandings with likely cause per zone type. Run when stock appears to be lost or unaccounted for, or as part of end-of-shift reconciliation.',
|
|
465
|
+
{
|
|
466
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
467
|
+
interimTypes: z.array(z.string()).optional().default(['999', '998', '902']).describe('Interim storage types to check — default [999, 998, 902]'),
|
|
468
|
+
minDaysStranded: z.number().optional().default(0).describe('Only return stock stranded for at least this many days. Default 0 = include same-day strandings.'),
|
|
469
|
+
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
470
|
+
top: z.number().optional().default(100).describe('Max quants to scan (scans top * 3 internally to account for client-side filtering)')
|
|
471
|
+
},
|
|
472
|
+
async (params) => {
|
|
473
|
+
try {
|
|
474
|
+
const result = await getInterimZoneAnomalies(params);
|
|
475
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
476
|
+
} catch (err) {
|
|
477
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
460
482
|
const transport = new StdioServerTransport();
|
|
461
483
|
await server.connect(transport);
|
|
462
484
|
console.error('SAP WM MCP Server running (stdio)...');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-wm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
import { esc } from '../lib/sanitize.js';
|
|
3
|
+
|
|
4
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMWarehouseStock`;
|
|
5
|
+
|
|
6
|
+
const INTERIM_CAUSE = {
|
|
7
|
+
'999': 'SU/GI interim — GI posted before TO confirmation; confirm open TO or create reversal',
|
|
8
|
+
'998': 'GR interim — goods receipt without putaway TO; check open TRs and create putaway',
|
|
9
|
+
'902': 'GR staging (WE-ZONE) — putaway TO not yet created or confirmed; check GR monitor',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function getInterimZoneAnomalies({
|
|
13
|
+
warehouse,
|
|
14
|
+
interimTypes = ['999', '998', '902'],
|
|
15
|
+
minDaysStranded = 0,
|
|
16
|
+
material,
|
|
17
|
+
top = 100
|
|
18
|
+
}) {
|
|
19
|
+
const typeFilter = interimTypes.map(t => `StorageType eq '${esc(t)}'`).join(' or ');
|
|
20
|
+
const filters = [
|
|
21
|
+
`WarehouseNumber eq '${esc(warehouse)}'`,
|
|
22
|
+
`(${typeFilter})`
|
|
23
|
+
];
|
|
24
|
+
if (material) filters.push(`Material eq '${esc(material)}'`);
|
|
25
|
+
|
|
26
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top * 3}`;
|
|
27
|
+
const data = await s4hGet(path);
|
|
28
|
+
const rows = data.value ?? [];
|
|
29
|
+
|
|
30
|
+
const today = new Date();
|
|
31
|
+
|
|
32
|
+
const stranded = rows
|
|
33
|
+
.filter(r => parseFloat(r.TotalStock ?? 0) > 0)
|
|
34
|
+
.map(r => {
|
|
35
|
+
const daysSinceMove = r.LastMovementDate
|
|
36
|
+
? Math.floor((today - new Date(r.LastMovementDate)) / 86400000)
|
|
37
|
+
: null;
|
|
38
|
+
const ageFlag = daysSinceMove === null ? 'unknown'
|
|
39
|
+
: daysSinceMove === 0 ? 'same-day'
|
|
40
|
+
: daysSinceMove === 1 ? 'overnight'
|
|
41
|
+
: 'multi-day';
|
|
42
|
+
return {
|
|
43
|
+
storageType: r.StorageType,
|
|
44
|
+
bin: r.StorageBin,
|
|
45
|
+
material: r.Material?.trimStart?.() ?? r.Material,
|
|
46
|
+
plant: r.Plant,
|
|
47
|
+
qty: parseFloat(r.TotalStock ?? 0),
|
|
48
|
+
uom: r.UnitOfMeasure,
|
|
49
|
+
lastMove: r.LastMovementDate ?? 'unknown',
|
|
50
|
+
daysSinceMove,
|
|
51
|
+
ageFlag,
|
|
52
|
+
likelyCause: INTERIM_CAUSE[r.StorageType] ?? 'Positive stock in interim zone — investigate movement chain'
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter(r => (r.daysSinceMove ?? 0) >= minDaysStranded)
|
|
56
|
+
.sort((a, b) => (b.daysSinceMove ?? 0) - (a.daysSinceMove ?? 0));
|
|
57
|
+
|
|
58
|
+
const byType = {};
|
|
59
|
+
for (const r of stranded) {
|
|
60
|
+
if (!byType[r.storageType]) byType[r.storageType] = { count: 0, totalQty: 0 };
|
|
61
|
+
byType[r.storageType].count++;
|
|
62
|
+
byType[r.storageType].totalQty += r.qty;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
count: stranded.length,
|
|
67
|
+
truncated: rows.length === top * 3,
|
|
68
|
+
warehouse,
|
|
69
|
+
filters: {
|
|
70
|
+
interimTypes,
|
|
71
|
+
minDaysStranded,
|
|
72
|
+
material: material ?? 'all'
|
|
73
|
+
},
|
|
74
|
+
summary: {
|
|
75
|
+
multiDay: stranded.filter(r => r.ageFlag === 'multi-day').length,
|
|
76
|
+
overnight: stranded.filter(r => r.ageFlag === 'overnight').length,
|
|
77
|
+
sameDay: stranded.filter(r => r.ageFlag === 'same-day').length,
|
|
78
|
+
},
|
|
79
|
+
byStorageType: byType,
|
|
80
|
+
stranded
|
|
81
|
+
};
|
|
82
|
+
}
|