@utilix-tech/mcp 0.48.0 → 0.50.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.50.0
6
+ - Add `calculate_cash_rounding` tool: round a cashless total to the nearest payable cash denomination ("Swedish rounding"), with round-nearest, round-up, and round-down modes.
7
+
8
+ ## 0.49.0
9
+ - Add `resolve_yaml_anchors` tool: expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.
10
+ - Add `split_sitemap` tool: split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.
11
+
5
12
  ## 0.48.0
6
13
  - Add `build_content_disposition`, `parse_content_disposition` tools: build a `Content-Disposition` response header from a disposition type and filename, with an RFC 5987 `filename*` extended parameter for non-ASCII filenames, or parse an existing header into its type, filename, and raw parameters.
7
14
  - Add `inspect_gif_frames` tool: walk a GIF87a/89a file's block structure to report frame count, total animation duration, and loop count, without decoding any LZW-compressed pixel data.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 181 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 189 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
 
@@ -160,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
160
160
  | `convert_timezone` | Convert a date/time between timezones |
161
161
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
162
162
 
163
- ### Units (29 tools)
163
+ ### Units (30 tools)
164
164
  | Tool | Description |
165
165
  |------|-------------|
166
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -192,8 +192,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
192
192
  | `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
193
193
  | `calculate_cd_maturity` | Compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net early-withdrawal payout |
194
194
  | `calculate_depreciation_schedule` | Build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits |
195
+ | `calculate_cash_rounding` | Round a cashless total to the nearest payable cash denomination ("Swedish rounding"), with round-nearest, round-up, and round-down modes |
195
196
 
196
- ### API / Network (27 tools)
197
+ ### API / Network (28 tools)
197
198
  | Tool | Description |
198
199
  |------|-------------|
199
200
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -204,6 +205,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
204
205
  | `cors_headers` | Generate CORS response headers for a configuration |
205
206
  | `parse_har` | Parse a DevTools .har network export into requests and summary stats |
206
207
  | `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
208
+ | `split_sitemap` | Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file |
207
209
  | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
208
210
  | `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
209
211
  | `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
@@ -248,10 +250,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
248
250
  | `lint_conventional_commit` | Lint a commit message against the Conventional Commits v1.0.0 spec: header format, blank-line separation, footer tokens, and BREAKING CHANGE casing |
249
251
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
250
252
 
251
- ### Data (10 tools)
253
+ ### Data (11 tools)
252
254
  | Tool | Description |
253
255
  |------|-------------|
254
256
  | `validate_yaml` | Validate YAML syntax |
257
+ | `resolve_yaml_anchors` | Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused |
255
258
  | `toml_to_json` | Convert TOML to JSON |
256
259
  | `xml_to_json` | Convert XML to JSON |
