@utilix-tech/mcp 0.19.0 → 0.21.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,13 @@
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.21.0
6
+ - Add `build_cache_control` tool: build and explain a `Cache-Control` response header from directives.
7
+ - Add `detect_filler_words` tool: scan text for filler words, hedge phrases, and clichés.
8
+
9
+ ## 0.20.0
10
+ - Add `detect_redos` tool: statically analyze a regex for ReDoS (catastrophic-backtracking) risk.
11
+
5
12
  ## 0.19.0
6
13
  - Added `calculate_time_value` (present/future value) and `vector_similarity` tools.
7
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 127 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 130 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
 
@@ -125,7 +125,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
125
125
  | `generate_qr` | Generate a QR code as an SVG string |
126
126
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
127
127
 
128
- ### Text (11 tools)
128
+ ### Text (12 tools)
129
129
  | Tool | Description |
130
130
  |------|-------------|
131
131
  | `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
@@ -139,6 +139,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
139
139
  | `line_operations` | Sort, deduplicate, reverse, shuffle, trim, or number lines |
140
140
  | `detect_passive_voice` | Flag sentences written in passive voice, e.g. "was written", "were approved by the team" |
141
141
  | `score_readability` | Score text with Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index |
142
+ | `detect_filler_words` | Scan text for filler words, hedge phrases, and clichés with per-phrase counts and a density score |
142
143
 
143
144
  ### Time (5 tools)
144
145
  | Tool | Description |
@@ -165,7 +166,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
165
166
  | `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
166
167
  | `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
167
168
 
168
- ### API / Network (9 tools)
169
+ ### API / Network (10 tools)
169
170
  | Tool | Description |
170
171
  |------|-------------|
171
172
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -177,6 +178,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
177
178
  | `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
178
179
  | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
179
180
  | `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
181
+ | `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
180
182
 
181
183
  ### Color (4 tools)
182
184
  | Tool | Description |
@@ -186,7 +188,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
186
188
  | `color_palette` | Generate a color palette from a base color |
187
189
  | `color_shades` | Generate shades or tints from a base color |
188
190
 
189
- ### Code (9 tools)
191
+ ### Code (10 tools)
190
192
  | Tool | Description |
191
193
  |------|-------------|
192
194
  | `format_sql` | Format a SQL query with proper indentation |
@@ -198,6 +200,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
198
200
  | `parse_docker_image` | Parse a Docker image reference into its components |
