@utilix-tech/mcp 0.3.1 → 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 +13 -3
- package/dist/index.js +255 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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
|
|
|
@@ -121,7 +121,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
121
121
|
| `generate_qr` | Generate a QR code as an SVG string |
|
|
122
122
|
| `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
|
|
123
123
|
|
|
124
|
-
### Text (
|
|
124
|
+
### Text (10 tools)
|
|
125
125
|
| Tool | Description |
|
|
126
126
|
|------|-------------|
|
|
127
127
|
| `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
|
|
@@ -133,6 +133,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
133
133
|
| `text_diff` | Diff two text blocks line-by-line |
|
|
134
134
|
| `html_to_markdown` | Convert HTML to Markdown |
|
|
135
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" |
|
|
136
137
|
|
|
137
138
|
### Time (5 tools)
|
|
138
139
|
| Tool | Description |
|
|
@@ -226,10 +227,19 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
226
227
|
| `compress_json` | Compress JSON for LLM context budgets |
|
|
227
228
|
| `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
|
|
228
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
|
+
|
|
229
235
|
## Requirements
|
|
230
236
|
|
|
231
237
|
- Node.js 18+
|
|
232
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
|
+
|
|
233
243
|
## Links
|
|
234
244
|
|
|
235
245
|
- [Utilix web app](https://utilix.tech) — use these tools in the browser
|
package/dist/index.js
CHANGED
|
@@ -1143,5 +1143,260 @@ server.tool("expand_query", "Expand a search query with synonyms and stemmed var
|
|
|
1143
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 }) => {
|
|
1144
1144
|
return { content: [{ type: "text", text: JSON.stringify(summarizeForLlm(text2, max_tokens, strategy), null, 2) }] };
|
|
1145
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
|
+
});
|
|
1146
1401
|
var transport = new StdioServerTransport();
|
|
1147
1402
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
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",
|
|
@@ -35,11 +35,12 @@
|
|
|
35
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.
|
|
43
|
+
"@utilix-tech/sdk": "^0.2.0",
|
|
43
44
|
"zod": "^3.23.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|