257
260
  | `parse_csv` | Parse a CSV string and return the data as JSON |
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import { createHash } from "crypto";
8
+ import * as yaml from "js-yaml";
8
9
  import {
9
10
  parseJson,
10
11
  minifyJson,
@@ -730,6 +731,78 @@ Documents: ${result.documentCount}
730
731
  Keys: ${result.keyCount}`);
731
732
  }
732
733
  );
734
+ var _YAML_ANCHOR_RE = /(?:^|\s)&([A-Za-z0-9_.-]+)/g;
735
+ var _YAML_ALIAS_RE = /(?:^|\s)\*([A-Za-z0-9_.-]+)/g;
736
+ function _stripYamlLineComment(line) {
737
+ let inSingle = false;
738
+ let inDouble = false;
739
+ for (let i = 0; i < line.length; i++) {
740
+ const ch = line[i];
741
+ if (ch === "'" && !inDouble) {
742
+ inSingle = !inSingle;
743
+ } else if (ch === '"' && !inSingle) {
744
+ inDouble = !inDouble;
745
+ } else if (ch === "#" && !inSingle && !inDouble && (i === 0 || /\s/.test(line[i - 1]))) {
746
+ return line.slice(0, i);
747
+ }
748
+ }
749
+ return line;
750
+ }
751
+ function _resolveYamlAnchors(yamlText) {
752
+ if (!yamlText || !yamlText.trim()) return { error: "Input is empty" };
753
+ const anchorDefs = [];
754
+ const referencedNames = /* @__PURE__ */ new Set();
755
+ let aliasCount = 0;
756
+ const lines = yamlText.split("\n");
757
+ for (let i = 0; i < lines.length; i++) {
758
+ const line = _stripYamlLineComment(lines[i]);
759
+ const lineNum = i + 1;
760
+ _YAML_ANCHOR_RE.lastIndex = 0;
761
+ let m;
762
+ while ((m = _YAML_ANCHOR_RE.exec(line)) !== null) {
763
+ anchorDefs.push({ name: m[1], line: lineNum });
764
+ }
765
+ _YAML_ALIAS_RE.lastIndex = 0;
766
+ while ((m = _YAML_ALIAS_RE.exec(line)) !== null) {
767
+ referencedNames.add(m[1]);
768
+ aliasCount++;
769
+ }
770
+ }
771
+ let parsed;
772
+ try {
773
+ parsed = yaml.load(yamlText);
774
+ } catch (e) {
775
+ const err = e;
776
+ return { error: err.reason ?? err.message };
777
+ }
778
+ let resolvedYaml;
779
+ try {
780
+ resolvedYaml = yaml.dump(parsed, { noRefs: true, lineWidth: -1 });
781
+ } catch (e) {
782
+ return { error: e instanceof Error ? e.message : String(e) };
783
+ }
784
+ const anchors = anchorDefs.map((a) => ({
785
+ name: a.name,
786
+ line: a.line,
787
+ referenced: referencedNames.has(a.name)
788
+ }));
789
+ const seenAnchorNames = [];
790
+ for (const a of anchorDefs) {
791
+ if (!seenAnchorNames.includes(a.name)) seenAnchorNames.push(a.name);
792
+ }
793
+ const unusedAnchors = seenAnchorNames.filter((name) => !referencedNames.has(name));
794
+ return { resolvedYaml, anchors, unusedAnchors, aliasCount };
795
+ }
796
+ server.tool(
797
+ "resolve_yaml_anchors",
798
+ "Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.",
799
+ { yaml: z.string().describe("The YAML document to resolve") },
800
+ async ({ yaml: yamlText }) => {
801
+ const result = _resolveYamlAnchors(yamlText);
802
+ if ("error" in result) return errText(result);
803
+ return text(JSON.stringify(result, null, 2));
804
+ }
805
+ );
733
806
  server.tool(
734
807
  "toml_to_json",
735
808
  "Convert a TOML string to JSON.",
@@ -1609,6 +1682,68 @@ server.tool(
1609
1682
  return text(JSON.stringify(result, null, 2));
1610
1683
  }
1611
1684
  );
1685
+ var _SITEMAP_SPLITTER_MAX_URLS_PER_FILE = 5e4;
1686
+ function _escapeXmlUrlForSplitter(url) {
1687
+ return url.replace(/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1688
+ }
1689
+ function _splitSitemap(urlsText, baseUrlInput, urlsPerFileInput, filePrefixInput) {
1690
+ const urls = (urlsText ?? "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1691
+ if (urls.length === 0) return { error: "No URLs provided" };
1692
+ for (let i = 0; i < urls.length; i++) {
1693
+ if (!/^https?:\/\//i.test(urls[i])) {
1694
+ return { error: `Line ${i + 1}: "${urls[i]}" is not an absolute URL starting with http:// or https://` };
1695
+ }
1696
+ }
1697
+ if (!baseUrlInput || !/^https?:\/\//i.test(baseUrlInput)) {
1698
+ return { error: "baseUrl must be an absolute URL starting with http:// or https://" };
1699
+ }
1700
+ const baseUrl = baseUrlInput.replace(/\/+$/, "") + "/";
1701
+ let urlsPerFile = urlsPerFileInput ?? _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1702
+ if (urlsPerFile <= 0) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1703
+ if (urlsPerFile > _SITEMAP_SPLITTER_MAX_URLS_PER_FILE) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1704
+ const filePrefix = (filePrefixInput || "sitemap").replace(/[^A-Za-z0-9_-]/g, "") || "sitemap";
1705
+ const files = [];
1706
+ for (let i = 0; i < urls.length; i += urlsPerFile) {
1707
+ const chunk = urls.slice(i, i + urlsPerFile);
1708
+ const fileIndex = files.length + 1;
1709
+ const filename = `${filePrefix}-${fileIndex}.xml`;
1710
+ const urlLines = chunk.map((url) => ` <url>
1711
+ <loc>${_escapeXmlUrlForSplitter(url)}</loc>
1712
+ </url>`).join("\n");
1713
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
1714
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1715
+ ${urlLines}
1716
+ </urlset>
1717
+ `;
1718
+ files.push({ filename, urlCount: chunk.length, xml });
1719
+ }
1720
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1721
+ const indexEntries = files.map((f) => ` <sitemap>
1722
+ <loc>${_escapeXmlUrlForSplitter(baseUrl + f.filename)}</loc>
1723
+ <lastmod>${today}</lastmod>
1724
+ </sitemap>`).join("\n");
1725
+ const indexXml = `<?xml version="1.0" encoding="UTF-8"?>
1726
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1727
+ ${indexEntries}
1728
+ </sitemapindex>
1729
+ `;
1730
+ return { files, indexXml, totalUrls: urls.length };
1731
+ }
1732
+ server.tool(
1733
+ "split_sitemap",
1734
+ "Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.",
1735
+ {
1736
+ urls: z.string().describe("Newline-separated list of absolute URLs"),
1737
+ baseUrl: z.string().describe("Base URL the generated sitemap files will be hosted at"),
1738
+ urlsPerFile: z.number().optional().describe("URLs per generated file, clamped to 50000 (default 50000)"),
1739
+ filePrefix: z.string().optional().describe('Filename prefix for generated files (default "sitemap")')
1740
+ },
1741
+ async ({ urls, baseUrl, urlsPerFile, filePrefix }) => {
1742
+ const result = _splitSitemap(urls, baseUrl, urlsPerFile, filePrefix);
1743
+ if ("error" in result) return errText(result);
1744
+ return text(JSON.stringify(result, null, 2));
1745
+ }
1746
+ );
1612
1747
  var _KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
1613
1748
  function _validateRobotsTxt(text2) {
1614
1749
  if (!text2 || !text2.trim()) return { error: "Input is empty" };
@@ -8169,5 +8304,41 @@ server.tool(
8169
8304
  return text(JSON.stringify(result, null, 2));
8170
8305
  }
8171
8306
  );
8307
+ function _calculateCashRoundingMcp(total, roundingUnit, mode = "nearest") {
8308
+ const round2 = (n) => Math.round(n * 100) / 100;
8309
+ if (typeof total !== "number" || !Number.isFinite(total)) {
8310
+ return { error: "total must be a finite number" };
8311
+ }
8312
+ if (typeof roundingUnit !== "number" || !Number.isFinite(roundingUnit) || roundingUnit <= 0) {
8313
+ return { error: "roundingUnit must be a finite number greater than zero" };
8314
+ }
8315
+ if (mode !== "nearest" && mode !== "up" && mode !== "down") {
8316
+ return { error: 'mode must be one of: "nearest", "up", "down"' };
8317
+ }
8318
+ const steps = total / roundingUnit;
8319
+ const roundedSteps = mode === "up" ? Math.ceil(steps) : mode === "down" ? Math.floor(steps) : Math.round(steps);
8320
+ const roundedTotal = round2(roundedSteps * roundingUnit);
8321
+ return {
8322
+ total: round2(total),
8323
+ roundingUnit,
8324
+ mode,
8325
+ roundedTotal,
8326
+ adjustment: round2(roundedTotal - total)
8327
+ };
8328
+ }
8329
+ server.tool(
8330
+ "calculate_cash_rounding",
8331
+ 'Round a cashless total to the nearest payable cash denomination, the way a till rounds a card-priced total to the nearest 5 cents (or whatever the smallest circulating coin is) when settling in physical cash, commonly called "Swedish rounding" or "cash rounding".',
8332
+ {
8333
+ total: z.number().describe("The cashless total to round"),
8334
+ roundingUnit: z.number().describe("The smallest payable cash denomination to round to, e.g. 0.05"),
8335
+ mode: z.enum(["nearest", "up", "down"]).optional().describe('Rounding mode (default "nearest")')
8336
+ },
8337
+ async ({ total, roundingUnit, mode }) => {
8338
+ const result = _calculateCashRoundingMcp(total, roundingUnit, mode);
8339
+ if ("error" in result) return errText(result);
8340
+ return text(JSON.stringify(result, null, 2));
8341
+ }
8342
+ );
8172
8343
  var transport = new StdioServerTransport();
8173
8344
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.48.0",
4
- "description": "MCP server exposing 175 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.50.0",
4
+ "description": "MCP server exposing 188 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",
@@ -54,9 +54,11 @@
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.12.0",
56
56
  "@utilix-tech/sdk": "^0.3.0",
57
+ "js-yaml": "^4.1.0",
57
58
  "zod": "^3.23.0"
58
59
  },
59
60
  "devDependencies": {
61
+ "@types/js-yaml": "^4.0.9",
60
62
  "@types/node": "^20.0.0",
61
63
  "tsup": "^8.0.0",
62
64
  "typescript": "^5.4.0"