@utilix-tech/mcp 0.5.0 → 0.6.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 +8 -2
  2. package/dist/index.js +190 -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 101 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 |
@@ -234,6 +235,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
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 |
236
237
 
238
+ ### PDF (1 tool)
239
+ | Tool | Description |
240
+ |------|-------------|
241
+ | `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
242
+
237
243
  ## Requirements
238
244
 
239
245
  - Node.js 18+
package/dist/index.js CHANGED
@@ -1496,5 +1496,195 @@ 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
+ function _parseJwks(input) {
1645
+ const trimmed = input.trim();
1646
+ if (!trimmed) return { error: "No input provided" };
1647
+ let data;
1648
+ try {
1649
+ data = JSON.parse(trimmed);
1650
+ } catch (e) {
1651
+ return { error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
1652
+ }
1653
+ let rawKeys;
1654
+ if (data && typeof data === "object" && Array.isArray(data.keys)) {
1655
+ rawKeys = data.keys;
1656
+ } else if (data && typeof data === "object" && typeof data.kty === "string") {
1657
+ rawKeys = [data];
1658
+ } else {
1659
+ return { error: 'Input must be a JWKS object with a "keys" array, or a single JWK object with a "kty" field' };
1660
+ }
1661
+ const base64urlByteLength = (s) => Math.floor(s.replace(/=+$/, "").length * 6 / 8);
1662
+ const keys = rawKeys.map((k) => {
1663
+ const key = k ?? {};
1664
+ const kty = typeof key.kty === "string" ? key.kty : "unknown";
1665
+ let keySize = null;
1666
+ let curve;
1667
+ if (kty === "RSA" && typeof key.n === "string") keySize = base64urlByteLength(key.n) * 8;
1668
+ else if ((kty === "EC" || kty === "OKP") && typeof key.crv === "string") curve = key.crv;
1669
+ else if (kty === "oct" && typeof key.k === "string") keySize = base64urlByteLength(key.k) * 8;
1670
+ return {
1671
+ kty,
1672
+ use: typeof key.use === "string" ? key.use : void 0,
1673
+ alg: typeof key.alg === "string" ? key.alg : void 0,
1674
+ kid: typeof key.kid === "string" ? key.kid : void 0,
1675
+ keySize,
1676
+ curve
1677
+ };
1678
+ });
1679
+ const kidCounts = /* @__PURE__ */ new Map();
1680
+ for (const k of keys) if (k.kid) kidCounts.set(k.kid, (kidCounts.get(k.kid) ?? 0) + 1);
1681
+ const duplicateKids = [...kidCounts.entries()].filter(([, c]) => c > 1).map(([kid]) => kid);
1682
+ return { keys, count: keys.length, duplicateKids };
1683
+ }
1684
+ 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 }) => {
1685
+ const result = _parseJwks(input);
1686
+ if ("error" in result) return errText(result);
1687
+ return text(JSON.stringify(result, null, 2));
1688
+ });
1499
1689
  var transport = new StdioServerTransport();
1500
1690
  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.6.0",
4
+ "description": "MCP server exposing 101 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",