@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 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.1.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 COPRAR message into structured segments + envelope metadata. Handles UNA delimiter overrides, UNB/UNZ + UNH/UNT envelopes, and release-character escapes. Returns the full ParsedMessage structure.",
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 COPRAR message and run all 11 SMDG-grade diagnostic rules against it. Returns the list of findings (errors + warnings + info). Empty list = clean message.",
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 · 6 resources\n`);
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.1.0",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.xultrax-web/prefixcheck-edi-mcp",
5
- "description": "MCP server for EDIFACT CODECO + COPRAR — 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.",
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"