@utilix-tech/mcp 0.44.0 → 0.45.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.45.0
6
+ - 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
+ - Add `calculate_payback_period` tool: compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
8
+
5
9
  ## 0.44.0
6
10
  - Add `validate_manifest` tool: validate a web app manifest.json against the W3C Web App Manifest spec's installability requirements, including a 512x512 icon and a maskable icon check.
7
11
  - Add `lint_tool_schema` tool: lint an LLM tool/function-calling definition (OpenAI, Anthropic, or MCP format) for missing descriptions, invalid names, empty enums, and required/properties mismatches.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 175 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
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.
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 (26 tools)
162
+ ### Units (27 tools)
163
163
  | Tool | Description |
164
164
  |------|-------------|
165
165
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -188,8 +188,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
188
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
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
+ | `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
191
192
 
192
- ### API / Network (21 tools)
193
+ ### API / Network (23 tools)
193
194
  | Tool | Description |
194
195
  |------|-------------|
195
196
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -213,6 +214,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
213
214
  | `build_vary_header` | Build a Vary response header from request-header names, with a plain-English caching note per header |
214
215
  | `parse_vary_header` | Parse an existing Vary header value into its individual header names, each with a caching note |
215
216
  | `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
+ | `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
+ | `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
216
219
 
217
220
  ### Color (4 tools)
218
221
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7436,5 +7436,199 @@ server.tool(
7436
7436
  return text(JSON.stringify(result, null, 2));
7437
7437
  }
7438
7438
  );
