@utilix-tech/mcp 0.71.0 → 0.73.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 +7 -3
- package/dist/index.js +226 -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.73.0
|
|
6
|
+
- 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
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.72.0
|
|
10
|
+
- Add `validate_vin` tool: 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.
|
|
11
|
+
- Add `validate_isin` tool: validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier.
|
|
12
|
+
|
|
5
13
|
## 0.71.0
|
|
6
14
|
- Add `read_midi_info` tool: parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds accounting for tempo changes.
|
|
7
15
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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.
|
|
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 (
|
|
165
|
+
### Units (43 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -205,8 +205,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
205
205
|
| `calculate_extra_payment_payoff` | Compute how much faster a fixed-rate loan pays off, and how much interest is saved, when a fixed extra amount is added to every monthly payment |
|
|
206
206
|
| `plan_debt_payoff` | Simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest |
|
|
207
207
|
| `calculate_loan_refinance` | Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs |
|
|
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
|
+
| `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
|
+
| `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 |
|
|
208
211
|
|
|
209
|
-
### API / Network (
|
|
212
|
+
### API / Network (49 tools)
|
|
210
213
|
| Tool | Description |
|
|
211
214
|
|------|-------------|
|
|
212
215
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -259,6 +262,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
259
262
|
| `list_websocket_opcodes` | List every WebSocket frame opcode (0x0-0xF) |
|
|
260
263
|
| `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 |
|
|
261
264
|
| `parse_robots_meta_tag` | Parse an existing robots meta tag (or bare content value) into the same shape |
|
|
265
|
+
| `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 |
|
|
262
266
|
|
|
263
267
|
### Color (4 tools)
|
|
264
268
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -11397,5 +11397,231 @@ server.tool(
|
|
|
11397
11397
|
return text(JSON.stringify(result, null, 2));
|
|
11398
11398
|
}
|
|
11399
11399
|
);
|
|
11400
|
+
var _VIN_TRANSLITERATION_MCP = {
|
|
11401
|
+
A: 1,
|
|
11402
|
+
B: 2,
|
|
11403
|
+
C: 3,
|
|
11404
|
+
D: 4,
|
|
11405
|
+
E: 5,
|
|
11406
|
+
F: 6,
|
|
11407
|
+
G: 7,
|
|
11408
|
+
H: 8,
|
|
11409
|
+
J: 1,
|
|
11410
|
+
K: 2,
|
|
11411
|
+
L: 3,
|
|
11412
|
+
M: 4,
|
|
11413
|
+
N: 5,
|
|
11414
|
+
P: 7,
|
|
11415
|
+
R: 9,
|
|
11416
|
+
S: 2,
|
|
11417
|
+
T: 3,
|
|
11418
|
+
U: 4,
|
|
11419
|
+
V: 5,
|
|
11420
|
+
W: 6,
|
|
11421
|
+
X: 7,
|
|
11422
|
+
Y: 8,
|
|
11423
|
+
Z: 9
|
|
11424
|
+
};
|
|
11425
|
+
var _VIN_WEIGHTS_MCP = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
11426
|
+
function _validateVin(input) {
|
|
11427
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
11428
|
+
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
11429
|
+
if (cleaned.length !== 17) return { error: `VIN length (${cleaned.length}) must be exactly 17 characters` };
|
|
11430
|
+
if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(cleaned)) {
|
|
11431
|
+
return { error: "VIN must contain only letters and digits, excluding I, O, and Q" };
|
|
11432
|
+
}
|
|
11433
|
+
let sum = 0;
|
|
11434
|
+
for (let i = 0; i < 17; i++) {
|
|
11435
|
+
const ch = cleaned[i];
|
|
11436
|
+
const value = /[0-9]/.test(ch) ? Number(ch) : _VIN_TRANSLITERATION_MCP[ch];
|
|
11437
|
+
sum += value * _VIN_WEIGHTS_MCP[i];
|
|
11438
|
+
}
|
|
11439
|
+
const remainder = sum % 11;
|
|
11440
|
+
const expectedCheckDigit = remainder === 10 ? "X" : String(remainder);
|
|
11441
|
+
const checkDigit = cleaned[8];
|
|
11442
|
+
return {
|
|
11443
|
+
valid: checkDigit === expectedCheckDigit,
|
|
11444
|
+
vin: cleaned,
|
|
11445
|
+
checkDigit,
|
|
11446
|
+
expectedCheckDigit,
|
|
11447
|
+
wmi: cleaned.slice(0, 3),
|
|
11448
|
+
vds: cleaned.slice(3, 9),
|
|
11449
|
+
vis: cleaned.slice(9, 17)
|
|
11450
|
+
};
|
|
11451
|
+
}
|
|
11452
|
+
server.tool(
|
|
11453
|
+
"validate_vin",
|
|
11454
|
+
"Validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit at position 9, and split it into its World Manufacturer Identifier (WMI), Vehicle Descriptor Section (VDS), and Vehicle Identifier Section (VIS). Does not decode make, model, or model year.",
|
|
11455
|
+
{ vin: z.string().describe("The 17-character VIN, with or without spaces") },
|
|
11456
|
+
async ({ vin }) => {
|
|
11457
|
+
const result = _validateVin(vin);
|
|
11458
|
+
if ("error" in result) return errText(result);
|
|
11459
|
+
return text(JSON.stringify(result, null, 2));
|
|
11460
|
+
}
|
|
11461
|
+
);
|
|
11462
|
+
function _isinLuhnCheckDigitMcp(payloadDigits) {
|
|
11463
|
+
let sum = 0;
|
|
11464
|
+
let shouldDouble = true;
|
|
11465
|
+
for (let i = payloadDigits.length - 1; i >= 0; i--) {
|
|
11466
|
+
let d = parseInt(payloadDigits[i], 10);
|
|
11467
|
+
if (shouldDouble) {
|
|
11468
|
+
d *= 2;
|
|
11469
|
+
if (d > 9) d -= 9;
|
|
11470
|
+
}
|
|
11471
|
+
sum += d;
|
|
11472
|
+
shouldDouble = !shouldDouble;
|
|
11473
|
+
}
|
|
11474
|
+
return (10 - sum % 10) % 10;
|
|
11475
|
+
}
|
|
11476
|
+
function _validateIsin(input) {
|
|
11477
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
11478
|
+
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
11479
|
+
if (cleaned.length !== 12) return { error: `ISIN length (${cleaned.length}) must be exactly 12 characters` };
|
|
11480
|
+
if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(cleaned)) {
|
|
11481
|
+
return { error: "ISIN must be a 2-letter country code, 9 alphanumeric characters, then 1 numeric check digit" };
|
|
11482
|
+
}
|
|
11483
|
+
const countryCode = cleaned.slice(0, 2);
|
|
11484
|
+
const securityIdentifier = cleaned.slice(2, 11);
|
|
11485
|
+
const checkDigit = Number(cleaned[11]);
|
|
11486
|
+
const payload = cleaned.slice(0, 11).split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
|
|
11487
|
+
const expectedCheckDigit = _isinLuhnCheckDigitMcp(payload);
|
|
11488
|
+
return {
|
|
11489
|
+
valid: checkDigit === expectedCheckDigit,
|
|
11490
|
+
isin: cleaned,
|
|
11491
|
+
countryCode,
|
|
11492
|
+
securityIdentifier,
|
|
11493
|
+
checkDigit,
|
|
11494
|
+
expectedCheckDigit,
|
|
11495
|
+
formatted: `${countryCode} ${securityIdentifier} ${checkDigit}`
|
|
11496
|
+
};
|
|
11497
|
+
}
|
|
11498
|
+
server.tool(
|
|
11499
|
+
"validate_isin",
|
|
11500
|
+
"Validate a 12-character ISIN (ISO 6166) via its Luhn-style mod-10 check digit. The 2-letter country code and 9-character alphanumeric security identifier are transliterated (A=10 ... Z=35) into a numeric string before the Luhn algorithm is applied, the same way credit card numbers are checksummed.",
|
|
11501
|
+
{ isin: z.string().describe("The 12-character ISIN, with or without spaces") },
|
|
11502
|
+
async ({ isin }) => {
|
|
11503
|
+
const result = _validateIsin(isin);
|
|
11504
|
+
if ("error" in result) return errText(result);
|
|
11505
|
+
return text(JSON.stringify(result, null, 2));
|
|
11506
|
+
}
|
|
11507
|
+
);
|
|
11508
|
+
function _roundMoneyMortgagePointsMcp(n) {
|
|
11509
|
+
return Math.round(n * 100) / 100;
|
|
11510
|
+
}
|
|
11511
|
+
function _calculateLoanForPointsMcp(principal, annualRatePercent, termMonths) {
|
|
11512
|
+
if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
|
|
11513
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
11514
|
+
return { error: "Annual interest rate must be zero or a positive number" };
|
|
11515
|
+
}
|
|
11516
|
+
if (!Number.isInteger(termMonths) || termMonths <= 0) {
|
|
11517
|
+
return { error: "Term must be a positive whole number of months" };
|
|
11518
|
+
}
|
|
11519
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
11520
|
+
let monthlyPayment;
|
|
11521
|
+
if (monthlyRate === 0) {
|
|
11522
|
+
monthlyPayment = principal / termMonths;
|
|
11523
|
+
} else {
|
|
11524
|
+
const factor = Math.pow(1 + monthlyRate, termMonths);
|
|
11525
|
+
monthlyPayment = principal * monthlyRate * factor / (factor - 1);
|
|
11526
|
+
}
|
|
11527
|
+
let balance = _roundMoneyMortgagePointsMcp(principal);
|
|
11528
|
+
let totalPayment = 0;
|
|
11529
|
+
for (let month = 1; month <= termMonths; month++) {
|
|
11530
|
+
const interest = monthlyRate === 0 ? 0 : _roundMoneyMortgagePointsMcp(balance * monthlyRate);
|
|
11531
|
+
let principalPortion = _roundMoneyMortgagePointsMcp(monthlyPayment) - interest;
|
|
11532
|
+
if (month === termMonths || principalPortion > balance) principalPortion = balance;
|
|
11533
|
+
const payment = _roundMoneyMortgagePointsMcp(principalPortion + interest);
|
|
11534
|
+
balance = _roundMoneyMortgagePointsMcp(balance - principalPortion);
|
|
11535
|
+
totalPayment += payment;
|
|
11536
|
+
}
|
|
11537
|
+
return { monthlyPayment: _roundMoneyMortgagePointsMcp(monthlyPayment), totalInterest: _roundMoneyMortgagePointsMcp(totalPayment - principal) };
|
|
11538
|
+
}
|
|
11539
|
+
function _calculateMortgagePointsMcp(loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent) {
|
|
11540
|
+
if (!Number.isFinite(pointsCostPercent) || pointsCostPercent < 0) {
|
|
11541
|
+
return { error: "Points cost must be zero or a positive percent of the loan amount" };
|
|
11542
|
+
}
|
|
11543
|
+
if (!Number.isFinite(rateReductionPercent) || rateReductionPercent < 0) {
|
|
11544
|
+
return { error: "Rate reduction must be zero or a positive number" };
|
|
11545
|
+
}
|
|
11546
|
+
if (rateReductionPercent > baseRatePercent) {
|
|
11547
|
+
return { error: "Rate reduction cannot exceed the base interest rate" };
|
|
11548
|
+
}
|
|
11549
|
+
const withoutPoints = _calculateLoanForPointsMcp(loanAmount, baseRatePercent, termMonths);
|
|
11550
|
+
if ("error" in withoutPoints) return withoutPoints;
|
|
11551
|
+
const withPoints = _calculateLoanForPointsMcp(loanAmount, baseRatePercent - rateReductionPercent, termMonths);
|
|
11552
|
+
if ("error" in withPoints) return withPoints;
|
|
11553
|
+
const pointsCost = _roundMoneyMortgagePointsMcp(loanAmount * (pointsCostPercent / 100));
|
|
11554
|
+
const monthlySavings = _roundMoneyMortgagePointsMcp(withoutPoints.monthlyPayment - withPoints.monthlyPayment);
|
|
11555
|
+
const breakEvenMonths = monthlySavings > 0 ? Math.ceil(pointsCost / monthlySavings) : null;
|
|
11556
|
+
return {
|
|
11557
|
+
pointsCost,
|
|
11558
|
+
monthlyPaymentWithoutPoints: withoutPoints.monthlyPayment,
|
|
11559
|
+
monthlyPaymentWithPoints: withPoints.monthlyPayment,
|
|
11560
|
+
monthlySavings,
|
|
11561
|
+
breakEvenMonths,
|
|
11562
|
+
totalInterestWithoutPoints: withoutPoints.totalInterest,
|
|
11563
|
+
totalInterestWithPoints: withPoints.totalInterest,
|
|
11564
|
+
totalInterestSavings: _roundMoneyMortgagePointsMcp(withoutPoints.totalInterest - withPoints.totalInterest)
|
|
11565
|
+
};
|
|
11566
|
+
}
|
|
11567
|
+
server.tool(
|
|
11568
|
+
"calculate_mortgage_points",
|
|
11569
|
+
"Compare a loan with no discount points against the same loan at a reduced rate bought with points costing a percentage of the loan amount, and compute the monthly savings and the break-even month where cumulative savings recoup the points cost.",
|
|
11570
|
+
{
|
|
11571
|
+
loanAmount: z.number().describe("The loan principal"),
|
|
11572
|
+
baseRatePercent: z.number().describe("The loan's annual interest rate with no points, as a percent, e.g. 6.5"),
|
|
11573
|
+
termMonths: z.number().int().describe("The loan term in months"),
|
|
11574
|
+
pointsCostPercent: z.number().describe("Cost of the discount points, as a percent of the loan amount, e.g. 1 for one point"),
|
|
11575
|
+
rateReductionPercent: z.number().describe("How many percentage points the rate drops when the points are bought, e.g. 0.25")
|
|
11576
|
+
},
|
|
11577
|
+
async ({ loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent }) => {
|
|
11578
|
+
const result = _calculateMortgagePointsMcp(loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent);
|
|
11579
|
+
if ("error" in result) return errText(result);
|
|
11580
|
+
return text(JSON.stringify(result, null, 2));
|
|
11581
|
+
}
|
|
11582
|
+
);
|
|
11583
|
+
var _PER_COOKIE_BYTE_LIMIT_MCP = 4096;
|
|
11584
|
+
var _DEFAULT_COOKIE_BUDGET_BYTES_MCP = 4096;
|
|
11585
|
+
function _calculateCookieSizeBudgetMcp(cookieHeader, budgetBytes = _DEFAULT_COOKIE_BUDGET_BYTES_MCP) {
|
|
11586
|
+
if (typeof cookieHeader !== "string" || !cookieHeader.trim()) {
|
|
11587
|
+
return { error: "A Cookie header value is required" };
|
|
11588
|
+
}
|
|
11589
|
+
if (!Number.isFinite(budgetBytes) || budgetBytes <= 0) {
|
|
11590
|
+
return { error: "Budget must be a positive number of bytes" };
|
|
11591
|
+
}
|
|
11592
|
+
const parts = cookieHeader.split(";").map((p) => p.trim()).filter(Boolean);
|
|
11593
|
+
if (parts.length === 0) return { error: "A Cookie header value is required" };
|
|
11594
|
+
const cookies = [];
|
|
11595
|
+
for (const part of parts) {
|
|
11596
|
+
const eq = part.indexOf("=");
|
|
11597
|
+
if (eq === -1) return { error: `Malformed cookie pair "${part}" is missing '='` };
|
|
11598
|
+
const name = part.slice(0, eq).trim();
|
|
11599
|
+
const value = part.slice(eq + 1).trim();
|
|
11600
|
+
if (!name) return { error: "Cookie name cannot be empty" };
|
|
11601
|
+
const bytes = Buffer.byteLength(`${name}=${value}`, "utf8");
|
|
11602
|
+
cookies.push({ name, value, bytes, exceedsPerCookieLimit: bytes > _PER_COOKIE_BYTE_LIMIT_MCP });
|
|
11603
|
+
}
|
|
11604
|
+
const totalBytes = cookies.reduce((sum, c) => sum + c.bytes, 0) + Math.max(cookies.length - 1, 0) * 2;
|
|
11605
|
+
return {
|
|
11606
|
+
cookies,
|
|
11607
|
+
cookieCount: cookies.length,
|
|
11608
|
+
totalBytes,
|
|
11609
|
+
budgetBytes,
|
|
11610
|
+
exceedsBudget: totalBytes > budgetBytes
|
|
11611
|
+
};
|
|
11612
|
+
}
|
|
11613
|
+
server.tool(
|
|
11614
|
+
"calculate_cookie_size_budget",
|
|
11615
|
+
"Parse a Cookie request header's name=value pairs and compute each cookie's serialized byte size and the total header size, flagging any cookie over RFC 6265's 4096-byte per-cookie guarantee and the total against a configurable budget (4096 bytes by default).",
|
|
11616
|
+
{
|
|
11617
|
+
cookieHeader: z.string().describe('The Cookie header value to parse, e.g. "session=abc123; theme=dark"'),
|
|
11618
|
+
budgetBytes: z.number().optional().describe("Total byte budget to check against; defaults to 4096")
|
|
11619
|
+
},
|
|
11620
|
+
async ({ cookieHeader, budgetBytes }) => {
|
|
11621
|
+
const result = _calculateCookieSizeBudgetMcp(cookieHeader, budgetBytes ?? _DEFAULT_COOKIE_BUDGET_BYTES_MCP);
|
|
11622
|
+
if ("error" in result) return errText(result);
|
|
11623
|
+
return text(JSON.stringify(result, null, 2));
|
|
11624
|
+
}
|
|
11625
|
+
);
|
|
11400
11626
|
var transport = new StdioServerTransport();
|
|
11401
11627
|
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.73.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",
|