@utilix-tech/mcp 0.45.0 → 0.46.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.46.0
6
+ - Add `calculate_cd_maturity` tool: 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 payout if withdrawn early.
7
+ - Add `build_client_hints`, `parse_client_hint_headers` tools: build an `Accept-CH`/`Critical-CH` response header from a list of client hint names with a plain-English note per hint, or decode raw Client Hints request-header text into structured values.
8
+
5
9
  ## 0.45.0
6
10
  - Add `build_permissions_policy`, `parse_permissions_policy` tools: build a `Permissions-Policy` response header from browser features and their per-feature allowlist with a plain-English note per feature, or parse an existing one into the same shape.
7
11
  - Add `calculate_payback_period` tool: compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 178 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
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.
4
4
 
5
5
  No API key required. All tools run locally. Requires Node.js 18 or later.
6
6
 
@@ -159,7 +159,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
159
159
  | `convert_timezone` | Convert a date/time between timezones |
160
160
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
161
161
 
162
- ### Units (27 tools)
162
+ ### Units (28 tools)
163
163
  | Tool | Description |
164
164
  |------|-------------|
165
165
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -189,8 +189,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
189
189
  | `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
190
190
  | `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
191
191
  | `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
192
+ | `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 |
192
193
 
193
- ### API / Network (23 tools)
194
+ ### API / Network (25 tools)
194
195
  | Tool | Description |
195
196
  |------|-------------|
196
197
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -216,6 +217,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
216
217
  | `validate_manifest` | Validate a web app manifest.json against the W3C spec's installability requirements: name/short_name, start_url, display mode, and icon coverage |
217
218
  | `build_permissions_policy` | Build a Permissions-Policy response header from browser features and their per-feature allowlist, with a plain-English note per feature |
218
219
  | `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
220
+ | `build_client_hints` | Build an Accept-CH (and optional Critical-CH) response header from a list of client hint names, with a plain-English note on what each hint reveals |
221
+ | `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
219
222
 
220
223
  ### Color (4 tools)
221
224
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7630,5 +7630,186 @@ server.tool(
7630
7630
  return text(JSON.stringify(result, null, 2));
7631
7631
  }
7632
7632
  );
