@utilix-tech/mcp 0.18.0 → 0.20.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 CHANGED
@@ -2,6 +2,12 @@
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.20.0
6
+ - Add `detect_redos` tool: statically analyze a regex for ReDoS (catastrophic-backtracking) risk.
7
+
8
+ ## 0.19.0
9
+ - Added `calculate_time_value` (present/future value) and `vector_similarity` tools.
10
+
5
11
  ## 0.18.0
6
12
  - Added `parse_user_agent` and `parse_github_url` tools.
7
13
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 125 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 128 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
 
@@ -149,7 +149,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
149
149
  | `date_diff` | Calculate the difference between two dates |
150
150
  | `convert_timezone` | Convert a date/time between timezones |
151
151
 
152
- ### Units (11 tools)
152
+ ### Units (12 tools)
153
153
  | Tool | Description |
154
154
  |------|-------------|
155
155
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -163,6 +163,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
163
163
  | `validate_isbn` | Validate an ISBN-10 or ISBN-13 checksum and convert it to the other format where possible |
164
164
  | `calculate_tip_split` | Compute a tip on a bill amount and split the bill, tip, and total evenly across N people |
165
165
  | `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
166
+ | `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
166
167
 
167
168
  ### API / Network (9 tools)
168
169
  | Tool | Description |
@@ -185,7 +186,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
185
186
  | `color_palette` | Generate a color palette from a base color |
186
187
  | `color_shades` | Generate shades or tints from a base color |
187
188
 
188
- ### Code (9 tools)
189
+ ### Code (10 tools)
189
190
  | Tool | Description |
190
191
  |------|-------------|
191
192
  | `format_sql` | Format a SQL query with proper indentation |
@@ -197,6 +198,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
197
198
  | `parse_docker_image` | Parse a Docker image reference into its components |
198
199
  | `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
199
200
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
201
+ | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
200
202
 
201
203
  ### Data (6 tools)
202
204
  | Tool | Description |
@@ -221,7 +223,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
221
223
  | `unicode_inspect` | Inspect Unicode code points of a string |
222
224
  | `optimize_svg` | Optimize and minify an SVG string |
223
225
 
224
- ### AI / Agent (25 tools)
226
+ ### AI / Agent (26 tools)
225
227
  | Tool | Description |
226
228
  |------|-------------|
227
229
  | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
@@ -249,6 +251,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
249
251
  | `compress_markdown` | Compress Markdown for LLM context budgets |
250
252
  | `compress_json` | Compress JSON for LLM context budgets |
251
253
  | `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
254
+ | `vector_similarity` | Cosine similarity, dot product, and Euclidean / Manhattan distance between two numeric vectors |
252
255
 
253
256
  ### Image (3 tools)
254
257
  | Tool | Description |
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ import { formatSql, formatHtml, testRegex, minifyJs, formatGql, parseDockerImage
74
74
  import { validateYaml, tomlToJson, xmlToJson, parseCsv } from "@utilix-tech/sdk/data";
75
75
  import { generateGradient, minifyCss } from "@utilix-tech/sdk/css";
76
76
  import { optimizeSvg, charToCodePoint, numberToWords } from "@utilix-tech/sdk/misc";
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
+ 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, vectorSimilarity } from "@utilix-tech/sdk/ai_agent";
78
78
  var CRON_DAY_MAP = {
79
79
  SUN: "0",
80
80
  MON: "1",
@@ -1140,6 +1140,11 @@ server.tool("score_relevance", "Score a passage relevance to a query (0-1 score
1140
1140
  server.tool("expand_query", "Expand a search query with synonyms and stemmed variants for better retrieval.", { query: z.string(), max_synonyms: z.number().int().min(1).max(5).default(2) }, async ({ query, max_synonyms }) => {
1141
1141
  return { content: [{ type: "text", text: JSON.stringify(expandQuery(query, max_synonyms), null, 2) }] };
1142
1142
  });
1143
+ server.tool("vector_similarity", "Compute cosine similarity, dot product, Euclidean distance, and Manhattan distance between two equal-length numeric vectors (e.g. embeddings from an external model).", { a: z.array(z.number()), b: z.array(z.number()) }, async ({ a, b }) => {
1144
+ const result = vectorSimilarity(a, b);
1145
+ if ("error" in result) return errText(result);
1146
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1147
+ });
1143
1148
  server.tool("summarize_for_llm", "Extractive summarization: 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
1149
  return { content: [{ type: "text", text: JSON.stringify(summarizeForLlm(text2, max_tokens, strategy), null, 2) }] };
1145
1150
  });
@@ -2295,6 +2300,55 @@ server.tool(
2295
2300
  return text(JSON.stringify(result, null, 2));
2296
2301
  }
2297
2302
  );
