@utilix-tech/mcp 0.73.0 → 0.75.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.75.0
6
+ - Add `verify_webhook_signature` tool: recompute an HMAC over a raw webhook payload and compare it to a provided signature, supporting the `sha256=` prefix style (Stripe/GitHub-style webhooks) plus raw hex or base64 encoding.
7
+ - Add `estimate_data_transfer_time` tool: estimate the theoretical time to transfer a file over a connection of a given speed, or the reverse: the bandwidth required to hit a target time.
8
+
9
+ ## 0.74.0
10
+ - Add `calculate_loan_prepayment_penalty` tool: compute the dollar penalty cost of paying a loan off early, given its payoff balance and a prepayment penalty structure (a flat percent of balance, or a fixed number of months of interest).
11
+ - Add `validate_aria` tool: check every role and aria-* attribute in an HTML snippet against a curated WAI-ARIA 1.2 role table, flagging unknown roles, disallowed attributes, and missing required states.
12
+
5
13
  ## 0.73.0
6
14
  - Add `calculate_mortgage_points` tool: compare a loan with no discount points against the same loan at a reduced rate bought with points, and compute the monthly savings and break-even month.
7
15
  - Add `calculate_cookie_size_budget` tool: parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's per-cookie limit and a configurable budget.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 241 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 245 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
 
@@ -162,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
162
162
  | `convert_timezone` | Convert a date/time between timezones |
163
163
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
164
164
 
165
- ### Units (43 tools)
165
+ ### Units (45 tools)
166
166
  | Tool | Description |
167
167
  |------|-------------|
168
168
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -208,8 +208,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
208
208
  | `validate_vin` | Validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections |
209
209
  | `validate_isin` | Validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier |
210
210
  | `calculate_mortgage_points` | Compare a loan with no discount points against the same loan at a reduced rate bought with points, and compute the monthly savings and the break-even month where cumulative savings recoup the points cost |
211
+ | `calculate_loan_prepayment_penalty` | Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate |
212
+ | `estimate_data_transfer_time` | Estimate the theoretical time to transfer a file of a given size over a connection of a given speed, or (given a target time instead) the bandwidth required to hit it |
211
213
 
212
- ### API / Network (49 tools)
214
+ ### API / Network (50 tools)
213
215
  | Tool | Description |
214
216
  |------|-------------|
215
217
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -263,6 +265,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
263
265
  | `build_robots_meta_tag` | Build a `<meta name="robots" content="...">` tag from directives (noindex, nofollow, max-snippet, etc.), the same vocabulary as X-Robots-Tag, optionally scoped to a crawler via the name attribute |
264
266
  | `parse_robots_meta_tag` | Parse an existing robots meta tag (or bare content value) into the same shape |
265
267
  | `calculate_cookie_size_budget` | Parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's 4096-byte per-cookie guarantee and a configurable budget |
268
+ | `verify_webhook_signature` | Recompute an HMAC over a raw webhook payload and compare it to a provided signature, accepting a `sha256=` prefix style and matching against hex or base64 encoding |
266
269
 
267
270
  ### Color (4 tools)
268
271
  | Tool | Description |
@@ -272,7 +275,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
272
275
  | `color_palette` | Generate a color palette from a base color |
273
276
  | `color_shades` | Generate shades or tints from a base color |
274
277
 
275
- ### Code (20 tools)
278
+ ### Code (21 tools)
276
279
  | Tool | Description |
277
280
  |------|-------------|
278
281
  | `format_sql` | Format a SQL query with proper indentation |
@@ -295,6 +298,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
295
298
  | `extract_svg_sprite_symbols` | Parse an SVG sprite sheet's `<symbol>` definitions and list each one's id, viewBox, width, and height, alongside a ready-to-paste `<use href="#id"></use>` usage snippet |
296
299
  | `parse_commit_trailers` | Parse a commit message's trailer block (Co-authored-by, Signed-off-by, Fixes, Reviewed-by, etc.) per git's RFC 822-style trailer convention, returning each trailer's key/value plus the body with the trailer block removed |
