@utilix-tech/mcp 0.29.0 → 0.30.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +218 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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.30.0
|
|
6
|
+
- Add `read_pdf_page_dimensions` tool: each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree and inheriting from ancestor nodes, plus a common paper size match (Letter, A4, etc.).
|
|
7
|
+
- Add `parse_ipv6_address` tool: expand an IPv6 address to its full 8-group form or compress it to its RFC 5952 canonical shortest form, classify its address type, and handle an embedded IPv4 suffix or zone ID.
|
|
8
|
+
|
|
5
9
|
## 0.29.0
|
|
6
10
|
- Add `read_video_info` tool: duration, resolution, and video/audio codec identifiers from an MP4 (ISO BMFF moov box) or WebM (EBML/Matroska Segment) container header, with no frame decoding or transcoding.
|
|
7
11
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 146 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
|
|
|
@@ -177,7 +177,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
177
177
|
| `calculate_video_bitrate` | Calculate a video's file size from a bitrate and duration, or the bitrate needed to hit a target file size |
|
|
178
178
|
| `calculate_rule_of_72` | Estimate years to double an investment from a rate, or the rate needed to double in a given number of years, via the Rule of 72 shortcut and the exact logarithmic result |
|
|
179
179
|
|
|
180
|
-
### API / Network (
|
|
180
|
+
### API / Network (12 tools)
|
|
181
181
|
| Tool | Description |
|
|
182
182
|
|------|-------------|
|
|
183
183
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -191,6 +191,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
191
191
|
| `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
|
|
192
192
|
| `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
|
|
193
193
|
| `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
|
|
194
|
+
| `parse_ipv6_address` | Expand an IPv6 address to its full 8-group form or compress it to its RFC 5952 canonical shortest form, classify its address type, and handle an embedded IPv4 suffix or zone ID |
|
|
194
195
|
|
|
195
196
|
### Color (4 tools)
|
|
196
197
|
| Tool | Description |
|
|
@@ -274,10 +275,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
274
275
|
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
275
276
|
| `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 |
|
|
276
277
|
|
|
277
|
-
### PDF (
|
|
278
|
+
### PDF (2 tools)
|
|
278
279
|
| Tool | Description |
|
|
279
280
|
|------|-------------|
|
|
280
281
|
| `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
|
|
282
|
+
| `read_pdf_page_dimensions` | List each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree: no PDF rendering library required |
|
|
281
283
|
|
|
282
284
|
### Audio (3 tools)
|
|
283
285
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -2592,6 +2592,131 @@ server.tool("read_pdf_metadata", "Read PDF metadata (title, author, dates, page
|
|
|
2592
2592
|
if ("error" in result) return errText(result);
|
|
2593
2593
|
return text(JSON.stringify(result, null, 2));
|
|
2594
2594
|
});
|
|
2595
|
+
var _PDF_COMMON_PAPER_SIZES = [
|
|
2596
|
+
{ name: "Letter", widthPt: 612, heightPt: 792 },
|
|
2597
|
+
{ name: "Legal", widthPt: 612, heightPt: 1008 },
|
|
2598
|
+
{ name: "Tabloid", widthPt: 792, heightPt: 1224 },
|
|
2599
|
+
{ name: "A3", widthPt: 842, heightPt: 1191 },
|
|
2600
|
+
{ name: "A4", widthPt: 595, heightPt: 842 },
|
|
2601
|
+
{ name: "A5", widthPt: 420, heightPt: 595 },
|
|
2602
|
+
{ name: "B5", widthPt: 499, heightPt: 709 }
|
|
2603
|
+
];
|
|
2604
|
+
function _matchPdfPaperSize(widthPt, heightPt) {
|
|
2605
|
+
for (const size of _PDF_COMMON_PAPER_SIZES) {
|
|
2606
|
+
const w = Math.abs(widthPt - size.widthPt) <= 1;
|
|
2607
|
+
const h = Math.abs(heightPt - size.heightPt) <= 1;
|
|
2608
|
+
const wSwapped = Math.abs(widthPt - size.heightPt) <= 1;
|
|
2609
|
+
const hSwapped = Math.abs(heightPt - size.widthPt) <= 1;
|
|
2610
|
+
if (w && h || wSwapped && hSwapped) return size.name;
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
function _parsePdfMediaBox(dict) {
|
|
2615
|
+
const m = dict.match(/\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/);
|
|
2616
|
+
if (!m) return null;
|
|
2617
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
2618
|
+
}
|
|
2619
|
+
function _parsePdfRotate(dict) {
|
|
2620
|
+
const m = dict.match(/\/Rotate\s+(-?\d+)/);
|
|
2621
|
+
return m ? Number(m[1]) : null;
|
|
2622
|
+
}
|
|
2623
|
+
function _round2Pdf(n) {
|
|
2624
|
+
return Math.round(n * 100) / 100;
|
|
2625
|
+
}
|
|
2626
|
+
function _buildPdfPageDimension(pageNumber, mediaBox, rotate) {
|
|
2627
|
+
const rotation = (rotate % 360 + 360) % 360;
|
|
2628
|
+
if (!mediaBox) {
|
|
2629
|
+
return { pageNumber, widthPt: null, heightPt: null, widthIn: null, heightIn: null, rotation, orientation: null, paperSize: null };
|
|
2630
|
+
}
|
|
2631
|
+
const [llx, lly, urx, ury] = mediaBox;
|
|
2632
|
+
let widthPt = Math.abs(urx - llx);
|
|
2633
|
+
let heightPt = Math.abs(ury - lly);
|
|
2634
|
+
if (rotation === 90 || rotation === 270) [widthPt, heightPt] = [heightPt, widthPt];
|
|
2635
|
+
const orientation = widthPt === heightPt ? "square" : widthPt > heightPt ? "landscape" : "portrait";
|
|
2636
|
+
return {
|
|
2637
|
+
pageNumber,
|
|
2638
|
+
widthPt: _round2Pdf(widthPt),
|
|
2639
|
+
heightPt: _round2Pdf(heightPt),
|
|
2640
|
+
widthIn: _round2Pdf(widthPt / 72),
|
|
2641
|
+
heightIn: _round2Pdf(heightPt / 72),
|
|
2642
|
+
rotation,
|
|
2643
|
+
orientation,
|
|
2644
|
+
paperSize: _matchPdfPaperSize(widthPt, heightPt)
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
function _walkPdfPageTree(objMap, nodeBody, inherited, visited, nodeKey, out) {
|
|
2648
|
+
if (visited.has(nodeKey)) return;
|
|
2649
|
+
visited.add(nodeKey);
|
|
2650
|
+
const mediaBox = _parsePdfMediaBox(nodeBody) ?? inherited.mediaBox;
|
|
2651
|
+
const rotateMatch = _parsePdfRotate(nodeBody);
|
|
2652
|
+
const rotate = rotateMatch !== null ? rotateMatch : inherited.rotate;
|
|
2653
|
+
if (/\/Type\s*\/Page(?!s)\b/.test(nodeBody)) {
|
|
2654
|
+
out.push(_buildPdfPageDimension(out.length + 1, mediaBox, rotate));
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
const kidsMatch = nodeBody.match(/\/Kids\s*\[([^\]]*)\]/);
|
|
2658
|
+
if (!kidsMatch) return;
|
|
2659
|
+
const kidRefs = [...kidsMatch[1].matchAll(/(\d+)\s+(\d+)\s+R/g)];
|
|
2660
|
+
for (const ref of kidRefs) {
|
|
2661
|
+
const key = `${ref[1]} ${ref[2]}`;
|
|
2662
|
+
const body = objMap.get(key);
|
|
2663
|
+
if (body) _walkPdfPageTree(objMap, body, { mediaBox, rotate }, visited, key, out);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
function _readPdfPageDimensions(bytes) {
|
|
2667
|
+
if (bytes.length < 8) return { error: "File too small to be a PDF" };
|
|
2668
|
+
const latin1 = (b) => {
|
|
2669
|
+
let out2 = "";
|
|
2670
|
+
const CHUNK = 32768;
|
|
2671
|
+
for (let i = 0; i < b.length; i += CHUNK) out2 += String.fromCharCode(...b.subarray(i, i + CHUNK));
|
|
2672
|
+
return out2;
|
|
2673
|
+
};
|
|
2674
|
+
const header = latin1(bytes.subarray(0, 16));
|
|
2675
|
+
const versionMatch = header.match(/%PDF-(\d\.\d)/);
|
|
2676
|
+
if (!versionMatch) return { error: "Not a valid PDF file (missing %PDF header)" };
|
|
2677
|
+
const version = versionMatch[1];
|
|
2678
|
+
const text2 = latin1(bytes);
|
|
2679
|
+
const objBlocks = [...text2.matchAll(/(\d+)\s+(\d+)\s+obj([\s\S]*?)endobj/g)];
|
|
2680
|
+
const objMap = /* @__PURE__ */ new Map();
|
|
2681
|
+
for (const b of objBlocks) objMap.set(`${b[1]} ${b[2]}`, b[3]);
|
|
2682
|
+
const out = [];
|
|
2683
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2684
|
+
let pagesRootBody = null;
|
|
2685
|
+
const trailerMatches = [...text2.matchAll(/trailer\s*<<([\s\S]*?)>>/g)];
|
|
2686
|
+
if (trailerMatches.length > 0) {
|
|
2687
|
+
const trailer = trailerMatches[trailerMatches.length - 1][1];
|
|
2688
|
+
const rootRef = trailer.match(/\/Root\s+(\d+)\s+(\d+)\s+R/);
|
|
2689
|
+
if (rootRef) {
|
|
2690
|
+
const catalogBody = objMap.get(`${rootRef[1]} ${rootRef[2]}`);
|
|
2691
|
+
const pagesRef = catalogBody?.match(/\/Pages\s+(\d+)\s+(\d+)\s+R/);
|
|
2692
|
+
if (pagesRef) pagesRootBody = objMap.get(`${pagesRef[1]} ${pagesRef[2]}`) ?? null;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (!pagesRootBody) {
|
|
2696
|
+
for (const b of objBlocks) {
|
|
2697
|
+
if (/\/Type\s*\/Pages\b/.test(b[3]) && !/\/Parent\s+\d+\s+\d+\s+R/.test(b[3])) {
|
|
2698
|
+
pagesRootBody = b[3];
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
if (pagesRootBody) _walkPdfPageTree(objMap, pagesRootBody, { mediaBox: null, rotate: 0 }, visited, "root", out);
|
|
2704
|
+
if (out.length === 0) {
|
|
2705
|
+
for (const b of objBlocks) {
|
|
2706
|
+
if (/\/Type\s*\/Page(?!s)\b/.test(b[3])) {
|
|
2707
|
+
out.push(_buildPdfPageDimension(out.length + 1, _parsePdfMediaBox(b[3]), _parsePdfRotate(b[3]) ?? 0));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (out.length === 0) return { error: "Could not locate any page objects in this PDF" };
|
|
2712
|
+
return { version, pageCount: out.length, pages: out };
|
|
2713
|
+
}
|
|
2714
|
+
server.tool("read_pdf_page_dimensions", "List each page's MediaBox width/height (points and inches), rotation, and orientation, in document order, by walking the Root -> Pages -> Kids tree: no PDF rendering library required.", { pdf_base64: z.string().describe("Base64-encoded PDF bytes, optionally prefixed with a data: URL scheme") }, async ({ pdf_base64 }) => {
|
|
2715
|
+
const bytes = new Uint8Array(Buffer.from(pdf_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
2716
|
+
const result = _readPdfPageDimensions(bytes);
|
|
2717
|
+
if ("error" in result) return errText(result);
|
|
2718
|
+
return text(JSON.stringify(result, null, 2));
|
|
2719
|
+
});
|
|
2595
2720
|
var _EXIF_ORIENTATION_LABELS = {
|
|
2596
2721
|
1: "Normal",
|
|
2597
2722
|
2: "Mirrored horizontal",
|
|
@@ -3940,6 +4065,99 @@ server.tool(
|
|
|
3940
4065
|
return text(JSON.stringify(result, null, 2));
|
|
3941
4066
|
}
|
|
3942
4067
|
);
|
|
4068
|
+
function _isValidIpv6HexGroup(g) {
|
|
4069
|
+
return /^[0-9a-fA-F]{1,4}$/.test(g);
|
|
4070
|
+
}
|
|
4071
|
+
function _isValidIpv6Ipv4Quad(quad) {
|
|
4072
|
+
const parts = quad.split(".");
|
|
4073
|
+
if (parts.length !== 4) return false;
|
|
4074
|
+
return parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255);
|
|
4075
|
+
}
|
|
4076
|
+
function _classifyIpv6(groups) {
|
|
4077
|
+
const first = parseInt(groups[0], 16);
|
|
4078
|
+
if (groups.every((g) => g === "0000")) return "unspecified";
|
|
4079
|
+
if (groups.slice(0, 7).every((g) => g === "0000") && groups[7] === "0001") return "loopback";
|
|
4080
|
+
if (groups.slice(0, 4).every((g) => g === "0000") && groups[4] === "0000" && groups[5] === "ffff") return "ipv4-mapped";
|
|
4081
|
+
if (groups[0] === "2001" && groups[1] === "0db8") return "documentation";
|
|
4082
|
+
if ((first & 65280) === 65280) return "multicast";
|
|
4083
|
+
if ((first & 65472) === 65152) return "link-local";
|
|
4084
|
+
if ((first & 65024) === 64512) return "unique-local";
|
|
4085
|
+
return "global-unicast";
|
|
4086
|
+
}
|
|
4087
|
+
function _compressIpv6Groups(groups) {
|
|
4088
|
+
const short = groups.map((g) => g.replace(/^0+(?=.)/, ""));
|
|
4089
|
+
let bestStart = -1;
|
|
4090
|
+
let bestLen = 0;
|
|
4091
|
+
let curStart = -1;
|
|
4092
|
+
let curLen = 0;
|
|
4093
|
+
for (let i = 0; i < short.length; i++) {
|
|
4094
|
+
if (short[i] === "0") {
|
|
4095
|
+
if (curStart === -1) curStart = i;
|
|
4096
|
+
curLen++;
|
|
4097
|
+
if (curLen > bestLen) {
|
|
4098
|
+
bestLen = curLen;
|
|
4099
|
+
bestStart = curStart;
|
|
4100
|
+
}
|
|
4101
|
+
} else {
|
|
4102
|
+
curStart = -1;
|
|
4103
|
+
curLen = 0;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
if (bestLen < 2) return short.join(":");
|
|
4107
|
+
const before = short.slice(0, bestStart).join(":");
|
|
4108
|
+
const after = short.slice(bestStart + bestLen).join(":");
|
|
4109
|
+
return `${before}::${after}`;
|
|
4110
|
+
}
|
|
4111
|
+
function _parseIpv6Address(input) {
|
|
4112
|
+
let s = input.trim();
|
|
4113
|
+
if (s.startsWith("[") && s.includes("]")) s = s.slice(1, s.indexOf("]"));
|
|
4114
|
+
let zoneId = null;
|
|
4115
|
+
const pctIdx = s.indexOf("%");
|
|
4116
|
+
if (pctIdx !== -1) {
|
|
4117
|
+
zoneId = s.slice(pctIdx + 1);
|
|
4118
|
+
s = s.slice(0, pctIdx);
|
|
4119
|
+
}
|
|
4120
|
+
if (!s) return { error: "Empty address" };
|
|
4121
|
+
const doubleColonCount = (s.match(/::/g) ?? []).length;
|
|
4122
|
+
if (doubleColonCount > 1) return { error: 'Address contains more than one "::"' };
|
|
4123
|
+
const ipv4Match = s.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
4124
|
+
if (ipv4Match) {
|
|
4125
|
+
const quad = ipv4Match[1];
|
|
4126
|
+
if (!_isValidIpv6Ipv4Quad(quad)) return { error: `Invalid embedded IPv4 address: ${quad}` };
|
|
4127
|
+
const parts = quad.split(".").map(Number);
|
|
4128
|
+
const hi = (parts[0] << 8 | parts[1]).toString(16);
|
|
4129
|
+
const lo = (parts[2] << 8 | parts[3]).toString(16);
|
|
4130
|
+
s = s.slice(0, s.length - quad.length) + `${hi}:${lo}`;
|
|
4131
|
+
}
|
|
4132
|
+
let groups;
|
|
4133
|
+
if (s.includes("::")) {
|
|
4134
|
+
const [left, right] = s.split("::");
|
|
4135
|
+
const leftGroups = left ? left.split(":") : [];
|
|
4136
|
+
const rightGroups = right ? right.split(":") : [];
|
|
4137
|
+
const missing = 8 - (leftGroups.length + rightGroups.length);
|
|
4138
|
+
if (missing < 1) return { error: 'The "::" shorthand must represent at least one group of zeros' };
|
|
4139
|
+
if (![...leftGroups, ...rightGroups].every(_isValidIpv6HexGroup)) return { error: "Invalid hex group in address" };
|
|
4140
|
+
groups = [...leftGroups, ...Array(missing).fill("0"), ...rightGroups];
|
|
4141
|
+
} else {
|
|
4142
|
+
groups = s.split(":");
|
|
4143
|
+
if (groups.length !== 8) return { error: `Expected 8 groups, got ${groups.length}` };
|
|
4144
|
+
if (!groups.every(_isValidIpv6HexGroup)) return { error: "Invalid hex group in address" };
|
|
4145
|
+
}
|
|
4146
|
+
const expandedGroups = groups.map((g) => g.padStart(4, "0").toLowerCase());
|
|
4147
|
+
const suffix = zoneId ? `%${zoneId}` : "";
|
|
4148
|
+
return {
|
|
4149
|
+
expanded: expandedGroups.join(":") + suffix,
|
|
4150
|
+
compressed: _compressIpv6Groups(expandedGroups) + suffix,
|
|
4151
|
+
groups: expandedGroups,
|
|
4152
|
+
zoneId,
|
|
4153
|
+
addressType: _classifyIpv6(expandedGroups)
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
server.tool("parse_ipv6_address", 'Expand an IPv6 address to its full 8-group form and compress it to its RFC 5952 canonical shortest form, classify its address type (loopback, link-local, unique-local, multicast, IPv4-mapped, documentation, or global unicast), and separate out an optional zone ID. Understands an embedded IPv4 suffix like "::ffff:192.168.1.1".', { address: z.string().describe('The IPv6 address to expand/compress, e.g. "2001:db8::ff00:42:8329"') }, async ({ address }) => {
|
|
4157
|
+
const result = _parseIpv6Address(address);
|
|
4158
|
+
if ("error" in result) return errText(result);
|
|
4159
|
+
return text(JSON.stringify(result, null, 2));
|
|
4160
|
+
});
|
|
3943
4161
|
var _FILLER_WORDS_MCP = ["very", "really", "just", "actually", "basically", "literally", "simply", "totally", "definitely", "certainly", "essentially", "absolutely", "completely", "extremely", "truly", "honestly", "seriously", "obviously", "clearly", "quite", "rather", "somewhat", "fairly", "virtually"];
|
|
3944
4162
|
var _HEDGE_PHRASES_MCP = ["maybe", "perhaps", "probably", "possibly", "arguably", "kind of", "sort of", "a bit", "a little", "more or less", "in my opinion", "i think", "i believe", "to some extent"];
|
|
3945
4163
|
var _CLICHE_PHRASES_MCP = ["at the end of the day", "think outside the box", "low-hanging fruit", "move the needle", "circle back", "touch base", "on the same page", "hit the ground running", "paradigm shift", "win-win", "game changer", "best of both worlds", "only time will tell", "back to the drawing board", "the tip of the iceberg", "a perfect storm", "needle in a haystack", "raise the bar", "push the envelope", "the elephant in the room", "boil the ocean", "when it rains it pours"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "MCP server exposing 143 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",
|