2303
+ function _calculateTimeValueMcp(amount, annualRatePercent, years, direction, compoundsPerYear = 1) {
2304
+ if (!Number.isFinite(amount) || amount < 0) return { error: "Amount must be zero or a positive number" };
2305
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) return { error: "Annual interest rate must be zero or a positive number" };
2306
+ if (!Number.isFinite(years) || years <= 0) return { error: "Years must be a positive number" };
2307
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) return { error: "Compounding frequency must be a positive whole number of periods per year" };
2308
+ if (direction !== "future" && direction !== "present") return { error: 'Direction must be "future" or "present"' };
2309
+ const round2 = (n) => Math.round(n * 100) / 100;
2310
+ const round6 = (n) => Math.round(n * 1e6) / 1e6;
2311
+ const ratePerPeriod = annualRatePercent / 100 / compoundsPerYear;
2312
+ const totalPeriods = years * compoundsPerYear;
2313
+ const growthFactor = Math.pow(1 + ratePerPeriod, totalPeriods);
2314
+ let presentValue;
2315
+ let futureValue;
2316
+ if (direction === "future") {
2317
+ presentValue = amount;
2318
+ futureValue = amount * growthFactor;
2319
+ } else {
2320
+ futureValue = amount;
2321
+ presentValue = amount / growthFactor;
2322
+ }
2323
+ return {
2324
+ direction,
2325
+ annualRatePercent,
2326
+ years,
2327
+ compoundsPerYear,
2328
+ totalPeriods,
2329
+ ratePerPeriodPercent: round6(ratePerPeriod * 100),
2330
+ growthFactor: round6(growthFactor),
2331
+ presentValue: round2(presentValue),
2332
+ futureValue: round2(futureValue),
2333
+ interest: round2(futureValue - presentValue)
2334
+ };
2335
+ }
2336
+ server.tool(
2337
+ "calculate_time_value",
2338
+ 'Convert between present and future value (time value of money). With direction "future" the amount is a present value grown to its future value at the given annual rate; with direction "present" it is a future value discounted back to present value. Compounds compoundsPerYear times per year (1 = annually).',
2339
+ {
2340
+ amount: z.number().describe('The known value: a present value when direction is "future", or a future value when direction is "present"'),
2341
+ annualRatePercent: z.number().describe("Annual interest / discount rate as a percent, e.g. 5"),
2342
+ years: z.number().describe("Number of years, may be fractional"),
2343
+ direction: z.enum(["future", "present"]).describe("Whether to grow the amount to a future value or discount it to a present value"),
2344
+ compoundsPerYear: z.number().int().default(1).describe("Compounding periods per year (1 = annually)")
2345
+ },
2346
+ async ({ amount, annualRatePercent, years, direction, compoundsPerYear }) => {
2347
+ const result = _calculateTimeValueMcp(amount, annualRatePercent, years, direction, compoundsPerYear);
2348
+ if ("error" in result) return errText(result);
2349
+ return text(JSON.stringify(result, null, 2));
2350
+ }
2351
+ );
2298
2352
  var _BASE58_ALPHABET_MCP = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2299
2353
  var _BASE62_ALPHABET_MCP = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2300
2354
  function _encodeBytesToBaseMcp(bytes, alphabet) {
@@ -3407,5 +3461,258 @@ server.tool(
3407
3461
  return text(JSON.stringify(result, null, 2));
3408
3462
  }
3409
3463
  );
