@utilix-tech/mcp 0.73.0 → 0.74.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,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.74.0
6
+ - 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).
7
+ - 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.
8
+
5
9
  ## 0.73.0
6
10
  - 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
11
  - 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 243 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 (44 tools)
166
166
  | Tool | Description |
167
167
  |------|-------------|
168
168
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -208,6 +208,7 @@ 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 |
211
212
 
212
213
  ### API / Network (49 tools)
213
214
  | Tool | Description |
@@ -272,7 +273,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
272
273
  | `color_palette` | Generate a color palette from a base color |
273
274
  | `color_shades` | Generate shades or tints from a base color |
274
275
 
275
- ### Code (20 tools)
276
+ ### Code (21 tools)
276
277
  | Tool | Description |
277
278
  |------|-------------|
278
279
  | `format_sql` | Format a SQL query with proper indentation |
@@ -295,6 +296,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
295
296
  | `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
297
  | `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
298
  | `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 |
299
+ | `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
300
 
299
301
  ### Data (16 tools)
300
302
  | Tool | Description |
package/dist/index.js CHANGED
@@ -11623,5 +11623,227 @@ 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
+ );
11626
11848
  var transport = new StdioServerTransport();
11627
11849
  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.74.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",