@utilix-tech/mcp 0.90.0 → 0.91.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 +123 -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.91.0
|
|
6
|
+
- Add `find_yaml_duplicate_keys` tool: report every mapping key that collides with an earlier key in the same YAML mapping scope, with the line and column of each repeated occurrence.
|
|
7
|
+
- Add `detect_negative_amortization` tool: check whether a proposed loan payment covers the interest accruing each period and, if not, project the shortfall and the resulting balance growth, optionally flagging the month a balance cap is crossed.
|
|
8
|
+
|
|
5
9
|
## 0.90.0
|
|
6
10
|
- Add `intersect_semver_ranges` tool: determine whether two semver ranges overlap and, if so, the intersecting range, supporting the same caret/tilde/comparison-operator/exact-version syntax as the semver tool.
|
|
7
11
|
- Add `check_markdown_internal_links` tool: scan a Markdown document's ATX headings, compute GitHub-style anchor slugs, and flag `[text](#slug)` links that point at a heading that doesn't exist.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 274 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
|
|
|
@@ -169,7 +169,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
169
169
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
170
170
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
171
171
|
|
|
172
|
-
### Units (
|
|
172
|
+
### Units (51 tools)
|
|
173
173
|
| Tool | Description |
|
|
174
174
|
|------|-------------|
|
|
175
175
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -220,6 +220,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
220
220
|
| `estimate_data_transfer_time` | Estimate the theoretical time to transfer a file of a given size over a connection of a given speed, or (given a target time instead) the bandwidth required to hit it |
|
|
221
221
|
| `calculate_real_rate_of_return` | Apply the Fisher equation to solve for whichever of the nominal rate, inflation rate, or inflation-adjusted real rate is missing, given exactly two of the three |
|
|
222
222
|
| `calculate_credit_card_minimum_payment` | Simulate paying only the minimum payment on a revolving credit card balance, where the minimum is the larger of a percent of the balance or a fixed dollar floor, and report months/years to payoff, total interest, and total paid |
|
|
223
|
+
| `detect_negative_amortization` | Given a loan's balance, annual rate, and a proposed monthly payment, check whether that payment covers the interest accruing each period; if not, report the shortfall and project how the balance grows instead of shrinking |
|
|
223
224
|
| `calculate_sales_commission` | Compute a sales commission from either a flat percent rate applied to the whole sale amount, or a tiered (marginal) rate structure where each tier applies only to the slice of sales within it, plus an optional base salary added to the total payout |
|
|
224
225
|
| `intersect_semver_ranges` | Determine whether two semver ranges overlap and, if so, the intersecting range, supporting the same caret/tilde/comparison-operator/exact-version syntax as the semver tool |
|
|
225
226
|
|
|
@@ -321,11 +322,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
321
322
|
| `extract_markdown_link_references` | Extract every reference-style Markdown link ([text][ref], [text][], ![alt][ref]) and match it against its [ref]: url definition, flagging unresolved references and unused definitions |
|
|
322
323
|
| `build_character_class` | Assemble a [...] regex character class from named building blocks (lowercase, uppercase, digits, whitespace), custom ranges, and literal custom characters, with optional negation |
|
|
323
324
|
|
|
324
|
-
### Data (
|
|
325
|
+
### Data (20 tools)
|
|
325
326
|
| Tool | Description |
|
|
326
327
|
|------|-------------|
|
|
327
328
|
| `validate_yaml` | Validate YAML syntax |
|
|
328
329
|
| `resolve_yaml_anchors` | Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused |
|
|
330
|
+
| `find_yaml_duplicate_keys` | Parse a YAML document and report every mapping key that collides with an earlier key in the same mapping scope, with the line and column of each repeated occurrence |
|
|
329
331
|
| `toml_to_json` | Convert TOML to JSON |
|
|
330
332
|
| `xml_to_json` | Convert XML to JSON |
|
|
331
333
|
| `parse_csv` | Parse a CSV string and return the data as JSON |
|
package/dist/index.js
CHANGED
|
@@ -803,6 +803,62 @@ server.tool(
|
|
|
803
803
|
return text(JSON.stringify(result, null, 2));
|
|
804
804
|
}
|
|
805
805
|
);
|
|
806
|
+
function _matchYamlKeyMcp(lineText) {
|
|
807
|
+
const dashMatch = /^(\s*)((?:-\s+)*)/.exec(lineText);
|
|
808
|
+
const basePrefix = (dashMatch?.[1].length ?? 0) + (dashMatch?.[2].length ?? 0);
|
|
809
|
+
const rest = lineText.slice(basePrefix);
|
|
810
|
+
const quoted = /^(["'])((?:\\.|(?!\1).)*)\1(\s*:(?:\s|$))/.exec(rest);
|
|
811
|
+
if (quoted) {
|
|
812
|
+
return { key: quoted[2], prefixLength: basePrefix, matchLength: quoted[1].length * 2 + quoted[2].length, quote: quoted[1] };
|
|
813
|
+
}
|
|
814
|
+
const plain = /^([^\s:#][^:]*?)(\s*:(?:\s|$))/.exec(rest);
|
|
815
|
+
if (plain) {
|
|
816
|
+
return { key: plain[1], prefixLength: basePrefix, matchLength: plain[1].length, quote: null };
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
function _maskYamlKeyMcp(lineText, match, n) {
|
|
821
|
+
const suffix = `__dup${n}__`;
|
|
822
|
+
const before = lineText.slice(0, match.prefixLength);
|
|
823
|
+
const keyPart = lineText.slice(match.prefixLength, match.prefixLength + match.matchLength);
|
|
824
|
+
const after = lineText.slice(match.prefixLength + match.matchLength);
|
|
825
|
+
if (match.quote) {
|
|
826
|
+
return `${before}${keyPart.slice(0, -1)}${suffix}${match.quote}${after}`;
|
|
827
|
+
}
|
|
828
|
+
return `${before}${keyPart}${suffix}${after}`;
|
|
829
|
+
}
|
|
830
|
+
function _findYamlDuplicateKeys(yamlText) {
|
|
831
|
+
if (!yamlText || !yamlText.trim()) return { error: "Input is empty" };
|
|
832
|
+
const lines = yamlText.split("\n");
|
|
833
|
+
const duplicates = [];
|
|
834
|
+
for (let pass = 0; pass < 500; pass++) {
|
|
835
|
+
try {
|
|
836
|
+
yaml.load(lines.join("\n"));
|
|
837
|
+
return { duplicateCount: duplicates.length, duplicates };
|
|
838
|
+
} catch (e) {
|
|
839
|
+
const err = e;
|
|
840
|
+
const reason = err.reason ?? err.message;
|
|
841
|
+
if (!/duplicated mapping key/i.test(reason)) return { error: reason };
|
|
842
|
+
const lineIdx = err.mark?.line;
|
|
843
|
+
if (lineIdx == null || lineIdx < 0 || lineIdx >= lines.length) return { error: reason };
|
|
844
|
+
const match = _matchYamlKeyMcp(lines[lineIdx]);
|
|
845
|
+
if (!match) return { error: reason };
|
|
846
|
+
duplicates.push({ key: match.key, line: lineIdx + 1, column: match.prefixLength + 1 });
|
|
847
|
+
lines[lineIdx] = _maskYamlKeyMcp(lines[lineIdx], match, duplicates.length);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return { error: "Too many duplicate keys to fully enumerate" };
|
|
851
|
+
}
|
|
852
|
+
server.tool(
|
|
853
|
+
"find_yaml_duplicate_keys",
|
|
854
|
+
"Parse a YAML document and report every mapping key that collides with an earlier key in the same mapping scope, with the line and column of each repeated occurrence.",
|
|
855
|
+
{ yaml: z.string().describe("The YAML document to check") },
|
|
856
|
+
async ({ yaml: yamlText }) => {
|
|
857
|
+
const result = _findYamlDuplicateKeys(yamlText);
|
|
858
|
+
if ("error" in result) return errText(result);
|
|
859
|
+
return text(JSON.stringify(result, null, 2));
|
|
860
|
+
}
|
|
861
|
+
);
|
|
806
862
|
server.tool(
|
|
807
863
|
"toml_to_json",
|
|
808
864
|
"Convert a TOML string to JSON.",
|
|
@@ -12370,6 +12426,73 @@ server.tool(
|
|
|
12370
12426
|
return text(JSON.stringify(result, null, 2));
|
|
12371
12427
|
}
|
|
12372
12428
|
);
|
|
12429
|
+
var _NEGATIVE_AMORTIZATION_MAX_PROJECTION_MONTHS_MCP = 600;
|
|
12430
|
+
function _detectNegativeAmortizationMcp(principal, annualRatePercent, monthlyPayment, projectionMonths, capPercent) {
|
|
12431
|
+
if (!Number.isFinite(principal) || principal <= 0) {
|
|
12432
|
+
return { error: "principal must be a positive number" };
|
|
12433
|
+
}
|
|
12434
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
12435
|
+
return { error: "annualRatePercent must be zero or a positive number" };
|
|
12436
|
+
}
|
|
12437
|
+
if (!Number.isFinite(monthlyPayment) || monthlyPayment < 0) {
|
|
12438
|
+
return { error: "monthlyPayment must be zero or a positive number" };
|
|
12439
|
+
}
|
|
12440
|
+
if (projectionMonths !== void 0 && (!Number.isInteger(projectionMonths) || projectionMonths <= 0)) {
|
|
12441
|
+
return { error: "projectionMonths must be a positive whole number" };
|
|
12442
|
+
}
|
|
12443
|
+
if (capPercent !== void 0 && (!Number.isFinite(capPercent) || capPercent <= 0)) {
|
|
12444
|
+
return { error: "capPercent must be a positive number" };
|
|
12445
|
+
}
|
|
12446
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
12447
|
+
const monthlyInterest = principal * monthlyRate;
|
|
12448
|
+
const shortfall = Math.max(0, _roundMoneyPrepaymentPenaltyMcp(monthlyInterest - monthlyPayment));
|
|
12449
|
+
const isNegativeAmortizing = monthlyPayment < monthlyInterest;
|
|
12450
|
+
const months = Math.min(projectionMonths ?? 12, _NEGATIVE_AMORTIZATION_MAX_PROJECTION_MONTHS_MCP);
|
|
12451
|
+
const capBalance = capPercent != null ? principal * (capPercent / 100) : null;
|
|
12452
|
+
const projection = [];
|
|
12453
|
+
let balance = principal;
|
|
12454
|
+
let capCrossedMonth = null;
|
|
12455
|
+
for (let month = 1; month <= months; month++) {
|
|
12456
|
+
const interest = balance * monthlyRate;
|
|
12457
|
+
const endingBalance = balance + interest - monthlyPayment;
|
|
12458
|
+
projection.push({
|
|
12459
|
+
month,
|
|
12460
|
+
startingBalance: _roundMoneyPrepaymentPenaltyMcp(balance),
|
|
12461
|
+
interest: _roundMoneyPrepaymentPenaltyMcp(interest),
|
|
12462
|
+
payment: _roundMoneyPrepaymentPenaltyMcp(monthlyPayment),
|
|
12463
|
+
endingBalance: _roundMoneyPrepaymentPenaltyMcp(Math.max(endingBalance, 0))
|
|
12464
|
+
});
|
|
12465
|
+
if (capBalance != null && capCrossedMonth == null && endingBalance >= capBalance) {
|
|
12466
|
+
capCrossedMonth = month;
|
|
12467
|
+
}
|
|
12468
|
+
balance = endingBalance;
|
|
12469
|
+
if (balance <= 0) break;
|
|
12470
|
+
}
|
|
12471
|
+
return {
|
|
12472
|
+
monthlyInterest: _roundMoneyPrepaymentPenaltyMcp(monthlyInterest),
|
|
12473
|
+
isNegativeAmortizing,
|
|
12474
|
+
shortfall,
|
|
12475
|
+
breakEvenPayment: _roundMoneyPrepaymentPenaltyMcp(monthlyInterest),
|
|
12476
|
+
projection,
|
|
12477
|
+
capCrossedMonth
|
|
12478
|
+
};
|
|
12479
|
+
}
|
|
12480
|
+
server.tool(
|
|
12481
|
+
"detect_negative_amortization",
|
|
12482
|
+
"Given a loan's balance, annual rate, and a proposed monthly payment, check whether that payment covers the interest accruing each period. If not, report the shortfall and project how the balance grows instead of shrinking.",
|
|
12483
|
+
{
|
|
12484
|
+
principal: z.number().describe("The loan's current balance"),
|
|
12485
|
+
annualRatePercent: z.number().describe("The loan's annual interest rate as a percent"),
|
|
12486
|
+
monthlyPayment: z.number().describe("The proposed monthly payment"),
|
|
12487
|
+
projectionMonths: z.number().optional().describe("How many months to project the balance (default 12, max 600)"),
|
|
12488
|
+
capPercent: z.number().optional().describe("Optional balance cap as a percent of the original principal (e.g. 110 for a 110% cap)")
|
|
12489
|
+
},
|
|
12490
|
+
async ({ principal, annualRatePercent, monthlyPayment, projectionMonths, capPercent }) => {
|
|
12491
|
+
const result = _detectNegativeAmortizationMcp(principal, annualRatePercent, monthlyPayment, projectionMonths, capPercent);
|
|
12492
|
+
if ("error" in result) return errText(result);
|
|
12493
|
+
return text(JSON.stringify(result, null, 2));
|
|
12494
|
+
}
|
|
12495
|
+
);
|
|
12373
12496
|
var _ARIA_GLOBAL_ATTRS_MCP = /* @__PURE__ */ new Set([
|
|
12374
12497
|
"aria-atomic",
|
|
12375
12498
|
"aria-busy",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
4
4
|
"description": "MCP server exposing 272 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",
|