@utilix-tech/mcp 0.20.0 → 0.22.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,14 @@
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.22.0
6
+ - Add `calculate_percentage_change` tool: percentage change between two numbers, with absolute change, direction, and multiplier.
7
+ - Add `resolve_json_pointer` tool: resolve an RFC 6901 JSON Pointer against a document, with ~0/~1 escaping and not-found reasons.
8
+
9
+ ## 0.21.0
10
+ - Add `build_cache_control` tool: build and explain a `Cache-Control` response header from directives.
11
+ - Add `detect_filler_words` tool: scan text for filler words, hedge phrases, and clichés.
12
+
5
13
  ## 0.20.0
6
14
  - Add `detect_redos` tool: statically analyze a regex for ReDoS (catastrophic-backtracking) risk.
7
15
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
3
+ MCP server exposing 132 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
 
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
75
75
 
76
76
  ## Tools
77
77
 
78
- ### JSON (13 tools)
78
+ ### JSON (14 tools)
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
81
  | `format_json` | Pretty-print and validate a JSON string |
@@ -84,6 +84,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
84
84
  | `json_to_csv` | Convert a JSON array of objects to CSV |
85
85
  | `csv_to_json` | Convert CSV text to a JSON array |
86
86
  | `jsonpath` | Evaluate a JSONPath expression |
87
+ | `resolve_json_pointer` | Resolve an RFC 6901 JSON Pointer against a JSON document |
87
88
  | `validate_json` | Validate JSON against a JSON Schema |
88
89
  | `yaml_to_json` | Convert YAML to JSON |
89
90
  | `json_to_yaml` | Convert JSON to YAML |
@@ -125,7 +126,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
125
126
  | `generate_qr` | Generate a QR code as an SVG string |
126
127
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
127
128
 
128
- ### Text (11 tools)
129
+ ### Text (12 tools)
129
130
  | Tool | Description |
130
131
  |------|-------------|
131
132
  | `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
@@ -139,6 +140,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
139
140
  | `line_operations` | Sort, deduplicate, reverse, shuffle, trim, or number lines |
140
141
  | `detect_passive_voice` | Flag sentences written in passive voice, e.g. "was written", "were approved by the team" |
141
142
  | `score_readability` | Score text with Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index |
143
+ | `detect_filler_words` | Scan text for filler words, hedge phrases, and clichés with per-phrase counts and a density score |
142
144
 
143
145
  ### Time (5 tools)
144
146
  | Tool | Description |
@@ -149,7 +151,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
149
151
  | `date_diff` | Calculate the difference between two dates |
150
152
  | `convert_timezone` | Convert a date/time between timezones |
151
153
 
152
- ### Units (12 tools)
154
+ ### Units (13 tools)
153
155
  | Tool | Description |
154
156
  |------|-------------|
155
157
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -164,8 +166,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
164
166
  | `calculate_tip_split` | Compute a tip on a bill amount and split the bill, tip, and total evenly across N people |
165
167
  | `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
166
168
  | `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
169
+ | `calculate_percentage_change` | Compute the percentage change between two numbers, with absolute change, direction, and multiplier |
167
170
 
168
- ### API / Network (9 tools)
171
+ ### API / Network (10 tools)
169
172
  | Tool | Description |
170
173
  |------|-------------|
171
174
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -177,6 +180,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
177
180
  | `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
178
181
  | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
179
182
  | `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
183
+ | `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
180
184
 
181
185
  ### Color (4 tools)
182
186
  | Tool | Description |
