@utilix-tech/mcp 0.63.0 → 0.64.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +135 -0
- package/package.json +1 -1
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.64.0
|
|
6
|
+
- Add `calculate_loan_refinance` tool: 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.
|
|
7
|
+
- Add `parse_multipart_form_data` tool: parse a raw multipart/form-data request body into its individual parts (field name, filename, headers, decoded value) per RFC 7578, with an optional Content-Type header value to extract the boundary from.
|
|
8
|
+
|
|
5
9
|
## 0.63.0
|
|
6
10
|
- Add `plan_debt_payoff` tool: 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.
|
|
7
11
|
- Add `audit_dependency_ranges` tool: classify each dependency's version specifier range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 220 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 (39 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -203,8 +203,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
203
203
|
| `calculate_freelancer_rate` | Solve for the minimum hourly rate a freelancer must charge to hit a target annual income, given billable hours, weeks worked, overhead, and a profit margin |
|
|
204
204
|
| `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 |
|
|
205
205
|
| `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 |
|
|
206
|
+
| `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 |
|
|
206
207
|
|
|
207
|
-
### API / Network (
|
|
208
|
+
### API / Network (38 tools)
|
|
208
209
|
| Tool | Description |
|
|
209
210
|
|------|-------------|
|
|
210
211
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -246,6 +247,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
246
247
|
| `parse_priority_header` | Parse an existing Priority header value into its urgency and incremental fields, with spec defaults applied |
|
|
247
248
|
| `lookup_close_code` | Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges |
|
|
248
249
|
| `list_close_codes` | List every well-known WebSocket close code (1000-1015) |
|
|
250
|
+
| `parse_multipart_form_data` | Parse a raw multipart/form-data request body into its individual parts: field name, filename, headers, and decoded value, per RFC 7578 |
|
|
249
251
|
|
|
250
252
|
### Color (4 tools)
|
|
251
253
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -10187,5 +10187,140 @@ server.tool(
|
|
|
10187
10187
|
return text(JSON.stringify(result, null, 2));
|
|
10188
10188
|
}
|
|
10189
10189
|
);
|
|
10190
|
+
function _calculateLoanForRefinanceMcp(principal, annualRatePercent, termMonths) {
|
|
10191
|
+
if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
|
|
10192
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
10193
|
+
return { error: "Annual interest rate must be zero or a positive number" };
|
|
10194
|
+
}
|
|
10195
|
+
if (!Number.isInteger(termMonths) || termMonths <= 0) {
|
|
10196
|
+
return { error: "Term must be a positive whole number of months" };
|
|
10197
|
+
}
|
|
10198
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
10199
|
+
let monthlyPayment;
|
|
10200
|
+
if (monthlyRate === 0) {
|
|
10201
|
+
monthlyPayment = principal / termMonths;
|
|
10202
|
+
} else {
|
|
10203
|
+
const factor = Math.pow(1 + monthlyRate, termMonths);
|
|
10204
|
+
monthlyPayment = principal * monthlyRate * factor / (factor - 1);
|
|
10205
|
+
}
|
|
10206
|
+
let balance = _roundMoneyMcp(principal);
|
|
10207
|
+
let totalPayment = 0;
|
|
10208
|
+
for (let month = 1; month <= termMonths; month++) {
|
|
10209
|
+
const interest = monthlyRate === 0 ? 0 : _roundMoneyMcp(balance * monthlyRate);
|
|
10210
|
+
let principalPortion = _roundMoneyMcp(monthlyPayment) - interest;
|
|
10211
|
+
if (month === termMonths || principalPortion > balance) principalPortion = balance;
|
|
10212
|
+
const payment = _roundMoneyMcp(principalPortion + interest);
|
|
10213
|
+
balance = _roundMoneyMcp(balance - principalPortion);
|
|
10214
|
+
totalPayment += payment;
|
|
10215
|
+
}
|
|
10216
|
+
return {
|
|
10217
|
+
monthlyPayment: _roundMoneyMcp(monthlyPayment),
|
|
10218
|
+
totalPayment: _roundMoneyMcp(totalPayment),
|
|
10219
|
+
totalInterest: _roundMoneyMcp(totalPayment - principal),
|
|
10220
|
+
termMonths
|
|
10221
|
+
};
|
|
10222
|
+
}
|
|
10223
|
+
function _calculateLoanRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts) {
|
|
10224
|
+
const current = _calculateLoanForRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths);
|
|
10225
|
+
if ("error" in current) return current;
|
|
10226
|
+
const refinanced = _calculateLoanForRefinanceMcp(currentBalance, newRatePercent, newTermMonths);
|
|
10227
|
+
if ("error" in refinanced) return refinanced;
|
|
10228
|
+
if (!Number.isFinite(closingCosts) || closingCosts < 0) {
|
|
10229
|
+
return { error: "Closing costs must be zero or a positive number" };
|
|
10230
|
+
}
|
|
10231
|
+
const monthlySavings = _roundMoneyMcp(current.monthlyPayment - refinanced.monthlyPayment);
|
|
10232
|
+
const breakEvenMonths = monthlySavings > 0 ? Math.ceil(closingCosts / monthlySavings) : null;
|
|
10233
|
+
return {
|
|
10234
|
+
currentMonthlyPayment: current.monthlyPayment,
|
|
10235
|
+
newMonthlyPayment: refinanced.monthlyPayment,
|
|
10236
|
+
monthlySavings,
|
|
10237
|
+
closingCosts: _roundMoneyMcp(closingCosts),
|
|
10238
|
+
breakEvenMonths,
|
|
10239
|
+
currentTotalInterest: current.totalInterest,
|
|
10240
|
+
newTotalInterest: refinanced.totalInterest,
|
|
10241
|
+
totalInterestSavings: _roundMoneyMcp(current.totalInterest - refinanced.totalInterest)
|
|
10242
|
+
};
|
|
10243
|
+
}
|
|
10244
|
+
server.tool(
|
|
10245
|
+
"calculate_loan_refinance",
|
|
10246
|
+
"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 monthly savings recoup the closing costs.",
|
|
10247
|
+
{
|
|
10248
|
+
currentBalance: z.number().describe("Outstanding balance on the current loan"),
|
|
10249
|
+
currentRatePercent: z.number().describe("Current loan's annual interest rate as a percent, e.g. 6.5"),
|
|
10250
|
+
currentRemainingMonths: z.number().int().describe("Months remaining on the current loan"),
|
|
10251
|
+
newRatePercent: z.number().describe("New (refinanced) loan's annual interest rate as a percent, e.g. 5.5"),
|
|
10252
|
+
newTermMonths: z.number().int().describe("New loan's term in months"),
|
|
10253
|
+
closingCosts: z.number().describe("Total closing costs to refinance")
|
|
10254
|
+
},
|
|
10255
|
+
async ({ currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts }) => {
|
|
10256
|
+
const result = _calculateLoanRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts);
|
|
10257
|
+
if ("error" in result) return errText(result);
|
|
10258
|
+
return text(JSON.stringify(result, null, 2));
|
|
10259
|
+
}
|
|
10260
|
+
);
|
|
10261
|
+
function _extractMultipartBoundaryMcp(contentType) {
|
|
10262
|
+
if (!contentType) return null;
|
|
10263
|
+
const match = contentType.match(/boundary=(?:"([^"]*)"|([^;\s]+))/i);
|
|
10264
|
+
if (!match) return null;
|
|
10265
|
+
return match[1] ?? match[2] ?? null;
|
|
10266
|
+
}
|
|
10267
|
+
function _parseMultipartFormDataMcp(body, boundary) {
|
|
10268
|
+
if (!body || !body.trim()) return { error: "Body is empty" };
|
|
10269
|
+
if (!boundary || !boundary.trim()) return { error: "Boundary is required" };
|
|
10270
|
+
const normalized = body.replace(/\r\n/g, "\n");
|
|
10271
|
+
const delimiter = `--${boundary}`;
|
|
10272
|
+
const segments = normalized.split(delimiter);
|
|
10273
|
+
if (segments.length < 3) {
|
|
10274
|
+
return { error: `No parts found for boundary "${boundary}"; check that it matches the boundary used in the body` };
|
|
10275
|
+
}
|
|
10276
|
+
const parts = [];
|
|
10277
|
+
for (let i = 1; i < segments.length; i++) {
|
|
10278
|
+
let segment = segments[i];
|
|
10279
|
+
if (segment.startsWith("--")) break;
|
|
10280
|
+
segment = segment.replace(/^\n/, "").replace(/\n$/, "");
|
|
10281
|
+
const headerEnd = segment.indexOf("\n\n");
|
|
10282
|
+
if (headerEnd === -1) continue;
|
|
10283
|
+
const headerBlock = segment.slice(0, headerEnd);
|
|
10284
|
+
const value = segment.slice(headerEnd + 2);
|
|
10285
|
+
const headers = {};
|
|
10286
|
+
for (const line of headerBlock.split("\n")) {
|
|
10287
|
+
const colon = line.indexOf(":");
|
|
10288
|
+
if (colon === -1) continue;
|
|
10289
|
+
headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
|
|
10290
|
+
}
|
|
10291
|
+
const disposition = headers["content-disposition"] ?? "";
|
|
10292
|
+
const nameMatch = disposition.match(/name="([^"]*)"/);
|
|
10293
|
+
const filenameMatch = disposition.match(/filename="([^"]*)"/);
|
|
10294
|
+
const partContentType = headers["content-type"] ?? "";
|
|
10295
|
+
const isBinary = !!filenameMatch && !partContentType.startsWith("text/") && !partContentType.includes("json") && !partContentType.includes("xml");
|
|
10296
|
+
parts.push({
|
|
10297
|
+
name: nameMatch ? nameMatch[1] : null,
|
|
10298
|
+
filename: filenameMatch ? filenameMatch[1] : null,
|
|
10299
|
+
headers,
|
|
10300
|
+
value,
|
|
10301
|
+
isBinary
|
|
10302
|
+
});
|
|
10303
|
+
}
|
|
10304
|
+
if (parts.length === 0) {
|
|
10305
|
+
return { error: `No parts found for boundary "${boundary}"; check that it matches the boundary used in the body` };
|
|
10306
|
+
}
|
|
10307
|
+
return { boundary, parts };
|
|
10308
|
+
}
|
|
10309
|
+
server.tool(
|
|
10310
|
+
"parse_multipart_form_data",
|
|
10311
|
+
"Parse a raw multipart/form-data request body into its individual parts: field name, filename (for file fields), headers, and decoded value, per RFC 7578. Give the boundary directly, or the request's Content-Type header value to extract it from automatically.",
|
|
10312
|
+
{
|
|
10313
|
+
body: z.string().min(1).describe("The raw multipart/form-data request body"),
|
|
10314
|
+
boundary: z.string().optional().describe("The multipart boundary (without the leading --), if known directly"),
|
|
10315
|
+
contentType: z.string().optional().describe("Alternative to boundary: the request's Content-Type header value, from which the boundary is extracted")
|
|
10316
|
+
},
|
|
10317
|
+
async ({ body, boundary, contentType }) => {
|
|
10318
|
+
const resolvedBoundary = boundary ?? (contentType ? _extractMultipartBoundaryMcp(contentType) : null);
|
|
10319
|
+
if (!resolvedBoundary) return errText({ error: "boundary is required (either directly, or via a contentType header containing a boundary parameter)" });
|
|
10320
|
+
const result = _parseMultipartFormDataMcp(body, resolvedBoundary);
|
|
10321
|
+
if ("error" in result) return errText(result);
|
|
10322
|
+
return text(JSON.stringify(result, null, 2));
|
|
10323
|
+
}
|
|
10324
|
+
);
|
|
10190
10325
|
var transport = new StdioServerTransport();
|
|
10191
10326
|
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.64.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",
|