@utilix-tech/mcp 0.44.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 +8 -0
- package/README.md +9 -3
- package/dist/index.js +375 -0
- package/package.json +1 -1
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.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
|
+
|
|
9
|
+
## 0.45.0
|
|
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.
|
|
11
|
+
- Add `calculate_payback_period` tool: compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
|
|
12
|
+
|
|
5
13
|
## 0.44.0
|
|
6
14
|
- 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
15
|
- 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
|
|
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 (
|
|
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 |
|
|
@@ -188,8 +188,10 @@ 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 |
|
|
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 |
|
|
191
193
|
|
|
192
|
-
### API / Network (
|
|
194
|
+
### API / Network (25 tools)
|
|
193
195
|
| Tool | Description |
|
|
194
196
|
|------|-------------|
|
|
195
197
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -213,6 +215,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
213
215
|
| `build_vary_header` | Build a Vary response header from request-header names, with a plain-English caching note per header |
|
|
214
216
|
| `parse_vary_header` | Parse an existing Vary header value into its individual header names, each with a caching note |
|
|
215
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 |
|
|
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 |
|
|
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 |
|
|
216
222
|
|
|
217
223
|
### Color (4 tools)
|
|
218
224
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -7436,5 +7436,380 @@ 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
|
+
);
|
|
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
|
+
);
|
|
7439
7814
|
var transport = new StdioServerTransport();
|
|
7440
7815
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "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",
|