@prefixcheck/edi-mcp 0.1.0 → 0.2.0
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 +1 -1
- package/dist/index.js +42 -6
- package/dist/samples.js +77 -0
- package/dist/schemas.js +258 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @prefixcheck/edi-mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing operator-grade EDIFACT **CODECO** + **COPRAR** tooling to any MCP client (Claude Desktop, Cursor, Cline, Continue, Claude Code).
|
|
3
|
+
MCP server exposing operator-grade EDIFACT **CODECO** + **COPRAR** + **IFTSTA** + **COREOR** tooling to any MCP client (Claude Desktop, Cursor, Cline, Continue, Claude Code).
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx -y @prefixcheck/edi-mcp
|
package/dist/index.js
CHANGED
|
@@ -18,18 +18,18 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
20
|
import { parse, extractContainerNumbers, extractUNLocodes } from "./parser.js";
|
|
21
|
-
import { CODECO, COPRAR, CODE_LISTS, SEGMENTS, decodeISOSizeType, detectMessageType, diagnoseSingle, lookup, reconcile, segmentInfo, validateCheckDigit, } from "./schemas.js";
|
|
22
|
-
import { SAMPLE_CODECO, SAMPLE_COPRAR } from "./samples.js";
|
|
21
|
+
import { CODECO, COPRAR, IFTSTA, COREOR, CODE_LISTS, SEGMENTS, decodeISOSizeType, detectMessageType, diagnoseSingle, lookup, reconcile, segmentInfo, validateCheckDigit, } from "./schemas.js";
|
|
22
|
+
import { SAMPLE_CODECO, SAMPLE_COPRAR, SAMPLE_IFTSTA, SAMPLE_COREOR } from "./samples.js";
|
|
23
23
|
// -------------------------------------------------------------
|
|
24
24
|
// Server setup
|
|
25
25
|
// -------------------------------------------------------------
|
|
26
26
|
const SERVER_NAME = "prefixcheck-edi-mcp";
|
|
27
|
-
const SERVER_VERSION = "0.
|
|
27
|
+
const SERVER_VERSION = "0.2.0";
|
|
28
28
|
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
29
29
|
const TOOLS = [
|
|
30
30
|
{
|
|
31
31
|
name: "parse_message",
|
|
32
|
-
description: "Tokenize a raw EDIFACT CODECO or
|
|
32
|
+
description: "Tokenize a raw EDIFACT CODECO, COPRAR, IFTSTA, or COREOR message into structured segments + envelope metadata. Handles UNA delimiter overrides, UNB/UNZ + UNH/UNT envelopes, and release-character escapes. Returns the full ParsedMessage structure.",
|
|
33
33
|
inputSchema: {
|
|
34
34
|
type: "object",
|
|
35
35
|
properties: {
|
|
@@ -40,7 +40,7 @@ const TOOLS = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: "diagnose_message",
|
|
43
|
-
description: "Parse a CODECO or
|
|
43
|
+
description: "Parse a CODECO, COPRAR, IFTSTA, or COREOR message and run all 11 SMDG-grade diagnostic rules against it. Returns the list of findings (errors + warnings + info). Empty list = clean message.",
|
|
44
44
|
inputSchema: {
|
|
45
45
|
type: "object",
|
|
46
46
|
properties: {
|
|
@@ -231,6 +231,18 @@ const RESOURCES = [
|
|
|
231
231
|
description: "COPRAR message metadata: name, longName, purpose, BGM codes, required segments.",
|
|
232
232
|
mimeType: "application/json",
|
|
233
233
|
},
|
|
234
|
+
{
|
|
235
|
+
uri: "edi://schema/iftsta",
|
|
236
|
+
name: "IFTSTA schema",
|
|
237
|
+
description: "IFTSTA message metadata.",
|
|
238
|
+
mimeType: "application/json",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
uri: "edi://schema/coreor",
|
|
242
|
+
name: "COREOR schema",
|
|
243
|
+
description: "COREOR message metadata.",
|
|
244
|
+
mimeType: "application/json",
|
|
245
|
+
},
|
|
234
246
|
{
|
|
235
247
|
uri: "edi://sample/codeco",
|
|
236
248
|
name: "CODECO sample",
|
|
@@ -243,6 +255,18 @@ const RESOURCES = [
|
|
|
243
255
|
description: "Real-shape SMDG D.00B COPRAR Load sample message (carrier → terminal, 3 containers including 1 reefer, matched-pair with the CODECO sample on MSCU1234566).",
|
|
244
256
|
mimeType: "text/plain",
|
|
245
257
|
},
|
|
258
|
+
{
|
|
259
|
+
uri: "edi://sample/iftsta",
|
|
260
|
+
name: "IFTSTA sample",
|
|
261
|
+
description: "Real-shape SMDG D.00B IFTSTA sample (terminal → carrier status report, two status events chained: loaded onto vessel + gate-out full import, same MSCU1234566 container).",
|
|
262
|
+
mimeType: "text/plain",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
uri: "edi://sample/coreor",
|
|
266
|
+
name: "COREOR sample",
|
|
267
|
+
description: "Real-shape SMDG D.00B COREOR Container Release Order sample (carrier MSC releases MSCU1234566 to consignee ACME at APMT NYC; one RFF+AAY release ref, expiration date set).",
|
|
268
|
+
mimeType: "text/plain",
|
|
269
|
+
},
|
|
246
270
|
{
|
|
247
271
|
uri: "edi://segments",
|
|
248
272
|
name: "Segment dictionary",
|
|
@@ -268,10 +292,22 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
268
292
|
return {
|
|
269
293
|
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(COPRAR, null, 2) }],
|
|
270
294
|
};
|
|
295
|
+
case "edi://schema/iftsta":
|
|
296
|
+
return {
|
|
297
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(IFTSTA, null, 2) }],
|
|
298
|
+
};
|
|
299
|
+
case "edi://schema/coreor":
|
|
300
|
+
return {
|
|
301
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(COREOR, null, 2) }],
|
|
302
|
+
};
|
|
271
303
|
case "edi://sample/codeco":
|
|
272
304
|
return { contents: [{ uri, mimeType: "text/plain", text: SAMPLE_CODECO }] };
|
|
273
305
|
case "edi://sample/coprar":
|
|
274
306
|
return { contents: [{ uri, mimeType: "text/plain", text: SAMPLE_COPRAR }] };
|
|
307
|
+
case "edi://sample/iftsta":
|
|
308
|
+
return { contents: [{ uri, mimeType: "text/plain", text: SAMPLE_IFTSTA }] };
|
|
309
|
+
case "edi://sample/coreor":
|
|
310
|
+
return { contents: [{ uri, mimeType: "text/plain", text: SAMPLE_COREOR }] };
|
|
275
311
|
case "edi://segments":
|
|
276
312
|
return {
|
|
277
313
|
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(SEGMENTS, null, 2) }],
|
|
@@ -295,7 +331,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
295
331
|
async function main() {
|
|
296
332
|
const transport = new StdioServerTransport();
|
|
297
333
|
await server.connect(transport);
|
|
298
|
-
process.stderr.write(`${SERVER_NAME} v${SERVER_VERSION} ready · 9 tools ·
|
|
334
|
+
process.stderr.write(`${SERVER_NAME} v${SERVER_VERSION} ready · 9 tools · 10 resources · CODECO + COPRAR + IFTSTA + COREOR\n`);
|
|
299
335
|
}
|
|
300
336
|
main().catch((err) => {
|
|
301
337
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
package/dist/samples.js
CHANGED
|
@@ -97,3 +97,80 @@ CNT+16:3'
|
|
|
97
97
|
CNT+7:5'
|
|
98
98
|
UNT+46+CPR0001'
|
|
99
99
|
UNZ+1+CPR00098765'`;
|
|
100
|
+
/**
|
|
101
|
+
* A complete SMDG-shape IFTSTA message: gate-out report from terminal
|
|
102
|
+
* APMT NYC to MSC. Two status events chained for the same container
|
|
103
|
+
* (MSCU1234566): "loaded onto vessel" at NLRTM, then "gate-out full
|
|
104
|
+
* import" at USNYC. Matches SAMPLE_CODECO and SAMPLE_COPRAR for
|
|
105
|
+
* 4-way reconciliation demos.
|
|
106
|
+
*/
|
|
107
|
+
export const SAMPLE_IFTSTA = `UNA:+.? '
|
|
108
|
+
UNB+UNOA:2+TERMINAL01:ZZZ+MSCU:ZZZ+260524:1500+IST00033445'
|
|
109
|
+
UNH+IST0001+IFTSTA:D:00B:UN:SMDG20'
|
|
110
|
+
BGM+23+IST0001+9'
|
|
111
|
+
DTM+137:202605241500:203'
|
|
112
|
+
NAD+MS+TERMINAL01:ZZZ:160'
|
|
113
|
+
NAD+MR+MSCU:172:20'
|
|
114
|
+
NAD+CF+MSC:160:20'
|
|
115
|
+
CNI+1+MSCUNLRTM0042:BM'
|
|
116
|
+
LOC+5+NLRTM:139:6'
|
|
117
|
+
LOC+7+USNYC:139:6'
|
|
118
|
+
STS+1++29:::GATE OUT FULL IMPORT'
|
|
119
|
+
DTM+334:202605241455:203'
|
|
120
|
+
LOC+9+USNYC:139:6+APMT:TER:ZZZ'
|
|
121
|
+
NAD+TR+APMT:160:20'
|
|
122
|
+
EQD+CN+MSCU1234566+45G1:102:5++2+4'
|
|
123
|
+
RFF+BN:BKG778899'
|
|
124
|
+
RFF+BM:MSCUNLRTM0042'
|
|
125
|
+
RFF+EQ:MSCU1234566'
|
|
126
|
+
MEA+AAE++KGM:28450'
|
|
127
|
+
MEA+AAL++KGM:3920'
|
|
128
|
+
SEL+CN789456+CA'
|
|
129
|
+
SEL+CU112233+CU'
|
|
130
|
+
EQA+CH+CHSU0099887'
|
|
131
|
+
STS+1++22:::LOADED ONTO VESSEL'
|
|
132
|
+
DTM+334:202605200420:203'
|
|
133
|
+
LOC+9+NLRTM:139:6+ECT:TER:ZZZ'
|
|
134
|
+
TPL+++147:021082'
|
|
135
|
+
EQD+CN+MSCU1234566+45G1:102:5++2+4'
|
|
136
|
+
RFF+VON:251NB'
|
|
137
|
+
UNT+29+IST0001'
|
|
138
|
+
UNZ+1+IST00033445'`;
|
|
139
|
+
/**
|
|
140
|
+
* A complete SMDG-shape COREOR Container Release Order: carrier MSC
|
|
141
|
+
* releases container MSCU1234566 (the same box from the CODECO + COPRAR
|
|
142
|
+
* + IFTSTA samples) to consignee ACME at APMT NYC for import gate-out.
|
|
143
|
+
* Release ref REL-MSCU-NYC-2026-00042; expires 2026-06-10.
|
|
144
|
+
*/
|
|
145
|
+
export const SAMPLE_COREOR = `UNA:+.? '
|
|
146
|
+
UNB+UNOA:2+MSCU:ZZZ+APMTNYC:ZZZ+260524:1300+COR00077665'
|
|
147
|
+
UNH+COR0001+COREOR:D:00B:UN:SMDG20'
|
|
148
|
+
BGM+12+COR0001+9'
|
|
149
|
+
DTM+137:202605241300:203'
|
|
150
|
+
RFF+AAY:REL-MSCU-NYC-2026-00042'
|
|
151
|
+
DTM+36:202606101200:203'
|
|
152
|
+
TDT+20+251NB+1+13+MSCU:172:20+++9876543:146:11::MSC MAYA'
|
|
153
|
+
DTM+132:202606051400:203'
|
|
154
|
+
LOC+9+NLRTM:139:6'
|
|
155
|
+
LOC+11+USNYC:139:6+APMT:TER:ZZZ'
|
|
156
|
+
LOC+7+USNYC:139:6'
|
|
157
|
+
NAD+CA+MSCU:172:20'
|
|
158
|
+
NAD+CF+MSC:160:20'
|
|
159
|
+
NAD+TR+APMT:160:20'
|
|
160
|
+
NAD+CN+ACMECORP:ZZZ:160++ACME CORPORATION+250 PARK AVE+NEW YORK+NY+10177+US'
|
|
161
|
+
NAD+BO+ACMECORP:ZZZ:160'
|
|
162
|
+
EQD+CN+MSCU1234566+45G1:102:5++4+4'
|
|
163
|
+
TMD+++2'
|
|
164
|
+
DTM+234:202606081000:203'
|
|
165
|
+
LOC+11+USNYC:139:6+APMT:TER:ZZZ'
|
|
166
|
+
LOC+7+USNYC:139:6'
|
|
167
|
+
MEA+AAE++KGM:28450'
|
|
168
|
+
MEA+AAL++KGM:3920'
|
|
169
|
+
SEL+CN789456+CA'
|
|
170
|
+
RFF+BN:BKG778899'
|
|
171
|
+
RFF+BM:MSCUNLRTM0042'
|
|
172
|
+
RFF+EQ:MSCU1234566'
|
|
173
|
+
RFF+GN:CBP-ENTRY-AB12345678'
|
|
174
|
+
NAD+FW+TRUCKCO:ZZZ:160++TRUCKING CO INC'
|
|
175
|
+
UNT+29+COR0001'
|
|
176
|
+
UNZ+1+COR00077665'`;
|
package/dist/schemas.js
CHANGED
|
@@ -87,6 +87,10 @@ export const SEGMENTS = {
|
|
|
87
87
|
},
|
|
88
88
|
CTA: { name: "Contact Information", brief: "Contact person within a NAD party." },
|
|
89
89
|
COM: { name: "Communication Contact", brief: "Phone / email / fax for a CTA." },
|
|
90
|
+
CNI: {
|
|
91
|
+
name: "Consignment Information",
|
|
92
|
+
brief: "Consignment identifier on IFTSTA — sequence + reference (B/L, booking, waybill). One per consignment block; many SG5 status events can attach to one CNI.",
|
|
93
|
+
},
|
|
90
94
|
RFF: {
|
|
91
95
|
name: "Reference",
|
|
92
96
|
brief: "External references — booking (BN), B/L (BM), equipment (EQ), release (AAY), voyage (VON).",
|
|
@@ -161,6 +165,8 @@ export const SEGMENTS = {
|
|
|
161
165
|
};
|
|
162
166
|
export const CODE_LISTS = {
|
|
163
167
|
"BGM.docname": {
|
|
168
|
+
"12": "Container release order (COREOR)",
|
|
169
|
+
"23": "Transport status report (IFTSTA)",
|
|
164
170
|
"34": "Transport equipment gate-in report (CODECO)",
|
|
165
171
|
"35": "Transport equipment gate-out report (CODECO)",
|
|
166
172
|
"36": "Transport equipment movement (CODECO)",
|
|
@@ -283,6 +289,74 @@ export const CODE_LISTS = {
|
|
|
283
289
|
OK: "Acceptable for next use",
|
|
284
290
|
TARE: "Tare weight verified",
|
|
285
291
|
},
|
|
292
|
+
// STS.qualifier (DE 3215) — type of status, the first element on
|
|
293
|
+
// IFTSTA STS segments. Specifies WHICH thing the status applies to.
|
|
294
|
+
"STS.qualifier": {
|
|
295
|
+
"1": "Equipment / container status",
|
|
296
|
+
"2": "Consignment status",
|
|
297
|
+
"3": "Goods item status",
|
|
298
|
+
"4": "Transport status",
|
|
299
|
+
"5": "Status at requested place",
|
|
300
|
+
"6": "Status reported by message sender",
|
|
301
|
+
},
|
|
302
|
+
// STS.detail (DE 4405) — what physically happened. The heart of
|
|
303
|
+
// IFTSTA. Covers booking through delivery + holds.
|
|
304
|
+
"STS.detail": {
|
|
305
|
+
"1": "Booking received",
|
|
306
|
+
"2": "Booking confirmed",
|
|
307
|
+
"3": "Empty container released from depot",
|
|
308
|
+
"5": "Equipment positioned at stuffing location",
|
|
309
|
+
"6": "Stuffing completed",
|
|
310
|
+
"11": "Goods received from shipper",
|
|
311
|
+
"14": "Gate-in at origin terminal (full export)",
|
|
312
|
+
"22": "Loaded onto vessel",
|
|
313
|
+
"23": "Vessel departed POL",
|
|
314
|
+
"24": "Transhipment loaded",
|
|
315
|
+
"25": "Transhipment discharged",
|
|
316
|
+
"27": "Vessel arrived POD",
|
|
317
|
+
"28": "Discharged from vessel",
|
|
318
|
+
"29": "Gate-out at destination terminal (full import)",
|
|
319
|
+
"32": "Empty container returned to depot",
|
|
320
|
+
"35": "Container delivered to consignee",
|
|
321
|
+
"40": "Customs cleared",
|
|
322
|
+
"89": "Bill of lading released",
|
|
323
|
+
"144": "Container hold placed",
|
|
324
|
+
"192": "Estimated time of arrival reported",
|
|
325
|
+
"198": "Estimated time of departure reported",
|
|
326
|
+
"201": "Damage reported",
|
|
327
|
+
},
|
|
328
|
+
// STS.reason (DE 9013) — why a status was set, especially for
|
|
329
|
+
// holds and exceptions.
|
|
330
|
+
"STS.reason": {
|
|
331
|
+
"1": "Awaiting documents",
|
|
332
|
+
"2": "Awaiting payment",
|
|
333
|
+
"3": "Customs hold",
|
|
334
|
+
"4": "Port authority hold",
|
|
335
|
+
"5": "Damage to equipment",
|
|
336
|
+
"6": "Damage to cargo",
|
|
337
|
+
"7": "Equipment unavailable",
|
|
338
|
+
"8": "Vessel delay",
|
|
339
|
+
"9": "Weather delay",
|
|
340
|
+
"10": "Strike / labour action",
|
|
341
|
+
"11": "Equipment off-hire",
|
|
342
|
+
"12": "Reefer plug failure",
|
|
343
|
+
"13": "Refused by consignee",
|
|
344
|
+
"14": "Awaiting customs inspection",
|
|
345
|
+
"15": "Quarantine hold",
|
|
346
|
+
"16": "Mis-routed / mis-loaded",
|
|
347
|
+
"17": "VGM missing",
|
|
348
|
+
"18": "Restow required",
|
|
349
|
+
ZZZ: "Mutually defined",
|
|
350
|
+
},
|
|
351
|
+
// CNI.qualifier — consignment reference qualifier on IFTSTA
|
|
352
|
+
// CNI segments. Reuses RFF qualifier semantics in practice.
|
|
353
|
+
"CNI.qualifier": {
|
|
354
|
+
BM: "Bill of lading number",
|
|
355
|
+
BN: "Booking reference number",
|
|
356
|
+
HB: "House bill of lading",
|
|
357
|
+
MB: "Master bill of lading",
|
|
358
|
+
XX: "Mutually defined reference",
|
|
359
|
+
},
|
|
286
360
|
"RFF.qualifier": {
|
|
287
361
|
AAY: "Release order number",
|
|
288
362
|
ABO: "Transhipment number",
|
|
@@ -477,6 +551,24 @@ export const COPRAR = {
|
|
|
477
551
|
bodyRequired: ["TDT", "NAD", "EQD"],
|
|
478
552
|
trailerRequired: ["CNT", "UNT"],
|
|
479
553
|
};
|
|
554
|
+
export const IFTSTA = {
|
|
555
|
+
name: "IFTSTA",
|
|
556
|
+
longName: "International Multimodal Status Report",
|
|
557
|
+
purpose: "Carrier / terminal / depot → cargo owner. Asynchronous push of 'what happened, where, when, why' for one or more containers. Always reports an event that has occurred — gate-in, loaded, discharged, gate-out, delivered, hold, ETA/ETD revision. One IFTSTA can carry many consignments (CNI) and many status events (SG5) per container.",
|
|
558
|
+
bgmCodes: ["23"],
|
|
559
|
+
headerRequired: ["UNH", "BGM", "DTM"],
|
|
560
|
+
bodyRequired: ["NAD", "CNI", "STS"],
|
|
561
|
+
trailerRequired: ["UNT"],
|
|
562
|
+
};
|
|
563
|
+
export const COREOR = {
|
|
564
|
+
name: "COREOR",
|
|
565
|
+
longName: "Container Release Order",
|
|
566
|
+
purpose: "Carrier → terminal / depot. Authorises a third party (consignee, trucker, agent) to collect a container. Import: full release once B/L surrendered + freight paid + customs cleared. Export-empty: empty release from depot to shipper. One release reference (RFF+AAY) per message; up to 999 containers can share the release.",
|
|
567
|
+
bgmCodes: ["12", "350"],
|
|
568
|
+
headerRequired: ["UNH", "BGM", "DTM"],
|
|
569
|
+
bodyRequired: ["RFF", "TDT", "NAD", "EQD"],
|
|
570
|
+
trailerRequired: ["UNT"],
|
|
571
|
+
};
|
|
480
572
|
/**
|
|
481
573
|
* Detect whether the parsed message is CODECO, COPRAR, or unknown.
|
|
482
574
|
* Inspects UNH.type first, then falls back to BGM document code.
|
|
@@ -484,7 +576,7 @@ export const COPRAR = {
|
|
|
484
576
|
export function detectMessageType(parsed) {
|
|
485
577
|
if (parsed.message && parsed.message.type) {
|
|
486
578
|
const t = parsed.message.type.toUpperCase();
|
|
487
|
-
if (t === "CODECO" || t === "COPRAR")
|
|
579
|
+
if (t === "CODECO" || t === "COPRAR" || t === "IFTSTA" || t === "COREOR")
|
|
488
580
|
return t;
|
|
489
581
|
}
|
|
490
582
|
for (const s of parsed.segments) {
|
|
@@ -494,6 +586,10 @@ export function detectMessageType(parsed) {
|
|
|
494
586
|
return "CODECO";
|
|
495
587
|
if (doc === "45" || doc === "46" || doc === "244" || doc === "245")
|
|
496
588
|
return "COPRAR";
|
|
589
|
+
if (doc === "23")
|
|
590
|
+
return "IFTSTA";
|
|
591
|
+
if (doc === "12" || doc === "350")
|
|
592
|
+
return "COREOR";
|
|
497
593
|
}
|
|
498
594
|
}
|
|
499
595
|
return null;
|
|
@@ -776,6 +872,167 @@ export function diagnoseSingle(parsed) {
|
|
|
776
872
|
});
|
|
777
873
|
}
|
|
778
874
|
}
|
|
875
|
+
// ── IFTSTA-specific rules ────────────────────────────────────
|
|
876
|
+
if (type === "IFTSTA") {
|
|
877
|
+
// 12. Missing CNI segment — SMDG requires at least one consignment block
|
|
878
|
+
const hasCNI = segments.some((s) => s.tag === "CNI");
|
|
879
|
+
if (!hasCNI) {
|
|
880
|
+
diags.push({
|
|
881
|
+
level: "error",
|
|
882
|
+
code: "MISSING_CNI",
|
|
883
|
+
message: "IFTSTA has no CNI consignment segment. SMDG requires at least one.",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// 13. STS without a DTM+334 (status timestamp) is unusable downstream
|
|
887
|
+
let lastSeenSTSIndex = -1;
|
|
888
|
+
segments.forEach((s, i) => {
|
|
889
|
+
if (s.tag === "STS") {
|
|
890
|
+
// Look ahead until next STS or end for a DTM with qualifier 334 or 7
|
|
891
|
+
let hasTimestamp = false;
|
|
892
|
+
for (let j = i + 1; j < segments.length; j++) {
|
|
893
|
+
if (segments[j].tag === "STS")
|
|
894
|
+
break;
|
|
895
|
+
if (segments[j].tag === "DTM") {
|
|
896
|
+
const q = (segments[j].elements[0] || [])[0];
|
|
897
|
+
if (q === "334" || q === "7" || q === "178") {
|
|
898
|
+
hasTimestamp = true;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (!hasTimestamp) {
|
|
904
|
+
diags.push({
|
|
905
|
+
level: "error",
|
|
906
|
+
code: "MISSING_STS_DTM",
|
|
907
|
+
message: "STS status event has no DTM timestamp (qualifier 334 / 7 / 178). Status without a date is unusable to downstream systems.",
|
|
908
|
+
segmentIndex: i,
|
|
909
|
+
tag: "STS",
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
lastSeenSTSIndex = i;
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
// 14. STS_FUTURE_TIMESTAMP — DTM+334 more than 15 min ahead of now
|
|
916
|
+
segments.forEach((s, i) => {
|
|
917
|
+
if (s.tag !== "DTM")
|
|
918
|
+
return;
|
|
919
|
+
const q = (s.elements[0] || [])[0];
|
|
920
|
+
if (q !== "334")
|
|
921
|
+
return;
|
|
922
|
+
const dtVal = (s.elements[0] || [])[1];
|
|
923
|
+
const fmt = (s.elements[0] || [])[2];
|
|
924
|
+
if (!dtVal || fmt !== "203" || dtVal.length !== 12)
|
|
925
|
+
return;
|
|
926
|
+
const yr = parseInt(dtVal.slice(0, 4), 10);
|
|
927
|
+
const mo = parseInt(dtVal.slice(4, 6), 10) - 1;
|
|
928
|
+
const day = parseInt(dtVal.slice(6, 8), 10);
|
|
929
|
+
const hr = parseInt(dtVal.slice(8, 10), 10);
|
|
930
|
+
const mn = parseInt(dtVal.slice(10, 12), 10);
|
|
931
|
+
const eventTime = Date.UTC(yr, mo, day, hr, mn);
|
|
932
|
+
const now = Date.now();
|
|
933
|
+
if (eventTime - now > 15 * 60 * 1000) {
|
|
934
|
+
diags.push({
|
|
935
|
+
level: "warn",
|
|
936
|
+
code: "STS_FUTURE_TIMESTAMP",
|
|
937
|
+
message: `DTM+334 status timestamp ${dtVal} is more than 15 min in the future. Likely a clock or timezone error at the sending system.`,
|
|
938
|
+
segmentIndex: i,
|
|
939
|
+
tag: "DTM",
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
// ── COREOR-specific rules ────────────────────────────────────
|
|
945
|
+
if (type === "COREOR") {
|
|
946
|
+
// 15. MISSING_AAY — every COREOR must carry exactly one release order ref
|
|
947
|
+
const aayRefs = segments.filter((s) => s.tag === "RFF" && (s.elements[0] || [])[0] === "AAY");
|
|
948
|
+
if (aayRefs.length === 0) {
|
|
949
|
+
diags.push({
|
|
950
|
+
level: "error",
|
|
951
|
+
code: "MISSING_AAY",
|
|
952
|
+
message: "COREOR has no RFF+AAY (release order number). A release without an order number is invalid.",
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
// 16. MULTIPLE_AAY — SMDG rule: one release per message
|
|
956
|
+
if (aayRefs.length > 1) {
|
|
957
|
+
diags.push({
|
|
958
|
+
level: "error",
|
|
959
|
+
code: "MULTIPLE_AAY",
|
|
960
|
+
message: `COREOR has ${aayRefs.length} RFF+AAY release-order references. SMDG profile: exactly one release per message.`,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
// 17. RELEASE_WITHOUT_ADDRESSEE — at least one CN or BO required
|
|
964
|
+
const hasCN = segments.some((s) => s.tag === "NAD" && (s.elements[0] || [])[0] === "CN");
|
|
965
|
+
const hasBO = segments.some((s) => s.tag === "NAD" && (s.elements[0] || [])[0] === "BO");
|
|
966
|
+
if (!hasCN && !hasBO) {
|
|
967
|
+
diags.push({
|
|
968
|
+
level: "error",
|
|
969
|
+
code: "RELEASE_WITHOUT_ADDRESSEE",
|
|
970
|
+
message: "COREOR has no NAD+CN (consignee) or NAD+BO (B/L recipient). A release must name who is authorised to collect.",
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
// 18. EXPIRED_RELEASE — DTM+36 (expiration) in the past
|
|
974
|
+
segments.forEach((s, i) => {
|
|
975
|
+
if (s.tag !== "DTM")
|
|
976
|
+
return;
|
|
977
|
+
const q = (s.elements[0] || [])[0];
|
|
978
|
+
if (q !== "36")
|
|
979
|
+
return;
|
|
980
|
+
const dtVal = (s.elements[0] || [])[1];
|
|
981
|
+
const fmt = (s.elements[0] || [])[2];
|
|
982
|
+
if (!dtVal || fmt !== "203" || dtVal.length !== 12)
|
|
983
|
+
return;
|
|
984
|
+
const yr = parseInt(dtVal.slice(0, 4), 10);
|
|
985
|
+
const mo = parseInt(dtVal.slice(4, 6), 10) - 1;
|
|
986
|
+
const day = parseInt(dtVal.slice(6, 8), 10);
|
|
987
|
+
const hr = parseInt(dtVal.slice(8, 10), 10);
|
|
988
|
+
const mn = parseInt(dtVal.slice(10, 12), 10);
|
|
989
|
+
const expiry = Date.UTC(yr, mo, day, hr, mn);
|
|
990
|
+
if (expiry < Date.now()) {
|
|
991
|
+
diags.push({
|
|
992
|
+
level: "error",
|
|
993
|
+
code: "EXPIRED_RELEASE",
|
|
994
|
+
message: `COREOR release validity (DTM+36) ${dtVal} is in the past. Terminal will reject the gate-out attempt.`,
|
|
995
|
+
segmentIndex: i,
|
|
996
|
+
tag: "DTM",
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
// 19. EMPTY_ON_IMPORT_RELEASE — BGM 12 with EQD empty is suspicious
|
|
1001
|
+
const bgm = segments.find((s) => s.tag === "BGM");
|
|
1002
|
+
const docCode = bgm ? (bgm.elements[0] || [])[0] : null;
|
|
1003
|
+
if (docCode === "12") {
|
|
1004
|
+
segments.forEach((s, i) => {
|
|
1005
|
+
if (s.tag !== "EQD")
|
|
1006
|
+
return;
|
|
1007
|
+
if ((s.elements[5] || [])[0] === "5") {
|
|
1008
|
+
diags.push({
|
|
1009
|
+
level: "warn",
|
|
1010
|
+
code: "EMPTY_ON_IMPORT_RELEASE",
|
|
1011
|
+
message: "COREOR release order (BGM 12) but EQD declares EMPTY. Import release is typically for full containers; verify intent.",
|
|
1012
|
+
segmentIndex: i,
|
|
1013
|
+
tag: "EQD",
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
// 20. MISSING_IMO — TDT names a vessel but no IMO number (code list 146)
|
|
1019
|
+
segments.forEach((s, i) => {
|
|
1020
|
+
if (s.tag !== "TDT")
|
|
1021
|
+
return;
|
|
1022
|
+
const vesselId = (s.elements[7] || [])[0];
|
|
1023
|
+
const vesselName = (s.elements[7] || [])[3];
|
|
1024
|
+
const idCodeList = (s.elements[7] || [])[2];
|
|
1025
|
+
if (vesselName && (!vesselId || idCodeList !== "146")) {
|
|
1026
|
+
diags.push({
|
|
1027
|
+
level: "warn",
|
|
1028
|
+
code: "MISSING_IMO",
|
|
1029
|
+
message: `TDT names vessel '${vesselName}' but lacks an IMO number (code list 146). SMDG strongly recommends including the IMO.`,
|
|
1030
|
+
segmentIndex: i,
|
|
1031
|
+
tag: "TDT",
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
779
1036
|
return diags;
|
|
780
1037
|
}
|
|
781
1038
|
function buildEqdMap(parsed) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prefixcheck/edi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"mcpName": "io.github.xultrax-web/prefixcheck-edi-mcp",
|
|
5
|
-
"description": "MCP server for EDIFACT CODECO
|
|
5
|
+
"description": "MCP server for EDIFACT CODECO, COPRAR, IFTSTA, COREOR — parse, validate against SMDG, reconcile cross-message, validate ISO 6346 check digits, decode size-type, look up SMDG code lists. Wraps @prefixcheck/edi inside the Model Context Protocol so any MCP client (Claude Desktop, Cursor, Cline, Continue, Claude Code) gets operator-grade container-shipping EDI knowledge.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"prefixcheck-edi-mcp": "dist/index.js"
|