sap-wm-mcp 0.3.0 → 0.3.2
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 +20 -9
- package/index.js +68 -25
- package/package.json +1 -1
- package/tools/createCycleCountDoc.js +54 -0
- package/tools/goodsIssueMonitor.js +92 -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 23 tools covering core operations, analytics, shift management, anomaly detection, audit history, proactive replenishment, interim zone reconciliation, and cycle count management. 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 **23 tools** across five capability areas. The 9 tools below are open-sourced in this repository. Analytics, shift management, anomaly detection, cycle count management, and operations tools are available in the published package.
|
|
378
378
|
|
|
379
379
|
---
|
|
380
380
|
|
|
@@ -541,6 +541,8 @@ The following tools are available in the published npm package and fully functio
|
|
|
541
541
|
| `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` |
|
|
542
542
|
| `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 |
|
|
543
543
|
| `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 |
|
|
544
|
+
| `get_goods_issue_monitor` | Open outbound deliveries — delivery qty vs picked qty, GI status, overdue flag, and stock still in GI zone (type 999). Tracks picking progress and stalled shipments. |
|
|
545
|
+
| `create_cycle_count_document` | Create a WM inventory document (LI01N equivalent) for a specific bin. Activates the bin immediately by default (`LAGP.KZINV = 'IL'`). Use after `get_cycle_count_candidates` to trigger a count. |
|
|
544
546
|
|
|
545
547
|
---
|
|
546
548
|
|
|
@@ -566,11 +568,17 @@ The ABAP source is not included in this public repository. To obtain the ABAP pa
|
|
|
566
568
|
| `ZR_WMCYCLECOUNTBIN` | CDS View | View over LAGP (cycle count indicators) |
|
|
567
569
|
| `ZR_WMTRANSFERORDER` | BDEF | Behavior definition — defines actions |
|
|
568
570
|
| `ZBP_R_WMTRANSFERORDER` | Class | RAP behavior implementation |
|
|
569
|
-
| `
|
|
571
|
+
| `ZR_WMGOODSISSUEDELIVERY` | CDS View | View over LIKP + LIPS (outbound deliveries for GI monitor) |
|
|
572
|
+
| `ZR_WMTRANSFERORDER` | BDEF | Behavior definition — defines actions |
|
|
573
|
+
| `ZBP_R_WMTRANSFERORDER` | Class | RAP behavior implementation |
|
|
574
|
+
| `ZWM_MFG` | Function Group | Contains all RFC wrapper FMs |
|
|
570
575
|
| `ZWM_TO_CREATE` | Function Module | RFC-enabled wrapper for `L_TO_CREATE_SINGLE` |
|
|
571
|
-
| `
|
|
572
|
-
| `
|
|
573
|
-
| `
|
|
576
|
+
| `ZWM_TO_CANCEL` | Function Module | RFC-enabled wrapper for `L_TO_CANCEL` |
|
|
577
|
+
| `ZWM_INV_CREATE` | Function Module | RFC-enabled wrapper — writes directly to LINK/LINP/LINV and locks bin in LAGP |
|
|
578
|
+
| `ZA_WMCREATETOPARAM` | Abstract Entity | Parameter type for CreateTransferOrder action |
|
|
579
|
+
| `ZA_WMCONFIRMTOSUPARAM` | Abstract Entity | Parameter type for ConfirmTransferOrderSU action |
|
|
580
|
+
| `ZA_WMCREATEINVDOCPARAM` | Abstract Entity | Parameter type for CreateInventoryDocument action |
|
|
581
|
+
| `ZSD_WMMCPSERVICE` | Service Def | OData V4 service definition (8 entity sets) |
|
|
574
582
|
| `ZSB_WMMCPSERVICE_ODATA4_UI` | Service Binding | OData V4 UI binding |
|
|
575
583
|
|
|
576
584
|
### abapGit compatibility
|
|
@@ -616,7 +624,8 @@ Classic WM has no standard OData APIs. This project builds one using **ABAP REST
|
|
|
616
624
|
| `CreateTransferOrder` | `L_TO_CREATE_SINGLE` | Called via RFC wrapper `ZWM_TO_CREATE` with `DESTINATION 'NONE'` |
|
|
617
625
|
| `ConfirmTransferOrder` | `L_TO_CONFIRM` | Called directly — no COMMIT needed |
|
|
618
626
|
| `ConfirmTransferOrderSU` | `L_TO_CONFIRM_SU` | Called directly — no COMMIT needed |
|
|
619
|
-
| `CancelTransferOrder` | `L_TO_CANCEL` | Called
|
|
627
|
+
| `CancelTransferOrder` | `L_TO_CANCEL` | Called via RFC wrapper `ZWM_TO_CANCEL` with `DESTINATION 'NONE'` |
|
|
628
|
+
| `CreateInventoryDocument` | Direct writes to LINK/LINP/LINV | Called via RFC wrapper `ZWM_INV_CREATE` with `DESTINATION 'NONE'` |
|
|
620
629
|
|
|
621
630
|
### Why the RFC wrapper?
|
|
622
631
|
|
|
@@ -683,7 +692,9 @@ sap-wm-mcp/
|
|
|
683
692
|
│ ├── cancelTransferOrder.js ← cancel_transfer_order
|
|
684
693
|
│ ├── transferOrderHistory.js ← get_transfer_order_history
|
|
685
694
|
│ ├── replenishmentNeeds.js ← get_replenishment_needs
|
|
686
|
-
│
|
|
695
|
+
│ ├── interimZoneAnomalies.js ← get_interim_zone_anomalies
|
|
696
|
+
│ ├── goodsIssueMonitor.js ← get_goods_issue_monitor
|
|
697
|
+
│ └── createCycleCountDoc.js ← create_cycle_count_document
|
|
687
698
|
├── .env.example
|
|
688
699
|
└── package.json
|
|
689
700
|
```
|
|
@@ -708,7 +719,7 @@ Running both side-by-side shows the contrast directly: same tools, same question
|
|
|
708
719
|
| Phase | Status | Description |
|
|
709
720
|
|---|---|---|
|
|
710
721
|
| Phase 0 — RAP Service | ✅ Complete | Custom OData V4 service with 7 entity sets over classic WM tables |
|
|
711
|
-
| Phase 1 — Local MCP | ✅ Complete |
|
|
722
|
+
| Phase 1 — Local MCP | ✅ Complete | 23 tools working, security hardened, published to npm |
|
|
712
723
|
| Phase 2 — BTP CF | 🔜 Planned | Deploy to SAP BTP Cloud Foundry with SSE transport + XSUAA + Cloud Connector |
|
|
713
724
|
| Phase 3 — Joule Agent | 💡 Future | Native Joule Studio agent using the same RAP service |
|
|
714
725
|
|
package/index.js
CHANGED
|
@@ -29,8 +29,10 @@ import { getTransferOrderHistory } from './tools/transferOrderHistory.js';
|
|
|
29
29
|
import { cancelTransferOrder } from './tools/cancelTransferOrder.js';
|
|
30
30
|
import { getReplenishmentNeeds } from './tools/replenishmentNeeds.js';
|
|
31
31
|
import { getInterimZoneAnomalies } from './tools/interimZoneAnomalies.js';
|
|
32
|
+
import { getGoodsIssueMonitor } from './tools/goodsIssueMonitor.js';
|
|
33
|
+
import { createCycleCountDoc } from './tools/createCycleCountDoc.js';
|
|
32
34
|
|
|
33
|
-
const server = new McpServer({ name: 'sap-wm-mcp', version: '0.3.
|
|
35
|
+
const server = new McpServer({ name: 'sap-wm-mcp', version: '0.3.2' });
|
|
34
36
|
|
|
35
37
|
// Tool 1 — get_bin_status
|
|
36
38
|
server.tool(
|
|
@@ -40,7 +42,7 @@ server.tool(
|
|
|
40
42
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
41
43
|
storageType: z.string().optional().describe('Storage type e.g. 001'),
|
|
42
44
|
bin: z.string().optional().describe('Specific bin number e.g. 01-01-01'),
|
|
43
|
-
top: z.number().optional().default(20).describe('Max records to return')
|
|
45
|
+
top: z.coerce.number().optional().default(20).describe('Max records to return')
|
|
44
46
|
},
|
|
45
47
|
async (params) => {
|
|
46
48
|
try {
|
|
@@ -60,7 +62,7 @@ server.tool(
|
|
|
60
62
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
61
63
|
material: z.string().optional().describe('Material number e.g. TG0001'),
|
|
62
64
|
storageType: z.string().optional().describe('Filter by storage type'),
|
|
63
|
-
top: z.number().optional().default(20).describe('Max records to return')
|
|
65
|
+
top: z.coerce.number().optional().default(20).describe('Max records to return')
|
|
64
66
|
},
|
|
65
67
|
async (params) => {
|
|
66
68
|
try {
|
|
@@ -80,7 +82,7 @@ server.tool(
|
|
|
80
82
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
81
83
|
storageType: z.string().optional().describe('Storage type to filter e.g. 001'),
|
|
82
84
|
binType: z.string().optional().describe('Bin type to filter e.g. E1, E2 — use when destination bin must match a specific storage unit type'),
|
|
83
|
-
top: z.number().optional().default(50).describe('Max records to return')
|
|
85
|
+
top: z.coerce.number().optional().default(50).describe('Max records to return')
|
|
84
86
|
},
|
|
85
87
|
async (params) => {
|
|
86
88
|
try {
|
|
@@ -99,7 +101,7 @@ server.tool(
|
|
|
99
101
|
{
|
|
100
102
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
101
103
|
storageType: z.string().optional().describe('Filter by storage type'),
|
|
102
|
-
top: z.number().optional().default(100).describe('Max bins to analyze')
|
|
104
|
+
top: z.coerce.number().optional().default(100).describe('Max bins to analyze')
|
|
103
105
|
},
|
|
104
106
|
async (params) => {
|
|
105
107
|
try {
|
|
@@ -120,7 +122,7 @@ server.tool(
|
|
|
120
122
|
movementType: z.string().describe('WM movement type e.g. 999'),
|
|
121
123
|
material: z.string().describe('Material number e.g. TG0001'),
|
|
122
124
|
plant: z.string().describe('Plant e.g. 1710'),
|
|
123
|
-
quantity: z.number().describe('Quantity to move'),
|
|
125
|
+
quantity: z.coerce.number().describe('Quantity to move'),
|
|
124
126
|
unitOfMeasure: z.string().optional().default('').describe('Unit of measure — leave empty to use material base UOM (recommended)'),
|
|
125
127
|
sourceType: z.string().optional().default('').describe('Source storage type e.g. 001'),
|
|
126
128
|
sourceBin: z.string().optional().default('').describe('Source bin e.g. 01-02-01'),
|
|
@@ -185,7 +187,7 @@ server.tool(
|
|
|
185
187
|
storageType: z.string().optional().describe('Filter by source or destination storage type e.g. 999'),
|
|
186
188
|
bin: z.string().optional().describe('Filter by source or destination bin e.g. AUFNAHME'),
|
|
187
189
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
188
|
-
top: z.number().optional().default(50).describe('Max TO headers to return')
|
|
190
|
+
top: z.coerce.number().optional().default(50).describe('Max TO headers to return')
|
|
189
191
|
},
|
|
190
192
|
async (params) => {
|
|
191
193
|
try {
|
|
@@ -205,7 +207,7 @@ server.tool(
|
|
|
205
207
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
206
208
|
storageType: z.string().optional().describe('Storage type e.g. 001. Omit to see all types.'),
|
|
207
209
|
bin: z.string().optional().describe('Narrow to a specific bin within the type'),
|
|
208
|
-
top: z.number().optional().default(100).describe('Max records to return')
|
|
210
|
+
top: z.coerce.number().optional().default(100).describe('Max records to return')
|
|
209
211
|
},
|
|
210
212
|
async (params) => {
|
|
211
213
|
try {
|
|
@@ -226,7 +228,7 @@ server.tool(
|
|
|
226
228
|
status: z.enum(['open', 'partial', 'to-created', 'completed']).optional().describe('TR status filter. Defaults to open + partial + to-created. Use to-created to see TRs where a TO exists but is not yet confirmed.'),
|
|
227
229
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
228
230
|
storageType: z.string().optional().describe('Filter by source or destination storage type'),
|
|
229
|
-
top: z.number().optional().default(50).describe('Max records to return')
|
|
231
|
+
top: z.coerce.number().optional().default(50).describe('Max records to return')
|
|
230
232
|
},
|
|
231
233
|
async (params) => {
|
|
232
234
|
try {
|
|
@@ -247,7 +249,7 @@ server.tool(
|
|
|
247
249
|
plant: z.string().describe('Plant e.g. 1010'),
|
|
248
250
|
storageLocation: z.string().optional().describe('Storage location (LGORT) linked to this warehouse e.g. 0002 — required for accurate results, otherwise MARD returns all plant stock'),
|
|
249
251
|
material: z.string().optional().describe('Narrow to a specific material'),
|
|
250
|
-
threshold: z.number().optional().default(0).describe('Ignore variances smaller than this quantity')
|
|
252
|
+
threshold: z.coerce.number().optional().default(0).describe('Ignore variances smaller than this quantity')
|
|
251
253
|
},
|
|
252
254
|
async (params) => {
|
|
253
255
|
try {
|
|
@@ -266,8 +268,8 @@ server.tool(
|
|
|
266
268
|
{
|
|
267
269
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
268
270
|
storageType: z.string().optional().describe('Limit to a specific storage type e.g. 001'),
|
|
269
|
-
daysSinceLastCount: z.number().optional().default(180).describe('Flag bins not counted within this many days (default 180)'),
|
|
270
|
-
top: z.number().optional().default(100).describe('Max bins to return')
|
|
271
|
+
daysSinceLastCount: z.coerce.number().optional().default(180).describe('Flag bins not counted within this many days (default 180)'),
|
|
272
|
+
top: z.coerce.number().optional().default(100).describe('Max bins to return')
|
|
271
273
|
},
|
|
272
274
|
async (params) => {
|
|
273
275
|
try {
|
|
@@ -287,8 +289,8 @@ server.tool(
|
|
|
287
289
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
288
290
|
storageType: z.string().optional().describe('Narrow to a specific storage type'),
|
|
289
291
|
material: z.string().optional().describe('Narrow to a specific material'),
|
|
290
|
-
daysSinceLastMove: z.number().optional().default(90).describe('Flag stock not moved in this many days (default 90)'),
|
|
291
|
-
top: z.number().optional().default(100).describe('Max quants to scan')
|
|
292
|
+
daysSinceLastMove: z.coerce.number().optional().default(90).describe('Flag stock not moved in this many days (default 90)'),
|
|
293
|
+
top: z.coerce.number().optional().default(100).describe('Max quants to scan')
|
|
292
294
|
},
|
|
293
295
|
async (params) => {
|
|
294
296
|
try {
|
|
@@ -343,8 +345,8 @@ server.tool(
|
|
|
343
345
|
{
|
|
344
346
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
345
347
|
storageType: z.string().optional().describe('Limit to a specific storage type e.g. 003'),
|
|
346
|
-
threshold: z.number().optional().default(3).describe('Flag bin+material combos with this many quants or more (default 3)'),
|
|
347
|
-
top: z.number().optional().default(200).describe('Max stock records to scan')
|
|
348
|
+
threshold: z.coerce.number().optional().default(3).describe('Flag bin+material combos with this many quants or more (default 3)'),
|
|
349
|
+
top: z.coerce.number().optional().default(200).describe('Max stock records to scan')
|
|
348
350
|
},
|
|
349
351
|
async (params) => {
|
|
350
352
|
try {
|
|
@@ -363,8 +365,8 @@ server.tool(
|
|
|
363
365
|
{
|
|
364
366
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
365
367
|
storageType: z.string().optional().describe('Specific zone type to check e.g. 999. Defaults to 999 + 998.'),
|
|
366
|
-
minAgeDays: z.number().optional().default(7).describe('Only show negatives older than this many days (default 7). Set to 0 to see all.'),
|
|
367
|
-
top: z.number().optional().default(500).describe('Max stock records to scan')
|
|
368
|
+
minAgeDays: z.coerce.number().optional().default(7).describe('Only show negatives older than this many days (default 7). Set to 0 to see all.'),
|
|
369
|
+
top: z.coerce.number().optional().default(500).describe('Max stock records to scan')
|
|
368
370
|
},
|
|
369
371
|
async (params) => {
|
|
370
372
|
try {
|
|
@@ -407,7 +409,7 @@ server.tool(
|
|
|
407
409
|
createdBy: z.string().optional().describe('Filter by the SAP user who created the TO e.g. NOMANH'),
|
|
408
410
|
executedBy: z.string().optional().describe('Filter by the SAP user who executed/confirmed the TO'),
|
|
409
411
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
410
|
-
top: z.number().optional().default(50).describe('Max number of TOs to return (default 50)')
|
|
412
|
+
top: z.coerce.number().optional().default(50).describe('Max number of TOs to return (default 50)')
|
|
411
413
|
},
|
|
412
414
|
async (params) => {
|
|
413
415
|
try {
|
|
@@ -445,10 +447,10 @@ server.tool(
|
|
|
445
447
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
446
448
|
storageType: z.string().optional().default('P01').describe('Forward pick storage type to check (default P01)'),
|
|
447
449
|
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
448
|
-
minimumQuantity: z.number().optional().default(0).describe('Flag bins at or below this level. Default 0 = empty/negative only. Overridden per bin when LAGP min qty (MNMNG) is configured.'),
|
|
449
|
-
targetQuantity: z.number().optional().describe('Fill-to target — overrides bin master MaximumQuantity when set.'),
|
|
450
|
-
defaultReplenishQty: z.number().optional().default(50).describe('Fallback replenishment qty when bin master max qty (MXMNG) is 0 and no targetQuantity given. Default 50.'),
|
|
451
|
-
top: z.number().optional().default(50).describe('Max records to return')
|
|
450
|
+
minimumQuantity: z.coerce.number().optional().default(0).describe('Flag bins at or below this level. Default 0 = empty/negative only. Overridden per bin when LAGP min qty (MNMNG) is configured.'),
|
|
451
|
+
targetQuantity: z.coerce.number().optional().describe('Fill-to target — overrides bin master MaximumQuantity when set.'),
|
|
452
|
+
defaultReplenishQty: z.coerce.number().optional().default(50).describe('Fallback replenishment qty when bin master max qty (MXMNG) is 0 and no targetQuantity given. Default 50.'),
|
|
453
|
+
top: z.coerce.number().optional().default(50).describe('Max records to return')
|
|
452
454
|
},
|
|
453
455
|
async (params) => {
|
|
454
456
|
try {
|
|
@@ -467,9 +469,9 @@ server.tool(
|
|
|
467
469
|
{
|
|
468
470
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
469
471
|
interimTypes: z.array(z.string()).optional().default(['999', '998', '902']).describe('Interim storage types to check — default [999, 998, 902]'),
|
|
470
|
-
minDaysStranded: z.number().optional().default(0).describe('Only return stock stranded for at least this many days. Default 0 = include same-day strandings.'),
|
|
472
|
+
minDaysStranded: z.coerce.number().optional().default(0).describe('Only return stock stranded for at least this many days. Default 0 = include same-day strandings.'),
|
|
471
473
|
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
472
|
-
top: z.number().optional().default(100).describe('Max quants to scan (scans top * 3 internally to account for client-side filtering)')
|
|
474
|
+
top: z.coerce.number().optional().default(100).describe('Max quants to scan (scans top * 3 internally to account for client-side filtering)')
|
|
473
475
|
},
|
|
474
476
|
async (params) => {
|
|
475
477
|
try {
|
|
@@ -481,6 +483,47 @@ server.tool(
|
|
|
481
483
|
}
|
|
482
484
|
);
|
|
483
485
|
|
|
486
|
+
// Tool 23 — get_goods_issue_monitor
|
|
487
|
+
server.tool(
|
|
488
|
+
'get_goods_issue_monitor',
|
|
489
|
+
'Show open outbound deliveries in a classic WM warehouse — delivery qty vs picked qty, GI status, overdue flag, and which items still have stock in the GI zone (type 999). Use to track picking progress, identify stalled shipments, and understand the outbound delivery context behind TOs sourced from the GI zone.',
|
|
490
|
+
{
|
|
491
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
492
|
+
shippingPoint: z.string().optional().describe('Filter by shipping point e.g. 1000'),
|
|
493
|
+
includeCompleted: z.boolean().optional().default(false).describe('Include deliveries where GI has already been posted (default false — open only)'),
|
|
494
|
+
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
495
|
+
top: z.coerce.number().optional().default(50).describe('Max delivery items to fetch (default 50)')
|
|
496
|
+
},
|
|
497
|
+
async (params) => {
|
|
498
|
+
try {
|
|
499
|
+
const result = await getGoodsIssueMonitor(params);
|
|
500
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
501
|
+
} catch (err) {
|
|
502
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// Tool 24 — create_cycle_count_document
|
|
508
|
+
server.tool(
|
|
509
|
+
'create_cycle_count_document',
|
|
510
|
+
'Create a classic WM inventory document (cycle count) for a specific bin — equivalent to LI01N. Activates the bin for counting immediately by default. Use after get_cycle_count_candidates to trigger a count on flagged bins.',
|
|
511
|
+
{
|
|
512
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
513
|
+
storageType: z.string().describe('Storage type of the bin to count e.g. 001'),
|
|
514
|
+
bin: z.string().describe('Storage bin to count e.g. 01-02-03'),
|
|
515
|
+
activateNow: z.boolean().optional().default(true).describe('Activate the inventory document immediately so the bin is ready for counting (default true)')
|
|
516
|
+
},
|
|
517
|
+
async (params) => {
|
|
518
|
+
try {
|
|
519
|
+
const result = await createCycleCountDoc(params);
|
|
520
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
|
|
484
527
|
const transport = new StdioServerTransport();
|
|
485
528
|
await server.connect(transport);
|
|
486
529
|
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.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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,54 @@
|
|
|
1
|
+
import { s4hPost, 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`;
|
|
5
|
+
const NS = `com.sap.gateway.srvd.zsd_wmmcpservice.v0001`;
|
|
6
|
+
|
|
7
|
+
export async function createCycleCountDoc({
|
|
8
|
+
warehouse,
|
|
9
|
+
storageType,
|
|
10
|
+
bin,
|
|
11
|
+
activateNow = true
|
|
12
|
+
}) {
|
|
13
|
+
const path = `${BASE}/WMTransferOrder/${NS}.CreateInventoryDocument`;
|
|
14
|
+
|
|
15
|
+
const body = {
|
|
16
|
+
WarehouseNumber: esc(warehouse),
|
|
17
|
+
StorageType: esc(storageType),
|
|
18
|
+
StorageBin: esc(bin),
|
|
19
|
+
ActivateNow: activateNow ? 'X' : ' '
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await s4hPost(path, body);
|
|
23
|
+
|
|
24
|
+
// RAP static action returns $self (WMTransferOrder) — field assignment does not
|
|
25
|
+
// propagate back through the OData layer. Instead, read the inventory document
|
|
26
|
+
// number from the bin master record (LAGP.IVNUM) via a follow-up GET on
|
|
27
|
+
// WMStorageBin. When activateNow=true the bin is locked and IVNUM is set.
|
|
28
|
+
const binFilter = `WarehouseNumber eq '${esc(warehouse)}' and StorageType eq '${esc(storageType)}' and StorageBin eq '${esc(bin)}'`;
|
|
29
|
+
const binData = await s4hGet(`${BASE}/WMStorageBin?$filter=${encodeURIComponent(binFilter)}&$select=InventoryDocNumber,InventoryStatus`);
|
|
30
|
+
const binRow = binData?.value?.[0];
|
|
31
|
+
const invDocNumber = binRow?.InventoryDocNumber?.trim() || null;
|
|
32
|
+
|
|
33
|
+
if (!invDocNumber) {
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
warning: 'Inventory document created but number unavailable via OData — verify in SAP via LI04.',
|
|
37
|
+
warehouse,
|
|
38
|
+
storageType,
|
|
39
|
+
bin,
|
|
40
|
+
activated: activateNow
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
warehouse,
|
|
47
|
+
storageType,
|
|
48
|
+
bin,
|
|
49
|
+
inventoryDocNumber: invDocNumber,
|
|
50
|
+
inventoryStatus: binRow?.InventoryStatus ?? '',
|
|
51
|
+
activated: activateNow,
|
|
52
|
+
message: `Inventory document ${invDocNumber} created${activateNow ? ' and activated' : ''} for bin ${storageType}/${bin}`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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/WMGoodsIssueDelivery`;
|
|
5
|
+
|
|
6
|
+
const GI_STATUS = {
|
|
7
|
+
'A': 'Not started',
|
|
8
|
+
'B': 'Partially picked',
|
|
9
|
+
'C': 'Goods issue posted',
|
|
10
|
+
' ': 'Not started'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function getGoodsIssueMonitor({
|
|
14
|
+
warehouse,
|
|
15
|
+
shippingPoint,
|
|
16
|
+
includeCompleted = false,
|
|
17
|
+
material,
|
|
18
|
+
top = 50
|
|
19
|
+
}) {
|
|
20
|
+
const filters = [
|
|
21
|
+
`WarehouseNumber eq '${esc(warehouse)}'`
|
|
22
|
+
];
|
|
23
|
+
if (!includeCompleted) filters.push(`GIStatus ne 'C'`);
|
|
24
|
+
if (shippingPoint) filters.push(`ShippingPoint eq '${esc(shippingPoint)}'`);
|
|
25
|
+
if (material) filters.push(`Material eq '${esc(material)}'`);
|
|
26
|
+
|
|
27
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}&$orderby=PlannedGIDate asc`;
|
|
28
|
+
const data = await s4hGet(path);
|
|
29
|
+
const rows = data.value ?? [];
|
|
30
|
+
|
|
31
|
+
const today = new Date();
|
|
32
|
+
const todayStr = today.toISOString().slice(0, 10);
|
|
33
|
+
|
|
34
|
+
// Group items by delivery number
|
|
35
|
+
const deliveryMap = {};
|
|
36
|
+
for (const r of rows) {
|
|
37
|
+
const del = r.DeliveryNumber;
|
|
38
|
+
if (!deliveryMap[del]) {
|
|
39
|
+
deliveryMap[del] = {
|
|
40
|
+
deliveryNumber: del,
|
|
41
|
+
shippingPoint: r.ShippingPoint,
|
|
42
|
+
customer: r.Customer?.trimStart?.() ?? r.Customer,
|
|
43
|
+
plannedGIDate: r.PlannedGIDate,
|
|
44
|
+
actualGIDate: r.ActualGIDate || null,
|
|
45
|
+
deliveryDate: r.DeliveryDate,
|
|
46
|
+
giStatus: r.GIStatus,
|
|
47
|
+
giStatusText: GI_STATUS[r.GIStatus] ?? r.GIStatus,
|
|
48
|
+
isOverdue: r.PlannedGIDate && r.PlannedGIDate < todayStr && r.GIStatus !== 'C',
|
|
49
|
+
items: []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const deliveryQty = parseFloat(r.DeliveryQuantity ?? 0);
|
|
53
|
+
const pickedQty = parseFloat(r.PickedQuantity ?? 0);
|
|
54
|
+
deliveryMap[del].items.push({
|
|
55
|
+
item: r.DeliveryItem,
|
|
56
|
+
material: r.Material?.trimStart?.() ?? r.Material,
|
|
57
|
+
plant: r.Plant,
|
|
58
|
+
storageType: r.StorageType,
|
|
59
|
+
bin: r.StorageBin,
|
|
60
|
+
wmMovementType: r.WMMovementType,
|
|
61
|
+
wmTORequired: r.WMTORequired,
|
|
62
|
+
deliveryQty,
|
|
63
|
+
pickedQty,
|
|
64
|
+
remainingQty: Math.max(0, deliveryQty - pickedQty),
|
|
65
|
+
uom: r.DeliveryUOM,
|
|
66
|
+
pickComplete: deliveryQty > 0 && pickedQty >= deliveryQty
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const deliveries = Object.values(deliveryMap);
|
|
71
|
+
const overdue = deliveries.filter(d => d.isOverdue);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
warehouse,
|
|
75
|
+
filters: {
|
|
76
|
+
shippingPoint: shippingPoint ?? 'all',
|
|
77
|
+
includeCompleted,
|
|
78
|
+
material: material ?? 'all'
|
|
79
|
+
},
|
|
80
|
+
summary: {
|
|
81
|
+
totalDeliveries: deliveries.length,
|
|
82
|
+
overdueDeliveries: overdue.length,
|
|
83
|
+
byStatus: {
|
|
84
|
+
notStarted: deliveries.filter(d => d.giStatus === 'A' || d.giStatus === ' ').length,
|
|
85
|
+
partial: deliveries.filter(d => d.giStatus === 'B').length,
|
|
86
|
+
complete: deliveries.filter(d => d.giStatus === 'C').length
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
truncated: rows.length === top,
|
|
90
|
+
deliveries
|
|
91
|
+
};
|
|
92
|
+
}
|