7633
+ function _calculateCdMaturityMcp(principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths) {
7634
+ const round2 = (n) => Math.round(n * 100) / 100;
7635
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7636
+ if (typeof principal !== "number" || !Number.isFinite(principal) || principal <= 0) {
7637
+ return { error: "Principal must be a finite number greater than zero" };
7638
+ }
7639
+ if (typeof annualRatePercent !== "number" || !Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
7640
+ return { error: "Annual interest rate must be zero or a positive number" };
7641
+ }
7642
+ if (typeof termMonths !== "number" || !Number.isFinite(termMonths) || termMonths <= 0) {
7643
+ return { error: "Term must be a positive number of months" };
7644
+ }
7645
+ if (typeof compoundsPerYear !== "number" || !Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
7646
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
7647
+ }
7648
+ if (earlyWithdrawalPenaltyMonths !== void 0 && (typeof earlyWithdrawalPenaltyMonths !== "number" || !Number.isFinite(earlyWithdrawalPenaltyMonths) || earlyWithdrawalPenaltyMonths < 0)) {
7649
+ return { error: "earlyWithdrawalPenaltyMonths must be zero or a positive number when provided" };
7650
+ }
7651
+ const ratePerPeriod = annualRatePercent / 100 / compoundsPerYear;
7652
+ const totalPeriods = compoundsPerYear * (termMonths / 12);
7653
+ const growth = ratePerPeriod === 0 ? 1 : Math.pow(1 + ratePerPeriod, totalPeriods);
7654
+ const maturityValue = principal * growth;
7655
+ const totalInterest = maturityValue - principal;
7656
+ const apy = ratePerPeriod === 0 ? 0 : (Math.pow(1 + ratePerPeriod, compoundsPerYear) - 1) * 100;
7657
+ const result = {
7658
+ principal: round2(principal),
7659
+ annualRatePercent: round4(annualRatePercent),
7660
+ termMonths,
7661
+ compoundsPerYear,
7662
+ maturityValue: round2(maturityValue),
7663
+ totalInterest: round2(totalInterest),
7664
+ apy: round4(apy)
7665
+ };
7666
+ if (earlyWithdrawalPenaltyMonths !== void 0) {
7667
+ const monthlyRate = annualRatePercent / 100 / 12;
7668
+ const forfeited = Math.min(principal * monthlyRate * earlyWithdrawalPenaltyMonths, Math.max(totalInterest, 0));
7669
+ result.earlyWithdrawalPenaltyMonths = earlyWithdrawalPenaltyMonths;
7670
+ result.earlyWithdrawalForfeitedInterest = round2(forfeited);
7671
+ result.earlyWithdrawalPayout = round2(maturityValue - forfeited);
7672
+ }
7673
+ return result;
7674
+ }
7675
+ server.tool(
7676
+ "calculate_cd_maturity",
7677
+ "Compute the maturity value of a certificate of deposit given a principal, a nominal annual interest rate (as a percent), a term in months, and a compounding frequency (periods per year, e.g. 1 = annually, 12 = monthly). Also reports the effective annual yield (APY) implied by the nominal rate and compounding frequency, and, when an early-withdrawal penalty in months of forfeited interest is given, the net payout if withdrawn before maturity.",
7678
+ {
7679
+ principal: z.number().describe("The deposit principal (must be greater than zero)"),
7680
+ annualRatePercent: z.number().describe("Nominal annual interest rate, as a percent, e.g. 5"),
7681
+ termMonths: z.number().describe("CD term in months"),
7682
+ compoundsPerYear: z.number().int().describe("Compounding periods per year, e.g. 1 = annually, 4 = quarterly, 12 = monthly, 365 = daily"),
7683
+ earlyWithdrawalPenaltyMonths: z.number().optional().describe("Optional early-withdrawal penalty, in months of forfeited interest; when given, also computes the net early-withdrawal payout")
7684
+ },
7685
+ async ({ principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths }) => {
7686
+ const result = _calculateCdMaturityMcp(principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths);
7687
+ if ("error" in result) return errText(result);
7688
+ return text(JSON.stringify(result, null, 2));
7689
+ }
7690
+ );
7691
+ var _CLIENT_HINTS_MCP = [
7692
+ { name: "Sec-CH-UA", description: "The browser's brand(s) and significant version, sent automatically on every request without an Accept-CH opt-in.", lowEntropy: true, valueType: "sf-list-brand" },
7693
+ { name: "Sec-CH-UA-Mobile", description: "Whether the browser is running on a mobile device (?1) or not (?0).", lowEntropy: true, valueType: "boolean" },
7694
+ { name: "Sec-CH-UA-Platform", description: 'The platform/OS the browser is running on, e.g. "Windows" or "Android".', lowEntropy: true, valueType: "string" },
7695
+ { name: "Sec-CH-UA-Full-Version-List", description: "The full version number for each brand in the user agent, not just the significant version.", lowEntropy: false, valueType: "sf-list-brand" },
7696
+ { name: "Sec-CH-UA-Platform-Version", description: 'The exact OS version, e.g. "15.0.0".', lowEntropy: false, valueType: "string" },
7697
+ { name: "Sec-CH-UA-Arch", description: 'The underlying CPU architecture, e.g. "x86" or "arm".', lowEntropy: false, valueType: "string" },
7698
+ { name: "Sec-CH-UA-Bitness", description: 'The CPU architecture bitness, e.g. "64".', lowEntropy: false, valueType: "string" },
7699
+ { name: "Sec-CH-UA-Model", description: "The device model, mostly populated on mobile devices.", lowEntropy: false, valueType: "string" },
7700
+ { name: "Sec-CH-UA-WoW64", description: "Whether a 32-bit browser is running on 64-bit Windows.", lowEntropy: false, valueType: "boolean" },
7701
+ { name: "Sec-CH-UA-Form-Factor", description: `The device's form factor, e.g. "Desktop", "Mobile", "Tablet", or "XR".`, lowEntropy: false, valueType: "string" },
7702
+ { name: "Sec-CH-Prefers-Color-Scheme", description: "The user's preferred color scheme, mirrors the prefers-color-scheme media feature.", lowEntropy: false, valueType: "string" },
7703
+ { name: "Sec-CH-Prefers-Reduced-Motion", description: "Whether the user has requested reduced motion.", lowEntropy: false, valueType: "string" },
7704
+ { name: "Sec-CH-Prefers-Reduced-Transparency", description: "Whether the user has requested reduced transparency.", lowEntropy: false, valueType: "string" },
7705
+ { name: "Sec-CH-Viewport-Width", description: "The layout viewport width in CSS pixels.", lowEntropy: false, valueType: "number" },
7706
+ { name: "Sec-CH-Viewport-Height", description: "The layout viewport height in CSS pixels.", lowEntropy: false, valueType: "number" },
7707
+ { name: "Sec-CH-DPR", description: "The device pixel ratio.", lowEntropy: false, valueType: "number" },
7708
+ { name: "Sec-CH-Width", description: "The intrinsic width of a requested image resource, set per-request via the image's sizes attribute.", lowEntropy: false, valueType: "number" },
7709
+ { name: "Sec-CH-Device-Memory", description: "Approximate device RAM in GiB, rounded to the nearest power of two for privacy.", lowEntropy: false, valueType: "number" },
7710
+ { name: "Save-Data", description: "Legacy hint signaling the user opted into a reduced-data mode; sent automatically like a low-entropy hint.", lowEntropy: true, valueType: "boolean" }
7711
+ ];
7712
+ function _findClientHintMcp(raw) {
7713
+ const normalized = raw.trim().toLowerCase();
7714
+ return _CLIENT_HINTS_MCP.find((d) => d.name.toLowerCase() === normalized);
7715
+ }
7716
+ function _buildClientHintsMcp(hintNames, criticalHintNames = []) {
7717
+ if (!Array.isArray(hintNames) || hintNames.length === 0) return { error: "At least one client hint is required" };
7718
+ const resolved = [];
7719
+ for (const raw of hintNames) {
7720
+ if (typeof raw !== "string" || raw.trim() === "") continue;
7721
+ const def = _findClientHintMcp(raw);
7722
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
7723
+ resolved.push(def);
7724
+ }
7725
+ if (resolved.length === 0) return { error: "At least one client hint is required" };
7726
+ const critical = [];
7727
+ for (const raw of criticalHintNames) {
7728
+ if (typeof raw !== "string" || raw.trim() === "") continue;
7729
+ const def = _findClientHintMcp(raw);
7730
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
7731
+ if (!resolved.some((d) => d.name === def.name)) {
7732
+ return { error: `Critical hint "${def.name}" must also be included in hintNames` };
7733
+ }
7734
+ critical.push(def);
7735
+ }
7736
+ const uniqueNames = Array.from(new Set(resolved.map((d) => d.name)));
7737
+ const result = {
7738
+ acceptCH: uniqueNames.join(", "),
7739
+ hints: resolved.map((d) => ({ name: d.name, description: d.description, lowEntropy: d.lowEntropy }))
7740
+ };
7741
+ if (critical.length > 0) {
7742
+ result.criticalCH = Array.from(new Set(critical.map((d) => d.name))).join(", ");
7743
+ }
7744
+ return result;
7745
+ }
7746
+ server.tool(
7747
+ "build_client_hints",
7748
+ "Build an Accept-CH response-header value from a list of client hint names (e.g. Sec-CH-UA-Platform, Sec-CH-DPR) the server wants the browser to send on future requests, attaching a plain-English note on what each hint reveals. Pass a subset of those names as criticalHintNames to also build a Critical-CH header value, for hints the server needs on the very first request of a session.",
7749
+ {
7750
+ hintNames: z.array(z.string()).min(1).describe('Client hint names to request, e.g. "Sec-CH-UA-Platform"'),
7751
+ criticalHintNames: z.array(z.string()).optional().describe("Optional subset of hintNames to also mark critical (builds a Critical-CH header value)")
7752
+ },
7753
+ async ({ hintNames, criticalHintNames }) => {
7754
+ const result = _buildClientHintsMcp(hintNames, criticalHintNames ?? []);
7755
+ if ("error" in result) return errText(result);
7756
+ return text(JSON.stringify(result, null, 2));
7757
+ }
7758
+ );
7759
+ function _decodeClientHintValueMcp(valueType, rawValue) {
7760
+ switch (valueType) {
7761
+ case "boolean":
7762
+ if (rawValue === "?1") return true;
7763
+ if (rawValue === "?0") return false;
7764
+ return rawValue;
7765
+ case "string":
7766
+ return rawValue.replace(/^"|"$/g, "");
7767
+ case "number": {
7768
+ const n = Number(rawValue);
7769
+ return Number.isFinite(n) ? n : rawValue;
7770
+ }
7771
+ case "sf-list-brand":
7772
+ return rawValue.split(",").map((part) => {
7773
+ const m = part.trim().match(/^"([^"]*)";v="([^"]*)"$/);
7774
+ return m ? { brand: m[1], version: m[2] } : { raw: part.trim() };
7775
+ });
7776
+ default:
7777
+ return rawValue;
7778
+ }
7779
+ }
7780
+ function _parseClientHintHeadersMcp(rawHeaders) {
7781
+ if (typeof rawHeaders !== "string" || rawHeaders.trim() === "") return { error: "Provide one or more header lines to parse" };
7782
+ const lines = rawHeaders.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
7783
+ const hints = [];
7784
+ const unrecognized = [];
7785
+ for (const line of lines) {
7786
+ const idx = line.indexOf(":");
7787
+ if (idx === -1) continue;
7788
+ const name = line.slice(0, idx).trim();
7789
+ const rawValue = line.slice(idx + 1).trim();
7790
+ const def = _findClientHintMcp(name);
7791
+ if (!def) {
7792
+ unrecognized.push({ name, rawValue });
7793
+ continue;
7794
+ }
7795
+ hints.push({ name: def.name, rawValue, description: def.description, value: _decodeClientHintValueMcp(def.valueType, rawValue) });
7796
+ }
7797
+ if (hints.length === 0 && unrecognized.length === 0) {
7798
+ return { error: 'No parsable header lines found (expected "Header-Name: value" per line)' };
7799
+ }
7800
+ return { hints, unrecognized };
7801
+ }
7802
+ server.tool(
7803
+ "parse_client_hint_headers",
7804
+ `Parse raw request-header text, one "Header-Name: value" per line, into decoded client hint values: brand/version pairs for Sec-CH-UA-style headers, booleans for ?1/?0 values, unquoted strings, and numbers. Lines whose header name isn't a recognized client hint are returned separately under "unrecognized" rather than dropped.`,
7805
+ {
7806
+ headers: z.string().min(1).describe('Raw request-header text to parse, one "Header-Name: value" per line')
7807
+ },
7808
+ async ({ headers }) => {
7809
+ const result = _parseClientHintHeadersMcp(headers);
7810
+ if ("error" in result) return errText(result);
7811
+ return text(JSON.stringify(result, null, 2));
7812
+ }
7813
+ );
7633
7814
  var transport = new StdioServerTransport();
7634
7815
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "MCP server exposing 175 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",