@utilix-tech/mcp 0.3.0 → 0.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 70 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 97 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
4
4
 
5
- No API key required. All tools run locally.
5
+ No API key required. All tools run locally. Requires Node.js 18 or later.
6
6
 
7
7
  ## Installation
8
8
 
@@ -52,6 +52,27 @@ Add to `.vscode/mcp.json`:
52
52
  }
53
53
  ```
54
54
 
55
+ ### Windsurf
56
+
57
+ Add to `~/.codeium/windsurf/mcp_config.json`:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "utilix": {
63
+ "command": "npx",
64
+ "args": ["-y", "@utilix-tech/mcp"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Claude Code
71
+
72
+ ```bash
73
+ claude mcp add utilix -- npx -y @utilix-tech/mcp
74
+ ```
75
+
55
76
  ## Tools
56
77
 
57
78
  ### JSON (13 tools)
@@ -100,7 +121,7 @@ Add to `.vscode/mcp.json`:
100
121
  | `generate_qr` | Generate a QR code as an SVG string |
101
122
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
102
123
 
103
- ### Text (9 tools)
124
+ ### Text (10 tools)
104
125
  | Tool | Description |
105
126
  |------|-------------|
106
127
  | `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
@@ -112,6 +133,7 @@ Add to `.vscode/mcp.json`:
112
133
  | `text_diff` | Diff two text blocks line-by-line |
113
134
  | `html_to_markdown` | Convert HTML to Markdown |
114
135
  | `line_operations` | Sort, deduplicate, reverse, shuffle, trim, or number lines |
136
+ | `detect_passive_voice` | Flag sentences written in passive voice, e.g. "was written", "were approved by the team" |
115
137
 
116
138
  ### Time (5 tools)
117
139
  | Tool | Description |
@@ -176,12 +198,52 @@ Add to `.vscode/mcp.json`:
176
198
  | `unicode_inspect` | Inspect Unicode code points of a string |
177
199
  | `optimize_svg` | Optimize and minify an SVG string |
178
200
 
201
+ ### AI / Agent (25 tools)
202
+ | Tool | Description |
203
+ |------|-------------|
204
+ | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
205
+ | `trim_to_tokens` | Trim text to fit a token budget |
206
+ | `chunk_text` | Split text into overlapping chunks for RAG pipelines |
207
+ | `rerank_chunks` | Rerank retrieved chunks by relevance to a query |
208
+ | `score_relevance` | Score a chunk's relevance to a query |
209
+ | `expand_query` | Expand a search query with synonyms/related terms |
210
+ | `summarize_for_llm` | Summarize text to fit a target length or token budget |
211
+ | `detect_pii` | Find emails, phones, SSNs, credit cards in text |
212
+ | `detect_secrets` | Scan text for leaked API keys, tokens, passwords |
213
+ | `detect_prompt_injection` | Score and flag adversarial/injection-style inputs |
214
+ | `repair_json` | Fix malformed JSON with a list of applied fixes |
215
+ | `sanitize_html` | Strip unsafe tags/attributes from HTML |
216
+ | `flatten_json` | Flatten nested JSON into dot-notation keys |
217
+ | `merge_json` | Deep or shallow merge two JSON objects |
218
+ | `extract_json` | Extract JSON blocks embedded in free text |
219
+ | `extract_urls` | Extract all URLs from text |
220
+ | `extract_entities` | Extract named entities from text |
221
+ | `extract_keywords` | Extract keywords from text |
222
+ | `extract_tables` | Extract tabular data from text |
223
+ | `deduplicate_lines` | Remove duplicate lines from text |
224
+ | `validate_json_schema` | Validate JSON against a JSON Schema |
225
+ | `compress_html` | Compress HTML for LLM context budgets |
226
+ | `compress_markdown` | Compress Markdown for LLM context budgets |
227
+ | `compress_json` | Compress JSON for LLM context budgets |
228
+ | `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
229
+
230
+ ### Image (1 tool)
231
+ | Tool | Description |
232
+ |------|-------------|
233
+ | `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes — no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
234
+
179
235
  ## Requirements
180
236
 
181
237
  - Node.js 18+
182
238
 
239
+ ## Publishing (maintainers)
240
+
241
+ CI publishes automatically via npm Trusted Publishing (OIDC) on every push to `main` that bumps `version`. Requires npm **>=11.5.1** — older versions (e.g. the npm bundled with Node 20) sign provenance but skip the OIDC auth exchange, so `npm publish` fails with a 404 as if unauthenticated.
242
+
183
243
  ## Links
