@utilix-tech/mcp 0.39.0 → 0.41.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.41.0
6
+ - Add `calculate_cash_breakdown`, `calculate_change` tools: break a cash amount down into the minimum count of bills/coins from a set of denominations (largest first), or compute change owed for a payment/tender pair and its breakdown.
7
+ - Add `generate_etag` tool: compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator (W/) prefix.
8
+
9
+ ## 0.40.0
10
+ - Add `generate_sri_hash` tool: compute SHA-256/384/512 Subresource Integrity hashes for a script or stylesheet's content, formatted as the `<algorithm>-<base64>` string an HTML `integrity` attribute expects.
11
+ - Add `analyze_email_headers` tool: parse a raw email header block into structured From/To/Subject fields, the Received hop chain in order, and SPF/DKIM/DMARC results from an Authentication-Results header.
12
+
5
13
  ## 0.39.0
6
14
  - Add `compare_unit_prices` tool: compare two or more package options by price and quantity to find the true cost per unit, flag the cheapest, and report how much more expensive each other option is by percentage.
7
15
  - Add `csv_to_sql_insert` tool: convert CSV data with a header row into parameterless SQL INSERT statements, with basic type inference (numbers/booleans unquoted, empty cells NULL) and ANSI/MySQL identifier quoting.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 163 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 168 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
 
@@ -110,13 +110,15 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
110
110
  | `base62_encode` | Encode text as Base62 (alphanumeric, URL-safe) |
111
111
  | `base62_decode` | Decode a Base62 string back to text |
112
112
 
113
- ### Hashing (4 tools)
113
+ ### Hashing (6 tools)
114
114
  | Tool | Description |
115
115
  |------|-------------|
116
116
  | `hash` | Hash a string with MD5, SHA-1, SHA-256, SHA-384, or SHA-512 |
117
117
  | `hash_all` | Hash a string with all algorithms at once |
118
118
  | `bcrypt_hash` | Hash a password using bcrypt |
119
119
  | `bcrypt_verify` | Verify a password against a bcrypt hash |
120
+ | `generate_sri_hash` | Compute a Subresource Integrity hash (SHA-256/384/512) for a script or stylesheet's content, formatted for an HTML `integrity` attribute |
121
+ | `generate_etag` | Compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator prefix |
120
122
 
121
123
  ### Generators (6 tools)
122
124
  | Tool | Description |
@@ -157,7 +159,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
157
159
  | `convert_timezone` | Convert a date/time between timezones |
158
160
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
159
161
 
160
- ### Units (24 tools)
162
+ ### Units (26 tools)
161
163
  | Tool | Description |
162
164
  |------|-------------|
163
165
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -184,8 +186,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
184
186
  | `calculate_savings_goal` | Solve for months to reach a savings goal at a fixed monthly contribution, or the monthly contribution required to hit a goal by a target month count |
185
187
  | `calculate_roi` | Compute return on investment percent from an initial and final value, plus the annualized (CAGR) return given an optional holding period in years |
186
188
  | `compare_unit_prices` | Compare two or more package options by price and quantity to find the true cost per unit, flag the cheapest, and report how much more expensive each other option is |
189
+ | `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
190
+ | `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
187
191
 
188
- ### API / Network (15 tools)
192
+ ### API / Network (16 tools)
189
193
  | Tool | Description |
190
194
  |------|-------------|
191
195
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -203,6 +207,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
203
207
  | `parse_ipv6_address` | Expand an IPv6 address to its full 8-group form or compress it to its RFC 5952 canonical shortest form, classify its address type, and handle an embedded IPv4 suffix or zone ID |
204
208
  | `build_link_header` | Build and explain an HTTP Link response header from URL/rel pairs (preload, preconnect, prefetch, canonical, pagination, etc.) |
205
209
  | `check_set_cookie_header` | Parse a Set-Cookie header and validate its attribute combination: SameSite=None requires Secure, __Host-/__Secure- prefix rules, missing Secure, overly long Max-Age |
210
+ | `analyze_email_headers` | Parse a raw email header block into structured fields, the Received hop chain, and SPF/DKIM/DMARC results from an Authentication-Results header |
206
211
 
207
212
  ### Color (4 tools)
208
213
  | Tool | Description |
package/dist/index.js CHANGED
@@ -6715,5 +6715,221 @@ server.tool(
6715
6715
  return text(JSON.stringify(result, null, 2));
6716
6716
  }