package/dist/index.js CHANGED
@@ -3714,5 +3714,203 @@ 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
+ );
3828
+ function _calculatePercentageChangeMcp(oldValue, newValue) {
3829
+ if (!Number.isFinite(oldValue)) return { error: "Original value must be a finite number" };
3830
+ if (!Number.isFinite(newValue)) return { error: "New value must be a finite number" };
3831
+ if (oldValue === 0 && newValue !== 0) return { error: "Percentage change from zero is undefined (division by zero); use a nonzero original value" };
3832
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
3833
+ const absoluteChange = newValue - oldValue;
3834
+ const percentChange = oldValue === 0 ? 0 : absoluteChange / Math.abs(oldValue) * 100;
3835
+ const multiplier = oldValue === 0 ? 1 : newValue / oldValue;
3836
+ let direction;
3837
+ if (absoluteChange > 0) direction = "increase";
3838
+ else if (absoluteChange < 0) direction = "decrease";
3839
+ else direction = "no change";
3840
+ return {
3841
+ oldValue,
3842
+ newValue,
3843
+ absoluteChange: round4(absoluteChange),
3844
+ percentChange: round4(percentChange),
3845
+ direction,
3846
+ multiplier: round4(multiplier)
3847
+ };
3848
+ }
3849
+ server.tool(
3850
+ "calculate_percentage_change",
3851
+ "Compute the percentage change from an original value to a new value as ((new - old) / |old|) * 100, plus the absolute difference, the direction (increase/decrease/no change), and the multiplier (new / old). The original value cannot be zero unless the new value is also zero.",
3852
+ {
3853
+ oldValue: z.number().describe("The original / starting value"),
3854
+ newValue: z.number().describe("The new / ending value")
3855
+ },
3856
+ async ({ oldValue, newValue }) => {
3857
+ const result = _calculatePercentageChangeMcp(oldValue, newValue);
3858
+ if ("error" in result) return errText(result);
3859
+ return text(JSON.stringify(result, null, 2));
3860
+ }
3861
+ );
3862
+ function _unescapePointerTokenMcp(token) {
3863
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
3864
+ }
3865
+ function _resolveJsonPointerMcp(json, pointer) {
3866
+ let parsed;
3867
+ try {
3868
+ parsed = JSON.parse(json);
3869
+ } catch {
3870
+ return { pointer, found: false, tokens: [], error: "Invalid JSON input" };
3871
+ }
3872
+ let ptr = pointer;
3873
+ if (ptr.startsWith("#")) {
3874
+ ptr = ptr.slice(1);
3875
+ try {
3876
+ ptr = decodeURIComponent(ptr);
3877
+ } catch {
3878
+ return { pointer, found: false, tokens: [], error: "Invalid percent-encoding in URI-fragment pointer" };
3879
+ }
3880
+ }
3881
+ if (ptr === "") return { pointer, found: true, value: parsed, tokens: [] };
3882
+ if (!ptr.startsWith("/")) return { pointer, found: false, tokens: [], error: "Pointer must be empty or start with '/' (RFC 6901)" };
3883
+ const tokens = ptr.split("/").slice(1).map(_unescapePointerTokenMcp);
3884
+ let current = parsed;
3885
+ for (const token of tokens) {
3886
+ if (Array.isArray(current)) {
3887
+ if (token === "-") return { pointer, found: false, tokens, failedAt: token, reason: '"-" refers to the nonexistent element after the end of the array' };
3888
+ if (!/^(0|[1-9][0-9]*)$/.test(token)) return { pointer, found: false, tokens, failedAt: token, reason: `"${token}" is not a valid array index` };
3889
+ const idx = Number(token);
3890
+ if (idx >= current.length) return { pointer, found: false, tokens, failedAt: token, reason: `array index ${idx} is out of bounds (length ${current.length})` };
3891
+ current = current[idx];
3892
+ } else if (current !== null && typeof current === "object") {
3893
+ const obj = current;
3894
+ if (!Object.prototype.hasOwnProperty.call(obj, token)) return { pointer, found: false, tokens, failedAt: token, reason: `key "${token}" not found` };
3895
+ current = obj[token];
3896
+ } else {
3897
+ return { pointer, found: false, tokens, failedAt: token, reason: `cannot descend into ${current === null ? "null" : typeof current} at "${token}"` };
3898
+ }
3899
+ }
3900
+ return { pointer, found: true, value: current, tokens };
3901
+ }
3902
+ server.tool(
3903
+ "resolve_json_pointer",
3904
+ 'Resolve an RFC 6901 JSON Pointer against a JSON document, in both the plain form ("" for the whole document, or "/foo/0") and the URI-fragment form ("#/foo/0"). Handles ~1 -> / and ~0 -> ~ escaping, and distinguishes an invalid document / malformed pointer from a syntactically valid pointer whose target is simply absent.',
3905
+ {
3906
+ json: z.string().describe("The JSON document to resolve against"),
3907
+ pointer: z.string().describe("The RFC 6901 JSON Pointer, e.g. /users/0/name")
3908
+ },
3909
+ async ({ json, pointer }) => {
3910
+ const result = _resolveJsonPointerMcp(json, pointer);
3911
+ if ("error" in result) return errText(result);
3912
+ return text(JSON.stringify(result, null, 2));
3913
+ }
3914
+ );
3717
3915
  var transport = new StdioServerTransport();
3718
3916
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.20.0",
3
+ "version": "0.22.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",