@utilix-tech/mcp 0.61.0 → 0.63.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 +10 -4
- package/dist/index.js +338 -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.63.0
|
|
6
|
+
- 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
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.62.0
|
|
10
|
+
- Add `build_priority_header`, `parse_priority_header` tools: build an HTTP Priority request/response-header value from an urgency (0-7) and/or an incremental flag per RFC 9218, or parse an existing one into the same shape.
|
|
11
|
+
- Add `lookup_close_code`, `list_close_codes` tools: look up what a WebSocket close code means per RFC 6455 and the IANA reserved ranges, or list every well-known code.
|
|
12
|
+
|
|
5
13
|
## 0.61.0
|
|
6
14
|
- 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
15
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 218 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 (38 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -202,8 +202,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
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
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
|
+
| `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 |
|
|
205
206
|
|
|
206
|
-
### API / Network (
|
|
207
|
+
### API / Network (37 tools)
|
|
207
208
|
| Tool | Description |
|
|
208
209
|
|------|-------------|
|
|
209
210
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -241,6 +242,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
241
242
|
| `parse_prefer_header` | Parse an existing Prefer header value into its list of preferences, values, and parameters |
|
|
242
243
|
| `build_retry_after_header` | Build a Retry-After response header from a delay in seconds or a target date, per RFC 9110 |
|
|
243
244
|
| `parse_retry_after_header` | Parse an existing Retry-After header value into the same shape |
|
|
245
|
+
| `build_priority_header` | Build an HTTP Priority request/response-header value from an urgency (0-7) and/or an incremental flag, per RFC 9218 |
|
|
246
|
+
| `parse_priority_header` | Parse an existing Priority header value into its urgency and incremental fields, with spec defaults applied |
|
|
247
|
+
| `lookup_close_code` | Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges |
|
|
248
|
+
| `list_close_codes` | List every well-known WebSocket close code (1000-1015) |
|
|
244
249
|
|
|
245
250
|
### Color (4 tools)
|
|
246
251
|
| Tool | Description |
|
|
@@ -250,7 +255,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
250
255
|
| `color_palette` | Generate a color palette from a base color |
|
|
251
256
|
| `color_shades` | Generate shades or tints from a base color |
|
|
252
257
|
|
|
253
|
-
### Code (
|
|
258
|
+
### Code (16 tools)
|
|
254
259
|
| Tool | Description |
|
|
255
260
|
|------|-------------|
|
|
256
261
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -268,6 +273,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
268
273
|
| `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
|
|
269
274
|
| `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
|
|
270
275
|
| `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
|
|
276
|
+
| `audit_dependency_ranges` | Classify each package.json dependency's version range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections |
|
|
271
277
|
|
|
272
278
|
### Data (14 tools)
|
|
273
279
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -9849,5 +9849,343 @@ server.tool(
|
|
|
9849
9849
|
return text(JSON.stringify(result, null, 2));
|
|
9850
9850
|
}
|
|
9851
9851
|
);
|
|
9852
|
+
function _isValidUrgencyMcp(n) {
|
|
9853
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 7;
|
|
9854
|
+
}
|
|
9855
|
+
function _buildPriorityHeaderMcp(input) {
|
|
9856
|
+
const { urgency, incremental } = input;
|
|
9857
|
+
if (urgency === void 0 && incremental === void 0) {
|
|
9858
|
+
return { error: "Provide at least one of urgency or incremental" };
|
|
9859
|
+
}
|
|
9860
|
+
if (urgency !== void 0 && !_isValidUrgencyMcp(urgency)) {
|
|
9861
|
+
return { error: "urgency must be an integer between 0 and 7" };
|
|
9862
|
+
}
|
|
9863
|
+
const parts = [];
|
|
9864
|
+
if (urgency !== void 0) parts.push(`u=${urgency}`);
|
|
9865
|
+
if (incremental === true) parts.push("i");
|
|
9866
|
+
else if (incremental === false) parts.push("i=?0");
|
|
9867
|
+
const headerValue = parts.join(", ");
|
|
9868
|
+
return { header: `Priority: ${headerValue}`, headerValue, urgency: urgency ?? 3, incremental: incremental ?? false };
|
|
9869
|
+
}
|
|
9870
|
+
function _parsePriorityHeaderMcp(raw) {
|
|
9871
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
9872
|
+
const withoutLabel = raw.trim().replace(/^priority:\s*/i, "");
|
|
9873
|
+
const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
9874
|
+
if (parts.length === 0) return { error: "Input is empty" };
|
|
9875
|
+
let urgency = 3;
|
|
9876
|
+
let incremental = false;
|
|
9877
|
+
for (const part of parts) {
|
|
9878
|
+
const eq = part.indexOf("=");
|
|
9879
|
+
const key = (eq === -1 ? part : part.slice(0, eq)).trim();
|
|
9880
|
+
const rawValue = eq === -1 ? void 0 : part.slice(eq + 1).trim();
|
|
9881
|
+
if (key === "u") {
|
|
9882
|
+
if (rawValue === void 0 || !/^\d+$/.test(rawValue)) return { error: `Invalid urgency value in "${part}"` };
|
|
9883
|
+
const n = Number(rawValue);
|
|
9884
|
+
if (n < 0 || n > 7) return { error: `urgency must be between 0 and 7, got ${n}` };
|
|
9885
|
+
urgency = n;
|
|
9886
|
+
} else if (key === "i") {
|
|
9887
|
+
if (rawValue === void 0 || rawValue === "?1") incremental = true;
|
|
9888
|
+
else if (rawValue === "?0") incremental = false;
|
|
9889
|
+
else return { error: `Invalid incremental value in "${part}"` };
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
const headerValue = incremental ? `u=${urgency}, i` : `u=${urgency}, i=?0`;
|
|
9893
|
+
return { header: `Priority: ${headerValue}`, headerValue, urgency, incremental };
|
|
9894
|
+
}
|
|
9895
|
+
server.tool(
|
|
9896
|
+
"build_priority_header",
|
|
9897
|
+
"Build a Priority request/response-header value from an urgency (0-7, default 3) and/or an incremental flag, per RFC 9218. At least one of the two must be given.",
|
|
9898
|
+
{
|
|
9899
|
+
urgency: z.number().int().min(0).max(7).optional().describe("Urgency from 0 (highest) to 7 (lowest), default 3"),
|
|
9900
|
+
incremental: z.boolean().optional().describe("Whether the response can be processed incrementally as it arrives, default false")
|
|
9901
|
+
},
|
|
9902
|
+
async ({ urgency, incremental }) => {
|
|
9903
|
+
const result = _buildPriorityHeaderMcp({ urgency, incremental });
|
|
9904
|
+
if ("error" in result) return errText(result);
|
|
9905
|
+
return text(JSON.stringify(result, null, 2));
|
|
9906
|
+
}
|
|
9907
|
+
);
|
|
9908
|
+
server.tool(
|
|
9909
|
+
"parse_priority_header",
|
|
9910
|
+
'Parse an existing Priority header value (with or without a leading "Priority:" label) into its urgency and incremental fields, per RFC 9218. Missing fields fall back to their spec default (u=3, i=false).',
|
|
9911
|
+
{
|
|
9912
|
+
header: z.string().min(1).describe("The Priority header value to parse")
|
|
9913
|
+
},
|
|
9914
|
+
async ({ header }) => {
|
|
9915
|
+
const result = _parsePriorityHeaderMcp(header);
|
|
9916
|
+
if ("error" in result) return errText(result);
|
|
9917
|
+
return text(JSON.stringify(result, null, 2));
|
|
9918
|
+
}
|
|
9919
|
+
);
|
|
9920
|
+
var _WELL_KNOWN_CLOSE_CODES_MCP = {
|
|
9921
|
+
1e3: { name: "Normal Closure", description: "The connection successfully completed the purpose for which it was created.", sentOnWire: true },
|
|
9922
|
+
1001: { name: "Going Away", description: "An endpoint is going away, such as a server shutting down or a browser navigating away from the page.", sentOnWire: true },
|
|
9923
|
+
1002: { name: "Protocol Error", description: "An endpoint is terminating the connection due to a protocol error.", sentOnWire: true },
|
|
9924
|
+
1003: { name: "Unsupported Data", description: "An endpoint received a data type it cannot accept, such as a binary-only endpoint receiving a text message.", sentOnWire: true },
|
|
9925
|
+
1004: { name: "Reserved", description: "Reserved. A meaning might be defined in the future.", sentOnWire: true },
|
|
9926
|
+
1005: { name: "No Status Received", description: "A reserved value used internally by libraries to indicate no status code was actually present in the close frame. Must not be sent on the wire.", sentOnWire: false },
|
|
9927
|
+
1006: { name: "Abnormal Closure", description: "A reserved value used internally by libraries to indicate the connection closed without a close frame, e.g. the TCP connection dropped. Must not be sent on the wire.", sentOnWire: false },
|
|
9928
|
+
1007: { name: "Invalid frame payload data", description: "An endpoint is terminating the connection because it received data inconsistent with the message type, such as non-UTF-8 data in a text message.", sentOnWire: true },
|
|
9929
|
+
1008: { name: "Policy Violation", description: "A generic status code used when no other code fits and the endpoint does not want to reveal further details, or a message violated its policy.", sentOnWire: true },
|
|
9930
|
+
1009: { name: "Message Too Big", description: "An endpoint is terminating the connection because it received a message too large to process.", sentOnWire: true },
|
|
9931
|
+
1010: { name: "Mandatory Extension", description: "A client is terminating the connection because the server did not negotiate one or more extensions the client required.", sentOnWire: true },
|
|
9932
|
+
1011: { name: "Internal Error", description: "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.", sentOnWire: true },
|
|
9933
|
+
1012: { name: "Service Restart", description: "The server is terminating the connection because it is restarting.", sentOnWire: true },
|
|
9934
|
+
1013: { name: "Try Again Later", description: "The server is terminating the connection due to a temporary condition, such as being overloaded, and the client should reconnect later.", sentOnWire: true },
|
|
9935
|
+
1014: { name: "Bad Gateway", description: "The server, acting as a gateway or proxy, received an invalid response from an upstream server it connected to while processing the request.", sentOnWire: true },
|
|
9936
|
+
1015: { name: "TLS Handshake", description: "A reserved value used internally by libraries to indicate the TLS handshake failed. Must not be sent on the wire.", sentOnWire: false }
|
|
9937
|
+
};
|
|
9938
|
+
function _lookupCloseCodeMcp(code) {
|
|
9939
|
+
if (!Number.isInteger(code) || code < 0 || code > 65535) {
|
|
9940
|
+
return { error: "code must be an integer between 0 and 65535" };
|
|
9941
|
+
}
|
|
9942
|
+
const known = _WELL_KNOWN_CLOSE_CODES_MCP[code];
|
|
9943
|
+
if (known) return { code, name: known.name, description: known.description, range: "standard", sentOnWire: known.sentOnWire };
|
|
9944
|
+
if (code < 1e3) return { error: `${code} is not a valid WebSocket close code; codes below 1000 are not permitted on the wire` };
|
|
9945
|
+
if (code <= 2999) return { code, name: "Unassigned", description: "Reserved for future use by the WebSocket protocol specification. No meaning is defined yet.", range: "reserved-future", sentOnWire: true };
|
|
9946
|
+
if (code <= 3999) return { code, name: "Library/Framework Use", description: "Reserved for use by libraries, frameworks, and applications. Codes in this range are meant to be registered directly with IANA in the WebSocket Close Code Number Registry.", range: "library-framework", sentOnWire: true };
|
|
9947
|
+
if (code <= 4999) return { code, name: "Private Use", description: "Reserved for private use between a specific client and server implementation. These codes cannot be registered with IANA.", range: "private-use", sentOnWire: true };
|
|
9948
|
+
return { error: `${code} is outside the defined close code ranges (0-4999); codes 5000 and above have no assigned meaning` };
|
|
9949
|
+
}
|
|
9950
|
+
server.tool(
|
|
9951
|
+
"lookup_close_code",
|
|
9952
|
+
"Look up what a WebSocket close code means, per the RFC 6455 close code definitions and the IANA WebSocket Close Code Number Registry reserved ranges.",
|
|
9953
|
+
{
|
|
9954
|
+
code: z.number().int().min(0).max(65535).describe("The WebSocket close code to look up")
|
|
9955
|
+
},
|
|
9956
|
+
async ({ code }) => {
|
|
9957
|
+
const result = _lookupCloseCodeMcp(code);
|
|
9958
|
+
if ("error" in result) return errText(result);
|
|
9959
|
+
return text(JSON.stringify(result, null, 2));
|
|
9960
|
+
}
|
|
9961
|
+
);
|
|
9962
|
+
server.tool(
|
|
9963
|
+
"list_close_codes",
|
|
9964
|
+
"List every well-known WebSocket close code (1000-1015) defined directly by RFC 6455, in ascending order.",
|
|
9965
|
+
{},
|
|
9966
|
+
async () => {
|
|
9967
|
+
const codes = Object.entries(_WELL_KNOWN_CLOSE_CODES_MCP).map(([code, info]) => ({ code: Number(code), name: info.name, description: info.description, range: "standard", sentOnWire: info.sentOnWire })).sort((a, b) => a.code - b.code);
|
|
9968
|
+
return text(JSON.stringify({ codes }, null, 2));
|
|
9969
|
+
}
|
|
9970
|
+
);
|
|
9971
|
+
var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
|
|
9972
|
+
function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
|
|
9973
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
9974
|
+
const balances = debts.map((d) => d.balance);
|
|
9975
|
+
const totalMinPayment = debts.reduce((sum, d) => sum + d.minPayment, 0);
|
|
9976
|
+
const totalBudget = totalMinPayment + extraMonthlyPayment;
|
|
9977
|
+
let totalInterestPaid = 0;
|
|
9978
|
+
let totalPaid = 0;
|
|
9979
|
+
const schedule = [];
|
|
9980
|
+
let month = 0;
|
|
9981
|
+
while (balances.some((b) => b > 5e-3) && month < _DEBT_PAYOFF_MAX_MONTHS_MCP) {
|
|
9982
|
+
month++;
|
|
9983
|
+
for (let i = 0; i < balances.length; i++) {
|
|
9984
|
+
if (balances[i] <= 0) continue;
|
|
9985
|
+
const interest = balances[i] * debts[i].ratePercent / 100 / 12;
|
|
9986
|
+
balances[i] += interest;
|
|
9987
|
+
totalInterestPaid += interest;
|
|
9988
|
+
}
|
|
9989
|
+
let remainingBudget = totalBudget;
|
|
9990
|
+
for (let i = 0; i < balances.length; i++) {
|
|
9991
|
+
if (balances[i] <= 0) continue;
|
|
9992
|
+
const pay = Math.min(balances[i], debts[i].minPayment);
|
|
9993
|
+
balances[i] -= pay;
|
|
9994
|
+
remainingBudget -= pay;
|
|
9995
|
+
totalPaid += pay;
|
|
9996
|
+
}
|
|
9997
|
+
for (const i of order) {
|
|
9998
|
+
if (remainingBudget <= 0) break;
|
|
9999
|
+
if (balances[i] <= 0) continue;
|
|
10000
|
+
const pay = Math.min(balances[i], remainingBudget);
|
|
10001
|
+
balances[i] -= pay;
|
|
10002
|
+
remainingBudget -= pay;
|
|
10003
|
+
totalPaid += pay;
|
|
10004
|
+
}
|
|
10005
|
+
for (const i of order) {
|
|
10006
|
+
if (balances[i] <= 5e-3 && !schedule.some((s) => s.label === debts[i].label)) {
|
|
10007
|
+
schedule.push({ label: debts[i].label, monthPaidOff: month });
|
|
10008
|
+
}
|
|
10009
|
+
}
|
|
10010
|
+
}
|
|
10011
|
+
if (balances.some((b) => b > 5e-3)) return null;
|
|
10012
|
+
return {
|
|
10013
|
+
order: order.map((i) => debts[i].label),
|
|
10014
|
+
monthsToPayoff: month,
|
|
10015
|
+
totalInterestPaid: round2(totalInterestPaid),
|
|
10016
|
+
totalPaid: round2(totalPaid),
|
|
10017
|
+
schedule
|
|
10018
|
+
};
|
|
10019
|
+
}
|
|
10020
|
+
function _planDebtPayoffMcp(debtsInput, extraMonthlyPayment) {
|
|
10021
|
+
if (!Array.isArray(debtsInput) || debtsInput.length === 0) {
|
|
10022
|
+
return { error: "debts must be a non-empty array" };
|
|
10023
|
+
}
|
|
10024
|
+
if (typeof extraMonthlyPayment !== "number" || !Number.isFinite(extraMonthlyPayment) || extraMonthlyPayment < 0) {
|
|
10025
|
+
return { error: "extraMonthlyPayment must be a finite, non-negative number" };
|
|
10026
|
+
}
|
|
10027
|
+
const normalized = [];
|
|
10028
|
+
for (let i = 0; i < debtsInput.length; i++) {
|
|
10029
|
+
const d = debtsInput[i];
|
|
10030
|
+
if (typeof d.balance !== "number" || !Number.isFinite(d.balance) || d.balance <= 0) {
|
|
10031
|
+
return { error: `debt ${i + 1}: balance must be a finite number greater than zero` };
|
|
10032
|
+
}
|
|
10033
|
+
if (typeof d.ratePercent !== "number" || !Number.isFinite(d.ratePercent) || d.ratePercent < 0) {
|
|
10034
|
+
return { error: `debt ${i + 1}: ratePercent must be a finite, non-negative number` };
|
|
10035
|
+
}
|
|
10036
|
+
if (typeof d.minPayment !== "number" || !Number.isFinite(d.minPayment) || d.minPayment <= 0) {
|
|
10037
|
+
return { error: `debt ${i + 1}: minPayment must be a finite number greater than zero` };
|
|
10038
|
+
}
|
|
10039
|
+
normalized.push({ label: d.label?.trim() || `Debt ${i + 1}`, balance: d.balance, ratePercent: d.ratePercent, minPayment: d.minPayment });
|
|
10040
|
+
}
|
|
10041
|
+
const snowballOrder = normalized.map((_, i) => i).sort((a, b) => normalized[a].balance - normalized[b].balance);
|
|
10042
|
+
const avalancheOrder = normalized.map((_, i) => i).sort((a, b) => normalized[b].ratePercent - normalized[a].ratePercent);
|
|
10043
|
+
const snowball = _simulateDebtPayoffMcp(normalized, snowballOrder, extraMonthlyPayment);
|
|
10044
|
+
const avalanche = _simulateDebtPayoffMcp(normalized, avalancheOrder, extraMonthlyPayment);
|
|
10045
|
+
if (!snowball || !avalanche) {
|
|
10046
|
+
return { error: `debts cannot be paid off within ${_DEBT_PAYOFF_MAX_MONTHS_MCP} months at this payment level; increase a minimum payment or the extra monthly payment` };
|
|
10047
|
+
}
|
|
10048
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
10049
|
+
const snowballMonths = snowball.monthsToPayoff;
|
|
10050
|
+
const avalancheMonths = avalanche.monthsToPayoff;
|
|
10051
|
+
const snowballInterest = snowball.totalInterestPaid;
|
|
10052
|
+
const avalancheInterest = avalanche.totalInterestPaid;
|
|
10053
|
+
const fasterStrategy = snowballMonths === avalancheMonths ? "tie" : snowballMonths < avalancheMonths ? "snowball" : "avalanche";
|
|
10054
|
+
const cheaperStrategy = snowballInterest === avalancheInterest ? "tie" : snowballInterest < avalancheInterest ? "snowball" : "avalanche";
|
|
10055
|
+
return {
|
|
10056
|
+
debts: normalized,
|
|
10057
|
+
totalMonthlyBudget: round2(normalized.reduce((sum, d) => sum + d.minPayment, 0) + extraMonthlyPayment),
|
|
10058
|
+
extraMonthlyPayment,
|
|
10059
|
+
snowball,
|
|
10060
|
+
avalanche,
|
|
10061
|
+
comparison: {
|
|
10062
|
+
fasterStrategy,
|
|
10063
|
+
monthsDifference: Math.abs(snowballMonths - avalancheMonths),
|
|
10064
|
+
cheaperStrategy,
|
|
10065
|
+
interestDifference: round2(Math.abs(snowballInterest - avalancheInterest))
|
|
10066
|
+
}
|
|
10067
|
+
};
|
|
10068
|
+
}
|
|
10069
|
+
server.tool(
|
|
10070
|
+
"plan_debt_payoff",
|
|
10071
|
+
"Simulate paying off a list of debts under both the snowball strategy (smallest balance first) and the avalanche strategy (highest interest rate first), given a fixed extra monthly payment stacked on top of everyone's minimum payment, and compare months to payoff and total interest between the two.",
|
|
10072
|
+
{
|
|
10073
|
+
debts: z.array(z.object({
|
|
10074
|
+
label: z.string().optional().describe("Optional debt label"),
|
|
10075
|
+
balance: z.number().positive().describe("Current balance owed"),
|
|
10076
|
+
ratePercent: z.number().min(0).describe("Annual interest rate for this debt, as a percent"),
|
|
10077
|
+
minPayment: z.number().positive().describe("Minimum monthly payment required on this debt")
|
|
10078
|
+
})).min(1).describe("A non-empty list of debts"),
|
|
10079
|
+
extraMonthlyPayment: z.number().min(0).optional().describe("Extra amount paid each month on top of every debt's minimum payment, default 0")
|
|
10080
|
+
},
|
|
10081
|
+
async ({ debts, extraMonthlyPayment }) => {
|
|
10082
|
+
const result = _planDebtPayoffMcp(debts, extraMonthlyPayment ?? 0);
|
|
10083
|
+
if ("error" in result) return errText(result);
|
|
10084
|
+
return text(JSON.stringify(result, null, 2));
|
|
10085
|
+
}
|
|
10086
|
+
);
|
|
10087
|
+
var _DEPENDENCY_SECTIONS_MCP = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
10088
|
+
var _EXACT_SEMVER_RE_MCP = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
10089
|
+
function _classifyDependencyRangeMcp(version) {
|
|
10090
|
+
const v = version.trim();
|
|
10091
|
+
if (v === "" || v === "*" || v.toLowerCase() === "x") return "wildcard";
|
|
10092
|
+
if (v.startsWith("workspace:")) return "workspace";
|
|
10093
|
+
if (v.startsWith("file:")) return "file";
|
|
10094
|
+
if (v.startsWith("link:")) return "link";
|
|
10095
|
+
if (v.startsWith("http://") || v.startsWith("https://")) return "url";
|
|
10096
|
+
if (v.startsWith("git+") || v.startsWith("git://") || v.startsWith("github:") || v.endsWith(".git") || /^[\w-]+\/[\w.-]+$/.test(v)) return "git";
|
|
10097
|
+
if (v.startsWith("^")) return "caret";
|
|
10098
|
+
if (v.startsWith("~")) return "tilde";
|
|
10099
|
+
if (v.includes("||") || v.includes(" ") || v.startsWith(">") || v.startsWith("<") || v.startsWith("=")) return "range";
|
|
10100
|
+
if (_EXACT_SEMVER_RE_MCP.test(v)) return "exact";
|
|
10101
|
+
if (/^[A-Za-z][\w.-]*$/.test(v)) return "tag";
|
|
10102
|
+
return "unknown";
|
|
10103
|
+
}
|
|
10104
|
+
function _auditDependencyRangesMcp(packageJsonText) {
|
|
10105
|
+
if (!packageJsonText || !packageJsonText.trim()) return { error: "Input is empty" };
|
|
10106
|
+
let parsed;
|
|
10107
|
+
try {
|
|
10108
|
+
parsed = JSON.parse(packageJsonText);
|
|
10109
|
+
} catch (e) {
|
|
10110
|
+
return { error: `Invalid JSON: ${e.message}` };
|
|
10111
|
+
}
|
|
10112
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10113
|
+
return { error: "Root of package.json must be an object" };
|
|
10114
|
+
}
|
|
10115
|
+
const obj = parsed;
|
|
10116
|
+
const presentSections = _DEPENDENCY_SECTIONS_MCP.filter((s) => obj[s] && typeof obj[s] === "object");
|
|
10117
|
+
if (presentSections.length === 0) {
|
|
10118
|
+
return { error: "package.json has no dependencies, devDependencies, peerDependencies, or optionalDependencies field" };
|
|
10119
|
+
}
|
|
10120
|
+
const dependencies = [];
|
|
10121
|
+
const issues = [];
|
|
10122
|
+
const byName = /* @__PURE__ */ new Map();
|
|
10123
|
+
for (const section of presentSections) {
|
|
10124
|
+
const map = obj[section];
|
|
10125
|
+
for (const [name, versionRaw] of Object.entries(map)) {
|
|
10126
|
+
const version = typeof versionRaw === "string" ? versionRaw : String(versionRaw);
|
|
10127
|
+
const rangeType = _classifyDependencyRangeMcp(version);
|
|
10128
|
+
const entryIssues = [];
|
|
10129
|
+
if (rangeType === "wildcard") {
|
|
10130
|
+
entryIssues.push("no real version constraint; any version, including breaking ones, could be installed");
|
|
10131
|
+
}
|
|
10132
|
+
if (rangeType === "tag" && version.trim().toLowerCase() === "latest") {
|
|
10133
|
+
entryIssues.push('"latest" is not reproducible; a fresh install can pull in a different, potentially breaking version at any time');
|
|
10134
|
+
}
|
|
10135
|
+
if (rangeType === "git" || rangeType === "url") {
|
|
10136
|
+
entryIssues.push(`not resolved from the npm registry (${rangeType}); can't be checked against registry advisories the same way`);
|
|
10137
|
+
}
|
|
10138
|
+
if (rangeType === "file" || rangeType === "link") {
|
|
10139
|
+
entryIssues.push(`local ${rangeType === "file" ? "path" : "symlink"} dependency; not portable to a machine or CI runner without the same local path`);
|
|
10140
|
+
}
|
|
10141
|
+
const entry = { name, version, section, rangeType, issues: entryIssues };
|
|
10142
|
+
dependencies.push(entry);
|
|
10143
|
+
for (const issue of entryIssues) {
|
|
10144
|
+
issues.push({ severity: "warning", name, message: issue });
|
|
10145
|
+
}
|
|
10146
|
+
const existing = byName.get(name);
|
|
10147
|
+
if (existing) existing.push(entry);
|
|
10148
|
+
else byName.set(name, [entry]);
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
10151
|
+
const duplicates = [];
|
|
10152
|
+
for (const [name, entries] of byName) {
|
|
10153
|
+
if (entries.length < 2) continue;
|
|
10154
|
+
const versions = new Set(entries.map((e) => e.version));
|
|
10155
|
+
const sameVersion = versions.size === 1;
|
|
10156
|
+
duplicates.push({ name, sections: entries.map((e) => e.section), sameVersion });
|
|
10157
|
+
if (!sameVersion) {
|
|
10158
|
+
issues.push({ severity: "error", name, message: `listed with different version ranges in ${entries.map((e) => `${e.section} ("${e.version}")`).join(" and ")}` });
|
|
10159
|
+
} else {
|
|
10160
|
+
issues.push({ severity: "warning", name, message: `listed in more than one section (${entries.map((e) => e.section).join(", ")}) with the same range; redundant` });
|
|
10161
|
+
}
|
|
10162
|
+
}
|
|
10163
|
+
const rangeTypeCounts = {};
|
|
10164
|
+
for (const dep of dependencies) {
|
|
10165
|
+
const rt = dep.rangeType;
|
|
10166
|
+
rangeTypeCounts[rt] = (rangeTypeCounts[rt] ?? 0) + 1;
|
|
10167
|
+
}
|
|
10168
|
+
return {
|
|
10169
|
+
parsed: true,
|
|
10170
|
+
sections: presentSections,
|
|
10171
|
+
totalDependencies: dependencies.length,
|
|
10172
|
+
dependencies,
|
|
10173
|
+
duplicates,
|
|
10174
|
+
rangeTypeCounts,
|
|
10175
|
+
issues
|
|
10176
|
+
};
|
|
10177
|
+
}
|
|
10178
|
+
server.tool(
|
|
10179
|
+
"audit_dependency_ranges",
|
|
10180
|
+
`Parse a package.json's dependencies, devDependencies, peerDependencies, and optionalDependencies fields, classify each version specifier's range type, and flag wildcard ranges, the non-reproducible "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections.`,
|
|
10181
|
+
{
|
|
10182
|
+
text: z.string().min(1).describe("The full package.json file contents")
|
|
10183
|
+
},
|
|
10184
|
+
async ({ text: packageJsonText }) => {
|
|
10185
|
+
const result = _auditDependencyRangesMcp(packageJsonText);
|
|
10186
|
+
if ("error" in result) return errText(result);
|
|
10187
|
+
return text(JSON.stringify(result, null, 2));
|
|
10188
|
+
}
|
|
10189
|
+
);
|
|
9852
10190
|
var transport = new StdioServerTransport();
|
|
9853
10191
|
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.63.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",
|