sap-wm-mcp 0.2.9 → 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 +23 -9
- package/index.js +70 -26
- package/package.json +1 -1
- package/tools/createCycleCountDoc.js +54 -0
- package/tools/createTransferOrder.js +4 -2
- 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
|
|
|
@@ -478,6 +478,9 @@ Internally calls `L_TO_CREATE_SINGLE` via an RFC-enabled wrapper, isolated from
|
|
|
478
478
|
| `destType` | string | ✅ | Destination storage type |
|
|
479
479
|
| `destBin` | string | ✅ | Destination bin |
|
|
480
480
|
| `destStorageUnit` | string | | Destination storage unit (LENUM) — for SU-managed destination types |
|
|
481
|
+
| `autoConfirm` | boolean | | When `true`, the TO is created and immediately confirmed in a single call — no separate `confirm_transfer_order` step needed. Maps to `I_SQUIT = 'X'` in `L_TO_CREATE_SINGLE`. Default: `false`. |
|
|
482
|
+
|
|
483
|
+
> **`autoConfirm`:** Use for internal replenishment or relocation moves where no physical scan or operator acknowledgement is required. Do not use for inbound putaway from GR zone or outbound picks where confirmation must happen on the warehouse floor.
|
|
481
484
|
|
|
482
485
|
> **Note on SU-managed storage types:** Storage types with `LPTYP` set in `LAGP` (e.g. type `001`) require a `destStorageUnit` (LENUM) when used as the TO destination. Storage types with `LPTYP` blank (e.g. type `003`) do not.
|
|
483
486
|
|
|
@@ -538,6 +541,8 @@ The following tools are available in the published npm package and fully functio
|
|
|
538
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` |
|
|
539
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 |
|
|
540
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. |
|
|
541
546
|
|
|
542
547
|
---
|
|
543
548
|
|
|
@@ -563,11 +568,17 @@ The ABAP source is not included in this public repository. To obtain the ABAP pa
|
|
|
563
568
|
| `ZR_WMCYCLECOUNTBIN` | CDS View | View over LAGP (cycle count indicators) |
|
|
564
569
|
| `ZR_WMTRANSFERORDER` | BDEF | Behavior definition — defines actions |
|
|
565
570
|
| `ZBP_R_WMTRANSFERORDER` | Class | RAP behavior implementation |
|
|
566
|
-
| `
|
|
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 |
|
|
567
575
|
| `ZWM_TO_CREATE` | Function Module | RFC-enabled wrapper for `L_TO_CREATE_SINGLE` |
|
|
568
|
-
| `
|
|
569
|
-
| `
|
|
570
|
-
| `
|
|
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) |
|
|
571
582
|
| `ZSB_WMMCPSERVICE_ODATA4_UI` | Service Binding | OData V4 UI binding |
|
|
572
583
|
|
|
573
584
|
### abapGit compatibility
|
|
@@ -613,7 +624,8 @@ Classic WM has no standard OData APIs. This project builds one using **ABAP REST
|
|
|
613
624
|
| `CreateTransferOrder` | `L_TO_CREATE_SINGLE` | Called via RFC wrapper `ZWM_TO_CREATE` with `DESTINATION 'NONE'` |
|
|
614
625
|
| `ConfirmTransferOrder` | `L_TO_CONFIRM` | Called directly — no COMMIT needed |
|
|
615
626
|
| `ConfirmTransferOrderSU` | `L_TO_CONFIRM_SU` | Called directly — no COMMIT needed |
|
|
616
|
-
| `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'` |
|
|
617
629
|
|
|
618
630
|
### Why the RFC wrapper?
|
|
619
631
|
|
|
@@ -680,7 +692,9 @@ sap-wm-mcp/
|
|
|
680
692
|
│ ├── cancelTransferOrder.js ← cancel_transfer_order
|
|
681
693
|
│ ├── transferOrderHistory.js ← get_transfer_order_history
|
|
682
694
|
│ ├── replenishmentNeeds.js ← get_replenishment_needs
|
|
683
|
-
│
|
|
695
|
+
│ ├── interimZoneAnomalies.js ← get_interim_zone_anomalies
|
|
696
|
+
│ ├── goodsIssueMonitor.js ← get_goods_issue_monitor
|
|
697
|
+
│ └── createCycleCountDoc.js ← create_cycle_count_document
|
|
684
698
|
├── .env.example
|
|
685
699
|
└── package.json
|
|
686
700
|
```
|
|
@@ -705,7 +719,7 @@ Running both side-by-side shows the contrast directly: same tools, same question
|
|
|
705
719
|
| Phase | Status | Description |
|
|
706
720
|
|---|---|---|
|
|
707
721
|
| Phase 0 — RAP Service | ✅ Complete | Custom OData V4 service with 7 entity sets over classic WM tables |
|
|
708
|
-
| Phase 1 — Local MCP | ✅ Complete |
|
|
722
|
+
| Phase 1 — Local MCP | ✅ Complete | 23 tools working, security hardened, published to npm |
|
|
709
723
|
| Phase 2 — BTP CF | 🔜 Planned | Deploy to SAP BTP Cloud Foundry with SSE transport + XSUAA + Cloud Connector |
|
|
710
724
|
| Phase 3 — Joule Agent | 💡 Future | Native Joule Studio agent using the same RAP service |
|
|
711
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.2
|
|
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,14 +122,15 @@ 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'),
|
|
127
129
|
sourceStorageUnit: z.string().optional().default('').describe('Source storage unit (LENUM) — required for SU-managed types e.g. 00000000001000000017'),
|
|
128
130
|
destType: z.string().describe('Destination storage type e.g. 001'),
|
|
129
131
|
destBin: z.string().describe('Destination bin e.g. 01-06-03'),
|
|
130
|
-
destStorageUnit: z.string().optional().default('').describe('Destination storage unit (LENUM) — for SU-managed types, same as source SU when moving full SU')
|
|
132
|
+
destStorageUnit: z.string().optional().default('').describe('Destination storage unit (LENUM) — for SU-managed types, same as source SU when moving full SU'),
|
|
133
|
+
autoConfirm: z.boolean().optional().default(false).describe('Immediately confirm the TO after creation (sets I_SQUIT=X in L_TO_CREATE_SINGLE) — use for internal replenishment moves where no physical confirmation step is needed')
|
|
131
134
|
},
|
|
132
135
|
async (params) => {
|
|
133
136
|
try {
|
|
@@ -184,7 +187,7 @@ server.tool(
|
|
|
184
187
|
storageType: z.string().optional().describe('Filter by source or destination storage type e.g. 999'),
|
|
185
188
|
bin: z.string().optional().describe('Filter by source or destination bin e.g. AUFNAHME'),
|
|
186
189
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
187
|
-
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')
|
|
188
191
|
},
|
|
189
192
|
async (params) => {
|
|
190
193
|
try {
|
|
@@ -204,7 +207,7 @@ server.tool(
|
|
|
204
207
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
205
208
|
storageType: z.string().optional().describe('Storage type e.g. 001. Omit to see all types.'),
|
|
206
209
|
bin: z.string().optional().describe('Narrow to a specific bin within the type'),
|
|
207
|
-
top: z.number().optional().default(100).describe('Max records to return')
|
|
210
|
+
top: z.coerce.number().optional().default(100).describe('Max records to return')
|
|
208
211
|
},
|
|
209
212
|
async (params) => {
|
|
210
213
|
try {
|
|
@@ -225,7 +228,7 @@ server.tool(
|
|
|
225
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.'),
|
|
226
229
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
227
230
|
storageType: z.string().optional().describe('Filter by source or destination storage type'),
|
|
228
|
-
top: z.number().optional().default(50).describe('Max records to return')
|
|
231
|
+
top: z.coerce.number().optional().default(50).describe('Max records to return')
|
|
229
232
|
},
|
|
230
233
|
async (params) => {
|
|
231
234
|
try {
|
|
@@ -246,7 +249,7 @@ server.tool(
|
|
|
246
249
|
plant: z.string().describe('Plant e.g. 1010'),
|
|
247
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'),
|
|
248
251
|
material: z.string().optional().describe('Narrow to a specific material'),
|
|
249
|
-
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')
|
|
250
253
|
},
|
|
251
254
|
async (params) => {
|
|
252
255
|
try {
|
|
@@ -265,8 +268,8 @@ server.tool(
|
|
|
265
268
|
{
|
|
266
269
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
267
270
|
storageType: z.string().optional().describe('Limit to a specific storage type e.g. 001'),
|
|
268
|
-
daysSinceLastCount: z.number().optional().default(180).describe('Flag bins not counted within this many days (default 180)'),
|
|
269
|
-
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')
|
|
270
273
|
},
|
|
271
274
|
async (params) => {
|
|
272
275
|
try {
|
|
@@ -286,8 +289,8 @@ server.tool(
|
|
|
286
289
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
287
290
|
storageType: z.string().optional().describe('Narrow to a specific storage type'),
|
|
288
291
|
material: z.string().optional().describe('Narrow to a specific material'),
|
|
289
|
-
daysSinceLastMove: z.number().optional().default(90).describe('Flag stock not moved in this many days (default 90)'),
|
|
290
|
-
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')
|
|
291
294
|
},
|
|
292
295
|
async (params) => {
|
|
293
296
|
try {
|
|
@@ -342,8 +345,8 @@ server.tool(
|
|
|
342
345
|
{
|
|
343
346
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
344
347
|
storageType: z.string().optional().describe('Limit to a specific storage type e.g. 003'),
|
|
345
|
-
threshold: z.number().optional().default(3).describe('Flag bin+material combos with this many quants or more (default 3)'),
|
|
346
|
-
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')
|
|
347
350
|
},
|
|
348
351
|
async (params) => {
|
|
349
352
|
try {
|
|
@@ -362,8 +365,8 @@ server.tool(
|
|
|
362
365
|
{
|
|
363
366
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
364
367
|
storageType: z.string().optional().describe('Specific zone type to check e.g. 999. Defaults to 999 + 998.'),
|
|
365
|
-
minAgeDays: z.number().optional().default(7).describe('Only show negatives older than this many days (default 7). Set to 0 to see all.'),
|
|
366
|
-
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')
|
|
367
370
|
},
|
|
368
371
|
async (params) => {
|
|
369
372
|
try {
|
|
@@ -406,7 +409,7 @@ server.tool(
|
|
|
406
409
|
createdBy: z.string().optional().describe('Filter by the SAP user who created the TO e.g. NOMANH'),
|
|
407
410
|
executedBy: z.string().optional().describe('Filter by the SAP user who executed/confirmed the TO'),
|
|
408
411
|
material: z.string().optional().describe('Filter by material number e.g. TG0001'),
|
|
409
|
-
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)')
|
|
410
413
|
},
|
|
411
414
|
async (params) => {
|
|
412
415
|
try {
|
|
@@ -444,10 +447,10 @@ server.tool(
|
|
|
444
447
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
445
448
|
storageType: z.string().optional().default('P01').describe('Forward pick storage type to check (default P01)'),
|
|
446
449
|
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
447
|
-
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.'),
|
|
448
|
-
targetQuantity: z.number().optional().describe('Fill-to target — overrides bin master MaximumQuantity when set.'),
|
|
449
|
-
defaultReplenishQty: z.number().optional().default(50).describe('Fallback replenishment qty when bin master max qty (MXMNG) is 0 and no targetQuantity given. Default 50.'),
|
|
450
|
-
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')
|
|
451
454
|
},
|
|
452
455
|
async (params) => {
|
|
453
456
|
try {
|
|
@@ -466,9 +469,9 @@ server.tool(
|
|
|
466
469
|
{
|
|
467
470
|
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
468
471
|
interimTypes: z.array(z.string()).optional().default(['999', '998', '902']).describe('Interim storage types to check — default [999, 998, 902]'),
|
|
469
|
-
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.'),
|
|
470
473
|
material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
|
|
471
|
-
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)')
|
|
472
475
|
},
|
|
473
476
|
async (params) => {
|
|
474
477
|
try {
|
|
@@ -480,6 +483,47 @@ server.tool(
|
|
|
480
483
|
}
|
|
481
484
|
);
|
|
482
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
|
+
|
|
483
527
|
const transport = new StdioServerTransport();
|
|
484
528
|
await server.connect(transport);
|
|
485
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.2
|
|
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
|
+
}
|
|
@@ -7,7 +7,8 @@ export async function createTransferOrder({
|
|
|
7
7
|
warehouse, movementType, material, plant,
|
|
8
8
|
quantity, unitOfMeasure = '',
|
|
9
9
|
sourceType = '', sourceBin = '', sourceStorageUnit = '',
|
|
10
|
-
destType, destBin, destStorageUnit = ''
|
|
10
|
+
destType, destBin, destStorageUnit = '',
|
|
11
|
+
autoConfirm = false
|
|
11
12
|
}) {
|
|
12
13
|
const path = `${BASE}/WMTransferOrder/${NS}.CreateTransferOrder`;
|
|
13
14
|
|
|
@@ -23,7 +24,8 @@ export async function createTransferOrder({
|
|
|
23
24
|
SourceStorageUnit: sourceStorageUnit,
|
|
24
25
|
DestStorageType: destType,
|
|
25
26
|
DestBin: destBin,
|
|
26
|
-
DestStorageUnit: destStorageUnit
|
|
27
|
+
DestStorageUnit: destStorageUnit,
|
|
28
|
+
AutoConfirm: autoConfirm ? 'X' : ' '
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// Snapshot the latest TO number BEFORE the call to avoid the race condition
|
|
@@ -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
|
+
}
|