3464
+ var _REDOS_UNBOUNDED_MCP = /* @__PURE__ */ new Set(["*", "+"]);
3465
+ var _REDOS_ALPHABET_MCP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ .,-@/:;#%&=";
3466
+ function _redosParseQuantMcp(src, i) {
3467
+ const ch = src[i];
3468
+ if (ch === "*" || ch === "+" || ch === "?") {
3469
+ let end = i + 1;
3470
+ if (src[end] === "?" || src[end] === "+") end++;
3471
+ return { unbounded: _REDOS_UNBOUNDED_MCP.has(ch), raw: src.slice(i, end), end };
3472
+ }
3473
+ if (ch === "{") {
3474
+ const close = src.indexOf("}", i);
3475
+ if (close === -1) return null;
3476
+ const inner = src.slice(i + 1, close);
3477
+ if (!/^\d+(,\d*)?$/.test(inner)) return null;
3478
+ let end = close + 1;
3479
+ if (src[end] === "?" || src[end] === "+") end++;
3480
+ return { unbounded: /^\d+,$/.test(inner), raw: src.slice(i, end), end };
3481
+ }
3482
+ return null;
3483
+ }
3484
+ function _redosParseAlternationMcp(src, start, endExclusive) {
3485
+ const alternatives = [];
3486
+ let current = [];
3487
+ let i = start;
3488
+ while (i < endExclusive) {
3489
+ const ch = src[i];
3490
+ if (ch === "|") {
3491
+ alternatives.push(current);
3492
+ current = [];
3493
+ i++;
3494
+ continue;
3495
+ }
3496
+ let node;
3497
+ if (ch === "\\") {
3498
+ const atomEnd = i + 2 <= endExclusive ? i + 2 : i + 1;
3499
+ node = { kind: "char", start: i, end: atomEnd, quant: null, quantEnd: atomEnd, raw: src.slice(i, atomEnd) };
3500
+ i = atomEnd;
3501
+ } else if (ch === "[") {
3502
+ let j = i + 1;
3503
+ if (src[j] === "^") j++;
3504
+ if (src[j] === "]") j++;
3505
+ while (j < endExclusive && src[j] !== "]") {
3506
+ j += src[j] === "\\" ? 2 : 1;
3507
+ }
3508
+ const classEnd = j < endExclusive ? j + 1 : endExclusive;
3509
+ node = { kind: "class", start: i, end: classEnd, quant: null, quantEnd: classEnd, raw: src.slice(i, classEnd) };
3510
+ i = classEnd;
3511
+ } else if (ch === "(") {
3512
+ let depth = 1;
3513
+ let j = i + 1;
3514
+ while (j < endExclusive && depth > 0) {
3515
+ const cj = src[j];
3516
+ if (cj === "\\") {
3517
+ j += 2;
3518
+ continue;
3519
+ }
3520
+ if (cj === "[") {
3521
+ j++;
3522
+ if (src[j] === "^") j++;
3523
+ if (src[j] === "]") j++;
3524
+ while (j < endExclusive && src[j] !== "]") {
3525
+ j += src[j] === "\\" ? 2 : 1;
3526
+ }
3527
+ j++;
3528
+ continue;
3529
+ }
3530
+ if (cj === "(") depth++;
3531
+ else if (cj === ")") depth--;
3532
+ j++;
3533
+ }
3534
+ const closeIdx = depth === 0 ? j - 1 : endExclusive;
3535
+ let contentStart = i + 1;
3536
+ let lookaround = false;
3537
+ if (src[i + 1] === "?") {
3538
+ const after = src[i + 2];
3539
+ if (after === ":") contentStart = i + 3;
3540
+ else if (after === "=" || after === "!") {
3541
+ contentStart = i + 3;
3542
+ lookaround = true;
3543
+ } else if (after === "<") {
3544
+ const a3 = src[i + 3];
3545
+ if (a3 === "=" || a3 === "!") {
3546
+ contentStart = i + 4;
3547
+ lookaround = true;
3548
+ } else {
3549
+ const gt = src.indexOf(">", i + 2);
3550
+ contentStart = gt !== -1 && gt < closeIdx ? gt + 1 : i + 3;
3551
+ }
3552
+ }
3553
+ }
3554
+ node = {
3555
+ kind: "group",
3556
+ start: i,
3557
+ end: closeIdx + 1,
3558
+ quant: null,
3559
+ quantEnd: closeIdx + 1,
3560
+ raw: src.slice(i, closeIdx + 1),
3561
+ alternatives: _redosParseAlternationMcp(src, contentStart, closeIdx),
3562
+ lookaround
3563
+ };
3564
+ i = closeIdx + 1;
3565
+ } else if (ch === ".") {
3566
+ node = { kind: "dot", start: i, end: i + 1, quant: null, quantEnd: i + 1, raw: "." };
3567
+ i++;
3568
+ } else if (ch === "^" || ch === "$") {
3569
+ node = { kind: "anchor", start: i, end: i + 1, quant: null, quantEnd: i + 1, raw: ch };
3570
+ i++;
3571
+ } else {
3572
+ node = { kind: "char", start: i, end: i + 1, quant: null, quantEnd: i + 1, raw: ch };
3573
+ i++;
3574
+ }
3575
+ if (node.kind !== "anchor") {
3576
+ const q = _redosParseQuantMcp(src, i);
3577
+ if (q) {
3578
+ node.quant = q;
3579
+ node.quantEnd = q.end;
3580
+ i = q.end;
3581
+ }
3582
+ }
3583
+ current.push(node);
3584
+ }
3585
+ alternatives.push(current);
3586
+ return alternatives;
3587
+ }
3588
+ function _redosFirstMatcherMcp(node) {
3589
+ if (node.kind === "dot") return (c) => c !== "\n";
3590
+ if (node.kind === "char") {
3591
+ if (node.raw.startsWith("\\")) {
3592
+ const body = node.raw.slice(1);
3593
+ if (/^[dDwWsS]$/.test(body)) {
3594
+ try {
3595
+ const re = new RegExp("\\" + body);
3596
+ return (c) => re.test(c);
3597
+ } catch {
3598
+ return null;
3599
+ }
3600
+ }
3601
+ const literal = { n: "\n", t: " ", r: "\r", f: "\f", v: "\v" }[body] ?? body;
3602
+ return (c) => c === literal;
3603
+ }
3604
+ const lit = node.raw;
3605
+ return (c) => c === lit;
3606
+ }
3607
+ if (node.kind === "class") {
3608
+ try {
3609
+ const re = new RegExp(node.raw);
3610
+ return (c) => re.test(c);
3611
+ } catch {
3612
+ return null;
3613
+ }
3614
+ }
3615
+ if (node.kind === "group") {
3616
+ const first = node.alternatives?.[0]?.[0];
3617
+ return first ? _redosFirstMatcherMcp(first) : null;
3618
+ }
3619
+ return null;
3620
+ }
3621
+ function _redosOverlapsMcp(a, b) {
3622
+ const fa = _redosFirstMatcherMcp(a);
3623
+ const fb = _redosFirstMatcherMcp(b);
3624
+ if (!fa || !fb) return false;
3625
+ for (const c of _REDOS_ALPHABET_MCP) if (fa(c) && fb(c)) return true;
3626
+ return false;
3627
+ }
3628
+ function _redosAnalyzeGroupMcp(node, findings) {
3629
+ const alts = node.alternatives ?? [];
3630
+ const outerUnbounded = node.quant?.unbounded === true && !node.lookaround;
3631
+ if (outerUnbounded) {
3632
+ for (const alt of alts) {
3633
+ const meaningful = alt.filter((n) => n.kind !== "anchor");
3634
+ if (meaningful.length === 1 && meaningful[0].quant?.unbounded) {
3635
+ findings.push({
3636
+ type: "nested-quantifier",
3637
+ severity: "high",
3638
+ message: "Nested unbounded quantifiers: a repeated group whose body is itself unbounded-repeated can match the same input in exponentially many ways, causing catastrophic backtracking.",
3639
+ snippet: node.raw + (node.quant?.raw ?? ""),
3640
+ index: node.start
3641
+ });
3642
+ break;
3643
+ }
3644
+ }
3645
+ if (alts.length > 1) {
3646
+ const firsts = alts.map((alt) => alt.find((n) => n.kind !== "anchor")).filter((n) => !!n);
3647
+ let flagged = false;
3648
+ for (let x = 0; x < firsts.length && !flagged; x++) {
3649
+ for (let y = x + 1; y < firsts.length; y++) {
3650
+ if (_redosOverlapsMcp(firsts[x], firsts[y])) {
3651
+ findings.push({
3652
+ type: "overlapping-alternation",
3653
+ severity: "medium",
3654
+ message: "Overlapping alternation under a repetition: two branches can match the same starting input, so the engine has multiple ways to split each repeat and may backtrack heavily.",
3655
+ snippet: node.raw + (node.quant?.raw ?? ""),
3656
+ index: node.start
3657
+ });
3658
+ flagged = true;
3659
+ break;
3660
+ }
3661
+ }
3662
+ }
3663
+ }
3664
+ }
3665
+ for (const alt of alts) _redosAnalyzeSequenceMcp(alt, findings);
3666
+ }
3667
+ function _redosAnalyzeSequenceMcp(seq, findings) {
3668
+ for (let k = 0; k + 1 < seq.length; k++) {
3669
+ const a = seq[k];
3670
+ const b = seq[k + 1];
3671
+ if (a.quant?.unbounded && b.quant?.unbounded && _redosOverlapsMcp(a, b)) {
3672
+ findings.push({
3673
+ type: "adjacent-quantifier",
3674
+ severity: "medium",
3675
+ message: "Adjacent unbounded quantifiers over overlapping characters: the engine can distribute the same input between them in many ways, causing quadratic backtracking.",
3676
+ snippet: seq[k].raw + (a.quant?.raw ?? "") + seq[k + 1].raw + (b.quant?.raw ?? ""),
3677
+ index: a.start
3678
+ });
3679
+ }
3680
+ }
3681
+ for (const node of seq) if (node.kind === "group") _redosAnalyzeGroupMcp(node, findings);
3682
+ }
3683
+ function _detectRedosMcp(pattern) {
3684
+ if (typeof pattern !== "string" || pattern === "") return { error: "Empty pattern" };
3685
+ if (pattern.length > 5e3) return { error: "Pattern too long (max 5,000 chars)" };
3686
+ let valid = true;
3687
+ let compileError;
3688
+ try {
3689
+ new RegExp(pattern);
3690
+ } catch (e) {
3691
+ valid = false;
3692
+ compileError = e instanceof Error ? e.message : String(e);
3693
+ }
3694
+ const findings = [];
3695
+ for (const alt of _redosParseAlternationMcp(pattern, 0, pattern.length)) _redosAnalyzeSequenceMcp(alt, findings);
3696
+ const seen = /* @__PURE__ */ new Set();
3697
+ const unique = findings.filter((f) => {
3698
+ const key = `${f.type}:${f.index}`;
3699
+ if (seen.has(key)) return false;
3700
+ seen.add(key);
3701
+ return true;
3702
+ });
3703
+ unique.sort((a, b) => a.index - b.index);
3704
+ const risk = unique.some((f) => f.severity === "high") ? "high" : unique.length > 0 ? "medium" : "none";
3705
+ return { pattern, valid, ...compileError ? { compileError } : {}, risk, safe: unique.length === 0, findings: unique };
3706
+ }
3707
+ server.tool(
3708
+ "detect_redos",
3709
+ "Statically analyze a regex pattern for catastrophic-backtracking (ReDoS) risk. Flags nested unbounded quantifiers ((a+)+), overlapping alternation under a repeat ((a|ab)*), and adjacent overlapping quantifiers (\\d+\\d+). Heuristic linter, not a formal proof. Pass the pattern body only, no delimiters or flags.",
3710
+ { pattern: z.string().describe("The regex pattern body to analyze, e.g. (a+)+") },
3711
+ async ({ pattern }) => {
3712
+ const result = _detectRedosMcp(pattern);
3713
+ if ("error" in result) return errText(result);
3714
+ return text(JSON.stringify(result, null, 2));
3715
+ }
3716
+ );
3410
3717
  var transport = new StdioServerTransport();
3411
3718
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "MCP server exposing 125 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",