@utilix-tech/mcp 0.87.0 → 0.89.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 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.89.0
6
+ - Add `check_acronym_expansion` tool: scan text for acronyms and flag any used before being expanded on first mention, recognizing both the "full term (ACRONYM)" and "ACRONYM (full term)" conventions, with acronyms never expanded anywhere reported separately.
7
+
8
+ ## 0.88.0
9
+ - 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.
10
+ - 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.
11
+
5
12
  ## 0.87.0
6
13
  - 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
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 267 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 270 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
 
@@ -134,7 +134,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
134
134
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
135
135
  | `generate_csp_nonce` | Generate a cryptographically random CSP nonce with ready-to-paste header directive and script/style tag snippets |
136
136
 
137
- ### Text (17 tools)
137
+ ### Text (18 tools)
138
138
  | Tool | Description |
139
139
  |------|-------------|
140
140
  | `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
@@ -156,6 +156,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
156
156
  | `lookup_emoji` | Search a curated table of ~280 common emoji by name, keyword, or GitHub-style shortcode, or look one up exactly by shortcode, character, or category |
157
157
  | `analyze_keyword_density` | Count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes |
158
158
  | `check_dash_usage` | Scan text for likely dash misuse: a hyphen surrounded by spaces, an unconverted double hyphen, and inconsistent spacing around em dashes across the same document |
159
+ | `check_acronym_expansion` | Scan text for acronyms and flag any that are used before being expanded on first mention, or never expanded at all |
159
160
 
160
161
  ### Time (6 tools)
161
162
  | Tool | Description |
@@ -194,6 +195,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
194
195
  | `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
196
  | `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
197
  | `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 |
198
+ | `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
199
  | `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
200
  | `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
201
  | `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
@@ -297,6 +299,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
297
299
  | `parse_env` | Parse a .env file and return all key-value pairs |
298
300
  | `parse_docker_image` | Parse a Docker image reference into its components |
299
301
  | `lint_dockerfile` | Lint a Dockerfile for an unpinned base image, missing WORKDIR/USER, and misused ADD |
302
+ | `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
303
  | `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
301
304
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
302
305
  | `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.",
@@ -13766,6 +13944,76 @@ server.tool(
13766
13944
  return text(JSON.stringify(_checkDashUsageMcp(input), null, 2));
13767
13945
  }
13768
13946
  );
13947
+ var _ACRONYM_TOKEN_RE_MCP = /\b[A-Z][A-Z0-9]{1,9}\b/g;
13948
+ var _ACRONYM_IN_PARENS_RE_MCP = /\(([A-Z][A-Z0-9]{1,9})\)/g;
13949
+ var _ACRONYM_THEN_EXPANSION_RE_MCP = /\b([A-Z][A-Z0-9]{1,9})\b\s*\(([a-zA-Z][^()]{1,100})\)/g;
13950
+ function _acronymContextSnippetMcp(input, index, matchLen) {
13951
+ const start = Math.max(0, index - 25);
13952
+ const end = Math.min(input.length, index + matchLen + 25);
13953
+ const body = input.slice(start, end).replace(/\s+/g, " ").trim();
13954
+ return (start > 0 ? "\u2026" : "") + body + (end < input.length ? "\u2026" : "");
13955
+ }
13956
+ function _checkAcronymExpansionMcp(input) {
13957
+ const definedAt = /* @__PURE__ */ new Map();
13958
+ for (const m of input.matchAll(_ACRONYM_IN_PARENS_RE_MCP)) {
13959
+ const acronym = m[1];
13960
+ const idx = (m.index ?? 0) + 1;
13961
+ const existing = definedAt.get(acronym);
13962
+ if (existing === void 0 || idx < existing) definedAt.set(acronym, idx);
13963
+ }
13964
+ for (const m of input.matchAll(_ACRONYM_THEN_EXPANSION_RE_MCP)) {
13965
+ const acronym = m[1];
13966
+ const idx = m.index ?? 0;
13967
+ const existing = definedAt.get(acronym);
13968
+ if (existing === void 0 || idx < existing) definedAt.set(acronym, idx);
13969
+ }
13970
+ const occurrencesByAcronym = /* @__PURE__ */ new Map();
13971
+ for (const m of input.matchAll(_ACRONYM_TOKEN_RE_MCP)) {
13972
+ const acronym = m[0];
13973
+ const idx = m.index ?? 0;
13974
+ const list = occurrencesByAcronym.get(acronym);
13975
+ if (list) list.push(idx);
13976
+ else occurrencesByAcronym.set(acronym, [idx]);
13977
+ }
13978
+ const acronyms = [];
13979
+ const issues = [];
13980
+ for (const [acronym, indicesUnsorted] of occurrencesByAcronym) {
13981
+ const indices = [...indicesUnsorted].sort((a, b) => a - b);
13982
+ const firstUsedAt = indices[0];
13983
+ const expandedAt = definedAt.has(acronym) ? definedAt.get(acronym) : null;
13984
+ const earlyUse = expandedAt !== null ? indices.find((i) => i < expandedAt) : void 0;
13985
+ const usedBeforeExpansion = earlyUse !== void 0;
13986
+ acronyms.push({ acronym, occurrences: indices.length, firstUsedAt, expandedAt, usedBeforeExpansion });
13987
+ if (usedBeforeExpansion) {
13988
+ issues.push({
13989
+ type: "used-before-expansion",
13990
+ acronym,
13991
+ index: earlyUse,
13992
+ context: _acronymContextSnippetMcp(input, earlyUse, acronym.length),
13993
+ message: `"${acronym}" is used here before it's expanded later in the document (at character ${expandedAt}).`
13994
+ });
13995
+ } else if (expandedAt === null) {
13996
+ issues.push({
13997
+ type: "never-expanded",
13998
+ acronym,
13999
+ index: firstUsedAt,
14000
+ context: _acronymContextSnippetMcp(input, firstUsedAt, acronym.length),
14001
+ message: `"${acronym}" is never expanded anywhere in this document.`
14002
+ });
14003
+ }
14004
+ }
14005
+ acronyms.sort((a, b) => a.firstUsedAt - b.firstUsedAt);
14006
+ issues.sort((a, b) => a.index - b.index);
14007
+ return { acronyms, issues };
14008
+ }
14009
+ server.tool(
14010
+ "check_acronym_expansion",
14011
+ "Scan text for acronyms (2-10 character all-caps tokens) and flag any that are used before being expanded on first mention, per standard technical writing style guides. Recognizes both expansion conventions: the full term followed by the acronym in parentheses, and the acronym followed by its expansion in parentheses. Pure regex heuristic, not a dictionary of real acronyms.",
14012
+ { text: z.string().describe("The text to scan for acronym expansion order") },
14013
+ async ({ text: input }) => {
14014
+ return text(JSON.stringify(_checkAcronymExpansionMcp(input), null, 2));
14015
+ }
14016
+ );
13769
14017
  var _CommissionTierSchema = z.object({
13770
14018
  upTo: z.number().nullable().describe("Upper bound of this tier (exclusive). null means unbounded and is only valid on the last tier."),
13771
14019
  ratePercent: z.number().min(0).describe("Commission rate as a percent applied to the slice of salesAmount within this tier")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.87.0",
4
- "description": "MCP server exposing 198 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.89.0",
4
+ "description": "MCP server exposing 270 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",
7
7
  "homepage": "https://utilix.tech/mcp",