199
201
  | `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
200
202
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
203
+ | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
201
204
 
202
205
  ### Data (6 tools)
203
206
  | Tool | Description |
package/dist/index.js CHANGED
@@ -3461,5 +3461,369 @@ 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
+ );
3717
+ var _CC_NUM_FIELDS_MCP = ["maxAge", "sMaxAge", "staleWhileRevalidate", "staleIfError"];
3718
+ function _humanizeCacheSecondsMcp(s) {
3719
+ if (s === 0) return "0 seconds";
3720
+ const units = [[86400, "day"], [3600, "hour"], [60, "minute"], [1, "second"]];
3721
+ const parts = [];
3722
+ let rem = s;
3723
+ for (const [size, label] of units) {
3724
+ if (rem >= size) {
3725
+ const n = Math.floor(rem / size);
3726
+ rem -= n * size;
3727
+ parts.push(`${n} ${label}${n === 1 ? "" : "s"}`);
3728
+ }
3729
+ }
3730
+ return parts.slice(0, 2).join(" ");
3731
+ }
3732
+ function _buildCacheControlMcp(options) {
3733
+ const nums = { maxAge: null, sMaxAge: null, staleWhileRevalidate: null, staleIfError: null };
3734
+ for (const field of _CC_NUM_FIELDS_MCP) {
3735
+ const v = options[field];
3736
+ if (v === void 0 || v === null) continue;
3737
+ if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v) || v < 0) {
3738
+ return { error: `${field} must be a non-negative integer number of seconds` };
3739
+ }
3740
+ nums[field] = v;
3741
+ }
3742
+ const d = [];
3743
+ const warnings = [];
3744
+ if (options.visibility === "public") d.push({ name: "public", value: null, explanation: "Any cache, including shared caches and CDNs, may store this response." });
3745
+ if (options.visibility === "private") d.push({ name: "private", value: null, explanation: "Only the user's browser may cache this response; shared caches and CDNs must not." });
3746
+ if (options.noCache) d.push({ name: "no-cache", value: null, explanation: "A cache may store the response but must revalidate it with the origin before every reuse." });
3747
+ if (options.noStore) d.push({ name: "no-store", value: null, explanation: "No cache may store any part of this request or response." });
3748
+ if (nums.maxAge !== null) d.push({ name: "max-age", value: String(nums.maxAge), explanation: `Considered fresh for ${_humanizeCacheSecondsMcp(nums.maxAge)} (${nums.maxAge}s), after which it becomes stale.` });
3749
+ if (nums.sMaxAge !== null) d.push({ name: "s-maxage", value: String(nums.sMaxAge), explanation: `For shared caches (CDNs, proxies) it is fresh for ${_humanizeCacheSecondsMcp(nums.sMaxAge)} (${nums.sMaxAge}s), overriding max-age there.` });
3750
+ if (options.mustRevalidate) d.push({ name: "must-revalidate", value: null, explanation: "Once stale, a cache must revalidate with the origin and never serve the stale copy." });
3751
+ if (options.proxyRevalidate) d.push({ name: "proxy-revalidate", value: null, explanation: "Like must-revalidate, but the requirement applies only to shared caches." });
3752
+ if (options.immutable) d.push({ name: "immutable", value: null, explanation: "The body will not change while fresh, so browsers should not revalidate it on reload." });
3753
+ if (options.noTransform) d.push({ name: "no-transform", value: null, explanation: "Intermediaries must not modify the response body (for example, recompressing images)." });
3754
+ if (nums.staleWhileRevalidate !== null) d.push({ name: "stale-while-revalidate", value: String(nums.staleWhileRevalidate), explanation: `A cache may serve the stale response for up to ${_humanizeCacheSecondsMcp(nums.staleWhileRevalidate)} (${nums.staleWhileRevalidate}s) while it revalidates in the background.` });
3755
+ if (nums.staleIfError !== null) d.push({ name: "stale-if-error", value: String(nums.staleIfError), explanation: `A cache may serve the stale response for up to ${_humanizeCacheSecondsMcp(nums.staleIfError)} (${nums.staleIfError}s) if the origin returns an error.` });
3756
+ if (options.mustUnderstand) d.push({ name: "must-understand", value: null, explanation: "A cache should store the response only if it understands the caching requirements of the response's status code." });
3757
+ if (options.noStore && d.length > 1) warnings.push("no-store means nothing is cached, so the other directives have no effect.");
3758
+ if (options.noCache && nums.maxAge !== null) warnings.push("no-cache forces revalidation on every use, so max-age has little practical effect alongside it.");
3759
+ if (options.immutable && nums.maxAge === null) warnings.push("immutable only applies while the response is fresh \u2014 add a max-age so it has a freshness lifetime.");
3760
+ if (d.length === 0) warnings.push("No directives set \u2014 an empty Cache-Control header has no effect.");
3761
+ const header = d.map((x) => x.value === null ? x.name : `${x.name}=${x.value}`).join(", ");
3762
+ return { header, headerName: "Cache-Control", snippet: `Cache-Control: ${header}`, directives: d, warnings };
3763
+ }
3764
+ server.tool(
3765
+ "build_cache_control",
3766
+ "Build an HTTP Cache-Control response-header value from a set of directives (public/private, no-cache, no-store, max-age, s-maxage, must-revalidate, immutable, stale-while-revalidate, etc.), with a plain-English explanation of each directive and warnings for combinations that cancel each other out. Pure string assembly, not validated against a live origin.",
3767
+ {
3768
+ visibility: z.enum(["public", "private"]).optional().describe("public (any cache) or private (browser only)"),
3769
+ noCache: z.boolean().optional(),
3770
+ noStore: z.boolean().optional(),
3771
+ noTransform: z.boolean().optional(),
3772
+ mustRevalidate: z.boolean().optional(),
3773
+ proxyRevalidate: z.boolean().optional(),
3774
+ immutable: z.boolean().optional(),
3775
+ mustUnderstand: z.boolean().optional(),
3776
+ maxAge: z.number().int().nonnegative().optional().describe("Freshness lifetime in seconds"),
3777
+ sMaxAge: z.number().int().nonnegative().optional().describe("Freshness lifetime for shared caches, in seconds"),
3778
+ staleWhileRevalidate: z.number().int().nonnegative().optional(),
3779
+ staleIfError: z.number().int().nonnegative().optional()
3780
+ },
3781
+ async (opts) => {
3782
+ const result = _buildCacheControlMcp(opts);
3783
+ if ("error" in result) return errText(result);
3784
+ return text(JSON.stringify(result, null, 2));
3785
+ }
3786
+ );
3787
+ var _FILLER_WORDS_MCP = ["very", "really", "just", "actually", "basically", "literally", "simply", "totally", "definitely", "certainly", "essentially", "absolutely", "completely", "extremely", "truly", "honestly", "seriously", "obviously", "clearly", "quite", "rather", "somewhat", "fairly", "virtually"];
3788
+ var _HEDGE_PHRASES_MCP = ["maybe", "perhaps", "probably", "possibly", "arguably", "kind of", "sort of", "a bit", "a little", "more or less", "in my opinion", "i think", "i believe", "to some extent"];
3789
+ var _CLICHE_PHRASES_MCP = ["at the end of the day", "think outside the box", "low-hanging fruit", "move the needle", "circle back", "touch base", "on the same page", "hit the ground running", "paradigm shift", "win-win", "game changer", "best of both worlds", "only time will tell", "back to the drawing board", "the tip of the iceberg", "a perfect storm", "needle in a haystack", "raise the bar", "push the envelope", "the elephant in the room", "boil the ocean", "when it rains it pours"];
3790
+ var _FILLER_DICTIONARY_MCP = [
3791
+ ..._FILLER_WORDS_MCP.map((phrase) => ({ phrase, category: "filler" })),
3792
+ ..._HEDGE_PHRASES_MCP.map((phrase) => ({ phrase, category: "hedge" })),
3793
+ ..._CLICHE_PHRASES_MCP.map((phrase) => ({ phrase, category: "cliche" }))
3794
+ ];
3795
+ function _fillerRegexMcp(phrase) {
3796
+ const tokens = phrase.split(/\s+/).map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3797
+ return new RegExp("\\b" + tokens.join("\\s+") + "\\b", "gi");
3798
+ }
3799
+ function _detectFillerWordsMcp(input) {
3800
+ const totalWords = (input.match(/[A-Za-z0-9']+/g) ?? []).length;
3801
+ const matches = [];
3802
+ for (const entry of _FILLER_DICTIONARY_MCP) {
3803
+ const re = _fillerRegexMcp(entry.phrase);
3804
+ const indices = [];
3805
+ let m;
3806
+ while ((m = re.exec(input)) !== null) {
3807
+ indices.push(m.index);
3808
+ if (m.index === re.lastIndex) re.lastIndex++;
3809
+ }
3810
+ if (indices.length > 0) matches.push({ phrase: entry.phrase, category: entry.category, count: indices.length, indices });
3811
+ }
3812
+ matches.sort((a, b) => b.count - a.count || a.category.localeCompare(b.category) || a.phrase.localeCompare(b.phrase));
3813
+ const fillerCount = matches.filter((x) => x.category === "filler").reduce((s, x) => s + x.count, 0);
3814
+ const hedgeCount = matches.filter((x) => x.category === "hedge").reduce((s, x) => s + x.count, 0);
3815
+ const clicheCount = matches.filter((x) => x.category === "cliche").reduce((s, x) => s + x.count, 0);
3816
+ const totalMatches = fillerCount + hedgeCount + clicheCount;
3817
+ const density = totalWords > 0 ? Math.round(totalMatches / totalWords * 1e3) / 10 : 0;
3818
+ return { totalWords, totalMatches, fillerCount, hedgeCount, clicheCount, density, matches };
3819
+ }
3820
+ server.tool(
3821
+ "detect_filler_words",
3822
+ "Scan prose for filler words (very, just, really), hedge phrases (kind of, I think), and clich\xE9s (at the end of the day, circle back), returning each matched phrase with its category, occurrence count, and character offsets, plus an overall density. Dictionary-based heuristic: a flagged word is a candidate to reconsider, not always wrong in context.",
3823
+ { text: z.string().describe("The prose to scan for filler words and clich\xE9s") },
3824
+ async ({ text: input }) => {
3825
+ return text(JSON.stringify(_detectFillerWordsMcp(input), null, 2));
3826
+ }
3827
+ );
3464
3828
  var transport = new StdioServerTransport();
3465
3829
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",