297
300
  | `extract_markdown_footnotes` | Extract every Markdown-Extra-style footnote reference and definition from a document, flagging references with no matching definition and definitions that are never referenced |
301
+ | `validate_aria` | Parse an HTML snippet and check every role and aria-* attribute against a curated table of WAI-ARIA 1.2 roles: unknown role names, an aria-* attribute not permitted on the given role, and a widget role missing its required aria-* state |
298
302
 
299
303
  ### Data (16 tools)
300
304
  | Tool | Description |
package/dist/index.js CHANGED
@@ -11623,5 +11623,342 @@ server.tool(
11623
11623
  return text(JSON.stringify(result, null, 2));
11624
11624
  }
11625
11625
  );
11626
+ function _roundMoneyPrepaymentPenaltyMcp(n) {
11627
+ return Math.round(n * 100) / 100;
11628
+ }
11629
+ function _calculateLoanPrepaymentPenaltyMcp(payoffBalance, penaltyType, params) {
11630
+ if (!Number.isFinite(payoffBalance) || payoffBalance <= 0) {
11631
+ return { error: "Payoff balance must be a positive number" };
11632
+ }
11633
+ if (penaltyType !== "percent-of-balance" && penaltyType !== "months-of-interest") {
11634
+ return { error: 'penaltyType must be "percent-of-balance" or "months-of-interest"' };
11635
+ }
11636
+ let penaltyAmount;
11637
+ if (penaltyType === "percent-of-balance") {
11638
+ const penaltyPercent = params.penaltyPercent;
11639
+ if (!Number.isFinite(penaltyPercent) || penaltyPercent < 0) {
11640
+ return { error: "penaltyPercent must be zero or a positive number" };
11641
+ }
11642
+ penaltyAmount = _roundMoneyPrepaymentPenaltyMcp(payoffBalance * (penaltyPercent / 100));
11643
+ } else {
11644
+ const monthsOfInterest = params.monthsOfInterest;
11645
+ const annualRatePercent = params.annualRatePercent;
11646
+ if (!Number.isFinite(monthsOfInterest) || monthsOfInterest < 0) {
11647
+ return { error: "monthsOfInterest must be zero or a positive number" };
11648
+ }
11649
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
11650
+ return { error: "annualRatePercent must be zero or a positive number" };
11651
+ }
11652
+ const monthlyInterest = payoffBalance * (annualRatePercent / 100 / 12);
11653
+ penaltyAmount = _roundMoneyPrepaymentPenaltyMcp(monthlyInterest * monthsOfInterest);
11654
+ }
11655
+ return {
11656
+ payoffBalance: _roundMoneyPrepaymentPenaltyMcp(payoffBalance),
11657
+ penaltyType,
11658
+ penaltyAmount,
11659
+ effectivePenaltyPercent: _roundMoneyPrepaymentPenaltyMcp(penaltyAmount / payoffBalance * 100),
11660
+ totalPayoffCost: _roundMoneyPrepaymentPenaltyMcp(payoffBalance + penaltyAmount)
11661
+ };
11662
+ }
11663
+ server.tool(
11664
+ "calculate_loan_prepayment_penalty",
11665
+ "Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate.",
11666
+ {
11667
+ payoffBalance: z.number().describe("The loan's remaining payoff balance"),
11668
+ penaltyType: z.enum(["percent-of-balance", "months-of-interest"]).describe("The prepayment penalty structure"),
11669
+ penaltyPercent: z.number().optional().describe('Penalty as a percent of the payoff balance (required when penaltyType is "percent-of-balance")'),
11670
+ monthsOfInterest: z.number().optional().describe('Number of months of interest charged as the penalty (required when penaltyType is "months-of-interest")'),
11671
+ annualRatePercent: z.number().optional().describe(`The loan's annual interest rate as a percent (required when penaltyType is "months-of-interest")`)
11672
+ },
11673
+ async ({ payoffBalance, penaltyType, penaltyPercent, monthsOfInterest, annualRatePercent }) => {
11674
+ const result = _calculateLoanPrepaymentPenaltyMcp(payoffBalance, penaltyType, { penaltyPercent, monthsOfInterest, annualRatePercent });
11675
+ if ("error" in result) return errText(result);
11676
+ return text(JSON.stringify(result, null, 2));
11677
+ }
11678
+ );
11679
+ var _ARIA_GLOBAL_ATTRS_MCP = /* @__PURE__ */ new Set([
11680
+ "aria-atomic",
11681
+ "aria-busy",
11682
+ "aria-controls",
11683
+ "aria-current",
11684
+ "aria-describedby",
11685
+ "aria-details",
11686
+ "aria-disabled",
11687
+ "aria-dropeffect",
11688
+ "aria-errormessage",
11689
+ "aria-flowto",
11690
+ "aria-grabbed",
11691
+ "aria-haspopup",
11692
+ "aria-hidden",
11693
+ "aria-invalid",
11694
+ "aria-keyshortcuts",
11695
+ "aria-label",
11696
+ "aria-labelledby",
11697
+ "aria-live",
11698
+ "aria-owns",
11699
+ "aria-relevant",
11700
+ "aria-roledescription"
11701
+ ]);
11702
+ var _ARIA_ROLE_TABLE_MCP = {
11703
+ alert: { supported: ["aria-live", "aria-atomic"] },
11704
+ alertdialog: { supported: ["aria-modal"] },
11705
+ article: {},
11706
+ banner: {},
11707
+ button: { supported: ["aria-pressed", "aria-expanded", "aria-disabled"] },
11708
+ checkbox: { required: ["aria-checked"], supported: ["aria-readonly"] },
11709
+ columnheader: { supported: ["aria-sort", "aria-selected", "aria-readonly"] },
11710
+ combobox: { required: ["aria-expanded"], supported: ["aria-controls", "aria-autocomplete", "aria-activedescendant"] },
11711
+ complementary: {},
11712
+ contentinfo: {},
11713
+ dialog: { supported: ["aria-modal"] },
11714
+ form: {},
11715
+ grid: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-level", "aria-readonly"] },
11716
+ gridcell: { supported: ["aria-selected", "aria-readonly", "aria-expanded"] },
11717
+ group: { supported: ["aria-activedescendant", "aria-disabled"] },
11718
+ heading: { required: ["aria-level"] },
11719
+ img: {},
11720
+ link: { supported: ["aria-disabled", "aria-expanded"] },
11721
+ list: {},
11722
+ listbox: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation"] },
11723
+ listitem: { supported: ["aria-level", "aria-posinset", "aria-setsize"] },
11724
+ log: { supported: ["aria-live", "aria-atomic"] },
11725
+ main: {},
11726
+ marquee: {},
11727
+ menu: { supported: ["aria-orientation", "aria-activedescendant"] },
11728
+ menubar: { supported: ["aria-orientation", "aria-activedescendant"] },
11729
+ menuitem: { supported: ["aria-disabled"] },
11730
+ menuitemcheckbox: { required: ["aria-checked"] },
11731
+ menuitemradio: { required: ["aria-checked"] },
11732
+ meter: { required: ["aria-valuenow"], supported: ["aria-valuemin", "aria-valuemax", "aria-valuetext"] },
11733
+ navigation: {},
11734
+ none: {},
11735
+ option: { supported: ["aria-selected", "aria-checked", "aria-posinset", "aria-setsize"] },
11736
+ presentation: {},
11737
+ progressbar: { supported: ["aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-valuetext"] },
11738
+ radio: { required: ["aria-checked"] },
11739
+ radiogroup: { supported: ["aria-activedescendant"] },
11740
+ region: {},
11741
+ row: { supported: ["aria-selected", "aria-level", "aria-expanded"] },
11742
+ rowheader: { supported: ["aria-sort", "aria-selected", "aria-readonly"] },
11743
+ scrollbar: { required: ["aria-valuenow", "aria-controls"], supported: ["aria-valuemin", "aria-valuemax", "aria-orientation"] },
11744
+ search: {},
11745
+ searchbox: { supported: ["aria-multiline", "aria-placeholder", "aria-readonly", "aria-required", "aria-activedescendant", "aria-autocomplete"] },
11746
+ separator: { supported: ["aria-orientation", "aria-valuenow", "aria-valuemin", "aria-valuemax"] },
11747
+ slider: { required: ["aria-valuenow"], supported: ["aria-valuemin", "aria-valuemax", "aria-valuetext", "aria-orientation"] },
11748
+ spinbutton: { supported: ["aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-valuetext", "aria-required"] },
11749
+ status: { supported: ["aria-live", "aria-atomic"] },
11750
+ switch: { required: ["aria-checked"] },
11751
+ tab: { supported: ["aria-selected", "aria-controls", "aria-disabled"] },
11752
+ table: { supported: ["aria-colcount", "aria-rowcount"] },
11753
+ tablist: { supported: ["aria-orientation", "aria-activedescendant", "aria-level"] },
11754
+ tabpanel: {},
11755
+ textbox: { supported: ["aria-multiline", "aria-placeholder", "aria-readonly", "aria-required", "aria-activedescendant", "aria-autocomplete"] },
11756
+ timer: {},
11757
+ toolbar: { supported: ["aria-orientation"] },
11758
+ tooltip: {},
11759
+ tree: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation"] },
11760
+ treegrid: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation", "aria-level", "aria-readonly"] },
11761
+ treeitem: { supported: ["aria-expanded", "aria-selected", "aria-checked", "aria-level"] }
11762
+ };
11763
+ var _ARIA_ALL_KNOWN_ATTRS_MCP = new Set(_ARIA_GLOBAL_ATTRS_MCP);
11764
+ for (const entry of Object.values(_ARIA_ROLE_TABLE_MCP)) {
11765
+ for (const a of entry.required ?? []) _ARIA_ALL_KNOWN_ATTRS_MCP.add(a);
11766
+ for (const a of entry.supported ?? []) _ARIA_ALL_KNOWN_ATTRS_MCP.add(a);
11767
+ }
11768
+ var _ARIA_TAG_REGEX_MCP = /<([a-zA-Z][a-zA-Z0-9]*)\b((?:[^>"'/]|"[^"]*"|'[^']*')*)\/?>/g;
11769
+ var _ARIA_ATTR_REGEX_MCP = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
11770
+ function _parseAriaElementsMcp(html) {
11771
+ const elements = [];
11772
+ let match;
11773
+ _ARIA_TAG_REGEX_MCP.lastIndex = 0;
11774
+ while ((match = _ARIA_TAG_REGEX_MCP.exec(html)) !== null) {
11775
+ const tag = match[1].toLowerCase();
11776
+ if (tag === "script" || tag === "style") continue;
11777
+ const attrsStr = match[2] ?? "";
11778
+ const attrs = {};
11779
+ let attrMatch;
11780
+ _ARIA_ATTR_REGEX_MCP.lastIndex = 0;
11781
+ while ((attrMatch = _ARIA_ATTR_REGEX_MCP.exec(attrsStr)) !== null) {
11782
+ attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? "";
11783
+ }
11784
+ elements.push({ tag, attrs, raw: match[0].length > 80 ? match[0].slice(0, 77) + "..." : match[0] });
11785
+ }
11786
+ return elements;
11787
+ }
11788
+ function _validateAriaMcp(html) {
11789
+ if (!html || !html.trim()) return { error: "Input is empty" };
11790
+ const elements = _parseAriaElementsMcp(html);
11791
+ if (elements.length === 0) return { error: "No HTML elements found" };
11792
+ const issues = [];
11793
+ let elementsWithRoleOrAria = 0;
11794
+ for (const el of elements) {
11795
+ const roleAttr = el.attrs["role"];
11796
+ const ariaAttrNames = Object.keys(el.attrs).filter((a) => a.startsWith("aria-"));
11797
+ if (roleAttr === void 0 && ariaAttrNames.length === 0) continue;
11798
+ elementsWithRoleOrAria++;
11799
+ const roles = roleAttr !== void 0 ? roleAttr.trim().split(/\s+/).filter(Boolean) : [];
11800
+ const knownRoles = roles.filter((r) => Object.prototype.hasOwnProperty.call(_ARIA_ROLE_TABLE_MCP, r));
11801
+ const unknownRoles = roles.filter((r) => !Object.prototype.hasOwnProperty.call(_ARIA_ROLE_TABLE_MCP, r));
11802
+ for (const r of unknownRoles) {
11803
+ issues.push({ element: el.raw, role: r, severity: "error", message: `Unknown ARIA role "${r}"` });
11804
+ }
11805
+ const primaryRole = knownRoles[0];
11806
+ const roleDef = primaryRole ? _ARIA_ROLE_TABLE_MCP[primaryRole] : void 0;
11807
+ if (roleDef?.required) {
11808
+ for (const req of roleDef.required) {
11809
+ if (el.attrs[req] === void 0) {
11810
+ issues.push({ element: el.raw, role: primaryRole, severity: "error", message: `role="${primaryRole}" is missing required attribute ${req}` });
11811
+ }
11812
+ }
11813
+ }
11814
+ for (const attrName of ariaAttrNames) {
11815
+ if (_ARIA_GLOBAL_ATTRS_MCP.has(attrName)) continue;
11816
+ if (roleDef) {
11817
+ const allowed = /* @__PURE__ */ new Set([...roleDef.required ?? [], ...roleDef.supported ?? []]);
11818
+ if (!allowed.has(attrName)) {
11819
+ issues.push({
11820
+ element: el.raw,
11821
+ role: primaryRole ?? null,
11822
+ severity: "error",
11823
+ message: primaryRole ? `${attrName} is not permitted on role="${primaryRole}"` : `${attrName} is not a global aria-* attribute`
11824
+ });
11825
+ }
11826
+ } else if (!_ARIA_ALL_KNOWN_ATTRS_MCP.has(attrName)) {
11827
+ issues.push({ element: el.raw, role: null, severity: "warning", message: `${attrName} is not a recognized aria-* attribute (possible typo)` });
11828
+ }
11829
+ }
11830
+ }
11831
+ return {
11832
+ elementCount: elements.length,
11833
+ elementsWithRoleOrAria,
11834
+ issues,
11835
+ valid: issues.every((i) => i.severity !== "error")
11836
+ };
11837
+ }
11838
+ server.tool(
11839
+ "validate_aria",
11840
+ 'Parse an HTML snippet and check every role and aria-* attribute against a curated table of WAI-ARIA 1.2 roles: unknown role names, an aria-* attribute not permitted on the given role, and a widget role missing its required aria-* state (e.g. role="checkbox" without aria-checked). Covers a curated subset of common roles, not the full ARIA spec.',
11841
+ { html: z.string().describe("The HTML snippet to check") },
11842
+ async ({ html }) => {
11843
+ const result = _validateAriaMcp(html);
11844
+ if ("error" in result) return errText(result);
11845
+ return text(JSON.stringify(result, null, 2));
11846
+ }
11847
+ );
11848
+ var _WEBHOOK_HASH_ALGORITHMS_MCP = ["sha1", "sha256", "sha512"];
11849
+ function _webhookConstantTimeEqualMcp(a, b) {
11850
+ if (a.length !== b.length) return false;
11851
+ let diff = 0;
11852
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
11853
+ return diff === 0;
11854
+ }
11855
+ function _verifyWebhookSignatureMcp(payload, secret, signature, algorithm) {
11856
+ if (!payload) return { error: "Payload is required" };
11857
+ if (!secret) return { error: "Secret is required" };
11858
+ if (!signature || !signature.trim()) return { error: "Signature is required" };
11859
+ if (!_WEBHOOK_HASH_ALGORITHMS_MCP.includes(algorithm)) {
11860
+ return { error: "Algorithm must be one of sha1, sha256, sha512" };
11861
+ }
11862
+ const trimmedSignature = signature.trim();
11863
+ const prefixMatch = trimmedSignature.match(/^(sha1|sha256|sha512)=(.+)$/i);
11864
+ const providedSignature = prefixMatch ? prefixMatch[2].trim() : trimmedSignature;
11865
+ const expectedHex = createHmac(algorithm, secret).update(payload, "utf8").digest("hex");
11866
+ const expectedBase64 = createHmac(algorithm, secret).update(payload, "utf8").digest("base64");
11867
+ let matchedEncoding = null;
11868
+ if (_webhookConstantTimeEqualMcp(providedSignature.toLowerCase(), expectedHex.toLowerCase())) {
11869
+ matchedEncoding = "hex";
11870
+ } else if (_webhookConstantTimeEqualMcp(providedSignature, expectedBase64)) {
11871
+ matchedEncoding = "base64";
11872
+ }
11873
+ return {
11874
+ valid: matchedEncoding !== null,
11875
+ algorithm,
11876
+ expectedHex,
11877
+ expectedBase64,
11878
+ providedSignature: trimmedSignature,
11879
+ matchedEncoding
11880
+ };
11881
+ }
11882
+ server.tool(
11883
+ "verify_webhook_signature",
11884
+ 'Recompute an HMAC over a raw webhook payload and compare it to a provided signature, the check every webhook receiver (Stripe, GitHub, Shopify, etc.) has to do before trusting a request. Accepts a signature with or without the common "sha256=" prefix style, and matches against either hex or base64 encoding since providers differ on which they send.',
11885
+ {
11886
+ payload: z.string().describe("The raw webhook request body, exactly as received (do not re-serialize JSON)"),
11887
+ secret: z.string().describe("The shared webhook signing secret"),
11888
+ signature: z.string().describe('The signature to verify, with or without a "sha256=" style prefix, hex or base64 encoded'),
11889
+ algorithm: z.enum(["sha1", "sha256", "sha512"]).optional().describe("The HMAC hash algorithm the signature was computed with (default sha256)")
11890
+ },
11891
+ async ({ payload, secret, signature, algorithm }) => {
11892
+ const result = _verifyWebhookSignatureMcp(payload, secret, signature, algorithm ?? "sha256");
11893
+ if ("error" in result) return errText(result);
11894
+ return text(JSON.stringify(result, null, 2));
11895
+ }
11896
+ );
11897
+ var _FILE_SIZE_UNIT_BYTES_MCP = { B: 1, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12 };
11898
+ var _SPEED_UNIT_BITS_PER_SECOND_MCP = {
11899
+ Kbps: 1e3,
11900
+ Mbps: 1e6,
11901
+ Gbps: 1e9,
11902
+ KBps: 1e3 * 8,
11903
+ MBps: 1e6 * 8,
11904
+ GBps: 1e9 * 8
11905
+ };
11906
+ function _formatDurationSecondsMcp(totalSeconds) {
11907
+ if (totalSeconds < 1) return `${(totalSeconds * 1e3).toFixed(0)}ms`;
11908
+ if (totalSeconds < 60) {
11909
+ const decimals = totalSeconds < 10 && totalSeconds % 1 !== 0 ? 1 : 0;
11910
+ return `${totalSeconds.toFixed(decimals)}s`;
11911
+ }
11912
+ const days = Math.floor(totalSeconds / 86400);
11913
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
11914
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
11915
+ const seconds = Math.round(totalSeconds % 60);
11916
+ const parts = [];
11917
+ if (days > 0) parts.push(`${days}d`);
11918
+ if (hours > 0) parts.push(`${hours}h`);
11919
+ if (minutes > 0) parts.push(`${minutes}m`);
11920
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
11921
+ return parts.join(" ");
11922
+ }
11923
+ function _calculateTransferTimeMcp(fileSize, fileSizeUnit, speed, speedUnit) {
11924
+ if (!Number.isFinite(fileSize) || fileSize <= 0) return { error: "File size must be a positive number" };
11925
+ if (!Number.isFinite(speed) || speed <= 0) return { error: "Connection speed must be a positive number" };
11926
+ if (!(fileSizeUnit in _FILE_SIZE_UNIT_BYTES_MCP)) return { error: `Unsupported file size unit: ${fileSizeUnit}` };
11927
+ if (!(speedUnit in _SPEED_UNIT_BITS_PER_SECOND_MCP)) return { error: `Unsupported speed unit: ${speedUnit}` };
11928
+ const fileSizeBytes = fileSize * _FILE_SIZE_UNIT_BYTES_MCP[fileSizeUnit];
11929
+ const speedBitsPerSecond = speed * _SPEED_UNIT_BITS_PER_SECOND_MCP[speedUnit];
11930
+ const transferTimeSeconds = fileSizeBytes * 8 / speedBitsPerSecond;
11931
+ return { fileSizeBytes, speedBitsPerSecond, transferTimeSeconds, formattedTime: _formatDurationSecondsMcp(transferTimeSeconds) };
11932
+ }
11933
+ function _calculateRequiredBandwidthMcp(fileSize, fileSizeUnit, targetSeconds) {
11934
+ if (!Number.isFinite(fileSize) || fileSize <= 0) return { error: "File size must be a positive number" };
11935
+ if (!Number.isFinite(targetSeconds) || targetSeconds <= 0) return { error: "Target time must be a positive number of seconds" };
11936
+ if (!(fileSizeUnit in _FILE_SIZE_UNIT_BYTES_MCP)) return { error: `Unsupported file size unit: ${fileSizeUnit}` };
11937
+ const fileSizeBytes = fileSize * _FILE_SIZE_UNIT_BYTES_MCP[fileSizeUnit];
11938
+ const requiredBitsPerSecond = fileSizeBytes * 8 / targetSeconds;
11939
+ return {
11940
+ fileSizeBytes,
11941
+ targetSeconds,
11942
+ requiredBitsPerSecond,
11943
+ requiredMbps: requiredBitsPerSecond / 1e6,
11944
+ requiredMBps: requiredBitsPerSecond / 8 / 1e6
11945
+ };
11946
+ }
11947
+ server.tool(
11948
+ "estimate_data_transfer_time",
11949
+ "Estimate the theoretical time to transfer a file of a given size over a connection of a given speed (decimal/SI file size units, standard networking speed units where Mbps is megabits/second and MBps is megabytes/second). Pass targetSeconds instead of speed/speedUnit to run the reverse calculation: the bandwidth required to hit that target time.",
11950
+ {
11951
+ fileSize: z.number().positive().describe("The file size, in fileSizeUnit"),
11952
+ fileSizeUnit: z.enum(["B", "KB", "MB", "GB", "TB"]).describe("The unit fileSize is expressed in"),
11953
+ speed: z.number().positive().optional().describe("The connection speed, in speedUnit (omit and pass targetSeconds for the reverse calculation)"),
11954
+ speedUnit: z.enum(["Kbps", "Mbps", "Gbps", "KBps", "MBps", "GBps"]).optional().describe("The unit speed is expressed in"),
11955
+ targetSeconds: z.number().positive().optional().describe("Target transfer time in seconds; when set, computes the required bandwidth instead of the transfer time")
11956
+ },
11957
+ async ({ fileSize, fileSizeUnit, speed, speedUnit, targetSeconds }) => {
11958
+ const result = targetSeconds !== void 0 ? _calculateRequiredBandwidthMcp(fileSize, fileSizeUnit, targetSeconds) : speed !== void 0 && speedUnit !== void 0 ? _calculateTransferTimeMcp(fileSize, fileSizeUnit, speed, speedUnit) : { error: "Provide either (speed and speedUnit) or targetSeconds" };
11959
+ if ("error" in result) return errText(result);
11960
+ return text(JSON.stringify(result, null, 2));
11961
+ }
11962
+ );
11626
11963
  var transport = new StdioServerTransport();
11627
11964
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "MCP server exposing 198 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",