184
244
 
185
245
  - [Utilix web app](https://utilix.tech) — use these tools in the browser
246
+ - [API Playground](https://utilix.tech/api) — try 10 endpoints live, no signup required
247
+ - [REST API docs](https://utilix.tech/docs#rest-api) — 100+ endpoints at `api.utilix.tech/v1` (1,000 req/day free)
186
248
  - [Node.js SDK](https://www.npmjs.com/package/@utilix-tech/sdk) — use in your Node.js projects
187
249
  - [Python SDK](https://pypi.org/project/utilix-sdk/) — use in your Python projects
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
  import {
8
8
  parseJson,
9
9
  minifyJson,
10
+ diffJson,
10
11
  jsonToCsv,
11
12
  csvToJson,
12
13
  evaluateJsonPath,
@@ -73,7 +74,7 @@ import { formatSql, formatHtml, testRegex, minifyJs, formatGql, parseDockerImage
73
74
  import { validateYaml, tomlToJson, xmlToJson, parseCsv } from "@utilix-tech/sdk/data";
74
75
  import { generateGradient, minifyCss } from "@utilix-tech/sdk/css";
75
76
  import { optimizeSvg, charToCodePoint, numberToWords } from "@utilix-tech/sdk/misc";
76
- import { estimateTokens, estimateCost, detectPii, redactPii, detectSecrets, detectPromptInjection, repairJson, trimToTokens, chunkText, extractUrls, sanitizeHtml, flattenJson, unflattenJson, mergeJson, extractJson, deduplicateLines, extractKeywords, validateJsonSchema, diffJson, extractTables, extractEntities, compressHtml, compressMarkdown, compressJson, rerankChunks, scoreRelevance, expandQuery, summarizeForLlm } from "@utilix-tech/sdk/ai_agent";
77
+ import { estimateTokens, estimateCost, detectPii, redactPii, detectSecrets, detectPromptInjection, repairJson, trimToTokens, chunkText, extractUrls, sanitizeHtml, flattenJson, unflattenJson, mergeJson, extractJson, deduplicateLines, extractKeywords, validateJsonSchema, diffJson as diffJsonDetailed, extractTables, extractEntities, compressHtml, compressMarkdown, compressJson, rerankChunks, scoreRelevance, expandQuery, summarizeForLlm } from "@utilix-tech/sdk/ai_agent";
77
78
  var CRON_DAY_MAP = {
78
79
  SUN: "0",
79
80
  MON: "1",
@@ -1112,8 +1113,8 @@ server.tool("extract_keywords", "Extract top keywords using TF-IDF scoring.", {
1112
1113
  server.tool("validate_json_schema", "Validate JSON data against a JSON Schema (Draft-07).", { data: z.unknown(), schema: z.record(z.unknown()) }, async ({ data, schema }) => {
1113
1114
  return { content: [{ type: "text", text: JSON.stringify(validateJsonSchema(data, schema), null, 2) }] };
1114
1115
  });
1115
- server.tool("diff_json", "Diff two JSON objects \u2014 returns added, removed, changed, and unchanged paths.", { a: z.unknown(), b: z.unknown() }, async ({ a, b }) => {
1116
- return { content: [{ type: "text", text: JSON.stringify(diffJson(a, b), null, 2) }] };
1116
+ server.tool("diff_json_detailed", "Diff two JSON objects (parsed values, not strings) \u2014 returns a flat list of every path with its op (added/removed/changed/unchanged), plus counts.", { a: z.unknown(), b: z.unknown() }, async ({ a, b }) => {
1117
+ return { content: [{ type: "text", text: JSON.stringify(diffJsonDetailed(a, b), null, 2) }] };
1117
1118
  });
1118
1119
  server.tool("extract_tables", "Extract HTML tables into JSON arrays with headers and rows.", { html: z.string() }, async ({ html }) => {
1119
1120
  return { content: [{ type: "text", text: JSON.stringify(extractTables(html), null, 2) }] };
@@ -1142,5 +1143,260 @@ server.tool("expand_query", "Expand a search query with synonyms and stemmed var
1142
1143
  server.tool("summarize_for_llm", "Extractive summarization \u2014 pick key sentences to fit a token budget.", { text: z.string(), max_tokens: z.number().int().min(10).default(200), strategy: z.enum(["extractive", "first", "last"]).default("extractive") }, async ({ text: text2, max_tokens, strategy }) => {
1143
1144
  return { content: [{ type: "text", text: JSON.stringify(summarizeForLlm(text2, max_tokens, strategy), null, 2) }] };
1144
1145
  });
1146
+ var _BE_VERBS = /* @__PURE__ */ new Set(["am", "is", "are", "was", "were", "be", "been", "being"]);
1147
+ var _IRREGULAR_PARTICIPLES = /* @__PURE__ */ new Set([
1148
+ "awoken",
1149
+ "been",
1150
+ "become",
1151
+ "begun",
1152
+ "bent",
1153
+ "bet",
1154
+ "bitten",
1155
+ "bled",
1156
+ "blown",
1157
+ "born",
1158
+ "bought",
1159
+ "bound",
1160
+ "bred",
1161
+ "broken",
1162
+ "brought",
1163
+ "built",
1164
+ "burnt",
1165
+ "burst",
1166
+ "caught",
1167
+ "chosen",
1168
+ "clung",
1169
+ "come",
1170
+ "cost",
1171
+ "crept",
1172
+ "cut",
1173
+ "dealt",
1174
+ "dived",
1175
+ "done",
1176
+ "drawn",
1177
+ "dreamt",
1178
+ "driven",
1179
+ "drunk",
1180
+ "dug",
1181
+ "eaten",
1182
+ "fallen",
1183
+ "fed",
1184
+ "felt",
1185
+ "fit",
1186
+ "fled",
1187
+ "flown",
1188
+ "forbidden",
1189
+ "forgiven",
1190
+ "forgotten",
1191
+ "fought",
1192
+ "found",
1193
+ "frozen",
1194
+ "given",
1195
+ "gone",
1196
+ "gotten",
1197
+ "grown",
1198
+ "hidden",
1199
+ "held",
1200
+ "hung",
1201
+ "hurt",
1202
+ "kept",
1203
+ "knelt",
1204
+ "knit",
1205
+ "known",
1206
+ "laid",
1207
+ "lain",
1208
+ "led",
1209
+ "left",
1210
+ "lent",
1211
+ "let",
1212
+ "lit",
1213
+ "lost",
1214
+ "made",
1215
+ "meant",
1216
+ "met",
1217
+ "mistaken",
1218
+ "paid",
1219
+ "proven",
1220
+ "put",
1221
+ "quit",
1222
+ "read",
1223
+ "rid",
1224
+ "ridden",
1225
+ "risen",
1226
+ "run",
1227
+ "said",
1228
+ "sat",
1229
+ "sawn",
1230
+ "seen",
1231
+ "sent",
1232
+ "set",
1233
+ "sewn",
1234
+ "shaken",
1235
+ "shed",
1236
+ "shone",
1237
+ "shot",
1238
+ "shown",
1239
+ "shrunk",
1240
+ "shut",
1241
+ "sold",
1242
+ "sought",
1243
+ "sown",
1244
+ "spent",
1245
+ "spoken",
1246
+ "spread",
1247
+ "sprung",
1248
+ "stolen",
1249
+ "stood",
1250
+ "stuck",
1251
+ "stung",
1252
+ "struck",
1253
+ "strung",
1254
+ "sung",
1255
+ "sunk",
1256
+ "swept",
1257
+ "sworn",
1258
+ "swum",
1259
+ "swung",
1260
+ "taken",
1261
+ "taught",
1262
+ "thought",
1263
+ "thrown",
1264
+ "told",
1265
+ "torn",
1266
+ "understood",
1267
+ "upset",
1268
+ "woken",
1269
+ "won",
1270
+ "worn",
1271
+ "wound",
1272
+ "woven",
1273
+ "written"
1274
+ ]);
1275
+ function _isPastParticiple(word) {
1276
+ const w = word.toLowerCase();
1277
+ if (_IRREGULAR_PARTICIPLES.has(w)) return true;
1278
+ return w.length > 3 && /^[a-z]+ed$/.test(w);
1279
+ }
1280
+ function _splitIntoSentences(text2) {
1281
+ const matches = text2.match(/[^.!?]+[.!?]*/g);
1282
+ if (!matches) return [];
1283
+ return matches.map((s) => s.trim()).filter((s) => s.length > 0);
1284
+ }
1285
+ function _detectPassiveVoice(text2) {
1286
+ const sentences = _splitIntoSentences(text2);
1287
+ const analyzed = sentences.map((sentence, index) => {
1288
+ const words = sentence.match(/[a-zA-Z']+/g) ?? [];
1289
+ const matches = [];
1290
+ for (let i = 0; i < words.length; i++) {
1291
+ if (!_BE_VERBS.has(words[i].toLowerCase())) continue;
1292
+ let j = i + 1;
1293
+ let adverbCount = 0;
1294
+ while (j < words.length && adverbCount < 2 && /ly$/i.test(words[j]) && !_isPastParticiple(words[j])) {
1295
+ j++;
1296
+ adverbCount++;
1297
+ }
1298
+ if (j < words.length && _isPastParticiple(words[j])) {
1299
+ const hasAgent = words.slice(j + 1, j + 5).some((w) => w.toLowerCase() === "by");
1300
+ matches.push({ phrase: words.slice(i, j + 1).join(" "), hasAgent });
1301
+ i = j;
1302
+ }
1303
+ }
1304
+ return { index, text: sentence, isPassive: matches.length > 0, matches };
1305
+ });
1306
+ const totalSentences = analyzed.length;
1307
+ const passiveSentences = analyzed.filter((s) => s.isPassive).length;
1308
+ const passivePercent = totalSentences > 0 ? Math.round(passiveSentences / totalSentences * 1e3) / 10 : 0;
1309
+ return { sentences: analyzed, totalSentences, passiveSentences, passivePercent };
1310
+ }
1311
+ var _PNG_COLOR_TYPES = { 0: "grayscale", 2: "rgb", 3: "palette", 4: "grayscale+alpha", 6: "rgba" };
1312
+ var _JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
1313
+ function _readImageInfo(bytes) {
1314
+ if (bytes.length < 10) return { error: "File too small to determine format" };
1315
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
1316
+ if (bytes.length < 33) return { error: "Truncated PNG file" };
1317
+ const width = (bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19]) >>> 0;
1318
+ const height = (bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23]) >>> 0;
1319
+ const bitDepth = bytes[24];
1320
+ const colorTypeNum = bytes[25];
1321
+ const colorType = _PNG_COLOR_TYPES[colorTypeNum] ?? "unknown";
1322
+ const hasAlpha = colorTypeNum === 4 || colorTypeNum === 6;
1323
+ return { format: "png", width, height, bitDepth, colorType, hasAlpha };
1324
+ }
1325
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
1326
+ if (bytes.length < 10) return { error: "Truncated GIF file" };
1327
+ const width = bytes[6] | bytes[7] << 8;
1328
+ const height = bytes[8] | bytes[9] << 8;
1329
+ return { format: "gif", width, height };
1330
+ }
1331
+ if (bytes[0] === 66 && bytes[1] === 77) {
1332
+ if (bytes.length < 30) return { error: "Truncated BMP file" };
1333
+ const width = bytes[18] | bytes[19] << 8 | bytes[20] << 16 | bytes[21] << 24;
1334
+ const heightRaw = bytes[22] | bytes[23] << 8 | bytes[24] << 16 | bytes[25] << 24;
1335
+ const bitDepth = bytes[28] | bytes[29] << 8;
1336
+ return { format: "bmp", width: Math.abs(width), height: Math.abs(heightRaw), bitDepth };
1337
+ }
1338
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
1339
+ const fourCC = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
1340
+ if (fourCC === "VP8 ") {
1341
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
1342
+ const width = (bytes[26] | bytes[27] << 8) & 16383;
1343
+ const height = (bytes[28] | bytes[29] << 8) & 16383;
1344
+ return { format: "webp", width, height };
1345
+ }
1346
+ if (fourCC === "VP8L") {
1347
+ if (bytes.length < 25) return { error: "Truncated WebP file" };
1348
+ const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
1349
+ const width = (bits & 16383) + 1;
1350
+ const height = (bits >>> 14 & 16383) + 1;
1351
+ const hasAlpha = !!(bits >>> 28 & 1);
1352
+ return { format: "webp", width, height, hasAlpha };
1353
+ }
1354
+ if (fourCC === "VP8X") {
1355
+ if (bytes.length < 30) return { error: "Truncated WebP file" };
1356
+ const flags = bytes[20];
1357
+ const hasAlpha = !!(flags & 16);
1358
+ const width = (bytes[24] | bytes[25] << 8 | bytes[26] << 16) + 1;
1359
+ const height = (bytes[27] | bytes[28] << 8 | bytes[29] << 16) + 1;
1360
+ return { format: "webp", width, height, hasAlpha };
1361
+ }
1362
+ return { error: "Unrecognized WebP chunk format" };
1363
+ }
1364
+ if (bytes[0] === 255 && bytes[1] === 216) {
1365
+ let offset = 2;
1366
+ while (offset < bytes.length - 1) {
1367
+ if (bytes[offset] !== 255) {
1368
+ offset++;
1369
+ continue;
1370
+ }
1371
+ const marker = bytes[offset + 1];
1372
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
1373
+ offset += 2;
1374
+ continue;
1375
+ }
1376
+ if (marker === 217) break;
1377
+ if (offset + 3 >= bytes.length) break;
1378
+ const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
1379
+ if (_JPEG_SOF_MARKERS.has(marker)) {
1380
+ if (offset + 9 >= bytes.length) return { error: "Truncated JPEG SOF segment" };
1381
+ const bitDepth = bytes[offset + 4];
1382
+ const height = bytes[offset + 5] << 8 | bytes[offset + 6];
1383
+ const width = bytes[offset + 7] << 8 | bytes[offset + 8];
1384
+ const components = bytes[offset + 9];
1385
+ const colorType = components === 1 ? "grayscale" : components === 3 ? "rgb" : components === 4 ? "cmyk" : "unknown";
1386
+ return { format: "jpeg", width, height, bitDepth, colorType };
1387
+ }
1388
+ offset += 2 + segmentLength;
1389
+ }
1390
+ return { error: "No SOF marker found in JPEG (file may be corrupt)" };
1391
+ }
1392
+ return { error: "Unrecognized image format \u2014 supported: PNG, JPEG, GIF, WebP, BMP" };
1393
+ }
1394
+ server.tool("detect_passive_voice", 'Heuristically flag sentences containing passive-voice constructions, e.g. "was written", "were approved by the team".', { text: z.string() }, async ({ text: text2 }) => {
1395
+ return { content: [{ type: "text", text: JSON.stringify(_detectPassiveVoice(text2), null, 2) }] };
1396
+ });
1397
+ server.tool("read_image_info", "Read image format, dimensions, and color info directly from file bytes \u2014 no decoding required. Supports PNG, JPEG, GIF, WebP, BMP.", { image_base64: z.string().describe("Base64-encoded image bytes, optionally prefixed with a data: URL scheme") }, async ({ image_base64 }) => {
1398
+ const bytes = new Uint8Array(Buffer.from(image_base64.replace(/^data:.*;base64,/, ""), "base64"));
1399
+ return { content: [{ type: "text", text: JSON.stringify(_readImageInfo(bytes), null, 2) }] };
1400
+ });
1145
1401
  var transport = new StdioServerTransport();
1146
1402
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.3.0",
4
- "description": "MCP server exposing 70 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.4.0",
4
+ "description": "MCP server exposing 97 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",
7
7
  "homepage": "https://utilix.tech/docs",
@@ -32,14 +32,15 @@
32
32
  "README.md"
33
33
  ],
34
34
  "scripts": {
35
- "build": "tsup",
35
+ "build": "tsup && chmod +x dist/index.js",
36
36
  "dev": "tsup --watch",
37
37
  "start": "node dist/index.js",
38
+ "test": "node scripts/check-duplicate-tools.mjs",
38
39
  "prepublishOnly": "npm run build"
39
40
  },
40
41
  "dependencies": {
41
42
  "@modelcontextprotocol/sdk": "^1.12.0",
42
- "@utilix-tech/sdk": "^0.1.4",
43
+ "@utilix-tech/sdk": "^0.2.0",
43
44
  "zod": "^3.23.0"
44
45
  },
45
46
  "devDependencies": {
package/smithery.yaml CHANGED
@@ -1,7 +1,8 @@
1
1
  name: utilix
2
- description: 70 developer utility tools JSON, hashing, encoding, JWT, UUID, cron, color, SQL, regex, CSS, QR codes and more. All run locally, no API key required.
3
- homepage: https://utilix.tech
2
+ description: 70 developer utility tools for Claude, Cursor, VS Code, Windsurf, and any MCP-compatible AI assistant. Covers JSON, hashing, encoding, JWT, UUID, cron, color, SQL, regex, CSS, QR codes, and more. All tools run locally no API key required.
3
+ homepage: https://utilix.tech/mcp
4
4
  license: MIT
5
+ repository: https://github.com/utilix-tech/utilix-mcp
5
6
 
6
7
  startCommand:
7
8
  type: stdio