@easysolutions906/mcp-ofac 1.0.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/Procfile +1 -0
- package/package.json +32 -0
- package/scripts/build-data.js +217 -0
- package/src/data/meta.json +88 -0
- package/src/data/sdn.json +1 -0
- package/src/index.js +497 -0
- package/src/keys.js +194 -0
- package/src/match.js +424 -0
- package/src/stripe.js +91 -0
package/Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: node src/index.js
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@easysolutions906/mcp-ofac",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OFAC SDN sanctions screening API with fuzzy matching - screen names against the Treasury Department SDN list",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/index.js",
|
|
9
|
+
"dev": "node --watch src/index.js",
|
|
10
|
+
"build-data": "node scripts/build-data.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ofac",
|
|
14
|
+
"sanctions",
|
|
15
|
+
"sdn",
|
|
16
|
+
"screening",
|
|
17
|
+
"compliance",
|
|
18
|
+
"aml",
|
|
19
|
+
"kyc",
|
|
20
|
+
"api"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
+
"double-metaphone": "^2.0.0",
|
|
27
|
+
"express": "^5.1.0",
|
|
28
|
+
"fast-xml-parser": "^5.2.0",
|
|
29
|
+
"stripe": "^20.4.1",
|
|
30
|
+
"zod": "^3.24.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
5
|
+
import { doubleMetaphone } from 'double-metaphone';
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = new URL('../src/data/', import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
// --- helpers ---
|
|
10
|
+
|
|
11
|
+
const ensureArray = (val) => {
|
|
12
|
+
if (!val) { return []; }
|
|
13
|
+
return Array.isArray(val) ? val : [val];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const buildName = (firstName, lastName) => {
|
|
17
|
+
const f = firstName != null ? String(firstName) : '';
|
|
18
|
+
const l = lastName != null ? String(lastName) : '';
|
|
19
|
+
if (f && l) { return `${f} ${l}`; }
|
|
20
|
+
return l || f || '';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const normalizeForSearch = (str) => {
|
|
24
|
+
if (!str) { return ''; }
|
|
25
|
+
return str
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.normalize('NFD')
|
|
28
|
+
.replace(/[\u0300-\u036f]/g, '') // strip diacritics
|
|
29
|
+
.replace(/[^a-z0-9\s]/g, ' ') // non-alphanum to space
|
|
30
|
+
.replace(/\b(al|el|bin|ibn|abd|abu)\b/g, '') // common prefixes
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const computePhonetic = (name) => {
|
|
36
|
+
if (!name) { return []; }
|
|
37
|
+
const tokens = normalizeForSearch(name).split(/\s+/).filter(Boolean);
|
|
38
|
+
return tokens.flatMap((t) => {
|
|
39
|
+
const [primary, secondary] = doubleMetaphone(t);
|
|
40
|
+
return [primary, secondary].filter(Boolean);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- parse one SDN entry ---
|
|
45
|
+
|
|
46
|
+
const parseEntry = (raw) => {
|
|
47
|
+
const uid = raw.uid;
|
|
48
|
+
const sdnType = raw.sdnType || 'Unknown';
|
|
49
|
+
const firstName = raw.firstName || null;
|
|
50
|
+
const lastName = raw.lastName || null;
|
|
51
|
+
const name = buildName(firstName, lastName);
|
|
52
|
+
const title = raw.title || null;
|
|
53
|
+
const remarks = raw.remarks || null;
|
|
54
|
+
|
|
55
|
+
const programs = ensureArray(raw.programList?.program);
|
|
56
|
+
|
|
57
|
+
const aliases = ensureArray(raw.akaList?.aka).map((a) => ({
|
|
58
|
+
uid: a.uid,
|
|
59
|
+
type: a.type || null,
|
|
60
|
+
category: a.category || null,
|
|
61
|
+
name: buildName(a.firstName, a.lastName),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const addresses = ensureArray(raw.addressList?.address).map((a) => ({
|
|
65
|
+
uid: a.uid,
|
|
66
|
+
address1: a.address1 || null,
|
|
67
|
+
address2: a.address2 || null,
|
|
68
|
+
address3: a.address3 || null,
|
|
69
|
+
city: a.city || null,
|
|
70
|
+
stateOrProvince: a.stateOrProvince || null,
|
|
71
|
+
postalCode: a.postalCode || null,
|
|
72
|
+
country: a.country || null,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const ids = ensureArray(raw.idList?.id).map((i) => ({
|
|
76
|
+
uid: i.uid,
|
|
77
|
+
idType: i.idType || null,
|
|
78
|
+
idNumber: i.idNumber != null ? String(i.idNumber) : null,
|
|
79
|
+
idCountry: i.idCountry || null,
|
|
80
|
+
issueDate: i.issueDate || null,
|
|
81
|
+
expirationDate: i.expirationDate || null,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const datesOfBirth = ensureArray(raw.dateOfBirthList?.dateOfBirthItem).map((d) => ({
|
|
85
|
+
uid: d.uid,
|
|
86
|
+
dateOfBirth: d.dateOfBirth != null ? String(d.dateOfBirth) : null,
|
|
87
|
+
mainEntry: d.mainEntry === true || d.mainEntry === 'true',
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const placesOfBirth = ensureArray(raw.placeOfBirthList?.placeOfBirthItem).map((p) => ({
|
|
91
|
+
uid: p.uid,
|
|
92
|
+
placeOfBirth: p.placeOfBirth || null,
|
|
93
|
+
mainEntry: p.mainEntry === true || p.mainEntry === 'true',
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const nationalities = ensureArray(raw.nationalityList?.nationality).map((n) => ({
|
|
97
|
+
uid: n.uid,
|
|
98
|
+
country: n.country || null,
|
|
99
|
+
mainEntry: n.mainEntry === true || n.mainEntry === 'true',
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const citizenships = ensureArray(raw.citizenshipList?.citizenship).map((c) => ({
|
|
103
|
+
uid: c.uid,
|
|
104
|
+
country: c.country || null,
|
|
105
|
+
mainEntry: c.mainEntry === true || c.mainEntry === 'true',
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const vesselInfo = raw.vesselInfo
|
|
109
|
+
? {
|
|
110
|
+
callSign: raw.vesselInfo.callSign || null,
|
|
111
|
+
vesselType: raw.vesselInfo.vesselType || null,
|
|
112
|
+
vesselFlag: raw.vesselInfo.vesselFlag || null,
|
|
113
|
+
vesselOwner: raw.vesselInfo.vesselOwner || null,
|
|
114
|
+
tonnage: raw.vesselInfo.tonnage || null,
|
|
115
|
+
grossRegisteredTonnage: raw.vesselInfo.grossRegisteredTonnage || null,
|
|
116
|
+
}
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
// pre-computed search fields
|
|
120
|
+
const normalizedName = normalizeForSearch(name);
|
|
121
|
+
const nameTokens = normalizedName.split(/\s+/).filter(Boolean);
|
|
122
|
+
const phonetic = computePhonetic(name);
|
|
123
|
+
|
|
124
|
+
const aliasSearch = aliases.map((a) => {
|
|
125
|
+
const norm = normalizeForSearch(a.name);
|
|
126
|
+
return {
|
|
127
|
+
name: a.name,
|
|
128
|
+
normalized: norm,
|
|
129
|
+
tokens: norm.split(/\s+/).filter(Boolean),
|
|
130
|
+
phonetic: computePhonetic(a.name),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
uid,
|
|
136
|
+
sdnType,
|
|
137
|
+
firstName,
|
|
138
|
+
lastName,
|
|
139
|
+
name,
|
|
140
|
+
title,
|
|
141
|
+
remarks,
|
|
142
|
+
programs,
|
|
143
|
+
aliases,
|
|
144
|
+
addresses,
|
|
145
|
+
ids,
|
|
146
|
+
datesOfBirth,
|
|
147
|
+
placesOfBirth,
|
|
148
|
+
nationalities,
|
|
149
|
+
citizenships,
|
|
150
|
+
vesselInfo,
|
|
151
|
+
search: {
|
|
152
|
+
normalizedName,
|
|
153
|
+
nameTokens,
|
|
154
|
+
phonetic,
|
|
155
|
+
aliases: aliasSearch,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// --- main ---
|
|
161
|
+
|
|
162
|
+
const main = async () => {
|
|
163
|
+
console.log('Reading sdn.xml...');
|
|
164
|
+
const xml = await readFile(`${DATA_DIR}sdn.xml`, 'utf-8');
|
|
165
|
+
|
|
166
|
+
console.log('Parsing XML...');
|
|
167
|
+
const parser = new XMLParser({
|
|
168
|
+
ignoreAttributes: true,
|
|
169
|
+
isArray: (name) => ['sdnEntry', 'program', 'aka', 'address', 'id', 'dateOfBirthItem', 'placeOfBirthItem', 'nationality', 'citizenship'].includes(name),
|
|
170
|
+
});
|
|
171
|
+
const parsed = parser.parse(xml);
|
|
172
|
+
|
|
173
|
+
const publishInfo = parsed.sdnList?.publshInformation || {};
|
|
174
|
+
const rawEntries = parsed.sdnList?.sdnEntry || [];
|
|
175
|
+
|
|
176
|
+
console.log(`Found ${rawEntries.length} entries. Processing...`);
|
|
177
|
+
const entries = rawEntries.map(parseEntry);
|
|
178
|
+
|
|
179
|
+
// type counts
|
|
180
|
+
const typeCounts = entries.reduce((acc, e) => {
|
|
181
|
+
acc[e.sdnType] = (acc[e.sdnType] || 0) + 1;
|
|
182
|
+
return acc;
|
|
183
|
+
}, {});
|
|
184
|
+
|
|
185
|
+
// program counts
|
|
186
|
+
const programCounts = entries.reduce((acc, e) => {
|
|
187
|
+
e.programs.forEach((p) => {
|
|
188
|
+
acc[p] = (acc[p] || 0) + 1;
|
|
189
|
+
});
|
|
190
|
+
return acc;
|
|
191
|
+
}, {});
|
|
192
|
+
|
|
193
|
+
const meta = {
|
|
194
|
+
buildDate: new Date().toISOString(),
|
|
195
|
+
publishDate: publishInfo.Publish_Date || null,
|
|
196
|
+
recordCount: entries.length,
|
|
197
|
+
typeCounts,
|
|
198
|
+
programCounts,
|
|
199
|
+
aliasCount: entries.reduce((sum, e) => sum + e.aliases.length, 0),
|
|
200
|
+
addressCount: entries.reduce((sum, e) => sum + e.addresses.length, 0),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
console.log('Writing sdn.json...');
|
|
204
|
+
await writeFile(`${DATA_DIR}sdn.json`, JSON.stringify(entries));
|
|
205
|
+
|
|
206
|
+
console.log('Writing meta.json...');
|
|
207
|
+
await writeFile(`${DATA_DIR}meta.json`, JSON.stringify(meta, null, 2));
|
|
208
|
+
|
|
209
|
+
console.log(`Done. ${entries.length} entries processed.`);
|
|
210
|
+
console.log('Type counts:', typeCounts);
|
|
211
|
+
console.log('Programs:', Object.keys(programCounts).length);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
main().catch((err) => {
|
|
215
|
+
console.error('Build failed:', err);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"buildDate": "2026-03-15T17:20:48.652Z",
|
|
3
|
+
"publishDate": "03/13/2026",
|
|
4
|
+
"recordCount": 18712,
|
|
5
|
+
"typeCounts": {
|
|
6
|
+
"Entity": 9521,
|
|
7
|
+
"Individual": 7394,
|
|
8
|
+
"Vessel": 1455,
|
|
9
|
+
"Aircraft": 342
|
|
10
|
+
},
|
|
11
|
+
"programCounts": {
|
|
12
|
+
"CUBA": 77,
|
|
13
|
+
"SDGT": 3052,
|
|
14
|
+
"SDNT": 162,
|
|
15
|
+
"IRAN": 669,
|
|
16
|
+
"IFSR": 1468,
|
|
17
|
+
"IRGC": 282,
|
|
18
|
+
"IRAN-EO13902": 604,
|
|
19
|
+
"FTO": 98,
|
|
20
|
+
"SDNTK": 1419,
|
|
21
|
+
"ILLICIT-DRUGS-EO14059": 589,
|
|
22
|
+
"GLOMAG": 744,
|
|
23
|
+
"BALKANS": 164,
|
|
24
|
+
"IRAQ2": 190,
|
|
25
|
+
"DRCONGO": 91,
|
|
26
|
+
"NPWMD": 1141,
|
|
27
|
+
"DPRK2": 124,
|
|
28
|
+
"IRAN-CON-ARMS-EO": 29,
|
|
29
|
+
"DARFUR": 8,
|
|
30
|
+
"NS-PLC": 2,
|
|
31
|
+
"BELARUS": 73,
|
|
32
|
+
"IRAN-HR": 222,
|
|
33
|
+
"HRIT-IR": 16,
|
|
34
|
+
"ELECTION-EO13848": 111,
|
|
35
|
+
"RUSSIA-EO14024": 6410,
|
|
36
|
+
"IRAQ3": 13,
|
|
37
|
+
"LEBANON": 16,
|
|
38
|
+
"PAARSSR-EO13894": 174,
|
|
39
|
+
"SOMALIA": 30,
|
|
40
|
+
"CAR": 29,
|
|
41
|
+
"IRAN-TRA": 31,
|
|
42
|
+
"VENEZUELA": 168,
|
|
43
|
+
"IFCA": 26,
|
|
44
|
+
"TCO": 386,
|
|
45
|
+
"IRAN-EO13876": 113,
|
|
46
|
+
"CYBER2": 260,
|
|
47
|
+
"HOSTAGES-EO14078": 18,
|
|
48
|
+
"DPRK": 69,
|
|
49
|
+
"LIBYA2": 20,
|
|
50
|
+
"HRIT-SY": 1,
|
|
51
|
+
"DPRK3": 160,
|
|
52
|
+
"MAGNIT": 64,
|
|
53
|
+
"UKRAINE-EO13660": 154,
|
|
54
|
+
"UKRAINE-EO13661": 196,
|
|
55
|
+
"SOUTH SUDAN": 24,
|
|
56
|
+
"UKRAINE-EO13662": 534,
|
|
57
|
+
"RUSSIA-EO14065": 8,
|
|
58
|
+
"YEMEN": 10,
|
|
59
|
+
"UKRAINE-EO13685": 101,
|
|
60
|
+
"VENEZUELA-EO13850": 177,
|
|
61
|
+
"LIBYA3": 51,
|
|
62
|
+
"PEESA-EO14039": 27,
|
|
63
|
+
"CAATSA - RUSSIA": 81,
|
|
64
|
+
"DPRK4": 196,
|
|
65
|
+
"BURMA-EO14014": 154,
|
|
66
|
+
"IRAN-EO13871": 51,
|
|
67
|
+
"SSIDES": 2,
|
|
68
|
+
"NICARAGUA": 66,
|
|
69
|
+
"VENEZUELA-EO13884": 67,
|
|
70
|
+
"NICARAGUA-NHRAA": 8,
|
|
71
|
+
"IRAN-EO13846": 445,
|
|
72
|
+
"MALI-EO13882": 5,
|
|
73
|
+
"DPRK-NKSPEA": 2,
|
|
74
|
+
"CAATSA - IRAN": 20,
|
|
75
|
+
"HK-EO13936": 48,
|
|
76
|
+
"BELARUS-EO14038": 190,
|
|
77
|
+
"ETHIOPIA-EO14046": 6,
|
|
78
|
+
"BALKANS-EO14033": 24,
|
|
79
|
+
"CYBER4": 59,
|
|
80
|
+
"UHRPA": 1,
|
|
81
|
+
"SUDAN-EO14098": 44,
|
|
82
|
+
"CYBER3": 9,
|
|
83
|
+
"ICC-EO14203": 15,
|
|
84
|
+
"PAIPA": 3
|
|
85
|
+
},
|
|
86
|
+
"aliasCount": 24588,
|
|
87
|
+
"addressCount": 21277
|
|
88
|
+
}
|