@utilix-tech/mcp 0.5.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +10 -3
  2. package/dist/index.js +380 -0
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 99 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 102 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
 
@@ -150,10 +150,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
150
150
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
151
151
  | `px_to_units` | Convert pixels to rem, em, pt, and vw |
152
152
 
153
- ### API / Network (5 tools)
153
+ ### API / Network (6 tools)
154
154
  | Tool | Description |
155
155
  |------|-------------|
156
156
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
157
+ | `parse_jwks` | Parse a JWKS (or a single bare JWK): type, use, algorithm, key ID, and size per key |
157
158
  | `build_curl` | Build a cURL command from method, URL, headers, and body |
158
159
  | `curl_to_code` | Convert a cURL command to fetch, axios, Python, Go, PHP, or Ruby |
159
160
  | `cors_headers` | Generate CORS response headers for a configuration |
@@ -229,10 +230,16 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
229
230
  | `compress_json` | Compress JSON for LLM context budgets |
230
231
  | `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
231
232
 
232
- ### Image (1 tool)
233
+ ### Image (2 tools)
233
234
  | Tool | Description |
234
235
  |------|-------------|
235
236
  | `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
237
+ | `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
238
+
239
+ ### PDF (1 tool)
240
+ | Tool | Description |
241
+ |------|-------------|
242
+ | `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
236
243
 
237
244
  ## Requirements
238
245
 
package/dist/index.js CHANGED
@@ -1496,5 +1496,385 @@ server.tool(
1496
1496
  return text(formatted);
1497
1497
  }
1498
1498
  );
