mcp-devutils 1.3.1 → 1.4.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 +57 -0
  2. package/index.js +275 -1
  3. package/package.json +8 -3
package/README.md CHANGED
@@ -1,4 +1,57 @@
1
+ # mcp-devutils
1
2
 
3
+ MCP server with **30 developer utilities** for Claude Desktop, Cursor, and any MCP-compatible AI assistant.
4
+
5
+ ## Install
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "devutils": {
11
+ "command": "npx",
12
+ "args": ["-y", "mcp-devutils"]
13
+ }
14
+ }
15
+ }
16
+ ```
17
+
18
+ ## Tools (30)
19
+
20
+ | Tool | Description |
21
+ |------|-------------|
22
+ | `uuid` | Generate UUID v4 (batch support) |
23
+ | `hash` | Hash text (md5, sha1, sha256) |
24
+ | `base64` | Encode/decode base64 |
25
+ | `timestamp` | Convert Unix ↔ ISO 8601 dates |
26
+ | `jwt_decode` | Decode JWT tokens (header + payload) |
27
+ | `random_string` | Generate random strings/passwords |
28
+ | `url_encode` | URL encode/decode |
29
+ | `json_format` | Pretty-print or minify JSON |
30
+ | `regex_test` | Test regex patterns with match details |
31
+ | `cron_explain` | Explain cron expressions + next run times |
32
+ | `hmac` | Generate HMAC signatures |
33
+ | `color_convert` | Convert hex ↔ RGB ↔ HSL colors |
34
+ | `semver_compare` | Compare semantic versions |
35
+ | `http_status` | Look up HTTP status code meanings |
36
+ | `slug` | Generate URL-safe slugs |
37
+ | `escape_html` | Escape/unescape HTML entities |
38
+ | `chmod_calc` | Convert numeric ↔ symbolic permissions |
39
+ | `diff` | Compare two text strings |
40
+ | `number_base` | Convert decimal/hex/octal/binary |
41
+ | `lorem_ipsum` | Generate placeholder text |
42
+ | `word_count` | Count chars, words, lines, bytes |
43
+ | `cidr` | Parse CIDR notation (network, broadcast, hosts) |
44
+ | `case_convert` | Convert camelCase/snake_case/PascalCase/kebab-case |
45
+ | `markdown_toc` | Generate table of contents from markdown |
46
+ | `env_parse` | Parse and validate .env files |
47
+ | `ip_info` | Analyze IP addresses (type, class, private/public) |
48
+ | `password_strength` | Analyze password entropy and strength |
49
+ | `data_size` | Convert between bytes/KB/MB/GB/TB (SI + IEC) |
50
+ | `string_escape` | Escape strings for JSON/CSV/regex/SQL/shell |
51
+
52
+ ## Zero dependencies
53
+
54
+ Only requires `@modelcontextprotocol/sdk`. All tools use Node.js built-ins.
2
55
 
3
56
  ## Support
4
57
 
@@ -6,3 +59,7 @@ If this tool saves you time, consider supporting development:
6
59
 
7
60
  - [Buy me a coffee](https://buymeacoffee.com/gl89tu25lp)
8
61
  - [Tip via Stripe ($3)](https://buy.stripe.com/dRm8wP8R295Z9VyeN59Zm00)
62
+
63
+ ## License
64
+
65
+ MIT
package/index.js CHANGED
@@ -5,7 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
5
5
  import crypto from "crypto";
6
6
 
7
7
  const server = new Server(
8
- { name: "mcp-devutils", version: "1.3.0" },
8
+ { name: "mcp-devutils", version: "1.4.0" },
9
9
  { capabilities: { tools: {} } }
10
10
  );
11
11
 
@@ -335,6 +335,76 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
335
335
  },
336
336
  required: ["markdown"]
337
337
  }
338
+ },
339
+ {
340
+ name: "env_parse",
341
+ description: "Parse and validate .env file contents — shows keys, detects issues like missing values, duplicate keys, or invalid lines",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ content: { type: "string", description: ".env file content to parse and validate" }
346
+ },
347
+ required: ["content"]
348
+ }
349
+ },
350
+ {
351
+ name: "ip_info",
352
+ description: "Parse and analyze an IP address — shows type (IPv4/IPv6), class, whether private/public/loopback/link-local",
353
+ inputSchema: {
354
+ type: "object",
355
+ properties: {
356
+ ip: { type: "string", description: "IP address to analyze (e.g. '192.168.1.1' or '::1')" }
357
+ },
358
+ required: ["ip"]
359
+ }
360
+ },
361
+ {
362
+ name: "password_strength",
363
+ description: "Analyze password strength — calculates entropy, checks length, character diversity, and common patterns",
364
+ inputSchema: {
365
+ type: "object",
366
+ properties: {
367
+ password: { type: "string", description: "Password to analyze" }
368
+ },
369
+ required: ["password"]
370
+ }
371
+ },
372
+ {
373
+ name: "data_size",
374
+ description: "Convert between data size units (bytes, KB, MB, GB, TB, PB) with both decimal (SI) and binary (IEC) representations",
375
+ inputSchema: {
376
+ type: "object",
377
+ properties: {
378
+ value: { type: "number", description: "Numeric value to convert" },
379
+ unit: {
380
+ type: "string",
381
+ enum: ["B", "KB", "MB", "GB", "TB", "PB", "KiB", "MiB", "GiB", "TiB", "PiB"],
382
+ description: "Source unit (default: B for bytes)"
383
+ }
384
+ },
385
+ required: ["value"]
386
+ }
387
+ },
388
+ {
389
+ name: "string_escape",
390
+ description: "Escape or unescape strings for various contexts: JSON, CSV, regex, SQL, or shell",
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {
394
+ text: { type: "string", description: "Text to escape or unescape" },
395
+ format: {
396
+ type: "string",
397
+ enum: ["json", "csv", "regex", "sql", "shell"],
398
+ description: "Target format to escape for"
399
+ },
400
+ action: {
401
+ type: "string",
402
+ enum: ["escape", "unescape"],
403
+ description: "Action: escape or unescape (default: escape)"
404
+ }
405
+ },
406
+ required: ["text", "format"]
407
+ }
338
408
  }
339
409
  ]
340
410
  };
@@ -1032,6 +1102,210 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1032
1102
  return { content: [{ type: "text", text: toc.join("\n") }] };
1033
1103
  }
1034
1104
 
1105
+ case "env_parse": {
1106
+ const { content } = args;
1107
+ const lines = content.split("\n");
1108
+ const keys = [];
1109
+ const issues = [];
1110
+ const seen = new Set();
1111
+ lines.forEach((line, i) => {
1112
+ const num = i + 1;
1113
+ const trimmed = line.trim();
1114
+ if (!trimmed || trimmed.startsWith("#")) return;
1115
+ const eqIdx = trimmed.indexOf("=");
1116
+ if (eqIdx === -1) {
1117
+ issues.push(`Line ${num}: Invalid format (no '=' found): ${trimmed}`);
1118
+ return;
1119
+ }
1120
+ const key = trimmed.substring(0, eqIdx).trim();
1121
+ const val = trimmed.substring(eqIdx + 1).trim();
1122
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1123
+ issues.push(`Line ${num}: Invalid key name '${key}'`);
1124
+ }
1125
+ if (seen.has(key)) {
1126
+ issues.push(`Line ${num}: Duplicate key '${key}'`);
1127
+ }
1128
+ seen.add(key);
1129
+ if (!val) {
1130
+ issues.push(`Line ${num}: Empty value for '${key}'`);
1131
+ }
1132
+ keys.push({ key, value: val.length > 50 ? val.substring(0, 50) + "..." : val, line: num });
1133
+ });
1134
+ const output = [`Parsed ${keys.length} key(s) from ${lines.length} line(s)`];
1135
+ if (keys.length > 0) {
1136
+ output.push("\nKeys:");
1137
+ keys.forEach(k => output.push(` ${k.key} = ${k.value} (line ${k.line})`));
1138
+ }
1139
+ if (issues.length > 0) {
1140
+ output.push(`\n⚠ ${issues.length} issue(s):`);
1141
+ issues.forEach(i => output.push(` ${i}`));
1142
+ } else {
1143
+ output.push("\n✓ No issues found");
1144
+ }
1145
+ return { content: [{ type: "text", text: output.join("\n") }] };
1146
+ }
1147
+
1148
+ case "ip_info": {
1149
+ const { ip } = args;
1150
+ const trimmed = ip.trim();
1151
+ const output = [];
1152
+ if (trimmed.includes(":")) {
1153
+ output.push(`IP: ${trimmed}`);
1154
+ output.push(`Version: IPv6`);
1155
+ if (trimmed === "::1") output.push("Type: Loopback");
1156
+ else if (trimmed.startsWith("fe80:")) output.push("Type: Link-local");
1157
+ else if (trimmed.startsWith("fc") || trimmed.startsWith("fd")) output.push("Type: Unique local (private)");
1158
+ else if (trimmed.startsWith("ff")) output.push("Type: Multicast");
1159
+ else if (trimmed === "::") output.push("Type: Unspecified");
1160
+ else output.push("Type: Global unicast (public)");
1161
+ } else {
1162
+ const parts = trimmed.split(".");
1163
+ if (parts.length !== 4 || parts.some(p => isNaN(p) || +p < 0 || +p > 255)) {
1164
+ throw new Error(`Invalid IPv4 address: ${trimmed}`);
1165
+ }
1166
+ const octets = parts.map(Number);
1167
+ output.push(`IP: ${trimmed}`);
1168
+ output.push(`Version: IPv4`);
1169
+ output.push(`Binary: ${octets.map(o => o.toString(2).padStart(8, "0")).join(".")}`);
1170
+ if (octets[0] === 127) output.push("Type: Loopback");
1171
+ else if (octets[0] === 10) output.push("Type: Private (10.0.0.0/8, Class A)");
1172
+ else if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) output.push("Type: Private (172.16.0.0/12, Class B)");
1173
+ else if (octets[0] === 192 && octets[1] === 168) output.push("Type: Private (192.168.0.0/16, Class C)");
1174
+ else if (octets[0] === 169 && octets[1] === 254) output.push("Type: Link-local (APIPA)");
1175
+ else if (octets[0] >= 224 && octets[0] <= 239) output.push("Type: Multicast");
1176
+ else if (octets[0] >= 240) output.push("Type: Reserved");
1177
+ else output.push("Type: Public");
1178
+ if (octets[0] < 128) output.push("Class: A");
1179
+ else if (octets[0] < 192) output.push("Class: B");
1180
+ else if (octets[0] < 224) output.push("Class: C");
1181
+ else if (octets[0] < 240) output.push("Class: D (Multicast)");
1182
+ else output.push("Class: E (Reserved)");
1183
+ }
1184
+ return { content: [{ type: "text", text: output.join("\n") }] };
1185
+ }
1186
+
1187
+ case "password_strength": {
1188
+ const { password } = args;
1189
+ const len = password.length;
1190
+ let charsetSize = 0;
1191
+ if (/[a-z]/.test(password)) charsetSize += 26;
1192
+ if (/[A-Z]/.test(password)) charsetSize += 26;
1193
+ if (/[0-9]/.test(password)) charsetSize += 10;
1194
+ if (/[^a-zA-Z0-9]/.test(password)) charsetSize += 32;
1195
+ const entropy = Math.round(len * Math.log2(charsetSize || 1) * 100) / 100;
1196
+ const issues = [];
1197
+ if (len < 8) issues.push("Too short (< 8 characters)");
1198
+ if (!/[A-Z]/.test(password)) issues.push("No uppercase letters");
1199
+ if (!/[a-z]/.test(password)) issues.push("No lowercase letters");
1200
+ if (!/[0-9]/.test(password)) issues.push("No digits");
1201
+ if (!/[^a-zA-Z0-9]/.test(password)) issues.push("No special characters");
1202
+ if (/(.)\1{2,}/.test(password)) issues.push("Contains repeated characters (3+)");
1203
+ if (/^(123|abc|qwerty|password|admin|letmein)/i.test(password)) issues.push("Starts with common pattern");
1204
+ let strength;
1205
+ if (entropy < 28) strength = "Very Weak";
1206
+ else if (entropy < 36) strength = "Weak";
1207
+ else if (entropy < 60) strength = "Moderate";
1208
+ else if (entropy < 80) strength = "Strong";
1209
+ else strength = "Very Strong";
1210
+ const output = [
1211
+ `Password length: ${len}`,
1212
+ `Charset size: ${charsetSize}`,
1213
+ `Entropy: ${entropy} bits`,
1214
+ `Strength: ${strength}`,
1215
+ ];
1216
+ if (issues.length > 0) {
1217
+ output.push(`\nIssues (${issues.length}):`);
1218
+ issues.forEach(i => output.push(` - ${i}`));
1219
+ } else {
1220
+ output.push("\n✓ No issues detected");
1221
+ }
1222
+ return { content: [{ type: "text", text: output.join("\n") }] };
1223
+ }
1224
+
1225
+ case "data_size": {
1226
+ const { value, unit = "B" } = args;
1227
+ const units = {
1228
+ B: 1, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12, PB: 1e15,
1229
+ KiB: 1024, MiB: 1048576, GiB: 1073741824, TiB: 1099511627776, PiB: 1125899906842624
1230
+ };
1231
+ if (!units[unit]) throw new Error(`Unknown unit: ${unit}. Use: ${Object.keys(units).join(", ")}`);
1232
+ const bytes = value * units[unit];
1233
+ const fmt = (n) => n < 0.01 ? n.toExponential(2) : (n % 1 === 0 ? n.toString() : n.toFixed(2));
1234
+ const output = [
1235
+ `Input: ${value} ${unit} = ${fmt(bytes)} bytes`,
1236
+ "",
1237
+ "Decimal (SI):",
1238
+ ` ${fmt(bytes)} B`,
1239
+ ` ${fmt(bytes / 1e3)} KB`,
1240
+ ` ${fmt(bytes / 1e6)} MB`,
1241
+ ` ${fmt(bytes / 1e9)} GB`,
1242
+ ` ${fmt(bytes / 1e12)} TB`,
1243
+ ` ${fmt(bytes / 1e15)} PB`,
1244
+ "",
1245
+ "Binary (IEC):",
1246
+ ` ${fmt(bytes)} B`,
1247
+ ` ${fmt(bytes / 1024)} KiB`,
1248
+ ` ${fmt(bytes / 1048576)} MiB`,
1249
+ ` ${fmt(bytes / 1073741824)} GiB`,
1250
+ ` ${fmt(bytes / 1099511627776)} TiB`,
1251
+ ` ${fmt(bytes / 1125899906842624)} PiB`,
1252
+ ];
1253
+ return { content: [{ type: "text", text: output.join("\n") }] };
1254
+ }
1255
+
1256
+ case "string_escape": {
1257
+ const { text, format, action = "escape" } = args;
1258
+ let result;
1259
+ if (action === "escape") {
1260
+ switch (format) {
1261
+ case "json":
1262
+ result = JSON.stringify(text).slice(1, -1);
1263
+ break;
1264
+ case "csv":
1265
+ result = text.includes(",") || text.includes('"') || text.includes("\n")
1266
+ ? '"' + text.replace(/"/g, '""') + '"'
1267
+ : text;
1268
+ break;
1269
+ case "regex":
1270
+ result = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1271
+ break;
1272
+ case "sql":
1273
+ result = text.replace(/'/g, "''");
1274
+ break;
1275
+ case "shell":
1276
+ result = "'" + text.replace(/'/g, "'\\''") + "'";
1277
+ break;
1278
+ default:
1279
+ throw new Error(`Unknown format: ${format}`);
1280
+ }
1281
+ } else {
1282
+ switch (format) {
1283
+ case "json":
1284
+ result = JSON.parse(`"${text}"`);
1285
+ break;
1286
+ case "csv":
1287
+ result = text.startsWith('"') && text.endsWith('"')
1288
+ ? text.slice(1, -1).replace(/""/g, '"')
1289
+ : text;
1290
+ break;
1291
+ case "regex":
1292
+ result = text.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
1293
+ break;
1294
+ case "sql":
1295
+ result = text.replace(/''/g, "'");
1296
+ break;
1297
+ case "shell":
1298
+ result = text.startsWith("'") && text.endsWith("'")
1299
+ ? text.slice(1, -1).replace(/'\\''/g, "'")
1300
+ : text;
1301
+ break;
1302
+ default:
1303
+ throw new Error(`Unknown format: ${format}`);
1304
+ }
1305
+ }
1306
+ return { content: [{ type: "text", text: result }] };
1307
+ }
1308
+
1035
1309
  default:
1036
1310
  throw new Error(`Unknown tool: ${name}`);
1037
1311
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-devutils",
3
- "version": "1.3.1",
4
- "description": "MCP server with 25 developer utilities - UUID, hash, HMAC, base64, timestamps, JWT decode, random strings, URL encode/decode, JSON format, regex test, cron explain, color convert, semver compare, HTTP status codes, slugify, HTML escape, chmod calculator, text diff, number base converter, lorem ipsum, word count, CIDR calculator, case converter, markdown TOC",
3
+ "version": "1.4.0",
4
+ "description": "MCP server with 30 developer utilities - UUID, hash, HMAC, base64, timestamps, JWT decode, random strings, URL encode/decode, JSON format, regex test, cron explain, color convert, semver compare, HTTP status codes, slugify, HTML escape, chmod calculator, text diff, number base converter, lorem ipsum, word count, CIDR calculator, case converter, markdown TOC, env parser, IP info, password strength, data size converter, string escape",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -36,7 +36,12 @@
36
36
  "word-count",
37
37
  "cidr",
38
38
  "case-convert",
39
- "markdown-toc"
39
+ "markdown-toc",
40
+ "env-parser",
41
+ "ip-address",
42
+ "password-strength",
43
+ "data-size",
44
+ "string-escape"
40
45
  ],
41
46
  "author": "Hong Teoh",
42
47
  "license": "MIT",