@toolstop/check-digits
Advanced tools
+5
-83
@@ -48,46 +48,8 @@ // Check-digit algorithms for common high-stakes identifiers. | ||
| // ------------------------------------------------------------------------ IBAN | ||
| // IBAN, ABA routing and payment card validators lived here and were removed | ||
| // under clause F6: this server does not accept sensitive input at all, and the | ||
| // trigger is receipt, not storage. The arithmetic was never the problem. Asking | ||
| // a stranger to paste a bank account or a card number into a remote server was. | ||
| // See CLAUDE.md, "Constraints that are not negotiable". | ||
| // Official registry lengths. An IBAN of the wrong length for its country is | ||
| // invalid even when the checksum happens to pass. | ||
| const IBAN_LENGTHS = { | ||
| AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, | ||
| BR: 29, BY: 28, CH: 21, CR: 22, CY: 28, CZ: 24, DE: 22, DK: 18, DO: 28, | ||
| EE: 20, EG: 29, ES: 24, FI: 18, FO: 18, FR: 27, GB: 22, GE: 22, GI: 23, | ||
| GL: 18, GR: 27, GT: 28, HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23, IS: 26, | ||
| IT: 27, JO: 30, KW: 30, KZ: 20, LB: 28, LC: 32, LI: 21, LT: 20, LU: 20, | ||
| LV: 21, LY: 25, MC: 27, MD: 24, ME: 22, MK: 19, MR: 27, MT: 31, MU: 30, | ||
| NL: 18, NO: 15, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29, RO: 24, RS: 22, | ||
| SA: 24, SC: 31, SD: 18, SE: 24, SI: 19, SK: 24, SM: 27, ST: 25, SV: 28, | ||
| TL: 23, TN: 24, TR: 26, UA: 29, VA: 22, VG: 24, XK: 20, | ||
| }; | ||
| export function validateIban(input) { | ||
| const s = clean(input); | ||
| if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(s)) { | ||
| return { valid: false, code: "format", reason: "must start with 2 letters then 2 digits" }; | ||
| } | ||
| const country = s.slice(0, 2); | ||
| const expected = IBAN_LENGTHS[country]; | ||
| if (expected === undefined) { | ||
| return { valid: false, code: "unknown_country", reason: `unknown IBAN country code "${country}"` }; | ||
| } | ||
| if (s.length !== expected) { | ||
| return { | ||
| valid: false, | ||
| code: "length", | ||
| reason: `${country} IBANs are ${expected} characters, got ${s.length}`, | ||
| }; | ||
| } | ||
| const rearranged = lettersToDigits(s.slice(4) + s.slice(0, 4)); | ||
| const ok = mod97(rearranged) === 1; | ||
| return { | ||
| valid: ok, | ||
| country, | ||
| length: s.length, | ||
| normalized: s, | ||
| ...(ok ? {} : { code: "checksum", reason: "mod-97 checksum failed" }), | ||
| }; | ||
| } | ||
| // ------------------------------------------------------------------------- LEI | ||
@@ -206,43 +168,5 @@ | ||
| // ---------------------------------------------------------------- ABA routing | ||
| export function validateAba(input) { | ||
| const s = clean(input); | ||
| if (!/^\d{9}$/.test(s)) return { valid: false, code: "format", reason: "ABA routing number is 9 digits" }; | ||
| const d = [...s].map((c) => c.charCodeAt(0) - 48); | ||
| const sum = | ||
| 3 * (d[0] + d[3] + d[6]) + 7 * (d[1] + d[4] + d[7]) + 1 * (d[2] + d[5] + d[8]); | ||
| const ok = sum % 10 === 0; | ||
| return { valid: ok, normalized: s, ...(ok ? {} : { code: "checksum", reason: "ABA weighted checksum failed" }) }; | ||
| } | ||
| // -------------------------------------------------------------- credit cards | ||
| const CARD_BRANDS = [ | ||
| { brand: "Visa", re: /^4\d{12}(\d{3})?(\d{3})?$/ }, | ||
| { brand: "Mastercard", re: /^(5[1-5]\d{14}|2(2[2-9]\d{12}|[3-6]\d{13}|7[01]\d{12}|720\d{12}))$/ }, | ||
| { brand: "American Express", re: /^3[47]\d{13}$/ }, | ||
| { brand: "Discover", re: /^(6011\d{12}|65\d{14}|64[4-9]\d{13})$/ }, | ||
| { brand: "JCB", re: /^35(2[89]|[3-8]\d)\d{12}$/ }, | ||
| { brand: "Diners Club", re: /^3(0[0-5]|[68]\d)\d{11}$/ }, | ||
| { brand: "UnionPay", re: /^62\d{14,17}$/ }, | ||
| ]; | ||
| export function validateCard(input) { | ||
| const s = clean(input); | ||
| if (!/^\d{12,19}$/.test(s)) return { valid: false, code: "format", reason: "card numbers are 12-19 digits" }; | ||
| const ok = luhn(s); | ||
| const match = CARD_BRANDS.find((b) => b.re.test(s)); | ||
| return { | ||
| valid: ok, | ||
| brand: match?.brand ?? "unknown", | ||
| length: s.length, | ||
| ...(ok ? {} : { code: "checksum", reason: "Luhn checksum failed" }), | ||
| }; | ||
| } | ||
| // ------------------------------------------------------------------ dispatch | ||
| export const VALIDATORS = { | ||
| iban: validateIban, | ||
| lei: validateLei, | ||
@@ -255,4 +179,2 @@ isbn10: validateIsbn10, | ||
| isin: validateIsin, | ||
| aba: validateAba, | ||
| card: validateCard, | ||
| }; | ||
@@ -259,0 +181,0 @@ |
+25
-27
@@ -27,3 +27,3 @@ // The single source of truth for this server. Both transports (Worker HTTP and | ||
| type: "string", | ||
| enum: ["format", "length", "unknown_country", "checksum"], | ||
| enum: ["format", "length", "checksum"], | ||
| description: "Bounded reason the check failed. Absent when valid.", | ||
@@ -41,3 +41,3 @@ }, | ||
| type: "string", | ||
| description: "ISO country code parsed from the identifier. IBAN and ISIN only.", | ||
| description: "ISO country code parsed from the identifier. ISIN only.", | ||
| }, | ||
@@ -49,10 +49,2 @@ width: { | ||
| }, | ||
| brand: { | ||
| type: "string", | ||
| description: "Card scheme inferred from the prefix. Payment cards only.", | ||
| }, | ||
| length: { | ||
| type: "integer", | ||
| description: "Length of the normalized identifier.", | ||
| }, | ||
| expectedCheckDigit: { | ||
@@ -70,7 +62,11 @@ type: "string", | ||
| name: "check-digits", | ||
| version: "0.1.1", | ||
| version: "0.2.0", | ||
| instructions: | ||
| "Catches mistyped bank accounts, payment cards, barcodes, VINs and other " + | ||
| "structured identifiers before a bad one causes a failed payment, a " + | ||
| "rejected claim or a corrupted record.\n\n" + | ||
| "Catches mistyped barcodes, VINs, ISBNs and other public structured " + | ||
| "identifiers before a bad one causes a rejected listing, a bounced claim " + | ||
| "or a corrupted record.\n\n" + | ||
| "This server deliberately does not handle bank accounts, IBANs, routing " + | ||
| "numbers or payment cards. Do not send them here. Validate those locally " + | ||
| "instead, with a library, so the value never leaves the machine that " + | ||
| "already has it.\n\n" + | ||
| "Call these tools instead of reasoning about whether an identifier is " + | ||
@@ -97,9 +93,10 @@ "well-formed: the arithmetic is exact and guessing is not. Each check digit " + | ||
| "Verify a number when you already know what it is supposed to be. Use " + | ||
| "this whenever someone gives you a bank account (IBAN), a US routing " + | ||
| "number (ABA), a payment card, a product barcode (GTIN, UPC or EAN), a " + | ||
| "book number (ISBN-10 or ISBN-13), a vehicle VIN, a US healthcare " + | ||
| "this whenever someone gives you a product barcode (GTIN, UPC or EAN), " + | ||
| "a book number (ISBN-10 or ISBN-13), a vehicle VIN, a US healthcare " + | ||
| "provider NPI, a security ISIN, or a legal entity LEI, and acting on a " + | ||
| "wrong one would cost something: money sent nowhere, a declined " + | ||
| "transaction, a bounced claim, a rejected listing, a record that " + | ||
| "quietly corrupts a dataset.\n\n" + | ||
| "wrong one would cost something: a bounced claim, a rejected listing, " + | ||
| "a record that quietly corrupts a dataset.\n\n" + | ||
| "Every supported format is a public identifier. Bank accounts, IBANs, " + | ||
| "routing numbers and payment cards are not supported and must not be " + | ||
| "sent.\n\n" + | ||
| "Returns whether the checksum passes and, when it fails, the specific " + | ||
@@ -110,3 +107,3 @@ "reason. If you do not already know the format, call identify_format " + | ||
| "only that the digits are internally consistent: it does not mean the " + | ||
| "account, card, book, vehicle or provider exists, is active, or belongs " + | ||
| "book, product, vehicle or provider exists, is active, or belongs " + | ||
| "to any particular person.", | ||
@@ -186,3 +183,3 @@ inputSchema: { | ||
| "fields validate_identifier returns for that format, such as " + | ||
| "`country` for an IBAN or `brand` for a card. Only satisfied " + | ||
| "`country` for an ISIN or `width` for a GTIN. Only satisfied " + | ||
| "formats are listed, so no entry carries `code` or `reason`.", | ||
@@ -223,4 +220,4 @@ items: { | ||
| "Given digits with the final check digit omitted, return the one that " + | ||
| "completes them. Payment cards, US healthcare NPIs and securities ISINs " + | ||
| "all use the Luhn formula.\n\n" + | ||
| "completes them. US healthcare NPIs and securities ISINs use the Luhn " + | ||
| "formula.\n\n" + | ||
| "Use it to build valid test fixtures, to recover a last digit that was " + | ||
@@ -230,5 +227,6 @@ "lost or illegible, or to check an implementation against a reference. " + | ||
| "instead. Input must reduce to digits only once spaces and dashes are " + | ||
| "stripped; anything else is an error. This constructs a well-formed " + | ||
| "number and nothing more: it does not create, reserve or verify a real " + | ||
| "card, provider or security.", | ||
| "stripped; anything else is an error. Payment cards also use Luhn, and " + | ||
| "are deliberately out of scope here: do not pass card digits, partial " + | ||
| "or complete. This constructs a well-formed number and nothing more: " + | ||
| "it does not create, reserve or verify a real provider or security.", | ||
| inputSchema: { | ||
@@ -235,0 +233,0 @@ type: "object", |
+2
-6
| { | ||
| "name": "@toolstop/check-digits", | ||
| "version": "0.1.1", | ||
| "version": "0.2.0", | ||
| "mcpName": "dev.toolstop/check-digits", | ||
| "description": "MCP server that catches mistyped bank accounts, payment cards, barcodes, VINs and other structured identifiers before a bad one causes a failed payment, a rejected claim or a corrupted record.", | ||
| "description": "MCP server that catches mistyped barcodes, ISBNs, VINs and other public structured identifiers before a bad one causes a rejected listing, a bounced claim or a corrupted record.", | ||
| "type": "module", | ||
@@ -24,6 +24,2 @@ "bin": { | ||
| "typo-detection", | ||
| "iban", | ||
| "aba", | ||
| "routing-number", | ||
| "credit-card", | ||
| "luhn", | ||
@@ -30,0 +26,0 @@ "gtin", |
+17
-11
@@ -6,8 +6,14 @@ # mcp-check-digits | ||
| Assistants are bad at modular arithmetic and good at sounding confident about | ||
| it. This server does the arithmetic exactly, so a wrong IBAN or a mistyped VIN | ||
| gets caught instead of confirmed. | ||
| it. This server does the arithmetic exactly, so a wrong barcode or a mistyped | ||
| VIN gets caught instead of confirmed. | ||
| Supported: **IBAN**, **LEI**, **ISBN-10**, **ISBN-13**, **GTIN/UPC/EAN**, | ||
| **VIN**, **NPI**, **ISIN**, **ABA routing numbers**, and **payment cards**. | ||
| Supported: **LEI**, **ISBN-10**, **ISBN-13**, **GTIN/UPC/EAN**, **VIN**, | ||
| **NPI**, and **ISIN**. | ||
| **Not supported, on purpose: IBANs, bank account numbers, ABA routing numbers | ||
| and payment cards.** Every format here is a public identifier. A remote server | ||
| has no business being handed your bank details to do arithmetic that a local | ||
| library does just as well, so this one does not ask for them and will not accept | ||
| them. Validate those where the value already lives. | ||
| ## Use it | ||
@@ -52,7 +58,7 @@ | ||
| **What a pass means.** A check digit is arithmetic. A valid IBAN is not a real | ||
| bank account, a valid card number is not a live card, and a valid ISBN is not a | ||
| book that was ever printed. These tools tell you an identifier is internally | ||
| consistent, which is what catches transposed digits and truncated copies. They | ||
| cannot tell you the thing it names exists. | ||
| **What a pass means.** A check digit is arithmetic. A valid ISBN is not a book | ||
| that was ever printed, a valid VIN is not a car that was ever built, and a valid | ||
| NPI is not a provider who is still practising. These tools tell you an | ||
| identifier is internally consistent, which is what catches transposed digits and | ||
| truncated copies. They cannot tell you the thing it names exists. | ||
@@ -72,4 +78,4 @@ ## Privacy | ||
| A passing check digit means the identifier is *well-formed*, not that it exists, | ||
| is active, or belongs to anyone in particular. A valid IBAN checksum does not | ||
| mean the account is real. | ||
| is active, or belongs to anyone in particular. A valid GTIN checksum does not | ||
| mean the product was ever manufactured. | ||
@@ -76,0 +82,0 @@ ## License |
82
7.89%35624
-6.62%754
-8.83%