1499
+ function _decodePdfString(raw, isHex) {
1500
+ const bytes = [];
1501
+ if (isHex) {
1502
+ const hex = raw.replace(/\s+/g, "");
1503
+ const padded = hex.length % 2 === 0 ? hex : hex + "0";
1504
+ for (let i = 0; i < padded.length; i += 2) bytes.push(parseInt(padded.slice(i, i + 2), 16));
1505
+ } else {
1506
+ const s = raw.replace(/\\\r\n|\\\r|\\\n/g, "");
1507
+ for (let i = 0; i < s.length; i++) {
1508
+ const ch = s[i];
1509
+ if (ch === "\\" && i + 1 < s.length) {
1510
+ const next = s[i + 1];
1511
+ if (next === "n") {
1512
+ bytes.push(10);
1513
+ i++;
1514
+ } else if (next === "r") {
1515
+ bytes.push(13);
1516
+ i++;
1517
+ } else if (next === "t") {
1518
+ bytes.push(9);
1519
+ i++;
1520
+ } else if (next === "b") {
1521
+ bytes.push(8);
1522
+ i++;
1523
+ } else if (next === "f") {
1524
+ bytes.push(12);
1525
+ i++;
1526
+ } else if (next === "(" || next === ")" || next === "\\") {
1527
+ bytes.push(next.charCodeAt(0));
1528
+ i++;
1529
+ } else if (/[0-7]/.test(next)) {
1530
+ let oct = next;
1531
+ i++;
1532
+ for (let k = 0; k < 2 && i + 1 < s.length && /[0-7]/.test(s[i + 1]); k++) {
1533
+ i++;
1534
+ oct += s[i];
1535
+ }
1536
+ bytes.push(parseInt(oct, 8) & 255);
1537
+ } else {
1538
+ bytes.push(next.charCodeAt(0));
1539
+ i++;
1540
+ }
1541
+ } else {
1542
+ bytes.push(ch.charCodeAt(0) & 255);
1543
+ }
1544
+ }
1545
+ }
1546
+ if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) {
1547
+ let out = "";
1548
+ for (let i = 2; i + 1 < bytes.length; i += 2) out += String.fromCharCode(bytes[i] << 8 | bytes[i + 1]);
1549
+ return out;
1550
+ }
1551
+ return bytes.map((b) => String.fromCharCode(b)).join("");
1552
+ }
1553
+ function _extractPdfField(dict, key) {
1554
+ const litMatch = dict.match(new RegExp(`/${key}\\s*\\(((?:\\\\.|[^\\\\()])*)\\)`));
1555
+ if (litMatch) return _decodePdfString(litMatch[1], false);
1556
+ const hexMatch = dict.match(new RegExp(`/${key}\\s*<([0-9A-Fa-f\\s]*)>`));
1557
+ if (hexMatch) return _decodePdfString(hexMatch[1], true);
1558
+ return null;
1559
+ }
1560
+ function _findPdfObject(text2, objNum, gen) {
1561
+ const m = text2.match(new RegExp(`(?:^|[^0-9])${objNum}\\s+${gen}\\s+obj([\\s\\S]*?)endobj`));
1562
+ return m ? m[1] : null;
1563
+ }
1564
+ function _parsePdfDate(raw) {
1565
+ const m = raw.match(/^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?([Z+-])?(\d{2})?'?(\d{2})?'?/);
1566
+ if (!m) return null;
1567
+ const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", tz, tzh, tzm] = m;
1568
+ let iso = `${y}-${mo}-${d}T${h}:${mi}:${s}`;
1569
+ if (tz === "Z") iso += "Z";
1570
+ else if (tz === "+" || tz === "-") iso += `${tz}${tzh ?? "00"}:${tzm ?? "00"}`;
1571
+ return iso;
1572
+ }
1573
+ function _readPdfMetadata(bytes) {
1574
+ if (bytes.length < 8) return { error: "File too small to be a PDF" };
1575
+ const latin1 = (b) => {
1576
+ let out = "";
1577
+ for (let i = 0; i < b.length; i += 32768) out += String.fromCharCode(...b.subarray(i, i + 32768));
1578
+ return out;
1579
+ };
1580
+ const header = latin1(bytes.subarray(0, 16));
1581
+ const versionMatch = header.match(/%PDF-(\d\.\d)/);
1582
+ if (!versionMatch) return { error: "Not a valid PDF file (missing %PDF header)" };
1583
+ const version = versionMatch[1];
1584
+ const text2 = latin1(bytes);
1585
+ const trailerMatches = [...text2.matchAll(/trailer\s*<<([\s\S]*?)>>/g)];
1586
+ let infoDict = null;
1587
+ let encrypted = false;
1588
+ if (trailerMatches.length > 0) {
1589
+ const trailer = trailerMatches[trailerMatches.length - 1][1];
1590
+ encrypted = /\/Encrypt\s+\d+\s+\d+\s+R/.test(trailer);
1591
+ const infoRef = trailer.match(/\/Info\s+(\d+)\s+(\d+)\s+R/);
1592
+ if (infoRef) infoDict = _findPdfObject(text2, Number(infoRef[1]), Number(infoRef[2]));
1593
+ }
1594
+ const objBlocks = [...text2.matchAll(/(\d+)\s+(\d+)\s+obj([\s\S]*?)endobj/g)];
1595
+ if (!infoDict) {
1596
+ for (const b of objBlocks) {
1597
+ if (/\/(Title|Author|Producer|Creator|CreationDate)\b/.test(b[3]) && !/\/Type\s*\/(Page|Pages|Catalog|Font)\b/.test(b[3])) {
1598
+ infoDict = b[3];
1599
+ break;
1600
+ }
1601
+ }
1602
+ }
1603
+ const title = infoDict ? _extractPdfField(infoDict, "Title") : null;
1604
+ const author = infoDict ? _extractPdfField(infoDict, "Author") : null;
1605
+ const subject = infoDict ? _extractPdfField(infoDict, "Subject") : null;
1606
+ const keywords = infoDict ? _extractPdfField(infoDict, "Keywords") : null;
1607
+ const creator = infoDict ? _extractPdfField(infoDict, "Creator") : null;
1608
+ const producer = infoDict ? _extractPdfField(infoDict, "Producer") : null;
1609
+ const creationDateRaw = infoDict ? _extractPdfField(infoDict, "CreationDate") : null;
1610
+ const modDateRaw = infoDict ? _extractPdfField(infoDict, "ModDate") : null;
1611
+ let pageCount = null;
1612
+ for (const b of objBlocks) {
1613
+ if (!/\/Type\s*\/Pages\b/.test(b[3])) continue;
1614
+ const countMatch = b[3].match(/\/Count\s+(\d+)/);
1615
+ if (countMatch) {
1616
+ const c = Number(countMatch[1]);
1617
+ if (pageCount === null || c > pageCount) pageCount = c;
1618
+ }
1619
+ }
1620
+ if (pageCount === null) {
1621
+ const pageMatches = text2.match(/\/Type\s*\/Page(?!s)\b/g);
1622
+ if (pageMatches) pageCount = pageMatches.length;
1623
+ }
1624
+ return {
1625
+ version,
1626
+ pageCount,
1627
+ title,
1628
+ author,
1629
+ subject,
1630
+ keywords,
1631
+ creator,
1632
+ producer,
1633
+ creationDate: creationDateRaw ? _parsePdfDate(creationDateRaw) ?? creationDateRaw : null,
1634
+ modDate: modDateRaw ? _parsePdfDate(modDateRaw) ?? modDateRaw : null,
1635
+ encrypted
1636
+ };
1637
+ }
1638
+ server.tool("read_pdf_metadata", "Read PDF metadata (title, author, dates, page count, encryption flag) directly from the trailer/Info dictionary: no PDF rendering library required.", { pdf_base64: z.string().describe("Base64-encoded PDF bytes, optionally prefixed with a data: URL scheme") }, async ({ pdf_base64 }) => {
1639
+ const bytes = new Uint8Array(Buffer.from(pdf_base64.replace(/^data:.*;base64,/, ""), "base64"));
1640
+ const result = _readPdfMetadata(bytes);
1641
+ if ("error" in result) return errText(result);
1642
+ return text(JSON.stringify(result, null, 2));
1643
+ });
1644
+ var _EXIF_ORIENTATION_LABELS = {
1645
+ 1: "Normal",
1646
+ 2: "Mirrored horizontal",
1647
+ 3: "Rotated 180\xB0",
1648
+ 4: "Mirrored vertical",
1649
+ 5: "Mirrored horizontal, rotated 90\xB0 CCW",
1650
+ 6: "Rotated 90\xB0 CW",
1651
+ 7: "Mirrored horizontal, rotated 90\xB0 CW",
1652
+ 8: "Rotated 90\xB0 CCW"
1653
+ };
1654
+ function _tu16(r, o) {
1655
+ return r.view.getUint16(o, r.little);
1656
+ }
1657
+ function _tu32(r, o) {
1658
+ return r.view.getUint32(o, r.little);
1659
+ }
1660
+ function _readIfdEntries(r, ifdOffset) {
1661
+ if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
1662
+ const numEntries = _tu16(r, ifdOffset);
1663
+ const entries = [];
1664
+ for (let i = 0; i < numEntries; i++) {
1665
+ const entryOffset = ifdOffset + 2 + i * 12;
1666
+ if (entryOffset + 12 > r.bytes.length) break;
1667
+ entries.push({ tag: _tu16(r, entryOffset), type: _tu16(r, entryOffset + 2), count: _tu32(r, entryOffset + 4), valueOffsetField: entryOffset + 8 });
1668
+ }
1669
+ return { entries };
1670
+ }
1671
+ function _findExifEntry(entries, tag) {
1672
+ return entries.find((e) => e.tag === tag);
1673
+ }
1674
+ function _readExifAscii(r, entry) {
1675
+ const size = entry.count;
1676
+ const inline = size <= 4;
1677
+ const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + _tu32(r, entry.valueOffsetField);
1678
+ if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
1679
+ let out = "";
1680
+ for (let i = 0; i < size; i++) {
1681
+ const c = r.bytes[dataOffset + i];
1682
+ if (c === 0) break;
1683
+ out += String.fromCharCode(c);
1684
+ }
1685
+ const trimmed = out.trim();
1686
+ return trimmed || null;
1687
+ }
1688
+ function _readExifRational(r, entry) {
1689
+ const dataOffset = r.tiffStart + _tu32(r, entry.valueOffsetField);
1690
+ if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
1691
+ return { num: _tu32(r, dataOffset), den: _tu32(r, dataOffset + 4) };
1692
+ }
1693
+ function _readExifGpsCoordinate(r, entries, valueTag, refTag) {
1694
+ const valueEntry = _findExifEntry(entries, valueTag);
1695
+ if (!valueEntry) return null;
1696
+ const dataOffset = r.tiffStart + _tu32(r, valueEntry.valueOffsetField);
1697
+ if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
1698
+ const deg = _tu32(r, dataOffset) / (_tu32(r, dataOffset + 4) || 1);
1699
+ const min = _tu32(r, dataOffset + 8) / (_tu32(r, dataOffset + 12) || 1);
1700
+ const sec = _tu32(r, dataOffset + 16) / (_tu32(r, dataOffset + 20) || 1);
1701
+ let decimal = deg + min / 60 + sec / 3600;
1702
+ const refEntry = _findExifEntry(entries, refTag);
1703
+ const ref = refEntry ? _readExifAscii(r, refEntry) : null;
1704
+ if (ref === "S" || ref === "W") decimal = -decimal;
1705
+ return Math.round(decimal * 1e6) / 1e6;
1706
+ }
1707
+ function _gcdExif(a, b) {
1708
+ return b === 0 ? a : _gcdExif(b, a % b);
1709
+ }
1710
+ function _round2Exif(n) {
1711
+ return Math.round(n * 100) / 100;
1712
+ }
1713
+ function _formatExposureTime(num, den) {
1714
+ if (num === 0) return "0s";
1715
+ if (num >= den) return `${_round2Exif(num / den)}s`;
1716
+ const divisor = _gcdExif(num, den) || 1;
1717
+ const n = num / divisor;
1718
+ const d = den / divisor;
1719
+ return `1/${Math.round(d / n)}`;
1720
+ }
1721
+ function _readExifData(bytes) {
1722
+ if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
1723
+ return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
1724
+ }
1725
+ let offset = 2;
1726
+ let exifStart = -1;
1727
+ while (offset + 4 <= bytes.length) {
1728
+ if (bytes[offset] !== 255) {
1729
+ offset++;
1730
+ continue;
1731
+ }
1732
+ const marker = bytes[offset + 1];
1733
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
1734
+ offset += 2;
1735
+ continue;
1736
+ }
1737
+ if (marker === 217 || marker === 218) break;
1738
+ const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
1739
+ if (marker === 225) {
1740
+ const idOffset = offset + 4;
1741
+ if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
1742
+ exifStart = idOffset + 6;
1743
+ break;
1744
+ }
1745
+ }
1746
+ offset += 2 + segmentLength;
1747
+ }
1748
+ if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
1749
+ if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
1750
+ const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
1751
+ if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
1752
+ const little = byteOrder === "II";
1753
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1754
+ const r = { bytes, view, little, tiffStart: exifStart };
1755
+ if (_tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
1756
+ const ifd0Offset = exifStart + _tu32(r, exifStart + 4);
1757
+ const ifd0 = _readIfdEntries(r, ifd0Offset);
1758
+ if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
1759
+ const result = { tagCount: ifd0.entries.length };
1760
+ const makeEntry = _findExifEntry(ifd0.entries, 271);
1761
+ if (makeEntry) {
1762
+ const v = _readExifAscii(r, makeEntry);
1763
+ if (v) result.make = v;
1764
+ }
1765
+ const modelEntry = _findExifEntry(ifd0.entries, 272);
1766
+ if (modelEntry) {
1767
+ const v = _readExifAscii(r, modelEntry);
1768
+ if (v) result.model = v;
1769
+ }
1770
+ const softwareEntry = _findExifEntry(ifd0.entries, 305);
1771
+ if (softwareEntry) {
1772
+ const v = _readExifAscii(r, softwareEntry);
1773
+ if (v) result.software = v;
1774
+ }
1775
+ const dateTimeEntry = _findExifEntry(ifd0.entries, 306);
1776
+ if (dateTimeEntry) {
1777
+ const v = _readExifAscii(r, dateTimeEntry);
1778
+ if (v) result.dateTime = v;
1779
+ }
1780
+ const orientEntry = _findExifEntry(ifd0.entries, 274);
1781
+ if (orientEntry) {
1782
+ const v = _tu16(r, orientEntry.valueOffsetField);
1783
+ result.orientation = v;
1784
+ result.orientationLabel = _EXIF_ORIENTATION_LABELS[v] ?? "Unknown";
1785
+ }
1786
+ const exifIfdPtr = _findExifEntry(ifd0.entries, 34665);
1787
+ if (exifIfdPtr) {
1788
+ const subOffset = exifStart + _tu32(r, exifIfdPtr.valueOffsetField);
1789
+ const sub = _readIfdEntries(r, subOffset);
1790
+ if (sub) {
1791
+ const dtoEntry = _findExifEntry(sub.entries, 36867);
1792
+ if (dtoEntry) {
1793
+ const v = _readExifAscii(r, dtoEntry);
1794
+ if (v) result.dateTimeOriginal = v;
1795
+ }
1796
+ const expEntry = _findExifEntry(sub.entries, 33434);
1797
+ if (expEntry) {
1798
+ const rat = _readExifRational(r, expEntry);
1799
+ if (rat && rat.den !== 0) result.exposureTime = _formatExposureTime(rat.num, rat.den);
1800
+ }
1801
+ const fEntry = _findExifEntry(sub.entries, 33437);
1802
+ if (fEntry) {
1803
+ const rat = _readExifRational(r, fEntry);
1804
+ if (rat && rat.den !== 0) result.fNumber = _round2Exif(rat.num / rat.den);
1805
+ }
1806
+ const isoEntry = _findExifEntry(sub.entries, 34855);
1807
+ if (isoEntry) result.isoSpeed = _tu16(r, isoEntry.valueOffsetField);
1808
+ const focalEntry = _findExifEntry(sub.entries, 37386);
1809
+ if (focalEntry) {
1810
+ const rat = _readExifRational(r, focalEntry);
1811
+ if (rat && rat.den !== 0) result.focalLength = _round2Exif(rat.num / rat.den);
1812
+ }
1813
+ }
1814
+ }
1815
+ const gpsIfdPtr = _findExifEntry(ifd0.entries, 34853);
1816
+ if (gpsIfdPtr) {
1817
+ const gpsOffset = exifStart + _tu32(r, gpsIfdPtr.valueOffsetField);
1818
+ const gps = _readIfdEntries(r, gpsOffset);
1819
+ if (gps) {
1820
+ const lat = _readExifGpsCoordinate(r, gps.entries, 2, 1);
1821
+ const lon = _readExifGpsCoordinate(r, gps.entries, 4, 3);
1822
+ if (lat !== null) result.gpsLatitude = lat;
1823
+ if (lon !== null) result.gpsLongitude = lon;
1824
+ }
1825
+ }
1826
+ return result;
1827
+ }
1828
+ server.tool("read_exif_data", "Read common EXIF tags (camera make/model, orientation, timestamps, exposure settings, GPS coordinates) directly from a JPEG's APP1 segment. Only JPEG is supported.", { image_base64: z.string().describe("Base64-encoded JPEG bytes, optionally prefixed with a data: URL scheme") }, async ({ image_base64 }) => {
1829
+ const bytes = new Uint8Array(Buffer.from(image_base64.replace(/^data:.*;base64,/, ""), "base64"));
1830
+ const result = _readExifData(bytes);
1831
+ if ("error" in result) return errText(result);
1832
+ return text(JSON.stringify(result, null, 2));
1833
+ });
1834
+ function _parseJwks(input) {
1835
+ const trimmed = input.trim();
1836
+ if (!trimmed) return { error: "No input provided" };
1837
+ let data;
1838
+ try {
1839
+ data = JSON.parse(trimmed);
1840
+ } catch (e) {
1841
+ return { error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
1842
+ }
1843
+ let rawKeys;
1844
+ if (data && typeof data === "object" && Array.isArray(data.keys)) {
1845
+ rawKeys = data.keys;
1846
+ } else if (data && typeof data === "object" && typeof data.kty === "string") {
1847
+ rawKeys = [data];
1848
+ } else {
1849
+ return { error: 'Input must be a JWKS object with a "keys" array, or a single JWK object with a "kty" field' };
1850
+ }
1851
+ const base64urlByteLength = (s) => Math.floor(s.replace(/=+$/, "").length * 6 / 8);
1852
+ const keys = rawKeys.map((k) => {
1853
+ const key = k ?? {};
1854
+ const kty = typeof key.kty === "string" ? key.kty : "unknown";
1855
+ let keySize = null;
1856
+ let curve;
1857
+ if (kty === "RSA" && typeof key.n === "string") keySize = base64urlByteLength(key.n) * 8;
1858
+ else if ((kty === "EC" || kty === "OKP") && typeof key.crv === "string") curve = key.crv;
1859
+ else if (kty === "oct" && typeof key.k === "string") keySize = base64urlByteLength(key.k) * 8;
1860
+ return {
1861
+ kty,
1862
+ use: typeof key.use === "string" ? key.use : void 0,
1863
+ alg: typeof key.alg === "string" ? key.alg : void 0,
1864
+ kid: typeof key.kid === "string" ? key.kid : void 0,
1865
+ keySize,
1866
+ curve
1867
+ };
1868
+ });
1869
+ const kidCounts = /* @__PURE__ */ new Map();
1870
+ for (const k of keys) if (k.kid) kidCounts.set(k.kid, (kidCounts.get(k.kid) ?? 0) + 1);
1871
+ const duplicateKids = [...kidCounts.entries()].filter(([, c]) => c > 1).map(([kid]) => kid);
1872
+ return { keys, count: keys.length, duplicateKids };
1873
+ }
1874
+ server.tool("parse_jwks", "Parse a JSON Web Key Set (or a single bare JWK) and summarize each key's type, use, algorithm, key ID, and approximate size. Does not verify or use the keys cryptographically.", { input: z.string().describe('JWKS JSON (a "keys" array) or a single JWK object') }, async ({ input }) => {
1875
+ const result = _parseJwks(input);
1876
+ if ("error" in result) return errText(result);
1877
+ return text(JSON.stringify(result, null, 2));
1878
+ });
1499
1879
  var transport = new StdioServerTransport();
1500
1880
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.5.0",
4
- "description": "MCP server exposing 99 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.7.0",
4
+ "description": "MCP server exposing 102 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",