mcp-devutils 1.5.0 → 1.7.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 +12 -2
  2. package/index.js +278 -1
  3. package/package.json +16 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mcp-devutils
2
2
 
3
- MCP server with **35 developer utilities** for Claude Desktop, Cursor, and any MCP-compatible AI assistant.
3
+ MCP server with **44 developer utilities** for Claude Desktop, Cursor, and any MCP-compatible AI assistant.
4
4
 
5
5
  ## Install
6
6
 
@@ -15,7 +15,7 @@ MCP server with **35 developer utilities** for Claude Desktop, Cursor, and any M
15
15
  }
16
16
  ```
17
17
 
18
- ## Tools (35)
18
+ ## Tools (44)
19
19
 
20
20
  | Tool | Description |
21
21
  |------|-------------|
@@ -53,6 +53,16 @@ MCP server with **35 developer utilities** for Claude Desktop, Cursor, and any M
53
53
  | `hex_encode` | Hex encode/decode text |
54
54
  | `char_info` | Unicode character info (codepoint, UTF-8 bytes, HTML entity) |
55
55
  | `byte_count` | Count string bytes in UTF-8/UTF-16/ASCII |
56
+ | `json_diff` | Compare two JSON objects — show added/removed/changed |
57
+ | `jwt_create` | Create HS256 JWT tokens for API testing |
58
+ | `sql_format` | Format SQL queries with proper indentation |
59
+ | `json_query` | Extract values from JSON using dot-notation paths |
60
+ | `epoch_convert` | Convert epoch timestamps across multiple timezones |
61
+ | `aes_encrypt` | AES-256-CBC encrypt text with any key |
62
+ | `aes_decrypt` | Decrypt AES-256-CBC encrypted text |
63
+ | `rsa_keygen` | Generate RSA key pairs (1024/2048/4096-bit) |
64
+ | `scrypt_hash` | Hash passwords with scrypt (RFC 7914) |
65
+ | `regex_replace` | Find & replace with regex + capture groups |
56
66
 
57
67
  ## Zero dependencies
58
68
 
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.5.0" },
8
+ { name: "mcp-devutils", version: "1.7.0" },
9
9
  { capabilities: { tools: {} } }
10
10
  );
11
11
 
@@ -464,6 +464,126 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
464
464
  },
465
465
  required: ["text"]
466
466
  }
467
+ },
468
+ {
469
+ name: "json_diff",
470
+ description: "Compare two JSON objects and show the differences — added, removed, and changed keys. Useful for debugging API responses, config changes, and state diffs.",
471
+ inputSchema: {
472
+ type: "object",
473
+ properties: {
474
+ a: { type: "string", description: "First JSON string" },
475
+ b: { type: "string", description: "Second JSON string" }
476
+ },
477
+ required: ["a", "b"]
478
+ }
479
+ },
480
+ {
481
+ name: "jwt_create",
482
+ description: "Create a JWT token signed with HS256. Useful for testing APIs, mocking auth, and generating test tokens.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ payload: { type: "string", description: "JSON string for the JWT payload (e.g. {\"sub\":\"1234\",\"name\":\"Test\"})" },
487
+ secret: { type: "string", description: "Secret key for HS256 signing (default: 'secret')" },
488
+ expiresIn: { type: "number", description: "Expiration in seconds from now (default: 3600)" }
489
+ },
490
+ required: ["payload"]
491
+ }
492
+ },
493
+ {
494
+ name: "sql_format",
495
+ description: "Format a SQL query with proper indentation and keyword capitalization. Supports SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, and JOIN queries.",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ sql: { type: "string", description: "SQL query to format" },
500
+ uppercase: { type: "boolean", description: "Uppercase SQL keywords (default: true)" }
501
+ },
502
+ required: ["sql"]
503
+ }
504
+ },
505
+ {
506
+ name: "json_query",
507
+ description: "Extract values from a JSON object using dot-notation paths (e.g. 'user.address.city', 'items[0].name', 'data[*].id'). Useful for quickly inspecting nested API responses.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ json: { type: "string", description: "JSON string to query" },
512
+ path: { type: "string", description: "Dot-notation path (e.g. 'user.name', 'items[0]', 'data[*].id')" }
513
+ },
514
+ required: ["json", "path"]
515
+ }
516
+ },
517
+ {
518
+ name: "epoch_convert",
519
+ description: "Convert between epoch milliseconds, seconds, and human-readable dates across multiple timezones. Shows UTC, US Eastern, US Pacific, Europe/London, and Asia/Singapore.",
520
+ inputSchema: {
521
+ type: "object",
522
+ properties: {
523
+ value: { type: "string", description: "Epoch seconds, epoch milliseconds, or ISO date string. Leave empty for current time." },
524
+ timezone: { type: "string", description: "Additional IANA timezone to show (e.g. 'Asia/Tokyo')" }
525
+ }
526
+ }
527
+ },
528
+ {
529
+ name: "aes_encrypt",
530
+ description: "Encrypt text using AES-256-CBC. Returns hex-encoded IV + ciphertext. Use a strong key (will be hashed to 256 bits internally).",
531
+ inputSchema: {
532
+ type: "object",
533
+ properties: {
534
+ text: { type: "string", description: "Plaintext to encrypt" },
535
+ key: { type: "string", description: "Encryption key (any string — will be SHA-256 hashed to derive 256-bit key)" }
536
+ },
537
+ required: ["text", "key"]
538
+ }
539
+ },
540
+ {
541
+ name: "aes_decrypt",
542
+ description: "Decrypt AES-256-CBC encrypted text. Expects hex-encoded input from aes_encrypt.",
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {
546
+ encrypted: { type: "string", description: "Hex-encoded string (IV + ciphertext) from aes_encrypt" },
547
+ key: { type: "string", description: "Same key used for encryption" }
548
+ },
549
+ required: ["encrypted", "key"]
550
+ }
551
+ },
552
+ {
553
+ name: "rsa_keygen",
554
+ description: "Generate an RSA key pair (PEM format). Useful for testing, dev environments, and learning.",
555
+ inputSchema: {
556
+ type: "object",
557
+ properties: {
558
+ bits: { type: "number", description: "Key size in bits: 1024, 2048, or 4096 (default: 2048)" }
559
+ }
560
+ }
561
+ },
562
+ {
563
+ name: "scrypt_hash",
564
+ description: "Hash a password using Node.js scrypt (RFC 7914). Returns hex-encoded salt + hash for secure password storage.",
565
+ inputSchema: {
566
+ type: "object",
567
+ properties: {
568
+ password: { type: "string", description: "Password to hash" },
569
+ salt: { type: "string", description: "Optional salt (hex). If omitted, a random 16-byte salt is generated." }
570
+ },
571
+ required: ["password"]
572
+ }
573
+ },
574
+ {
575
+ name: "regex_replace",
576
+ description: "Find and replace text using a regular expression. Supports capture groups ($1, $2, etc.) in the replacement string.",
577
+ inputSchema: {
578
+ type: "object",
579
+ properties: {
580
+ text: { type: "string", description: "Input text" },
581
+ pattern: { type: "string", description: "Regular expression pattern" },
582
+ replacement: { type: "string", description: "Replacement string (use $1, $2 for capture groups)" },
583
+ flags: { type: "string", description: "Regex flags (default: 'g'). Common: 'gi' for global case-insensitive." }
584
+ },
585
+ required: ["text", "pattern", "replacement"]
586
+ }
467
587
  }
468
588
  ]
469
589
  };
@@ -1449,6 +1569,163 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1449
1569
  };
1450
1570
  }
1451
1571
 
1572
+ case "json_diff": {
1573
+ const objA = JSON.parse(args.a);
1574
+ const objB = JSON.parse(args.b);
1575
+ const diffs = [];
1576
+ const diffObj = (a, b, prefix = "") => {
1577
+ const allKeys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
1578
+ for (const key of allKeys) {
1579
+ const path = prefix ? `${prefix}.${key}` : key;
1580
+ if (!(key in (a || {}))) {
1581
+ diffs.push({ path, type: "added", value: b[key] });
1582
+ } else if (!(key in (b || {}))) {
1583
+ diffs.push({ path, type: "removed", value: a[key] });
1584
+ } else if (typeof a[key] === "object" && typeof b[key] === "object" && a[key] !== null && b[key] !== null && !Array.isArray(a[key]) && !Array.isArray(b[key])) {
1585
+ diffObj(a[key], b[key], path);
1586
+ } else if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) {
1587
+ diffs.push({ path, type: "changed", from: a[key], to: b[key] });
1588
+ }
1589
+ }
1590
+ };
1591
+ diffObj(objA, objB);
1592
+ if (diffs.length === 0) {
1593
+ return { content: [{ type: "text", text: "No differences found." }] };
1594
+ }
1595
+ return { content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }] };
1596
+ }
1597
+
1598
+ case "jwt_create": {
1599
+ const payload = JSON.parse(args.payload);
1600
+ const secret = args.secret || "secret";
1601
+ const expiresIn = args.expiresIn || 3600;
1602
+ const header = { alg: "HS256", typ: "JWT" };
1603
+ const now = Math.floor(Date.now() / 1000);
1604
+ payload.iat = payload.iat || now;
1605
+ payload.exp = payload.exp || now + expiresIn;
1606
+ const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
1607
+ const headerB64 = b64url(header);
1608
+ const payloadB64 = b64url(payload);
1609
+ const signature = crypto.createHmac("sha256", secret).update(`${headerB64}.${payloadB64}`).digest("base64url");
1610
+ const token = `${headerB64}.${payloadB64}.${signature}`;
1611
+ return { content: [{ type: "text", text: `Token: ${token}\n\nHeader: ${JSON.stringify(header, null, 2)}\nPayload: ${JSON.stringify(payload, null, 2)}\n\nSigned with: HS256\nExpires: ${new Date(payload.exp * 1000).toISOString()}` }] };
1612
+ }
1613
+
1614
+ case "sql_format": {
1615
+ const { sql, uppercase = true } = args;
1616
+ const keywords = ["SELECT", "FROM", "WHERE", "AND", "OR", "ORDER BY", "GROUP BY", "HAVING", "LIMIT", "OFFSET", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "OUTER JOIN", "FULL JOIN", "CROSS JOIN", "ON", "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", "AS", "IN", "NOT", "NULL", "IS", "BETWEEN", "LIKE", "EXISTS", "UNION", "UNION ALL", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END"];
1617
+ let formatted = sql.replace(/\s+/g, " ").trim();
1618
+ const newlineBefore = ["SELECT", "FROM", "WHERE", "ORDER BY", "GROUP BY", "HAVING", "LIMIT", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "OUTER JOIN", "FULL JOIN", "CROSS JOIN", "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", "CREATE TABLE", "UNION", "UNION ALL"];
1619
+ for (const kw of newlineBefore) {
1620
+ const regex = new RegExp(`\\b${kw}\\b`, "gi");
1621
+ const replacement = uppercase ? kw : kw.toLowerCase();
1622
+ formatted = formatted.replace(regex, `\n${replacement}`);
1623
+ }
1624
+ const indentAfter = ["AND", "OR"];
1625
+ for (const kw of indentAfter) {
1626
+ const regex = new RegExp(`\\b${kw}\\b`, "gi");
1627
+ const replacement = uppercase ? kw : kw.toLowerCase();
1628
+ formatted = formatted.replace(regex, `\n ${replacement}`);
1629
+ }
1630
+ if (uppercase) {
1631
+ for (const kw of ["ON", "AS", "IN", "NOT", "NULL", "IS", "BETWEEN", "LIKE", "EXISTS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END"]) {
1632
+ const regex = new RegExp(`\\b${kw}\\b`, "gi");
1633
+ formatted = formatted.replace(regex, kw);
1634
+ }
1635
+ }
1636
+ formatted = formatted.trim();
1637
+ return { content: [{ type: "text", text: formatted }] };
1638
+ }
1639
+
1640
+ case "json_query": {
1641
+ const obj = JSON.parse(args.json);
1642
+ const { path } = args;
1643
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").replace(/\[\*\]/g, ".*").split(".");
1644
+ const resolve = (current, parts) => {
1645
+ if (parts.length === 0) return current;
1646
+ const [head, ...rest] = parts;
1647
+ if (head === "*" && Array.isArray(current)) {
1648
+ return current.map(item => resolve(item, rest));
1649
+ }
1650
+ if (current === null || current === undefined) return undefined;
1651
+ const next = Array.isArray(current) ? current[parseInt(head)] : current[head];
1652
+ return resolve(next, rest);
1653
+ };
1654
+ const result = resolve(obj, parts);
1655
+ return { content: [{ type: "text", text: result === undefined ? "undefined (path not found)" : JSON.stringify(result, null, 2) }] };
1656
+ }
1657
+
1658
+ case "epoch_convert": {
1659
+ const { value, timezone } = args || {};
1660
+ const zones = ["UTC", "America/New_York", "America/Los_Angeles", "Europe/London", "Asia/Singapore"];
1661
+ if (timezone && !zones.includes(timezone)) zones.push(timezone);
1662
+ let date;
1663
+ if (!value) {
1664
+ date = new Date();
1665
+ } else if (/^\d+$/.test(value.trim())) {
1666
+ const num = parseInt(value.trim());
1667
+ date = new Date(num > 1e12 ? num : num * 1000);
1668
+ } else {
1669
+ date = new Date(value);
1670
+ }
1671
+ if (isNaN(date.getTime())) throw new Error(`Invalid date/time: ${value}`);
1672
+ const lines = [`Epoch seconds: ${Math.floor(date.getTime() / 1000)}`, `Epoch milliseconds: ${date.getTime()}`, `ISO 8601: ${date.toISOString()}`, ""];
1673
+ for (const tz of zones) {
1674
+ try {
1675
+ lines.push(`${tz}: ${date.toLocaleString("en-US", { timeZone: tz, dateStyle: "full", timeStyle: "long" })}`);
1676
+ } catch { lines.push(`${tz}: (unsupported timezone)`); }
1677
+ }
1678
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1679
+ }
1680
+
1681
+ case "aes_encrypt": {
1682
+ const keyHash = crypto.createHash("sha256").update(args.key).digest();
1683
+ const iv = crypto.randomBytes(16);
1684
+ const cipher = crypto.createCipheriv("aes-256-cbc", keyHash, iv);
1685
+ let encrypted = cipher.update(args.text, "utf8", "hex");
1686
+ encrypted += cipher.final("hex");
1687
+ const result = iv.toString("hex") + encrypted;
1688
+ return { content: [{ type: "text", text: `Encrypted (hex): ${result}\n\nIV (first 32 hex chars): ${iv.toString("hex")}\nCiphertext: ${encrypted}\nTotal length: ${result.length} hex chars` }] };
1689
+ }
1690
+
1691
+ case "aes_decrypt": {
1692
+ const keyHash = crypto.createHash("sha256").update(args.key).digest();
1693
+ const encHex = args.encrypted;
1694
+ if (encHex.length < 34) throw new Error("Encrypted string too short — must contain 32-char IV + ciphertext");
1695
+ const iv = Buffer.from(encHex.slice(0, 32), "hex");
1696
+ const ciphertext = encHex.slice(32);
1697
+ const decipher = crypto.createDecipheriv("aes-256-cbc", keyHash, iv);
1698
+ let decrypted = decipher.update(ciphertext, "hex", "utf8");
1699
+ decrypted += decipher.final("utf8");
1700
+ return { content: [{ type: "text", text: decrypted }] };
1701
+ }
1702
+
1703
+ case "rsa_keygen": {
1704
+ const bits = [1024, 2048, 4096].includes(args?.bits) ? args.bits : 2048;
1705
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
1706
+ modulusLength: bits,
1707
+ publicKeyEncoding: { type: "spki", format: "pem" },
1708
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
1709
+ });
1710
+ return { content: [{ type: "text", text: `=== RSA ${bits}-bit Key Pair ===\n\n--- Public Key ---\n${publicKey}\n--- Private Key ---\n${privateKey}\n⚠️ This is for dev/testing. Never share private keys.` }] };
1711
+ }
1712
+
1713
+ case "scrypt_hash": {
1714
+ const salt = args.salt ? Buffer.from(args.salt, "hex") : crypto.randomBytes(16);
1715
+ const derived = crypto.scryptSync(args.password, salt, 64);
1716
+ const saltHex = salt.toString("hex");
1717
+ const hashHex = derived.toString("hex");
1718
+ return { content: [{ type: "text", text: `Salt (hex): ${saltHex}\nHash (hex): ${hashHex}\nCombined: ${saltHex}:${hashHex}\n\nTo verify, use the same salt with scrypt_hash.` }] };
1719
+ }
1720
+
1721
+ case "regex_replace": {
1722
+ const flags = args.flags || "g";
1723
+ const regex = new RegExp(args.pattern, flags);
1724
+ const result = args.text.replace(regex, args.replacement);
1725
+ const matchCount = (args.text.match(regex) || []).length;
1726
+ return { content: [{ type: "text", text: `Matches found: ${matchCount}\n\n--- Result ---\n${result}` }] };
1727
+ }
1728
+
1452
1729
  default:
1453
1730
  throw new Error(`Unknown tool: ${name}`);
1454
1731
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-devutils",
3
- "version": "1.5.0",
4
- "description": "MCP server with 35 developer utilities - UUID, nanoid, hash, HMAC, base64, hex encode, timestamps, JWT decode, random strings, URL encode/decode, JSON format, CSV/JSON convert, 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, byte count, CIDR calculator, case converter, markdown TOC, env parser, IP info, password strength, data size converter, string escape, char info",
3
+ "version": "1.7.0",
4
+ "description": "MCP server with 44 developer utilities - UUID, nanoid, hash, HMAC, base64, hex encode, timestamps, JWT decode/create, random strings, URL encode/decode, JSON format/diff/query, CSV/JSON convert, regex test/replace, cron explain, color convert, semver compare, HTTP status codes, slugify, HTML escape, chmod calculator, text diff, number base converter, lorem ipsum, word count, byte count, CIDR calculator, case converter, markdown TOC, env parser, IP info, password strength, data size converter, string escape, char info, SQL format, epoch convert, AES encrypt/decrypt, RSA keygen, scrypt password hash",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -46,7 +46,20 @@
46
46
  "csv-json",
47
47
  "hex-encode",
48
48
  "unicode",
49
- "byte-count"
49
+ "byte-count",
50
+ "json-diff",
51
+ "jwt-create",
52
+ "sql-format",
53
+ "json-query",
54
+ "epoch-convert",
55
+ "timezone",
56
+ "aes",
57
+ "encryption",
58
+ "rsa",
59
+ "keygen",
60
+ "scrypt",
61
+ "password-hash",
62
+ "regex-replace"
50
63
  ],
51
64
  "author": "Hong Teoh",
52
65
  "license": "MIT",