@utilix-tech/mcp 0.10.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
+ ## 0.12.0
6
+ - Added IBAN Validator / Formatter (`validate_iban`) and Loan / Mortgage Calculator (`calculate_loan`).
7
+ - (npm publish for this version was retried after fixing a CI pin: `npm install -g npm@latest` started pulling npm 12, which requires Node >=22 and broke on this job's pinned Node 20 — now pinned to the npm 11 line.)
8
+
9
+ ## 0.11.0
10
+ - Added robots.txt Validator (`validate_robots_txt`) and Credit Card Number Validator (`validate_card_number`).
11
+
5
12
  ## 0.10.0
6
13
  - Added Sitemap.xml Validator (`validate_sitemap`).
7
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 107 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 111 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
4
4
 
5
5
  No API key required. All tools run locally. Requires Node.js 18 or later.
6
6
 
@@ -145,13 +145,16 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
145
145
  | `date_diff` | Calculate the difference between two dates |
146
146
  | `convert_timezone` | Convert a date/time between timezones |
147
147
 
148
- ### Units (2 tools)
148
+ ### Units (5 tools)
149
149
  | Tool | Description |
150
150
  |------|-------------|
151
151
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
152
152
  | `px_to_units` | Convert pixels to rem, em, pt, and vw |
153
+ | `validate_card_number` | Validate a credit card number via the Luhn checksum and detect its network |
154
+ | `validate_iban` | Validate an IBAN via the mod-97 checksum and format it into grouped blocks |
155
+ | `calculate_loan` | Compute a loan's monthly payment, total interest, and amortization schedule |
153
156
 
154
- ### API / Network (7 tools)
157
+ ### API / Network (8 tools)
155
158
  | Tool | Description |
156
159
  |------|-------------|
157
160
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -161,6 +164,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
161
164
  | `cors_headers` | Generate CORS response headers for a configuration |
162
165
  | `parse_har` | Parse a DevTools .har network export into requests and summary stats |
163
166
  | `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
167
+ | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
164
168
 
165
169
  ### Color (4 tools)
166
170
  | Tool | Description |
package/dist/index.js CHANGED
@@ -1579,6 +1579,353 @@ server.tool(
1579
1579
  return text(JSON.stringify(result, null, 2));
1580
1580
  }
1581
1581
  );
1582
+ var _KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
1583
+ function _validateRobotsTxt(text2) {
1584
+ if (!text2 || !text2.trim()) return { error: "Input is empty" };
1585
+ const lines = text2.split("\n");
1586
+ const issues = [];
1587
+ const groups = [];
1588
+ const sitemaps = [];
1589
+ let currentGroup = null;
1590
+ for (let i = 0; i < lines.length; i++) {
1591
+ const rawLine = lines[i];
1592
+ const lineNum = i + 1;
1593
+ const commentIdx = rawLine.indexOf("#");
1594
+ const line = (commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine).trim();
1595
+ if (!line) continue;
1596
+ const colonIdx = line.indexOf(":");
1597
+ if (colonIdx === -1) {
1598
+ issues.push({ severity: "error", line: lineNum, message: `Malformed line (missing ':'): "${line}"` });
1599
+ continue;
1600
+ }
1601
+ const directive = line.slice(0, colonIdx).trim().toLowerCase();
1602
+ const value = line.slice(colonIdx + 1).trim();
1603
+ if (!_KNOWN_ROBOTS_DIRECTIVES.has(directive)) {
1604
+ issues.push({ severity: "warning", line: lineNum, message: `Unknown directive "${directive}"` });
1605
+ continue;
1606
+ }
1607
+ if (directive === "sitemap") {
1608
+ if (!/^https?:\/\//i.test(value)) {
1609
+ issues.push({ severity: "error", line: lineNum, message: `Sitemap URL "${value}" must be absolute (start with http:// or https://)` });
1610
+ }
1611
+ sitemaps.push(value);
1612
+ continue;
1613
+ }
1614
+ if (directive === "user-agent") {
1615
+ if (!value) {
1616
+ issues.push({ severity: "error", line: lineNum, message: "User-agent directive is missing a value" });
1617
+ }
1618
+ if (currentGroup && currentGroup.rules.length === 0 && currentGroup.crawlDelay === void 0) {
1619
+ currentGroup.userAgents.push(value);
1620
+ } else {
1621
+ currentGroup = { userAgents: [value], rules: [], line: lineNum };
1622
+ groups.push(currentGroup);
1623
+ }
1624
+ continue;
1625
+ }
1626
+ if (!currentGroup) {
1627
+ issues.push({ severity: "error", line: lineNum, message: `"${directive}" directive found before any User-agent line` });
1628
+ continue;
1629
+ }
1630
+ if (directive === "disallow" || directive === "allow") {
1631
+ if (value !== "" && !value.startsWith("/") && !value.startsWith("*")) {
1632
+ issues.push({ severity: "warning", line: lineNum, message: `${directive === "disallow" ? "Disallow" : "Allow"} value "${value}" should start with "/" (or be empty)` });
1633
+ }
1634
+ if (currentGroup.rules.some((r) => r.type === directive && r.value === value)) {
1635
+ issues.push({ severity: "warning", line: lineNum, message: `Duplicate ${directive} rule for "${value}" in this group` });
1636
+ }
1637
+ currentGroup.rules.push({ type: directive, value, line: lineNum });
1638
+ } else if (directive === "crawl-delay") {
1639
+ const n = Number(value);
1640
+ if (Number.isNaN(n) || n < 0) {
1641
+ issues.push({ severity: "error", line: lineNum, message: `Crawl-delay "${value}" must be a non-negative number` });
1642
+ } else {
1643
+ currentGroup.crawlDelay = n;
1644
+ }
1645
+ }
1646
+ }
1647
+ if (groups.length === 0) {
1648
+ issues.push({ severity: "error", line: 1, message: "No User-agent groups found" });
1649
+ }
1650
+ const seenUA = /* @__PURE__ */ new Map();
1651
+ for (const g of groups) {
1652
+ for (const ua of g.userAgents) {
1653
+ const key = ua.toLowerCase();
1654
+ const firstLine = seenUA.get(key);
1655
+ if (firstLine !== void 0) {
1656
+ issues.push({ severity: "warning", line: g.line, message: `Duplicate User-agent "${ua}" (first seen on line ${firstLine})` });
1657
+ } else {
1658
+ seenUA.set(key, g.line);
1659
+ }
1660
+ }
1661
+ }
1662
+ const valid = !issues.some((i) => i.severity === "error");
1663
+ return { groups, sitemaps, issues, valid };
1664
+ }
1665
+ server.tool(
1666
+ "validate_robots_txt",
1667
+ "Validate a robots.txt file: parse it into User-agent groups and flag structural mistakes \u2014 directives before any User-agent, unknown directives, non-absolute Sitemap URLs, malformed Crawl-delay values, missing leading slashes, and duplicate rules.",
1668
+ { text: z.string().describe("The robots.txt file contents") },
1669
+ async ({ text: robotsTxt }) => {
1670
+ const result = _validateRobotsTxt(robotsTxt);
1671
+ if ("error" in result) return errText(result);
1672
+ return text(JSON.stringify(result, null, 2));
1673
+ }
1674
+ );
1675
+ var _CARD_NETWORK_RULES = [
1676
+ { name: "American Express", lengths: [15], test: (d) => /^3[47]/.test(d) },
1677
+ { name: "Diners Club", lengths: [14], test: (d) => /^3(?:0[0-5]|[68])/.test(d) },
1678
+ {
1679
+ name: "Discover",
1680
+ lengths: [16, 19],
1681
+ test: (d) => /^(?:6011|65|64[4-9])/.test(d) || /^622(?:12[6-9]|1[3-9]\d|[2-8]\d{2}|9[01]\d|92[0-5])/.test(d)
1682
+ },
1683
+ { name: "JCB", lengths: [16], test: (d) => /^35(?:2[89]|[3-8]\d)/.test(d) },
1684
+ {
1685
+ name: "Mastercard",
1686
+ lengths: [16],
1687
+ test: (d) => {
1688
+ const two = parseInt(d.slice(0, 2), 10);
1689
+ const four = parseInt(d.slice(0, 4), 10);
1690
+ return two >= 51 && two <= 55 || four >= 2221 && four <= 2720;
1691
+ }
1692
+ },
1693
+ { name: "Maestro", lengths: [12, 13, 14, 15, 16, 17, 18, 19], test: (d) => /^(?:50|5[6-8]|6304|6390|67)/.test(d) },
1694
+ { name: "UnionPay", lengths: [16, 17, 18, 19], test: (d) => /^62/.test(d) },
1695
+ { name: "Visa", lengths: [13, 16, 19], test: (d) => /^4/.test(d) }
1696
+ ];
1697
+ function _luhnCheck(digits) {
1698
+ let sum = 0;
1699
+ let shouldDouble = false;
1700
+ for (let i = digits.length - 1; i >= 0; i--) {
1701
+ let d = parseInt(digits[i], 10);
1702
+ if (shouldDouble) {
1703
+ d *= 2;
1704
+ if (d > 9) d -= 9;
1705
+ }
1706
+ sum += d;
1707
+ shouldDouble = !shouldDouble;
1708
+ }
1709
+ return sum % 10 === 0;
1710
+ }
1711
+ function _detectCardNetwork(digits) {
1712
+ for (const rule of _CARD_NETWORK_RULES) {
1713
+ if (rule.lengths.includes(digits.length) && rule.test(digits)) return rule.name;
1714
+ }
1715
+ return null;
1716
+ }
1717
+ function _formatCardNumber(digits, network) {
1718
+ if (network === "American Express") {
1719
+ return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10)}`.trim();
1720
+ }
1721
+ return digits.match(/.{1,4}/g)?.join(" ") ?? digits;
1722
+ }
1723
+ function _validateCardNumber(input) {
1724
+ if (!input || !input.trim()) return { error: "Input is empty" };
1725
+ const digits = input.replace(/[\s-]/g, "");
1726
+ if (!/^\d+$/.test(digits)) return { error: "Card number must contain only digits, spaces, or hyphens" };
1727
+ if (digits.length < 12 || digits.length > 19) {
1728
+ return { error: `Card number length (${digits.length}) is outside the valid range of 12-19 digits` };
1729
+ }
1730
+ const valid = _luhnCheck(digits);
1731
+ const network = _detectCardNetwork(digits);
1732
+ const formatted = _formatCardNumber(digits, network);
1733
+ return { valid, network, length: digits.length, formatted };
1734
+ }
1735
+ server.tool(
1736
+ "validate_card_number",
1737
+ "Validate a credit card number via the Luhn checksum and detect its network (Visa, Mastercard, American Express, Discover, Diners Club, JCB, Maestro, UnionPay) from its prefix and length. No card data is stored or transmitted.",
1738
+ { number: z.string().describe("The card number, digits with optional spaces or hyphens") },
1739
+ async ({ number }) => {
1740
+ const result = _validateCardNumber(number);
1741
+ if ("error" in result) return errText(result);
1742
+ return text(JSON.stringify(result, null, 2));
1743
+ }
1744
+ );
1745
+ var _IBAN_COUNTRY_LENGTHS = {
1746
+ AD: 24,
1747
+ AE: 23,
1748
+ AL: 28,
1749
+ AT: 20,
1750
+ AZ: 28,
1751
+ BA: 20,
1752
+ BE: 16,
1753
+ BG: 22,
1754
+ BH: 22,
1755
+ BR: 29,
1756
+ BY: 28,
1757
+ CH: 21,
1758
+ CR: 22,
1759
+ CY: 28,
1760
+ CZ: 24,
1761
+ DE: 22,
1762
+ DK: 18,
1763
+ DO: 28,
1764
+ EE: 20,
1765
+ EG: 29,
1766
+ ES: 24,
1767
+ FI: 18,
1768
+ FO: 18,
1769
+ FR: 27,
1770
+ GB: 22,
1771
+ GE: 22,
1772
+ GI: 23,
1773
+ GL: 18,
1774
+ GR: 27,
1775
+ GT: 28,
1776
+ HR: 21,
1777
+ HU: 28,
1778
+ IE: 22,
1779
+ IL: 23,
1780
+ IQ: 23,
1781
+ IS: 26,
1782
+ IT: 27,
1783
+ JO: 30,
1784
+ KW: 30,
1785
+ KZ: 20,
1786
+ LB: 28,
1787
+ LC: 32,
1788
+ LI: 21,
1789
+ LT: 20,
1790
+ LU: 20,
1791
+ LV: 21,
1792
+ LY: 25,
1793
+ MC: 27,
1794
+ MD: 24,
1795
+ ME: 22,
1796
+ MK: 19,
1797
+ MR: 27,
1798
+ MT: 31,
1799
+ MU: 30,
1800
+ NL: 18,
1801
+ NO: 15,
1802
+ PK: 24,
1803
+ PL: 28,
1804
+ PS: 29,
1805
+ PT: 25,
1806
+ QA: 29,
1807
+ RO: 24,
1808
+ RS: 22,
1809
+ SA: 24,
1810
+ SC: 31,
1811
+ SE: 24,
1812
+ SI: 19,
1813
+ SK: 24,
1814
+ SM: 27,
1815
+ ST: 25,
1816
+ SV: 28,
1817
+ TL: 23,
1818
+ TN: 24,
1819
+ TR: 26,
1820
+ UA: 29,
1821
+ VA: 22,
1822
+ VG: 24,
1823
+ XK: 20
1824
+ };
1825
+ function _ibanMod97(numeric) {
1826
+ let remainder = 0;
1827
+ for (let i = 0; i < numeric.length; i += 7) {
1828
+ remainder = parseInt(String(remainder) + numeric.substr(i, 7), 10) % 97;
1829
+ }
1830
+ return remainder;
1831
+ }
1832
+ function _formatIban(cleaned) {
1833
+ return cleaned.match(/.{1,4}/g)?.join(" ") ?? cleaned;
1834
+ }
1835
+ function _validateIban(input) {
1836
+ if (!input || !input.trim()) return { error: "Input is empty" };
1837
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
1838
+ if (!/^[A-Z0-9]+$/.test(cleaned)) return { error: "IBAN must contain only letters and digits" };
1839
+ if (cleaned.length < 4 || cleaned.length > 34) {
1840
+ return { error: `IBAN length (${cleaned.length}) is outside the valid range of 4-34 characters` };
1841
+ }
1842
+ if (!/^[A-Z]{2}\d{2}/.test(cleaned)) {
1843
+ return { error: "IBAN must start with a 2-letter country code followed by 2 check digits" };
1844
+ }
1845
+ const countryCode = cleaned.slice(0, 2);
1846
+ const checkDigits = cleaned.slice(2, 4);
1847
+ const bban = cleaned.slice(4);
1848
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
1849
+ const numeric = rearranged.split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
1850
+ const checksumValid = _ibanMod97(numeric) === 1;
1851
+ const expectedLength = _IBAN_COUNTRY_LENGTHS[countryCode] ?? null;
1852
+ const lengthValid = expectedLength === null ? null : cleaned.length === expectedLength;
1853
+ const valid = checksumValid && lengthValid !== false;
1854
+ return {
1855
+ valid,
1856
+ formatted: _formatIban(cleaned),
1857
+ countryCode,
1858
+ checkDigits,
1859
+ bban,
1860
+ length: cleaned.length,
1861
+ expectedLength,
1862
+ checksumValid,
1863
+ lengthValid
1864
+ };
1865
+ }
1866
+ server.tool(
1867
+ "validate_iban",
1868
+ "Validate an IBAN via the mod-97 checksum defined by ISO 13616 and format it into the standard human-readable grouped form. Cross-checks the total length against the country's registered fixed length where known.",
1869
+ { iban: z.string().describe("The IBAN, with or without spaces") },
1870
+ async ({ iban }) => {
1871
+ const result = _validateIban(iban);
1872
+ if ("error" in result) return errText(result);
1873
+ return text(JSON.stringify(result, null, 2));
1874
+ }
1875
+ );
1876
+ function _roundMoneyMcp(n) {
1877
+ return Math.round(n * 100) / 100;
1878
+ }
1879
+ function _calculateLoan(principal, annualRatePercent, termMonths) {
1880
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
1881
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
1882
+ return { error: "Annual interest rate must be zero or a positive number" };
1883
+ }
1884
+ if (!Number.isInteger(termMonths) || termMonths <= 0) {
1885
+ return { error: "Term must be a positive whole number of months" };
1886
+ }
1887
+ const monthlyRate = annualRatePercent / 100 / 12;
1888
+ let monthlyPayment;
1889
+ if (monthlyRate === 0) {
1890
+ monthlyPayment = principal / termMonths;
1891
+ } else {
1892
+ const factor = Math.pow(1 + monthlyRate, termMonths);
1893
+ monthlyPayment = principal * monthlyRate * factor / (factor - 1);
1894
+ }
1895
+ const schedule = [];
1896
+ let balance = _roundMoneyMcp(principal);
1897
+ let totalPayment = 0;
1898
+ for (let month = 1; month <= termMonths; month++) {
1899
+ const interest = monthlyRate === 0 ? 0 : _roundMoneyMcp(balance * monthlyRate);
1900
+ let principalPortion = _roundMoneyMcp(monthlyPayment) - interest;
1901
+ if (month === termMonths || principalPortion > balance) principalPortion = balance;
1902
+ const payment = _roundMoneyMcp(principalPortion + interest);
1903
+ balance = _roundMoneyMcp(balance - principalPortion);
1904
+ totalPayment += payment;
1905
+ schedule.push({ month, payment, principal: _roundMoneyMcp(principalPortion), interest, balance: Math.max(balance, 0) });
1906
+ }
1907
+ return {
1908
+ monthlyPayment: _roundMoneyMcp(monthlyPayment),
1909
+ totalPayment: _roundMoneyMcp(totalPayment),
1910
+ totalInterest: _roundMoneyMcp(totalPayment - principal),
1911
+ termMonths,
1912
+ schedule
1913
+ };
1914
+ }
1915
+ server.tool(
1916
+ "calculate_loan",
1917
+ "Compute the fixed monthly payment, total interest, and full month-by-month amortization schedule for a loan or mortgage from principal, annual interest rate (as a percent), and term in months.",
1918
+ {
1919
+ principal: z.number().describe("Loan principal amount"),
1920
+ annualRatePercent: z.number().describe("Annual interest rate as a percent, e.g. 6.5"),
1921
+ termMonths: z.number().int().describe("Loan term in months")
1922
+ },
1923
+ async ({ principal, annualRatePercent, termMonths }) => {
1924
+ const result = _calculateLoan(principal, annualRatePercent, termMonths);
1925
+ if ("error" in result) return errText(result);
1926
+ return text(JSON.stringify(result, null, 2));
1927
+ }
1928
+ );
1582
1929
  server.tool(
1583
1930
  "format_ndjson",
1584
1931
  "Validate newline-delimited JSON (NDJSON/JSON Lines), report per-line errors, and pretty-print each valid line.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.10.0",
4
- "description": "MCP server exposing 107 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.12.0",
4
+ "description": "MCP server exposing 111 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
5
5
  "author": "Utilix <hello@utilix.tech>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://utilix.tech/mcp",