@utilix-tech/mcp 0.29.0 → 0.31.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,14 @@
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.31.0
6
+ - Add `json_schema_to_typescript` tool: convert a JSON Schema document into TypeScript interfaces/types, reading `required`/`enum`/`oneOf`/`anyOf`/`allOf`/`$ref` directly instead of inferring from an example payload.
7
+ - Add `estimate_password_entropy` tool: bits of entropy from a password's character pool, a strength label, and crack-time estimates across four guess-rate scenarios.
8
+
9
+ ## 0.30.0
10
+ - 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.).
11
+ - 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.
12
+
5
13
  ## 0.29.0
6
14
  - 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
15
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 144 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 148 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
 
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
75
75
 
76
76
  ## Tools
77
77
 
78
- ### JSON (14 tools)
78
+ ### JSON (15 tools)
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
81
  | `format_json` | Pretty-print and validate a JSON string |
@@ -92,6 +92,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
92
92
  | `json_to_go` | Generate Go structs from JSON |
93
93
  | `json_to_python` | Generate Python dataclasses from JSON |
94
94
  | `json_to_zod` | Generate a Zod schema from JSON |
95
+ | `json_schema_to_typescript` | Generate TypeScript interfaces/types from a JSON Schema document (reads required/enum/$ref directly) |
95
96
 
96
97
  ### Encoding (12 tools)
97
98
  | Tool | Description |
@@ -117,12 +118,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
117
118
  | `bcrypt_hash` | Hash a password using bcrypt |
118
119
  | `bcrypt_verify` | Verify a password against a bcrypt hash |
119
120
 
120
- ### Generators (5 tools)
121
+ ### Generators (6 tools)
121
122
  | Tool | Description |
122
123
  |------|-------------|
123
124
  | `generate_uuid` | Generate UUID v4 or v7 |
124
125
  | `generate_ulid` | Generate a ULID |
125
126
  | `generate_password` | Generate a secure random password |
127
+ | `estimate_password_entropy` | Estimate a password's entropy in bits, strength label, and crack-time estimates |
126
128
  | `generate_qr` | Generate a QR code as an SVG string |
127
129
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
128
130
 
@@ -177,7 +179,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
177
179
  | `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
180
  | `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
181
 
180
- ### API / Network (11 tools)
182
+ ### API / Network (12 tools)
181
183
  | Tool | Description |
182
184
  |------|-------------|
183
185
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -191,6 +193,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
191
193
  | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
192
194
  | `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
193
195
  | `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
196
+ | `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
197
 
195
198
  ### Color (4 tools)
196
199
  | Tool | Description |
@@ -274,10 +277,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
274
277
  | `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
275
278
  | `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
279
 
277
- ### PDF (1 tool)
280
+ ### PDF (2 tools)
278
281
  | Tool | Description |
279
282
  |------|-------------|
280
283
  | `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
