@utilix-tech/mcp 0.11.0 → 0.13.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 +7 -0
- package/README.md +7 -3
- package/dist/index.js +257 -0
- package/package.json +2 -2
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.13.0
|
|
6
|
+
- Added ICO Favicon Inspector (`read_ico_info`) and Barcode Checksum Validator (`validate_barcode`).
|
|
7
|
+
|
|
8
|
+
## 0.12.0
|
|
9
|
+
- Added IBAN Validator / Formatter (`validate_iban`) and Loan / Mortgage Calculator (`calculate_loan`).
|
|
10
|
+
- (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.)
|
|
11
|
+
|
|
5
12
|
## 0.11.0
|
|
6
13
|
- Added robots.txt Validator (`validate_robots_txt`) and Credit Card Number Validator (`validate_card_number`).
|
|
7
14
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 113 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,12 +145,15 @@ 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 (
|
|
148
|
+
### Units (6 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
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 |
|
|
156
|
+
| `validate_barcode` | Validate an EAN-13, EAN-8, or UPC-A barcode via its weighted mod-10 check digit |
|
|
154
157
|
|
|
155
158
|
### API / Network (8 tools)
|
|
156
159
|
| Tool | Description |
|
|
@@ -235,11 +238,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
235
238
|
| `compress_json` | Compress JSON for LLM context budgets |
|
|
236
239
|
| `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
|
|
237
240
|
|
|
238
|
-
### Image (
|
|
241
|
+
### Image (3 tools)
|
|
239
242
|
| Tool | Description |
|
|
240
243
|
|------|-------------|
|
|
241
244
|
| `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
|
|
242
245
|
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
246
|
+
| `read_ico_info` | Read how many images a .ico file embeds and each one's width, height, color depth, and size from its ICONDIR header |
|
|
243
247
|
|
|
244
248
|
### PDF (1 tool)
|
|
245
249
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -1742,6 +1742,224 @@ server.tool(
|
|
|
1742
1742
|
return text(JSON.stringify(result, null, 2));
|
|
1743
1743
|
}
|
|
1744
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
|
+
);
|
|
1929
|
+
var _BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
|
|
1930
|
+
function _computeBarcodeCheckDigit(dataDigits) {
|
|
1931
|
+
let sum = 0;
|
|
1932
|
+
const n = dataDigits.length;
|
|
1933
|
+
for (let i = 0; i < n; i++) {
|
|
1934
|
+
const distanceFromEnd = n - 1 - i;
|
|
1935
|
+
const weight = distanceFromEnd % 2 === 0 ? 3 : 1;
|
|
1936
|
+
sum += dataDigits[i] * weight;
|
|
1937
|
+
}
|
|
1938
|
+
return (10 - sum % 10) % 10;
|
|
1939
|
+
}
|
|
1940
|
+
function _validateBarcode(input) {
|
|
1941
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
1942
|
+
const digits = input.replace(/[\s-]/g, "");
|
|
1943
|
+
if (!/^\d+$/.test(digits)) return { error: "Barcode must contain only digits (spaces and hyphens are ignored)" };
|
|
1944
|
+
const format = _BARCODE_FORMAT_BY_LENGTH[digits.length];
|
|
1945
|
+
if (!format) {
|
|
1946
|
+
return { error: `Unsupported barcode length (${digits.length} digits); expected 8 (EAN-8), 12 (UPC-A), or 13 (EAN-13)` };
|
|
1947
|
+
}
|
|
1948
|
+
const dataDigits = digits.slice(0, -1).split("").map(Number);
|
|
1949
|
+
const checkDigit = Number(digits[digits.length - 1]);
|
|
1950
|
+
const expectedCheckDigit = _computeBarcodeCheckDigit(dataDigits);
|
|
1951
|
+
return { valid: checkDigit === expectedCheckDigit, format, digits, checkDigit, expectedCheckDigit };
|
|
1952
|
+
}
|
|
1953
|
+
server.tool(
|
|
1954
|
+
"validate_barcode",
|
|
1955
|
+
"Validate an EAN-13, EAN-8, or UPC-A barcode number via its weighted mod-10 check digit algorithm. The format is inferred from the digit length (8, 12, or 13).",
|
|
1956
|
+
{ barcode: z.string().describe("The barcode digits, with optional spaces or hyphens") },
|
|
1957
|
+
async ({ barcode }) => {
|
|
1958
|
+
const result = _validateBarcode(barcode);
|
|
1959
|
+
if ("error" in result) return errText(result);
|
|
1960
|
+
return text(JSON.stringify(result, null, 2));
|
|
1961
|
+
}
|
|
1962
|
+
);
|
|
1745
1963
|
server.tool(
|
|
1746
1964
|
"format_ndjson",
|
|
1747
1965
|
"Validate newline-delimited JSON (NDJSON/JSON Lines), report per-line errors, and pretty-print each valid line.",
|
|
@@ -2485,6 +2703,45 @@ server.tool("read_id3_tags", "Read ID3 tags (title, artist, album, year, genre,
|
|
|
2485
2703
|
if ("error" in result) return errText(result);
|
|
2486
2704
|
return text(JSON.stringify(result, null, 2));
|
|
2487
2705
|
});
|
|
2706
|
+
function _readU16LE(bytes, offset) {
|
|
2707
|
+
return bytes[offset] | bytes[offset + 1] << 8;
|
|
2708
|
+
}
|
|
2709
|
+
function _readU32LE(bytes, offset) {
|
|
2710
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
|
|
2711
|
+
}
|
|
2712
|
+
function _readIcoInfo(bytes) {
|
|
2713
|
+
if (bytes.length < 6) return { error: "File too small to be a valid ICO file" };
|
|
2714
|
+
if (_readU16LE(bytes, 0) !== 0) return { error: "Not a valid ICO file (reserved header field must be 0)" };
|
|
2715
|
+
const type = _readU16LE(bytes, 2);
|
|
2716
|
+
if (type === 2) return { error: "This is a .cur cursor file (type 2), not a .ico icon file" };
|
|
2717
|
+
if (type !== 1) return { error: `Unrecognized ICONDIR type (${type}); expected 1 for an icon file` };
|
|
2718
|
+
const count = _readU16LE(bytes, 4);
|
|
2719
|
+
if (count === 0) return { error: "ICO file declares zero embedded images" };
|
|
2720
|
+
const images = [];
|
|
2721
|
+
for (let i = 0; i < count; i++) {
|
|
2722
|
+
const offset = 6 + i * 16;
|
|
2723
|
+
if (offset + 16 > bytes.length) return { error: `Truncated ICONDIRENTRY for image ${i}` };
|
|
2724
|
+
const widthRaw = bytes[offset];
|
|
2725
|
+
const heightRaw = bytes[offset + 1];
|
|
2726
|
+
const colorCountRaw = bytes[offset + 2];
|
|
2727
|
+
images.push({
|
|
2728
|
+
width: widthRaw === 0 ? 256 : widthRaw,
|
|
2729
|
+
height: heightRaw === 0 ? 256 : heightRaw,
|
|
2730
|
+
colorCount: colorCountRaw === 0 ? null : colorCountRaw,
|
|
2731
|
+
planes: _readU16LE(bytes, offset + 4),
|
|
2732
|
+
bitCount: _readU16LE(bytes, offset + 6),
|
|
2733
|
+
bytesInRes: _readU32LE(bytes, offset + 8),
|
|
2734
|
+
imageOffset: _readU32LE(bytes, offset + 12)
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
return { imageCount: count, images };
|
|
2738
|
+
}
|
|
2739
|
+
server.tool("read_ico_info", "Read a .ico file's ICONDIR/ICONDIRENTRY header table directly from its bytes to report how many images it embeds and each one's width, height, color depth, and size. No image decoding required.", { ico_base64: z.string().describe("Base64-encoded ICO file bytes, optionally prefixed with a data: URL scheme") }, async ({ ico_base64 }) => {
|
|
2740
|
+
const bytes = new Uint8Array(Buffer.from(ico_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
2741
|
+
const result = _readIcoInfo(bytes);
|
|
2742
|
+
if ("error" in result) return errText(result);
|
|
2743
|
+
return text(JSON.stringify(result, null, 2));
|
|
2744
|
+
});
|
|
2488
2745
|
function _parseEnvForDiff(input) {
|
|
2489
2746
|
const entries = [];
|
|
2490
2747
|
const errors = [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "MCP server exposing 113 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",
|