@utilix-tech/mcp 0.19.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 +3 -0
- package/README.md +3 -2
- package/dist/index.js +253 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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
|
+
|
|
5
8
|
## 0.19.0
|
|
6
9
|
- Added `calculate_time_value` (present/future value) and `vector_similarity` tools.
|
|
7
10
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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
|
|
|
@@ -186,7 +186,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
186
186
|
| `color_palette` | Generate a color palette from a base color |
|
|
187
187
|
| `color_shades` | Generate shades or tints from a base color |
|
|
188
188
|
|
|
189
|
-
### Code (
|
|
189
|
+
### Code (10 tools)
|
|
190
190
|
| Tool | Description |
|
|
191
191
|
|------|-------------|
|
|
192
192
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -198,6 +198,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
198
198
|
| `parse_docker_image` | Parse a Docker image reference into its components |
|
|
199
199
|
| `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
|
|
200
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 |
|
|
201
202
|
|
|
202
203
|
### Data (6 tools)
|
|
203
204
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -3461,5 +3461,258 @@ server.tool(
|
|
|
3461
3461
|
return text(JSON.stringify(result, null, 2));
|
|
3462
3462
|
}
|
|
3463
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
|
+
);
|
|
3464
3717
|
var transport = new StdioServerTransport();
|
|
3465
3718
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "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",
|