@utilix-tech/mcp 0.87.0 → 0.88.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 +3 -1
- package/dist/index.js +178 -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.88.0
|
|
6
|
+
- Add `validate_mermaid_syntax` tool: check a Mermaid diagram definition for syntax errors with line numbers, including an unterminated subgraph, unbalanced node brackets, a malformed edge arrow, and a sequence-diagram participant referenced before its explicit declaration.
|
|
7
|
+
- Add `calculate_annuity_payout` tool: solve the standard annuity formula in either direction, the periodic payout a lump sum funds, or the lump sum required to fund a target payout, for an ordinary or annuity-due structure.
|
|
8
|
+
|
|
5
9
|
## 0.87.0
|
|
6
10
|
- Add `validate_markdown_heading_hierarchy` tool: scan a Markdown document's ATX headings and flag heading-structure issues: skipped levels, multiple H1s, empty headings, duplicate headings at the same level, and a document that doesn't start at H1.
|
|
7
11
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 269 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
|
|
|
@@ -194,6 +194,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
194
194
|
| `calculate_rule_of_72` | Estimate years to double an investment from a rate, or the rate needed to double in a given number of years, via the Rule of 72 shortcut and the exact logarithmic result |
|
|
195
195
|
| `calculate_emi` | Compute the Equated Monthly Installment for a loan from principal, annual rate, and tenure in years or months, with a year-by-year principal/interest breakdown |
|
|
196
196
|
| `calculate_savings_goal` | Solve for months to reach a savings goal at a fixed monthly contribution, or the monthly contribution required to hit a goal by a target month count |
|
|
197
|
+
| `calculate_annuity_payout` | Solve the standard annuity formula in either direction: the periodic payout a lump sum funds, or the lump sum required to fund a target payout, for an ordinary or annuity-due structure |
|
|
197
198
|
| `calculate_roi` | Compute return on investment percent from an initial and final value, plus the annualized (CAGR) return given an optional holding period in years |
|
|
198
199
|
| `compare_unit_prices` | Compare two or more package options by price and quantity to find the true cost per unit, flag the cheapest, and report how much more expensive each other option is |
|
|
199
200
|
| `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
|
|
@@ -297,6 +298,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
297
298
|
| `parse_env` | Parse a .env file and return all key-value pairs |
|
|
298
299
|
| `parse_docker_image` | Parse a Docker image reference into its components |
|
|
299
300
|
| `lint_dockerfile` | Lint a Dockerfile for an unpinned base image, missing WORKDIR/USER, and misused ADD |
|
|
301
|
+
| `validate_mermaid_syntax` | Check a Mermaid diagram definition for syntax errors with line numbers: unterminated subgraph, unbalanced brackets, malformed edges, and a sequence-diagram participant referenced before its declaration |
|
|
300
302
|
| `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
|
|
301
303
|
| `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
|
|
302
304
|
| `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
|
package/dist/index.js
CHANGED
|
@@ -7095,6 +7095,73 @@ server.tool(
|
|
|
7095
7095
|
return text(JSON.stringify(result, null, 2));
|
|
7096
7096
|
}
|
|
7097
7097
|
);
|
|
7098
|
+
function _calculateAnnuityPayoutMcp(mode, structure, years, paymentsPerYear, annualRatePercent, principal, periodicPayout) {
|
|
7099
|
+
if (mode !== "payout-from-principal" && mode !== "principal-from-payout") {
|
|
7100
|
+
return { error: "mode must be 'payout-from-principal' or 'principal-from-payout'" };
|
|
7101
|
+
}
|
|
7102
|
+
if (structure !== "ordinary" && structure !== "due") {
|
|
7103
|
+
return { error: "structure must be 'ordinary' or 'due'" };
|
|
7104
|
+
}
|
|
7105
|
+
if (!Number.isInteger(years) || years <= 0) return { error: "years must be a positive whole number" };
|
|
7106
|
+
if (!Number.isInteger(paymentsPerYear) || paymentsPerYear <= 0) {
|
|
7107
|
+
return { error: "paymentsPerYear must be a positive whole number" };
|
|
7108
|
+
}
|
|
7109
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
7110
|
+
return { error: "annualRatePercent must be a finite, non-negative number" };
|
|
7111
|
+
}
|
|
7112
|
+
const periods = years * paymentsPerYear;
|
|
7113
|
+
const r = annualRatePercent / 100 / paymentsPerYear;
|
|
7114
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
7115
|
+
let factor = r === 0 ? periods : (1 - Math.pow(1 + r, -periods)) / r;
|
|
7116
|
+
if (structure === "due") factor *= 1 + r;
|
|
7117
|
+
let resolvedPrincipal;
|
|
7118
|
+
let resolvedPayout;
|
|
7119
|
+
if (mode === "payout-from-principal") {
|
|
7120
|
+
if (typeof principal !== "number" || !Number.isFinite(principal) || principal <= 0) {
|
|
7121
|
+
return { error: 'principal must be a finite number greater than zero for mode "payout-from-principal"' };
|
|
7122
|
+
}
|
|
7123
|
+
resolvedPrincipal = principal;
|
|
7124
|
+
resolvedPayout = principal / factor;
|
|
7125
|
+
} else {
|
|
7126
|
+
if (typeof periodicPayout !== "number" || !Number.isFinite(periodicPayout) || periodicPayout <= 0) {
|
|
7127
|
+
return { error: 'periodicPayout must be a finite number greater than zero for mode "principal-from-payout"' };
|
|
7128
|
+
}
|
|
7129
|
+
resolvedPayout = periodicPayout;
|
|
7130
|
+
resolvedPrincipal = periodicPayout * factor;
|
|
7131
|
+
}
|
|
7132
|
+
const totalPaid = resolvedPayout * periods;
|
|
7133
|
+
const totalInterest = totalPaid - resolvedPrincipal;
|
|
7134
|
+
return {
|
|
7135
|
+
mode,
|
|
7136
|
+
structure,
|
|
7137
|
+
years,
|
|
7138
|
+
paymentsPerYear,
|
|
7139
|
+
periods,
|
|
7140
|
+
annualRatePercent,
|
|
7141
|
+
principal: round2(resolvedPrincipal),
|
|
7142
|
+
periodicPayout: round2(resolvedPayout),
|
|
7143
|
+
totalPaid: round2(totalPaid),
|
|
7144
|
+
totalInterest: round2(totalInterest)
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
7147
|
+
server.tool(
|
|
7148
|
+
"calculate_annuity_payout",
|
|
7149
|
+
'Standard time-value-of-money annuity formula, solved in either direction. Mode "payout-from-principal" takes a lump sum and computes the fixed periodic payout a given number of periods at a given rate would produce; mode "principal-from-payout" takes a desired periodic payout and computes the lump sum required to fund it. Structure "ordinary" assumes payments at the end of each period, "due" at the start (worth one extra period of growth each). Pure closed-form formulas, no live rate data.',
|
|
7150
|
+
{
|
|
7151
|
+
mode: z.enum(["payout-from-principal", "principal-from-payout"]).describe("Which quantity to solve for"),
|
|
7152
|
+
structure: z.enum(["ordinary", "due"]).describe('Whether payments land at the end ("ordinary") or start ("due") of each period'),
|
|
7153
|
+
years: z.number().int().positive().describe("Payout term in years"),
|
|
7154
|
+
paymentsPerYear: z.number().int().positive().describe("Payments per year, e.g. 12 = monthly"),
|
|
7155
|
+
annualRatePercent: z.number().describe("Annual interest rate as a percent, e.g. 5 (cannot be negative)"),
|
|
7156
|
+
principal: z.number().optional().describe('Required for mode "payout-from-principal": the lump sum to annuitize'),
|
|
7157
|
+
periodicPayout: z.number().optional().describe('Required for mode "principal-from-payout": the desired payout per period')
|
|
7158
|
+
},
|
|
7159
|
+
async ({ mode, structure, years, paymentsPerYear, annualRatePercent, principal, periodicPayout }) => {
|
|
7160
|
+
const result = _calculateAnnuityPayoutMcp(mode, structure, years, paymentsPerYear, annualRatePercent, principal, periodicPayout);
|
|
7161
|
+
if ("error" in result) return errText(result);
|
|
7162
|
+
return text(JSON.stringify(result, null, 2));
|
|
7163
|
+
}
|
|
7164
|
+
);
|
|
7098
7165
|
function _calculateRoiMcp(initialValue, finalValue, years) {
|
|
7099
7166
|
if (typeof initialValue !== "number" || !Number.isFinite(initialValue) || initialValue <= 0) {
|
|
7100
7167
|
return { error: "initialValue must be a finite number greater than zero" };
|
|
@@ -13596,6 +13663,117 @@ server.tool(
|
|
|
13596
13663
|
return text(JSON.stringify(result, null, 2));
|
|
13597
13664
|
}
|
|
13598
13665
|
);
|
|
13666
|
+
var _MERMAID_DIAGRAM_TYPES_MCP = {
|
|
13667
|
+
flowchart: "flowchart",
|
|
13668
|
+
graph: "flowchart",
|
|
13669
|
+
sequencediagram: "sequenceDiagram",
|
|
13670
|
+
classdiagram: "classDiagram",
|
|
13671
|
+
statediagram: "stateDiagram",
|
|
13672
|
+
"statediagram-v2": "stateDiagram",
|
|
13673
|
+
erdiagram: "erDiagram",
|
|
13674
|
+
pie: "pie"
|
|
13675
|
+
};
|
|
13676
|
+
var _MERMAID_ARROW_RE_MCP = /(-\.->|==>|--x|--o|-\.-|-->|---|===)/i;
|
|
13677
|
+
function _checkMermaidFlowchartMcp(lines, fromLine, issues) {
|
|
13678
|
+
const subgraphStack = [];
|
|
13679
|
+
for (let i = fromLine; i < lines.length; i++) {
|
|
13680
|
+
const lineNo = i + 1;
|
|
13681
|
+
const trimmed = lines[i].split("%%")[0].trim();
|
|
13682
|
+
if (trimmed === "") continue;
|
|
13683
|
+
const firstToken = trimmed.split(/\s+/)[0];
|
|
13684
|
+
if (firstToken === "subgraph") {
|
|
13685
|
+
subgraphStack.push(lineNo);
|
|
13686
|
+
continue;
|
|
13687
|
+
}
|
|
13688
|
+
if (trimmed === "end") {
|
|
13689
|
+
if (subgraphStack.length === 0) {
|
|
13690
|
+
issues.push({ rule: "unexpected-end", line: lineNo, message: "'end' with no matching 'subgraph' to close" });
|
|
13691
|
+
} else {
|
|
13692
|
+
subgraphStack.pop();
|
|
13693
|
+
}
|
|
13694
|
+
continue;
|
|
13695
|
+
}
|
|
13696
|
+
const opens = (trimmed.match(/[[({]/g) || []).length;
|
|
13697
|
+
const closes = (trimmed.match(/[\])}]/g) || []).length;
|
|
13698
|
+
if (opens !== closes) {
|
|
13699
|
+
issues.push({ rule: "unbalanced-brackets", line: lineNo, message: `Unbalanced brackets in node/edge definition: ${opens} opening vs ${closes} closing` });
|
|
13700
|
+
}
|
|
13701
|
+
const segments = trimmed.split(_MERMAID_ARROW_RE_MCP);
|
|
13702
|
+
if (segments.length > 1) {
|
|
13703
|
+
let missingNode = false;
|
|
13704
|
+
for (let s = 0; s < segments.length; s += 2) {
|
|
13705
|
+
if (segments[s].trim() === "") missingNode = true;
|
|
13706
|
+
}
|
|
13707
|
+
if (missingNode) issues.push({ rule: "malformed-edge", line: lineNo, message: "Edge arrow is missing a node on one side" });
|
|
13708
|
+
}
|
|
13709
|
+
}
|
|
13710
|
+
for (const openLine of subgraphStack) {
|
|
13711
|
+
issues.push({ rule: "unterminated-subgraph", line: openLine, message: "'subgraph' opened here is never closed with a matching 'end'" });
|
|
13712
|
+
}
|
|
13713
|
+
}
|
|
13714
|
+
var _MERMAID_SEQ_DECL_RE_MCP = /^(participant|actor)\s+(\S+)/i;
|
|
13715
|
+
var _MERMAID_SEQ_MSG_RE_MCP = /^([A-Za-z0-9_]+)\s*(-{1,2}>{1,2}|-{1,2}x|-{1,2}\))\s*([A-Za-z0-9_]+)\s*:/i;
|
|
13716
|
+
function _checkMermaidSequenceDiagramMcp(lines, fromLine, issues) {
|
|
13717
|
+
const declaredAtLine = {};
|
|
13718
|
+
for (let i = fromLine; i < lines.length; i++) {
|
|
13719
|
+
const trimmed = lines[i].split("%%")[0].trim();
|
|
13720
|
+
if (trimmed === "") continue;
|
|
13721
|
+
const m = trimmed.match(_MERMAID_SEQ_DECL_RE_MCP);
|
|
13722
|
+
if (m && declaredAtLine[m[2]] === void 0) declaredAtLine[m[2]] = i + 1;
|
|
13723
|
+
}
|
|
13724
|
+
for (let i = fromLine; i < lines.length; i++) {
|
|
13725
|
+
const lineNo = i + 1;
|
|
13726
|
+
const trimmed = lines[i].split("%%")[0].trim();
|
|
13727
|
+
if (trimmed === "") continue;
|
|
13728
|
+
const m = trimmed.match(_MERMAID_SEQ_MSG_RE_MCP);
|
|
13729
|
+
if (!m) continue;
|
|
13730
|
+
for (const id of [m[1], m[3]]) {
|
|
13731
|
+
const declaredLine = declaredAtLine[id];
|
|
13732
|
+
if (declaredLine !== void 0 && declaredLine > lineNo) {
|
|
13733
|
+
issues.push({ rule: "participant-before-declared", line: lineNo, message: `Participant '${id}' is referenced here but not explicitly declared (with 'participant' or 'actor') until line ${declaredLine}` });
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13736
|
+
}
|
|
13737
|
+
}
|
|
13738
|
+
function _validateMermaidSyntaxMcp(input) {
|
|
13739
|
+
if (!input || !input.trim()) return { error: "Diagram text is required" };
|
|
13740
|
+
const lines = input.replace(/\r\n/g, "\n").split("\n");
|
|
13741
|
+
const issues = [];
|
|
13742
|
+
let headerLine = -1;
|
|
13743
|
+
let headerText = "";
|
|
13744
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13745
|
+
const trimmed = lines[i].trim();
|
|
13746
|
+
if (trimmed === "" || trimmed.startsWith("%%")) continue;
|
|
13747
|
+
headerLine = i + 1;
|
|
13748
|
+
headerText = trimmed;
|
|
13749
|
+
break;
|
|
13750
|
+
}
|
|
13751
|
+
if (headerLine === -1) return { error: "Diagram text is required" };
|
|
13752
|
+
const firstWord = headerText.split(/\s+/)[0];
|
|
13753
|
+
const diagramType = _MERMAID_DIAGRAM_TYPES_MCP[firstWord.toLowerCase()] ?? null;
|
|
13754
|
+
if (!diagramType) {
|
|
13755
|
+
issues.push({ rule: "unrecognized-diagram-type", line: headerLine, message: `"${firstWord}" is not a recognized Mermaid diagram type (expected one of: flowchart/graph, sequenceDiagram, classDiagram, stateDiagram, erDiagram, pie)` });
|
|
13756
|
+
return { diagramType: null, valid: false, issues };
|
|
13757
|
+
}
|
|
13758
|
+
if (diagramType === "flowchart") {
|
|
13759
|
+
_checkMermaidFlowchartMcp(lines, headerLine, issues);
|
|
13760
|
+
} else if (diagramType === "sequenceDiagram") {
|
|
13761
|
+
_checkMermaidSequenceDiagramMcp(lines, headerLine, issues);
|
|
13762
|
+
}
|
|
13763
|
+
return { diagramType, valid: issues.length === 0, issues };
|
|
13764
|
+
}
|
|
13765
|
+
server.tool(
|
|
13766
|
+
"validate_mermaid_syntax",
|
|
13767
|
+
"Heuristic syntax validation for a Mermaid diagram definition: recognizes flowchart/graph, sequenceDiagram, classDiagram, stateDiagram(-v2), erDiagram, and pie by their header keyword, then runs diagram-specific structural checks (unterminated subgraph, unbalanced brackets, malformed edges for flowcharts; a participant referenced before its explicit declaration for sequence diagrams). A line-based heuristic, not a full Mermaid grammar; never renders the diagram.",
|
|
13768
|
+
{
|
|
13769
|
+
diagram: z.string().min(1).describe("The full Mermaid diagram definition text")
|
|
13770
|
+
},
|
|
13771
|
+
async ({ diagram }) => {
|
|
13772
|
+
const result = _validateMermaidSyntaxMcp(diagram);
|
|
13773
|
+
if ("error" in result) return errText(result);
|
|
13774
|
+
return text(JSON.stringify(result, null, 2));
|
|
13775
|
+
}
|
|
13776
|
+
);
|
|
13599
13777
|
server.tool(
|
|
13600
13778
|
"find_cron_overlaps",
|
|
13601
13779
|
"Given two or more standard 5-field cron expressions, find every minute over the next N days where two or more of them fire at the same time, for example a nightly backup job colliding with a report job. Returns each colliding minute with the zero-based indexes of every expression that matched.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.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",
|