@utilix-tech/mcp 0.59.0 → 0.61.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 +7 -0
- package/README.md +7 -3
- package/dist/index.js +157 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.61.0
|
|
6
|
+
- Add `calculate_extra_payment_payoff` tool: 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.
|
|
7
|
+
|
|
8
|
+
## 0.60.0
|
|
9
|
+
- Add `build_retry_after_header`, `parse_retry_after_header` tools: build an HTTP Retry-After response header from a delay in seconds or a target date, or parse an existing one, per RFC 9110.
|
|
10
|
+
- Add `calculate_freelancer_rate` tool: 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.
|
|
11
|
+
|
|
5
12
|
## 0.59.0
|
|
6
13
|
- Add `build_prefer_header`, `parse_prefer_header` tools: build an HTTP Prefer request-header value from preferences with optional values and parameters per RFC 7240, or parse an existing one into the same shape.
|
|
7
14
|
- Add `dedupe_ndjson` tool: remove duplicate records from NDJSON input, either by exact structural equality or by a chosen key field, keeping the first occurrence of each duplicate.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 212 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 (37 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -200,8 +200,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
200
200
|
| `calculate_credit_utilization` | Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance |
|
|
201
201
|
| `calculate_debt_to_income_ratio` | Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance |
|
|
202
202
|
| `calculate_blended_loan_rate` | Compute the single weighted-average interest rate across a list of loan balances, weighted by each loan's share of the combined balance, plus combined monthly and annual interest accrual |
|
|
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
|
+
| `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 |
|
|
203
205
|
|
|
204
|
-
### API / Network (
|
|
206
|
+
### API / Network (33 tools)
|
|
205
207
|
| Tool | Description |
|
|
206
208
|
|------|-------------|
|
|
207
209
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -237,6 +239,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
237
239
|
| `parse_accept_language` | Parse an existing Accept-Language header value into its ranked locale/quality pairs |
|
|
238
240
|
| `build_prefer_header` | Build an HTTP Prefer request-header value from preferences with optional values/params, per RFC 7240 |
|
|
239
241
|
| `parse_prefer_header` | Parse an existing Prefer header value into its list of preferences, values, and parameters |
|
|
242
|
+
| `build_retry_after_header` | Build a Retry-After response header from a delay in seconds or a target date, per RFC 9110 |
|
|
243
|
+
| `parse_retry_after_header` | Parse an existing Retry-After header value into the same shape |
|
|
240
244
|
|
|
241
245
|
### Color (4 tools)
|
|
242
246
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -2091,6 +2091,55 @@ server.tool(
|
|
|
2091
2091
|
return text(JSON.stringify(result, null, 2));
|
|
2092
2092
|
}
|
|
2093
2093
|
);
|
|
2094
|
+
function _calculateExtraPaymentPayoff(principal, annualRatePercent, termMonths, extraMonthlyPayment) {
|
|
2095
|
+
const baseline = _calculateLoan(principal, annualRatePercent, termMonths);
|
|
2096
|
+
if ("error" in baseline) return baseline;
|
|
2097
|
+
if (!Number.isFinite(extraMonthlyPayment) || extraMonthlyPayment < 0) {
|
|
2098
|
+
return { error: "Extra monthly payment must be zero or a positive number" };
|
|
2099
|
+
}
|
|
2100
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
2101
|
+
const payment = _roundMoneyMcp(baseline.monthlyPayment) + _roundMoneyMcp(extraMonthlyPayment);
|
|
2102
|
+
let balance = _roundMoneyMcp(principal);
|
|
2103
|
+
let totalInterestPaid = 0;
|
|
2104
|
+
let payoffMonths = termMonths;
|
|
2105
|
+
for (let month = 1; month <= termMonths; month++) {
|
|
2106
|
+
const interest = monthlyRate === 0 ? 0 : _roundMoneyMcp(balance * monthlyRate);
|
|
2107
|
+
let principalPortion = payment - interest;
|
|
2108
|
+
if (principalPortion > balance) principalPortion = balance;
|
|
2109
|
+
totalInterestPaid += interest;
|
|
2110
|
+
balance = _roundMoneyMcp(balance - principalPortion);
|
|
2111
|
+
if (balance <= 0) {
|
|
2112
|
+
payoffMonths = month;
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
const baselineTotalInterest = baseline.totalInterest;
|
|
2117
|
+
return {
|
|
2118
|
+
monthlyPayment: _roundMoneyMcp(baseline.monthlyPayment),
|
|
2119
|
+
extraMonthlyPayment: _roundMoneyMcp(extraMonthlyPayment),
|
|
2120
|
+
baselinePayoffMonths: termMonths,
|
|
2121
|
+
baselineTotalInterest,
|
|
2122
|
+
newPayoffMonths: payoffMonths,
|
|
2123
|
+
newTotalInterest: _roundMoneyMcp(totalInterestPaid),
|
|
2124
|
+
monthsSaved: termMonths - payoffMonths,
|
|
2125
|
+
interestSaved: _roundMoneyMcp(baselineTotalInterest - totalInterestPaid)
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
server.tool(
|
|
2129
|
+
"calculate_extra_payment_payoff",
|
|
2130
|
+
"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 on top of the standard amortized payment.",
|
|
2131
|
+
{
|
|
2132
|
+
principal: z.number().describe("Loan principal amount"),
|
|
2133
|
+
annualRatePercent: z.number().describe("Annual interest rate as a percent, e.g. 6.5"),
|
|
2134
|
+
termMonths: z.number().int().describe("Original loan term in months"),
|
|
2135
|
+
extraMonthlyPayment: z.number().describe("Fixed extra amount added to every monthly payment")
|
|
2136
|
+
},
|
|
2137
|
+
async ({ principal, annualRatePercent, termMonths, extraMonthlyPayment }) => {
|
|
2138
|
+
const result = _calculateExtraPaymentPayoff(principal, annualRatePercent, termMonths, extraMonthlyPayment);
|
|
2139
|
+
if ("error" in result) return errText(result);
|
|
2140
|
+
return text(JSON.stringify(result, null, 2));
|
|
2141
|
+
}
|
|
2142
|
+
);
|
|
2094
2143
|
var _BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
|
|
2095
2144
|
function _computeBarcodeCheckDigit(dataDigits) {
|
|
2096
2145
|
let sum = 0;
|
|
@@ -9692,5 +9741,113 @@ server.tool(
|
|
|
9692
9741
|
}, null, 2));
|
|
9693
9742
|
}
|
|
9694
9743
|
);
|
|
9744
|
+
function _buildRetryAfterHeaderMcp(input) {
|
|
9745
|
+
const { seconds, date } = input;
|
|
9746
|
+
const hasSeconds = seconds !== void 0;
|
|
9747
|
+
const hasDate = date !== void 0 && date !== "";
|
|
9748
|
+
if (hasSeconds && hasDate) return { error: "Provide either seconds or date, not both" };
|
|
9749
|
+
if (!hasSeconds && !hasDate) return { error: "seconds or date is required" };
|
|
9750
|
+
if (hasSeconds) {
|
|
9751
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || !Number.isInteger(seconds) || seconds < 0) {
|
|
9752
|
+
return { error: "seconds must be a non-negative integer" };
|
|
9753
|
+
}
|
|
9754
|
+
return { header: `Retry-After: ${seconds}`, headerValue: String(seconds), type: "seconds", seconds };
|
|
9755
|
+
}
|
|
9756
|
+
const parsed = new Date(date);
|
|
9757
|
+
if (Number.isNaN(parsed.getTime())) return { error: "date must be a valid date/time string" };
|
|
9758
|
+
const httpDate = parsed.toUTCString();
|
|
9759
|
+
return { header: `Retry-After: ${httpDate}`, headerValue: httpDate, type: "http-date", httpDate, iso: parsed.toISOString() };
|
|
9760
|
+
}
|
|
9761
|
+
function _parseRetryAfterHeaderMcp(raw) {
|
|
9762
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
9763
|
+
const value = raw.trim().replace(/^retry-after:\s*/i, "").trim();
|
|
9764
|
+
if (value === "") return { error: "Input is empty" };
|
|
9765
|
+
if (/^\d+$/.test(value)) {
|
|
9766
|
+
const seconds = parseInt(value, 10);
|
|
9767
|
+
return { header: `Retry-After: ${seconds}`, headerValue: String(seconds), type: "seconds", seconds };
|
|
9768
|
+
}
|
|
9769
|
+
const parsed = new Date(value);
|
|
9770
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
9771
|
+
return { error: "Value is neither a non-negative integer (seconds) nor a valid HTTP-date" };
|
|
9772
|
+
}
|
|
9773
|
+
const httpDate = parsed.toUTCString();
|
|
9774
|
+
return { header: `Retry-After: ${httpDate}`, headerValue: httpDate, type: "http-date", httpDate, iso: parsed.toISOString() };
|
|
9775
|
+
}
|
|
9776
|
+
server.tool(
|
|
9777
|
+
"build_retry_after_header",
|
|
9778
|
+
'Build a Retry-After response header value from either a non-negative delay in seconds or a target date/time, per RFC 9110 Section 10.2.3. A date is formatted as an HTTP-date (IMF-fixdate, e.g. "Wed, 21 Oct 2026 07:28:00 GMT"), the only date format the header allows.',
|
|
9779
|
+
{
|
|
9780
|
+
seconds: z.number().int().min(0).optional().describe("A non-negative delay in seconds (required unless date is given)"),
|
|
9781
|
+
date: z.string().optional().describe("A target date/time string to format as an HTTP-date (required unless seconds is given)")
|
|
9782
|
+
},
|
|
9783
|
+
async ({ seconds, date }) => {
|
|
9784
|
+
const result = _buildRetryAfterHeaderMcp({ seconds, date });
|
|
9785
|
+
if ("error" in result) return errText(result);
|
|
9786
|
+
return text(JSON.stringify(result, null, 2));
|
|
9787
|
+
}
|
|
9788
|
+
);
|
|
9789
|
+
server.tool(
|
|
9790
|
+
"parse_retry_after_header",
|
|
9791
|
+
'Parse an existing Retry-After header value (with or without a leading "Retry-After:" label) into either its delay-in-seconds form or its HTTP-date form, per RFC 9110 Section 10.2.3.',
|
|
9792
|
+
{
|
|
9793
|
+
header: z.string().min(1).describe("The Retry-After header value to parse")
|
|
9794
|
+
},
|
|
9795
|
+
async ({ header }) => {
|
|
9796
|
+
const result = _parseRetryAfterHeaderMcp(header);
|
|
9797
|
+
if ("error" in result) return errText(result);
|
|
9798
|
+
return text(JSON.stringify(result, null, 2));
|
|
9799
|
+
}
|
|
9800
|
+
);
|
|
9801
|
+
function _calculateFreelancerRateMcp(input) {
|
|
9802
|
+
const { targetAnnualIncome, billableHoursPerWeek, weeksPerYear } = input;
|
|
9803
|
+
const annualOverheadCosts = input.annualOverheadCosts ?? 0;
|
|
9804
|
+
const profitMarginPercent = input.profitMarginPercent ?? 0;
|
|
9805
|
+
if (!Number.isFinite(targetAnnualIncome) || targetAnnualIncome < 0) {
|
|
9806
|
+
return { error: "targetAnnualIncome must be a finite, non-negative number" };
|
|
9807
|
+
}
|
|
9808
|
+
if (!Number.isFinite(billableHoursPerWeek) || billableHoursPerWeek <= 0 || billableHoursPerWeek > 168) {
|
|
9809
|
+
return { error: "billableHoursPerWeek must be a finite number greater than zero and at most 168" };
|
|
9810
|
+
}
|
|
9811
|
+
if (!Number.isFinite(weeksPerYear) || weeksPerYear <= 0 || weeksPerYear > 52) {
|
|
9812
|
+
return { error: "weeksPerYear must be a finite number greater than zero and at most 52" };
|
|
9813
|
+
}
|
|
9814
|
+
if (!Number.isFinite(annualOverheadCosts) || annualOverheadCosts < 0) {
|
|
9815
|
+
return { error: "annualOverheadCosts must be a finite, non-negative number" };
|
|
9816
|
+
}
|
|
9817
|
+
if (!Number.isFinite(profitMarginPercent) || profitMarginPercent < 0) {
|
|
9818
|
+
return { error: "profitMarginPercent must be a finite, non-negative number" };
|
|
9819
|
+
}
|
|
9820
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
9821
|
+
const annualBillableHours = billableHoursPerWeek * weeksPerYear;
|
|
9822
|
+
const baseRevenueNeeded = targetAnnualIncome + annualOverheadCosts;
|
|
9823
|
+
const requiredAnnualRevenue = baseRevenueNeeded * (1 + profitMarginPercent / 100);
|
|
9824
|
+
const hourlyRate = requiredAnnualRevenue / annualBillableHours;
|
|
9825
|
+
return {
|
|
9826
|
+
targetAnnualIncome: round2(targetAnnualIncome),
|
|
9827
|
+
annualOverheadCosts: round2(annualOverheadCosts),
|
|
9828
|
+
profitMarginPercent: round2(profitMarginPercent),
|
|
9829
|
+
annualBillableHours: round2(annualBillableHours),
|
|
9830
|
+
requiredAnnualRevenue: round2(requiredAnnualRevenue),
|
|
9831
|
+
hourlyRate: round2(hourlyRate),
|
|
9832
|
+
dailyRate: round2(hourlyRate * 8),
|
|
9833
|
+
monthlyRevenueTarget: round2(requiredAnnualRevenue / 12)
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
server.tool(
|
|
9837
|
+
"calculate_freelancer_rate",
|
|
9838
|
+
"Solve for the minimum hourly rate a freelancer must charge to hit a target annual take-home income, given billable hours per week, weeks actually worked per year, annual business overhead, and an optional profit margin/buffer applied on top.",
|
|
9839
|
+
{
|
|
9840
|
+
targetAnnualIncome: z.number().min(0).describe("Desired annual take-home income"),
|
|
9841
|
+
billableHoursPerWeek: z.number().gt(0).max(168).describe("Billable hours worked per week"),
|
|
9842
|
+
weeksPerYear: z.number().gt(0).max(52).describe("Weeks actually worked per year, after vacation/holidays"),
|
|
9843
|
+
annualOverheadCosts: z.number().min(0).optional().describe("Annual business expenses (software, insurance, etc.), default 0"),
|
|
9844
|
+
profitMarginPercent: z.number().min(0).optional().describe("Extra buffer/profit margin percent applied on top of income plus overhead, default 0")
|
|
9845
|
+
},
|
|
9846
|
+
async ({ targetAnnualIncome, billableHoursPerWeek, weeksPerYear, annualOverheadCosts, profitMarginPercent }) => {
|
|
9847
|
+
const result = _calculateFreelancerRateMcp({ targetAnnualIncome, billableHoursPerWeek, weeksPerYear, annualOverheadCosts, profitMarginPercent });
|
|
9848
|
+
if ("error" in result) return errText(result);
|
|
9849
|
+
return text(JSON.stringify(result, null, 2));
|
|
9850
|
+
}
|
|
9851
|
+
);
|
|
9695
9852
|
var transport = new StdioServerTransport();
|
|
9696
9853
|
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.61.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",
|