@utilix-tech/mcp 0.30.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +230 -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.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
|
+
|
|
5
9
|
## 0.30.0
|
|
6
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.).
|
|
7
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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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 (
|
|
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 (
|
|
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
|
|
package/dist/index.js
CHANGED
|
@@ -5177,5 +5177,235 @@ server.tool("read_video_info", "Read duration, resolution, and video/audio codec
|
|
|
5177
5177
|
if ("error" in result) return errText(result);
|
|
5178
5178
|
return text(JSON.stringify(result, null, 2));
|
|
5179
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
|
+
);
|
|
5180
5410
|
var transport = new StdioServerTransport();
|
|
5181
5411
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "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",
|