@utilix-tech/mcp 0.86.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 +7 -0
- package/README.md +5 -2
- package/dist/index.js +267 -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.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
|
+
|
|
9
|
+
## 0.87.0
|
|
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.
|
|
11
|
+
|
|
5
12
|
## 0.86.0
|
|
6
13
|
- Add `calculate_sales_commission` tool: compute a flat or tiered (marginal) sales commission from a sale amount, where tiered mode applies each rate only to the slice of sales within that tier, plus an optional base salary added to the total payout.
|
|
7
14
|
- Add `build_character_class` tool: assemble a `[...]` regex character class from named building blocks (lowercase, uppercase, digits, whitespace), custom ranges, and literal custom characters, with optional negation.
|
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 |
|
|
@@ -317,7 +319,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
317
319
|
| `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 |
|
|
318
320
|
| `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 |
|
|
319
321
|
|
|
320
|
-
### Data (
|
|
322
|
+
### Data (18 tools)
|
|
321
323
|
| Tool | Description |
|
|
322
324
|
|------|-------------|
|
|
323
325
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -337,6 +339,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
337
339
|
| `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
|
|
338
340
|
| `infer_csv_column_types` | Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string), flagging nullable columns and empty-cell counts |
|
|
339
341
|
| `align_markdown_tables` | Reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, preserving left/center/right alignment markers |
|
|
342
|
+
| `validate_markdown_heading_hierarchy` | Scan a Markdown document's ATX headings and flag heading-structure issues: skipped levels, multiple H1s, empty headings, duplicate headings, and a document that doesn't start at H1 |
|
|
340
343
|
|
|
341
344
|
### CSS (2 tools)
|
|
342
345
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -6793,6 +6793,95 @@ server.tool(
|
|
|
6793
6793
|
return text(JSON.stringify(result, null, 2));
|
|
6794
6794
|
}
|
|
6795
6795
|
);
|
|
6796
|
+
var _HEADING_HIERARCHY_RE_MCP = /^(#{1,6})(?!#)(?:[ \t]+(.*?))?[ \t]*#*[ \t]*$/;
|
|
6797
|
+
function _validateMarkdownHeadingHierarchyMcp(markdown) {
|
|
6798
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
6799
|
+
const headings = [];
|
|
6800
|
+
let inFence = false;
|
|
6801
|
+
let fenceMarker = "";
|
|
6802
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6803
|
+
const line = lines[i];
|
|
6804
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
|
6805
|
+
if (fenceMatch) {
|
|
6806
|
+
if (!inFence) {
|
|
6807
|
+
inFence = true;
|
|
6808
|
+
fenceMarker = fenceMatch[1][0];
|
|
6809
|
+
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
6810
|
+
inFence = false;
|
|
6811
|
+
}
|
|
6812
|
+
continue;
|
|
6813
|
+
}
|
|
6814
|
+
if (inFence) continue;
|
|
6815
|
+
const match = line.match(_HEADING_HIERARCHY_RE_MCP);
|
|
6816
|
+
if (!match) continue;
|
|
6817
|
+
const level = match[1].length;
|
|
6818
|
+
const text2 = _stripInlineMarkdownMcp(match[2] ?? "");
|
|
6819
|
+
headings.push({ line: i + 1, level, text: text2 });
|
|
6820
|
+
}
|
|
6821
|
+
const issues = [];
|
|
6822
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6823
|
+
let h1Count = 0;
|
|
6824
|
+
let firstH1Line = null;
|
|
6825
|
+
let prevLevel = null;
|
|
6826
|
+
headings.forEach((h, idx) => {
|
|
6827
|
+
if (h.text === "") {
|
|
6828
|
+
issues.push({ line: h.line, type: "empty-heading", message: `Heading at line ${h.line} has no text` });
|
|
6829
|
+
}
|
|
6830
|
+
if (idx === 0 && h.level !== 1) {
|
|
6831
|
+
issues.push({
|
|
6832
|
+
line: h.line,
|
|
6833
|
+
type: "starts-below-h1",
|
|
6834
|
+
message: `Document starts with an H${h.level} heading instead of H1`
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
if (h.level === 1) {
|
|
6838
|
+
h1Count++;
|
|
6839
|
+
if (h1Count === 1) {
|
|
6840
|
+
firstH1Line = h.line;
|
|
6841
|
+
} else {
|
|
6842
|
+
issues.push({
|
|
6843
|
+
line: h.line,
|
|
6844
|
+
type: "multiple-h1",
|
|
6845
|
+
message: `Multiple H1 headings found: another H1 at line ${h.line} (first was at line ${firstH1Line})`
|
|
6846
|
+
});
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
if (prevLevel !== null && h.level > prevLevel + 1) {
|
|
6850
|
+
const skipped = h.level - prevLevel > 2 ? `H${prevLevel + 1}-H${h.level - 1}` : `H${prevLevel + 1}`;
|
|
6851
|
+
issues.push({
|
|
6852
|
+
line: h.line,
|
|
6853
|
+
type: "skipped-level",
|
|
6854
|
+
message: `Heading level jumps from H${prevLevel} to H${h.level} at line ${h.line}, skipping ${skipped}`
|
|
6855
|
+
});
|
|
6856
|
+
}
|
|
6857
|
+
prevLevel = h.level;
|
|
6858
|
+
if (h.text !== "") {
|
|
6859
|
+
const key = `${h.level}:${h.text.toLowerCase()}`;
|
|
6860
|
+
const earlierLine = seen.get(key);
|
|
6861
|
+
if (earlierLine !== void 0) {
|
|
6862
|
+
issues.push({
|
|
6863
|
+
line: h.line,
|
|
6864
|
+
type: "duplicate-heading",
|
|
6865
|
+
message: `Heading "${h.text}" at line ${h.line} duplicates an H${h.level} heading at line ${earlierLine}`
|
|
6866
|
+
});
|
|
6867
|
+
} else {
|
|
6868
|
+
seen.set(key, h.line);
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
});
|
|
6872
|
+
return { headings, issues, h1Count };
|
|
6873
|
+
}
|
|
6874
|
+
server.tool(
|
|
6875
|
+
"validate_markdown_heading_hierarchy",
|
|
6876
|
+
"Scan a Markdown document's ATX headings and flag heading-structure issues: a level that skips one or more levels going deeper, more than one H1, an empty heading, a duplicate heading at the same level, or a document that doesn't start at H1. Setext-style headings are not recognized, and headings inside fenced code blocks are ignored.",
|
|
6877
|
+
{
|
|
6878
|
+
markdown: z.string().describe("The Markdown document to scan")
|
|
6879
|
+
},
|
|
6880
|
+
async ({ markdown }) => {
|
|
6881
|
+
const result = _validateMarkdownHeadingHierarchyMcp(markdown);
|
|
6882
|
+
return text(JSON.stringify(result, null, 2));
|
|
6883
|
+
}
|
|
6884
|
+
);
|
|
6796
6885
|
var _COMMIT_DEFAULT_TYPES_MCP = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"];
|
|
6797
6886
|
var _COMMIT_HEADER_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*)(\(([^()]*)\))?(!)?: (.+)$/;
|
|
6798
6887
|
var _COMMIT_FOOTER_LINE_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*|BREAKING CHANGE)(: ?| #)(.*)$/;
|
|
@@ -7006,6 +7095,73 @@ server.tool(
|
|
|
7006
7095
|
return text(JSON.stringify(result, null, 2));
|
|
7007
7096
|
}
|
|
7008
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
|
+
);
|
|
7009
7165
|
function _calculateRoiMcp(initialValue, finalValue, years) {
|
|
7010
7166
|
if (typeof initialValue !== "number" || !Number.isFinite(initialValue) || initialValue <= 0) {
|
|
7011
7167
|
return { error: "initialValue must be a finite number greater than zero" };
|
|
@@ -13507,6 +13663,117 @@ server.tool(
|
|
|
13507
13663
|
return text(JSON.stringify(result, null, 2));
|
|
13508
13664
|
}
|
|
13509
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
|
+
);
|
|
13510
13777
|
server.tool(
|
|
13511
13778
|
"find_cron_overlaps",
|
|
13512
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",
|