284
+ | `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
285
 
282
286
  ### Audio (3 tools)
283
287
  | 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"];
@@ -4959,5 +5177,235 @@ server.tool("read_video_info", "Read duration, resolution, and video/audio codec
4959
5177
  if ("error" in result) return errText(result);
4960
5178
  return text(JSON.stringify(result, null, 2));
4961
5179
  });
5180
+ function _tsCapitalizeMcp(str) {
5181
+ if (!str) return str;
5182
+ return str.charAt(0).toUpperCase() + str.slice(1);
5183
+ }
5184
+ function _schemaRefNameMcp(ref) {
5185
+ const parts = ref.split("/");
5186
+ return _tsCapitalizeMcp(parts[parts.length - 1] || "Ref");
5187
+ }
5188
+ function _schemaLiteralTypeMcp(value) {
5189
+ if (typeof value === "string") return JSON.stringify(value);
5190
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
5191
+ if (value === null) return "null";
5192
+ return JSON.stringify(value);
5193
+ }
5194
+ function _safePropertyKeyMcp(key) {
5195
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
5196
+ }
5197
+ function _schemaToTsTypeMcp(schema, name, interfaces) {
5198
+ if (schema.$ref) return _schemaRefNameMcp(schema.$ref);
5199
+ if (schema.const !== void 0) return _schemaLiteralTypeMcp(schema.const);
5200
+ if (schema.enum) return schema.enum.map(_schemaLiteralTypeMcp).join(" | ");
5201
+ if (schema.oneOf) return schema.oneOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Variant${i}`, interfaces)).join(" | ");
5202
+ if (schema.anyOf) return schema.anyOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Variant${i}`, interfaces)).join(" | ");
5203
+ if (schema.allOf) return schema.allOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Part${i}`, interfaces)).join(" & ");
5204
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
5205
+ const nullable = types.includes("null");
5206
+ const nonNullTypes = types.filter((t) => t !== "null");
5207
+ const primaryType = nonNullTypes[0] ?? (schema.properties || schema.additionalProperties !== void 0 ? "object" : void 0);
5208
+ let result;
5209
+ switch (primaryType) {
5210
+ case "string":
5211
+ result = "string";
5212
+ break;
5213
+ case "number":
5214
+ case "integer":
5215
+ result = "number";
5216
+ break;
5217
+ case "boolean":
5218
+ result = "boolean";
5219
+ break;
5220
+ case "array": {
5221
+ const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
5222
+ const itemType = itemsSchema ? _schemaToTsTypeMcp(itemsSchema, `${name}Item`, interfaces) : "unknown";
5223
+ result = `${itemType}[]`;
5224
+ break;
5225
+ }
5226
+ case "object": {
5227
+ _buildSchemaInterfaceMcp(schema, name, interfaces);
5228
+ result = name;
5229
+ break;
5230
+ }
5231
+ default:
5232
+ result = "unknown";
5233
+ }
5234
+ return nullable ? `${result} | null` : result;
5235
+ }
5236
+ function _buildSchemaInterfaceMcp(schema, name, interfaces) {
5237
+ if (interfaces.has(name)) return;
5238
+ const required = new Set(schema.required ?? []);
5239
+ const lines = [`interface ${name} {`];
5240
+ for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
5241
+ if (propSchema.description) lines.push(` /** ${propSchema.description} */`);
5242
+ const optional = required.has(key) ? "" : "?";
5243
+ const propType = _schemaToTsTypeMcp(propSchema, _tsCapitalizeMcp(key), interfaces);
5244
+ lines.push(` ${_safePropertyKeyMcp(key)}${optional}: ${propType};`);
5245
+ }
5246
+ if (schema.additionalProperties === true) {
5247
+ lines.push(" [key: string]: unknown;");
5248
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
5249
+ const addType = _schemaToTsTypeMcp(schema.additionalProperties, `${name}Value`, interfaces);
5250
+ lines.push(` [key: string]: ${addType};`);
5251
+ }
5252
+ lines.push("}");
5253
+ interfaces.set(name, lines.join("\n"));
5254
+ }
5255
+ function _jsonSchemaToTypescriptMcp(schemaJson, rootName) {
5256
+ let schema;
5257
+ try {
5258
+ schema = JSON.parse(schemaJson);
5259
+ } catch {
5260
+ return { error: "Invalid JSON input" };
5261
+ }
5262
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
5263
+ return { error: "Schema must be a JSON object" };
5264
+ }
5265
+ const typedSchema = schema;
5266
+ const interfaces = /* @__PURE__ */ new Map();
5267
+ const defs = { ...typedSchema.$defs ?? {}, ...typedSchema.definitions ?? {} };
5268
+ for (const [defName, defSchema] of Object.entries(defs)) {
5269
+ const defTypeName = _tsCapitalizeMcp(defName);
5270
+ const defType = _schemaToTsTypeMcp(defSchema, defTypeName, interfaces);
5271
+ if (!interfaces.has(defTypeName)) {
5272
+ interfaces.set(defTypeName, `type ${defTypeName} = ${defType};`);
5273
+ }
5274
+ }
5275
+ const rootType = _schemaToTsTypeMcp(typedSchema, rootName, interfaces);
5276
+ const parts = [...interfaces.values()];
5277
+ if (!interfaces.has(rootName)) {
5278
+ parts.push(`type ${rootName} = ${rootType};`);
5279
+ }
5280
+ return { output: parts.join("\n\n") };
5281
+ }
5282
+ server.tool(
5283
+ "json_schema_to_typescript",
5284
+ "Convert a JSON Schema document into TypeScript interfaces/types. Unlike json_to_typescript (which infers types from an example data payload), this reads the schema's declared type/enum/required keywords directly, so optional vs. required fields and enum/union types come through accurately. Understands local $ref into $defs/definitions, items for arrays, additionalProperties, and renders description as a JSDoc comment.",
5285
+ {
5286
+ schema: z.string().describe("The JSON Schema document, as a JSON string"),
5287
+ root_name: z.string().default("Root").describe("Name for the root generated interface/type")
5288
+ },
5289
+ async ({ schema, root_name }) => {
5290
+ const result = _jsonSchemaToTypescriptMcp(schema, root_name);
5291
+ if ("error" in result) return errText(result);
5292
+ return text(result.output);
5293
+ }
5294
+ );
5295
+ var _SYMBOL_POOL_CHARS_PW = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
5296
+ var _SYMBOL_POOL_SET_PW = new Set(_SYMBOL_POOL_CHARS_PW.split(""));
5297
+ function _hasSymbolCharPw(password) {
5298
+ for (const ch of password) {
5299
+ if (_SYMBOL_POOL_SET_PW.has(ch)) return true;
5300
+ }
5301
+ return false;
5302
+ }
5303
+ function _hasSequentialRunPw(password, minRun) {
5304
+ let ascRun = 1;
5305
+ let descRun = 1;
5306
+ for (let i = 1; i < password.length; i++) {
5307
+ const prev = password.charCodeAt(i - 1);
5308
+ const curr = password.charCodeAt(i);
5309
+ if (curr === prev + 1) {
5310
+ ascRun++;
5311
+ descRun = 1;
5312
+ } else if (curr === prev - 1) {
5313
+ descRun++;
5314
+ ascRun = 1;
5315
+ } else {
5316
+ ascRun = 1;
5317
+ descRun = 1;
5318
+ }
5319
+ if (ascRun >= minRun || descRun >= minRun) return true;
5320
+ }
5321
+ return false;
5322
+ }
5323
+ function _formatCrackDurationPw(seconds) {
5324
+ if (!isFinite(seconds)) return "effectively never";
5325
+ if (seconds < 1) return "instantly";
5326
+ const units = [
5327
+ ["century", 315576e4],
5328
+ ["year", 31557600],
5329
+ ["month", 2629800],
5330
+ ["day", 86400],
5331
+ ["hour", 3600],
5332
+ ["minute", 60],
5333
+ ["second", 1]
5334
+ ];
5335
+ for (const [name, unitSeconds] of units) {
5336
+ if (seconds >= unitSeconds) {
5337
+ const value = seconds / unitSeconds;
5338
+ if (name === "century" && value > 1e3) return "longer than the age of the universe";
5339
+ const rounded = value >= 100 ? Math.round(value) : Math.round(value * 10) / 10;
5340
+ const plural = name === "century" ? "centuries" : `${name}s`;
5341
+ return `${rounded} ${rounded === 1 ? name : plural}`;
5342
+ }
5343
+ }
5344
+ return "instantly";
5345
+ }
5346
+ function _estimatePasswordEntropyMcp(password) {
5347
+ if (!password) return { error: "Password is required" };
5348
+ const hasLowercase = /[a-z]/.test(password);
5349
+ const hasUppercase = /[A-Z]/.test(password);
5350
+ const hasDigits = /[0-9]/.test(password);
5351
+ const hasSymbols = _hasSymbolCharPw(password);
5352
+ const hasOther = /[^\x20-\x7e]/.test(password);
5353
+ let poolSize = 0;
5354
+ if (hasLowercase) poolSize += 26;
5355
+ if (hasUppercase) poolSize += 26;
5356
+ if (hasDigits) poolSize += 10;
5357
+ if (hasSymbols) poolSize += _SYMBOL_POOL_CHARS_PW.length;
5358
+ if (hasOther) poolSize += 100;
5359
+ const entropyBits = Math.round(password.length * Math.log2(poolSize) * 100) / 100;
5360
+ let strength;
5361
+ if (entropyBits < 28) strength = "very-weak";
5362
+ else if (entropyBits < 36) strength = "weak";
5363
+ else if (entropyBits < 60) strength = "fair";
5364
+ else if (entropyBits < 128) strength = "strong";
5365
+ else strength = "very-strong";
5366
+ const averageGuesses = Math.pow(2, entropyBits) / 2;
5367
+ const scenarios = [
5368
+ { scenario: "Online, throttled (100 guesses/hour)", guessesPerSecond: 100 / 3600 },
5369
+ { scenario: "Online, unthrottled (10 guesses/sec)", guessesPerSecond: 10 },
5370
+ { scenario: "Offline, slow hash e.g. bcrypt (10k guesses/sec)", guessesPerSecond: 1e4 },
5371
+ { scenario: "Offline, fast hash / GPU cluster (10B guesses/sec)", guessesPerSecond: 1e10 }
5372
+ ];
5373
+ const crackTimes = scenarios.map((s) => ({
5374
+ scenario: s.scenario,
5375
+ guessesPerSecond: s.guessesPerSecond,
5376
+ humanReadable: _formatCrackDurationPw(averageGuesses / s.guessesPerSecond)
5377
+ }));
5378
+ const warnings = [];
5379
+ if (password.length < 8) {
5380
+ warnings.push("Shorter than 8 characters \u2014 most sites and cracking tools treat this as trivially weak regardless of entropy.");
5381
+ }
5382
+ if (/(.)\1{2,}/.test(password)) {
5383
+ warnings.push('Contains a repeated character run (e.g. "aaa"), which is easier to guess than the entropy figure implies.');
5384
+ }
5385
+ if (_hasSequentialRunPw(password, 3)) {
5386
+ warnings.push('Contains a sequential run (e.g. "abc" or "321"), which is easier to guess than the entropy figure implies.');
5387
+ }
5388
+ if (poolSize <= 26) {
5389
+ warnings.push("Uses only one character class \u2014 mixing in uppercase, digits, or symbols raises the character pool and entropy.");
5390
+ }
5391
+ return {
5392
+ length: password.length,
5393
+ charset: { hasLowercase, hasUppercase, hasDigits, hasSymbols, hasOther, poolSize },
5394
+ entropyBits,
5395
+ strength,
5396
+ crackTimes,
5397
+ warnings
5398
+ };
5399
+ }
5400
+ server.tool(
5401
+ "estimate_password_entropy",
5402
+ "Estimate a password's entropy in bits from the character classes it actually draws from (lowercase, uppercase, digits, symbols, other/Unicode), using the pool-size x length method. Returns a strength label and crack-time estimates at four guess-rate scenarios, plus warnings for repeated runs, sequential runs, short length, and single-character-class passwords. Measures resistance to brute force over the theoretical character space, not real-world guessability against a breach database.",
5403
+ { password: z.string().describe("The password to analyze") },
5404
+ async ({ password }) => {
5405
+ const result = _estimatePasswordEntropyMcp(password);
5406
+ if ("error" in result) return errText(result);
5407
+ return text(JSON.stringify(result, null, 2));
5408
+ }
5409
+ );
4962
5410
  var transport = new StdioServerTransport();
4963
5411
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.29.0",
3
+ "version": "0.31.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",