6717
6717
  );
6718
+ var _SRI_ALGORITHMS_MCP = ["SHA-256", "SHA-384", "SHA-512"];
6719
+ var _SRI_NODE_ALG_MAP_MCP = {
6720
+ "SHA-256": "sha256",
6721
+ "SHA-384": "sha384",
6722
+ "SHA-512": "sha512"
6723
+ };
6724
+ function _generateSriAllMcp(content) {
6725
+ if (!content) return { error: "Content is required" };
6726
+ const results = _SRI_ALGORITHMS_MCP.map((algorithm) => {
6727
+ const nodeAlg = _SRI_NODE_ALG_MAP_MCP[algorithm];
6728
+ const base64 = createHash(nodeAlg).update(content, "utf8").digest("base64");
6729
+ return { algorithm, base64, integrity: `${nodeAlg}-${base64}` };
6730
+ });
6731
+ return { results };
6732
+ }
6733
+ server.tool(
6734
+ "generate_sri_hash",
6735
+ "Compute a Subresource Integrity (SRI) hash for a script or stylesheet's content: digest it with SHA-256, SHA-384, and SHA-512 and return each as the `<algorithm>-<base64>` string an HTML `integrity` attribute expects. Returns all three algorithms at once.",
6736
+ {
6737
+ content: z.string().min(1).describe("The exact file/script content to hash, byte-for-byte")
6738
+ },
6739
+ async ({ content }) => {
6740
+ const result = _generateSriAllMcp(content);
6741
+ if ("error" in result) return errText(result);
6742
+ return text(JSON.stringify(result, null, 2));
6743
+ }
6744
+ );
6745
+ function _analyzeEmailHeadersMcp(raw) {
6746
+ if (!raw || !raw.trim()) return { error: "Raw email header text is required" };
6747
+ const normalized = raw.replace(/\r\n/g, "\n");
6748
+ const blockEnd = normalized.indexOf("\n\n");
6749
+ const headerBlock = blockEnd === -1 ? normalized : normalized.slice(0, blockEnd);
6750
+ const rawLines = headerBlock.split("\n");
6751
+ const unfolded = [];
6752
+ for (const line of rawLines) {
6753
+ if (/^[ \t]/.test(line) && unfolded.length > 0) {
6754
+ unfolded[unfolded.length - 1] += " " + line.trim();
6755
+ } else if (line.trim() !== "") {
6756
+ unfolded.push(line);
6757
+ }
6758
+ }
6759
+ const headers = [];
6760
+ for (const line of unfolded) {
6761
+ const colonIdx = line.indexOf(":");
6762
+ if (colonIdx === -1) continue;
6763
+ const name = line.slice(0, colonIdx).trim();
6764
+ const value = line.slice(colonIdx + 1).trim();
6765
+ if (!name) continue;
6766
+ headers.push({ name, value });
6767
+ }
6768
+ if (headers.length === 0) {
6769
+ return { error: 'No parseable headers found \u2014 expected lines like "From: ..." or "Received: ..."' };
6770
+ }
6771
+ const findFirst = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? null;
6772
+ const findAll = (name) => headers.filter((h) => h.name.toLowerCase() === name.toLowerCase());
6773
+ const receivedHops = findAll("received").map((h, i) => {
6774
+ const fromMatch = h.value.match(/from\s+([^\s;]+(?:\s*\([^)]*\))?)/i);
6775
+ const byMatch = h.value.match(/\bby\s+([^\s;]+)/i);
6776
+ const segments = h.value.split(";");
6777
+ const date = segments.length > 1 ? segments[segments.length - 1].trim() : null;
6778
+ return {
6779
+ index: i + 1,
6780
+ raw: h.value,
6781
+ from: fromMatch ? fromMatch[1].trim() : null,
6782
+ by: byMatch ? byMatch[1].trim() : null,
6783
+ date: date || null
6784
+ };
6785
+ });
6786
+ const authResultsHeader = findFirst("authentication-results");
6787
+ const authResults = [];
6788
+ if (authResultsHeader) {
6789
+ const re = /\b(spf|dkim|dmarc)=(\w+)/gi;
6790
+ let m;
6791
+ while ((m = re.exec(authResultsHeader)) !== null) {
6792
+ authResults.push({ mechanism: m[1].toLowerCase(), result: m[2].toLowerCase() });
6793
+ }
6794
+ }
6795
+ const findAuth = (mechanism) => authResults.find((a) => a.mechanism === mechanism)?.result ?? null;
6796
+ const warnings = [];
6797
+ if (receivedHops.length === 0) {
6798
+ warnings.push("No Received headers found \u2014 this may be a partial header block rather than a full raw source.");
6799
+ }
6800
+ if (!authResultsHeader) {
6801
+ warnings.push("No Authentication-Results header found \u2014 SPF/DKIM/DMARC cannot be determined from this header block alone.");
6802
+ }
6803
+ return {
6804
+ headers,
6805
+ from: findFirst("from"),
6806
+ to: findFirst("to"),
6807
+ subject: findFirst("subject"),
6808
+ date: findFirst("date"),
6809
+ messageId: findFirst("message-id"),
6810
+ returnPath: findFirst("return-path"),
6811
+ replyTo: findFirst("reply-to"),
6812
+ receivedHops,
6813
+ hopCount: receivedHops.length,
6814
+ authResults,
6815
+ spf: findAuth("spf"),
6816
+ dkim: findAuth("dkim"),
6817
+ dmarc: findAuth("dmarc"),
6818
+ warnings
6819
+ };
6820
+ }
6821
+ server.tool(
6822
+ "analyze_email_headers",
6823
+ `Parse a raw email header block (e.g. Gmail's "Show original", or the headers from a .eml file) into structured fields, the Received hop chain in the order they appear, and any SPF/DKIM/DMARC verdicts already present in an Authentication-Results header. Never performs a live DNS/SPF/DKIM check, it only reports what the pasted headers already claim.`,
6824
+ {
6825
+ headers: z.string().min(1).describe("The raw email header block (From/To/Received/Authentication-Results/...)")
6826
+ },
6827
+ async ({ headers }) => {
6828
+ const result = _analyzeEmailHeadersMcp(headers);
6829
+ if ("error" in result) return errText(result);
6830
+ return text(JSON.stringify(result, null, 2));
6831
+ }
6832
+ );
6833
+ var _USD_DENOMINATIONS_MCP = [100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01];
6834
+ function _cashToCentsMcp(n) {
6835
+ return Math.round(n * 100);
6836
+ }
6837
+ function _calculateCashBreakdownMcp(amount, denominations) {
6838
+ if (!Number.isFinite(amount) || amount < 0) return { error: "Amount must be zero or a positive number" };
6839
+ if (denominations.length === 0) return { error: "At least one denomination is required" };
6840
+ for (const d of denominations) {
6841
+ if (!Number.isFinite(d) || d <= 0) return { error: "Each denomination must be a positive number" };
6842
+ }
6843
+ const round2 = (n) => Math.round(n * 100) / 100;
6844
+ const sorted = Array.from(new Set(denominations)).sort((a, b) => b - a);
6845
+ let remainingCents = _cashToCentsMcp(amount);
6846
+ const breakdown = [];
6847
+ let totalCount = 0;
6848
+ for (const d of sorted) {
6849
+ const dCents = _cashToCentsMcp(d);
6850
+ const count = Math.floor(remainingCents / dCents);
6851
+ if (count > 0) {
6852
+ breakdown.push({ denomination: d, count, subtotal: round2(count * d) });
6853
+ remainingCents -= count * dCents;
6854
+ totalCount += count;
6855
+ }
6856
+ }
6857
+ return {
6858
+ amount: round2(amount),
6859
+ denominations: sorted,
6860
+ breakdown,
6861
+ totalCount,
6862
+ remainder: round2(remainingCents / 100)
6863
+ };
6864
+ }
6865
+ server.tool(
6866
+ "calculate_cash_breakdown",
6867
+ "Break a cash amount down into the minimum count of bills/coins from a set of denominations, using the standard greedy largest-first approach (optimal for canonical currency denomination sets like USD, EUR, and GBP). Any amount that can't be represented exactly with the given denominations is reported as a remainder rather than rounded away.",
6868
+ {
6869
+ amount: z.number().describe("The cash amount to break down"),
6870
+ denominations: z.array(z.number()).min(1).optional().describe("Available bill/coin denominations, largest to smallest (defaults to US denominations: 100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01)")
6871
+ },
6872
+ async ({ amount, denominations }) => {
6873
+ const result = _calculateCashBreakdownMcp(amount, denominations && denominations.length > 0 ? denominations : _USD_DENOMINATIONS_MCP);
6874
+ if ("error" in result) return errText(result);
6875
+ return text(JSON.stringify(result, null, 2));
6876
+ }
6877
+ );
6878
+ server.tool(
6879
+ "calculate_change",
6880
+ "Compute the change owed for a payment/tender pair (amount tendered minus amount due) and break that change down into the minimum count of bills/coins from the given denominations.",
6881
+ {
6882
+ amountDue: z.number().describe("The amount owed"),
6883
+ amountTendered: z.number().describe("The amount paid, must be greater than or equal to amountDue"),
6884
+ denominations: z.array(z.number()).min(1).optional().describe("Available bill/coin denominations, largest to smallest (defaults to US denominations: 100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01)")
6885
+ },
6886
+ async ({ amountDue, amountTendered, denominations }) => {
6887
+ if (!Number.isFinite(amountDue) || amountDue < 0) return errText({ error: "Amount due must be zero or a positive number" });
6888
+ if (!Number.isFinite(amountTendered) || amountTendered < 0) return errText({ error: "Amount tendered must be zero or a positive number" });
6889
+ if (_cashToCentsMcp(amountTendered) < _cashToCentsMcp(amountDue)) {
6890
+ return errText({ error: "Amount tendered must be greater than or equal to the amount due" });
6891
+ }
6892
+ const round2 = (n) => Math.round(n * 100) / 100;
6893
+ const denoms = denominations && denominations.length > 0 ? denominations : _USD_DENOMINATIONS_MCP;
6894
+ const changeDue = round2(amountTendered - amountDue);
6895
+ const breakdown = _calculateCashBreakdownMcp(changeDue, denoms);
6896
+ if ("error" in breakdown) return errText(breakdown);
6897
+ return text(JSON.stringify({
6898
+ amountDue: round2(amountDue),
6899
+ amountTendered: round2(amountTendered),
6900
+ changeDue,
6901
+ denominations: breakdown.denominations,
6902
+ breakdown: breakdown.breakdown,
6903
+ totalCount: breakdown.totalCount,
6904
+ remainder: breakdown.remainder
6905
+ }, null, 2));
6906
+ }
6907
+ );
6908
+ var _ETAG_ALGORITHMS_MCP = ["MD5", "SHA-1", "SHA-256"];
6909
+ var _ETAG_NODE_ALG_MAP_MCP = { "MD5": "md5", "SHA-1": "sha1", "SHA-256": "sha256" };
6910
+ function _generateEtagMcp(content, algorithm, weak) {
6911
+ if (content.length === 0) return { error: "Content is required" };
6912
+ if (!_ETAG_ALGORITHMS_MCP.includes(algorithm)) {
6913
+ return { error: `Unknown algorithm: ${algorithm}. Must be one of ${_ETAG_ALGORITHMS_MCP.join(", ")}` };
6914
+ }
6915
+ const hash = createHash(_ETAG_NODE_ALG_MAP_MCP[algorithm]).update(content, "utf8").digest("hex");
6916
+ const quoted = `"${hash}"`;
6917
+ const etag = weak ? `W/${quoted}` : quoted;
6918
+ return { algorithm, weak, hash, etag, headerName: "ETag", snippet: `ETag: ${etag}` };
6919
+ }
6920
+ server.tool(
6921
+ "generate_etag",
6922
+ "Compute a content-hash ETag for the given content: digest it with MD5, SHA-1, or SHA-256, hex-encode it, and quote it into the exact string an ETag response header expects, with an optional weak-validator (W/) prefix. Hashes whatever content you give it; does not replicate any specific web server's own size/modification-time-based ETag algorithm.",
6923
+ {
6924
+ content: z.string().min(1).describe("The content to hash into an ETag"),
6925
+ algorithm: z.enum(_ETAG_ALGORITHMS_MCP).default("SHA-1").describe("Hash algorithm to use"),
6926
+ weak: z.boolean().default(false).describe("Whether to produce a weak validator (W/ prefix)")
6927
+ },
6928
+ async ({ content, algorithm, weak }) => {
6929
+ const result = _generateEtagMcp(content, algorithm, weak);
6930
+ if ("error" in result) return errText(result);
6931
+ return text(JSON.stringify(result, null, 2));
6932
+ }
6933
+ );
6718
6934
  var transport = new StdioServerTransport();
6719
6935
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.39.0",
4
- "description": "MCP server exposing 152 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.41.0",
4
+ "description": "MCP server exposing 168 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",
7
7
  "homepage": "https://utilix.tech/mcp",