7439
+ var _PERMISSIONS_POLICY_NOTES_MCP = {
7440
+ camera: "Controls access to video capture devices (getUserMedia video).",
7441
+ microphone: "Controls access to audio capture devices (getUserMedia audio).",
7442
+ geolocation: "Controls access to the Geolocation API.",
7443
+ autoplay: "Controls whether media elements can autoplay without a user gesture.",
7444
+ fullscreen: "Controls access to the Fullscreen API (requestFullscreen).",
7445
+ payment: "Controls access to the Payment Request API.",
7446
+ usb: "Controls access to the WebUSB API.",
7447
+ accelerometer: "Controls access to the Accelerometer sensor API.",
7448
+ gyroscope: "Controls access to the Gyroscope sensor API.",
7449
+ magnetometer: "Controls access to the Magnetometer sensor API.",
7450
+ midi: "Controls access to the Web MIDI API.",
7451
+ "picture-in-picture": "Controls whether video elements can enter picture-in-picture mode.",
7452
+ "sync-xhr": "Controls whether synchronous XMLHttpRequest is allowed.",
7453
+ "display-capture": "Controls access to screen/tab capture (getDisplayMedia).",
7454
+ "encrypted-media": "Controls access to the Encrypted Media Extensions API (DRM playback).",
7455
+ "clipboard-read": "Controls read access to the async Clipboard API.",
7456
+ "clipboard-write": "Controls write access to the async Clipboard API.",
7457
+ "screen-wake-lock": "Controls access to the Screen Wake Lock API.",
7458
+ "web-share": "Controls access to the Web Share API.",
7459
+ "xr-spatial-tracking": "Controls access to WebXR device sensors and spatial tracking."
7460
+ };
7461
+ function _formatPermissionsPolicyAllowlistMcp(allowlist) {
7462
+ if (allowlist.includes("*")) return "*";
7463
+ if (allowlist.length === 0) return "()";
7464
+ return "(" + allowlist.map((o) => o === "self" ? "self" : `"${o}"`).join(" ") + ")";
7465
+ }
7466
+ function _buildPermissionsPolicyMcp(features) {
7467
+ if (!Array.isArray(features) || features.length === 0) return { error: "At least one feature is required" };
7468
+ const warnings = [];
7469
+ const seen = /* @__PURE__ */ new Map();
7470
+ for (const f of features) {
7471
+ if (!f || typeof f.feature !== "string" || f.feature.trim() === "") continue;
7472
+ const name = f.feature.trim().toLowerCase();
7473
+ const allowlist = Array.isArray(f.allowlist) ? f.allowlist.map((o) => typeof o === "string" ? o.trim() : "").filter((o) => o.length > 0) : [];
7474
+ if (seen.has(name)) warnings.push(`duplicate feature "${name}" was merged, the last occurrence wins`);
7475
+ seen.set(name, { feature: name, allowlist });
7476
+ }
7477
+ if (seen.size === 0) return { error: "At least one feature is required" };
7478
+ const directives = [];
7479
+ for (const { feature, allowlist } of seen.values()) {
7480
+ if (!_PERMISSIONS_POLICY_NOTES_MCP[feature]) {
7481
+ warnings.push(`"${feature}" is not a widely recognized feature name, it will still be included as written`);
7482
+ }
7483
+ if (allowlist.includes("*") && allowlist.length > 1) {
7484
+ warnings.push(`"${feature}" combines "*" with other origins; "*" already allows every origin so the others are redundant`);
7485
+ }
7486
+ const invalidOrigin = allowlist.find((o) => o !== "self" && o !== "*" && !/^https?:\/\//i.test(o));
7487
+ if (invalidOrigin) {
7488
+ warnings.push(`"${invalidOrigin}" on "${feature}" doesn't look like an origin (expected "self", "*", or an http(s):// URL); it will still be quoted as written`);
7489
+ }
7490
+ directives.push({
7491
+ feature,
7492
+ value: _formatPermissionsPolicyAllowlistMcp(allowlist),
7493
+ note: _PERMISSIONS_POLICY_NOTES_MCP[feature] ?? `Controls access to the "${feature}" feature.`
7494
+ });
7495
+ }
7496
+ const headerValue = directives.map((d) => `${d.feature}=${d.value}`).join(", ");
7497
+ return { header: `Permissions-Policy: ${headerValue}`, headerValue, directives, warnings };
7498
+ }
7499
+ function _splitPermissionsPolicyTopLevelMcp(input) {
7500
+ const segments = [];
7501
+ let depth = 0;
7502
+ let current = "";
7503
+ for (const ch of input) {
7504
+ if (ch === "(") depth++;
7505
+ if (ch === ")") depth--;
7506
+ if (ch === "," && depth === 0) {
7507
+ if (current.trim()) segments.push(current.trim());
7508
+ current = "";
7509
+ } else {
7510
+ current += ch;
7511
+ }
7512
+ }
7513
+ if (current.trim()) segments.push(current.trim());
7514
+ return segments;
7515
+ }
7516
+ function _parsePermissionsPolicyAllowlistMcp(rawValue) {
7517
+ if (rawValue === "*") return ["*"];
7518
+ if (rawValue === "()" || rawValue === "") return [];
7519
+ const inner = rawValue.replace(/^\(/, "").replace(/\)$/, "");
7520
+ const tokens = inner.match(/"[^"]*"|\S+/g) ?? [];
7521
+ return tokens.map((t) => t.replace(/^"|"$/g, ""));
7522
+ }
7523
+ function _parsePermissionsPolicyMcp(raw) {
7524
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
7525
+ const withoutLabel = raw.trim().replace(/^permissions-policy:\s*/i, "");
7526
+ const segments = _splitPermissionsPolicyTopLevelMcp(withoutLabel);
7527
+ if (segments.length === 0) return { error: "No directives found" };
7528
+ const features = [];
7529
+ for (const segment of segments) {
7530
+ const eq = segment.indexOf("=");
7531
+ if (eq === -1) continue;
7532
+ const feature = segment.slice(0, eq).trim();
7533
+ const rawValue = segment.slice(eq + 1).trim();
7534
+ features.push({ feature, allowlist: _parsePermissionsPolicyAllowlistMcp(rawValue) });
7535
+ }
7536
+ if (features.length === 0) return { error: "No directives found" };
7537
+ return _buildPermissionsPolicyMcp(features);
7538
+ }
7539
+ server.tool(
7540
+ "build_permissions_policy",
7541
+ 'Build a Permissions-Policy response-header value from a list of browser features (e.g. camera, microphone, geolocation) and their per-feature allowlist ("self", specific origins, "*" for all origins, or an empty allowlist to disable the feature entirely), attaching a plain-English note on what each feature restricts.',
7542
+ {
7543
+ features: z.array(z.object({
7544
+ feature: z.string().describe('The Permissions-Policy feature name, e.g. "camera"'),
7545
+ allowlist: z.array(z.string()).describe('Origins allowed to use the feature: "self", "*" for all origins, specific https:// origins, or an empty array to disable it entirely')
7546
+ })).min(1).describe("Features and their allowlists to include in the header")
7547
+ },
7548
+ async ({ features }) => {
7549
+ const result = _buildPermissionsPolicyMcp(features);
7550
+ if ("error" in result) return errText(result);
7551
+ return text(JSON.stringify(result, null, 2));
7552
+ }
7553
+ );
7554
+ server.tool(
7555
+ "parse_permissions_policy",
7556
+ 'Parse an existing Permissions-Policy header value (with or without a leading "Permissions-Policy:" label) into its individual feature directives, each with a plain-English note on what it restricts.',
7557
+ {
7558
+ header: z.string().min(1).describe("The Permissions-Policy header value to parse")
7559
+ },
7560
+ async ({ header }) => {
7561
+ const result = _parsePermissionsPolicyMcp(header);
7562
+ if ("error" in result) return errText(result);
7563
+ return text(JSON.stringify(result, null, 2));
7564
+ }
7565
+ );
7566
+ function _computePaybackMcp(initialInvestment, flows) {
7567
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7568
+ let cumulative = 0;
7569
+ const cumulativeCashFlows = [];
7570
+ let period = null;
7571
+ for (let i = 0; i < flows.length; i++) {
7572
+ const prevCumulative = cumulative;
7573
+ cumulative += flows[i];
7574
+ cumulativeCashFlows.push(round4(cumulative));
7575
+ if (period === null && cumulative >= initialInvestment) {
7576
+ const remaining = initialInvestment - prevCumulative;
7577
+ const fraction = flows[i] !== 0 ? remaining / flows[i] : 0;
7578
+ period = i + fraction;
7579
+ }
7580
+ }
7581
+ return { cumulative: cumulativeCashFlows, period };
7582
+ }
7583
+ function _calculatePaybackPeriodMcp(initialInvestment, cashFlows, discountRatePercent) {
7584
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7585
+ if (typeof initialInvestment !== "number" || !Number.isFinite(initialInvestment) || initialInvestment <= 0) {
7586
+ return { error: "initialInvestment must be a finite number greater than zero" };
7587
+ }
7588
+ if (!Array.isArray(cashFlows) || cashFlows.length === 0) {
7589
+ return { error: "cashFlows must be a non-empty array of numbers" };
7590
+ }
7591
+ if (cashFlows.some((f) => typeof f !== "number" || !Number.isFinite(f))) {
7592
+ return { error: "Every cash flow must be a finite number" };
7593
+ }
7594
+ const { cumulative, period } = _computePaybackMcp(initialInvestment, cashFlows);
7595
+ const result = {
7596
+ initialInvestment: round4(initialInvestment),
7597
+ cashFlows: cashFlows.map(round4),
7598
+ cumulativeCashFlows: cumulative,
7599
+ paybackPeriod: period !== null ? round4(period) : null,
7600
+ paybackPeriodRounded: period !== null ? Math.ceil(period) : null,
7601
+ recovered: period !== null
7602
+ };
7603
+ if (discountRatePercent !== void 0) {
7604
+ if (typeof discountRatePercent !== "number" || !Number.isFinite(discountRatePercent) || discountRatePercent < 0) {
7605
+ return { error: "discountRatePercent must be a finite, non-negative number when provided" };
7606
+ }
7607
+ const r = discountRatePercent / 100;
7608
+ const discountedCashFlows = cashFlows.map((f, i) => f / Math.pow(1 + r, i + 1));
7609
+ const { cumulative: discountedCumulative, period: discountedPeriod } = _computePaybackMcp(initialInvestment, discountedCashFlows);
7610
+ result.discountRatePercent = round4(discountRatePercent);
7611
+ result.discountedCashFlows = discountedCashFlows.map(round4);
7612
+ result.discountedCumulativeCashFlows = discountedCumulative;
7613
+ result.discountedPaybackPeriod = discountedPeriod !== null ? round4(discountedPeriod) : null;
7614
+ result.discountedPaybackPeriodRounded = discountedPeriod !== null ? Math.ceil(discountedPeriod) : null;
7615
+ result.discountedRecovered = discountedPeriod !== null;
7616
+ }
7617
+ return result;
7618
+ }
7619
+ server.tool(
7620
+ "calculate_payback_period",
7621
+ "From an initial investment and a series of periodic cash flows, compute the simple payback period (the point where cumulative cash flow turns positive), plus the discounted payback period when a discount rate is supplied. Pure arithmetic over a running cumulative sum, no live rate feed.",
7622
+ {
7623
+ initialInvestment: z.number().describe("The initial investment amount (must be greater than zero)"),
7624
+ cashFlows: z.array(z.number()).min(1).describe("Net cash flow for each period, in order"),
7625
+ discountRatePercent: z.number().optional().describe("Optional discount rate per period, as a percent; when provided, also computes the discounted payback period")
7626
+ },
7627
+ async ({ initialInvestment, cashFlows, discountRatePercent }) => {
7628
+ const result = _calculatePaybackPeriodMcp(initialInvestment, cashFlows, discountRatePercent);
7629
+ if ("error" in result) return errText(result);
7630
+ return text(JSON.stringify(result, null, 2));
7631
+ }
7632
+ );
7439
7633
  var transport = new StdioServerTransport();
7440
7634
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.44.0",
3
+ "version": "0.45.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",