@utilix-tech/mcp 0.20.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +111 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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
|
+
|
|
5
9
|
## 0.20.0
|
|
6
10
|
- Add `detect_redos` tool: statically analyze a regex for ReDoS (catastrophic-backtracking) risk.
|
|
7
11
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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 (
|
|
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 (
|
|
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 |
|
package/dist/index.js
CHANGED
|
@@ -3714,5 +3714,116 @@ server.tool(
|
|
|
3714
3714
|
return text(JSON.stringify(result, null, 2));
|
|
3715
3715
|
}
|
|
3716
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
|
+
);
|
|
3717
3828
|
var transport = new StdioServerTransport();
|
|
3718
3829
|
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.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",
|