@utilix-tech/mcp 0.89.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 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.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
+
9
+ ## 0.90.0
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.
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.
12
+
5
13
  ## 0.89.0
6
14
  - 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
15
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
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 (48 tools)
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,7 +220,9 @@ 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 |
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 |
224
226
 
225
227
  ### API / Network (51 tools)
226
228
  | Tool | Description |
@@ -320,11 +322,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
320
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 |
321
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 |
322
324
 
323
- ### Data (18 tools)
325
+ ### Data (20 tools)
324
326
  | Tool | Description |
325
327
  |------|-------------|
326
328
  | `validate_yaml` | Validate YAML syntax |
327
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 |
328
331
  | `toml_to_json` | Convert TOML to JSON |
329
332
  | `xml_to_json` | Convert XML to JSON |
330
333
  | `parse_csv` | Parse a CSV string and return the data as JSON |
@@ -341,6 +344,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
341
344
  | `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 |
342
345
  | `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 |
343
346
  | `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 |
347
+ | `check_markdown_internal_links` | Scan a Markdown document's ATX headings, compute the GitHub-style anchor slug for each, and check every [text](#slug) internal link against them, flagging links that point at a heading that doesn't exist |
344
348
 
345
349
  ### CSS (2 tools)
346
350
  | Tool | Description |
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",
@@ -14189,5 +14312,253 @@ server.tool(
14189
14312
  return text(JSON.stringify(result, null, 2));
14190
14313
  }
14191
14314
  );
14315
+ var _SEMVER_RANGE_RE_MCP = /^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+?))?(?:\+([a-zA-Z0-9._-]+))?$/;
14316
+ function _parseSemverRangeMcp(version) {
14317
+ const m = version.trim().match(_SEMVER_RANGE_RE_MCP);
14318
+ if (!m) return { error: `Invalid semver: '${version}'` };
14319
+ const result = { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10) };
14320
+ if (m[4] !== void 0) result.prerelease = m[4];
14321
+ if (m[5] !== void 0) result.build = m[5];
14322
+ return result;
14323
+ }
14324
+ function _compareSemverPrereleaseMcp(a, b) {
14325
+ if (a === void 0 && b === void 0) return 0;
14326
+ if (a === void 0) return 1;
14327
+ if (b === void 0) return -1;
14328
+ const aParts = a.split(".");
14329
+ const bParts = b.split(".");
14330
+ const len = Math.max(aParts.length, bParts.length);
14331
+ for (let i = 0; i < len; i++) {
14332
+ const ap = aParts[i];
14333
+ const bp = bParts[i];
14334
+ if (ap === void 0) return -1;
14335
+ if (bp === void 0) return 1;
14336
+ const aNum = parseInt(ap, 10);
14337
+ const bNum = parseInt(bp, 10);
14338
+ const aIsNum = !isNaN(aNum) && String(aNum) === ap;
14339
+ const bIsNum = !isNaN(bNum) && String(bNum) === bp;
14340
+ if (aIsNum && bIsNum) {
14341
+ if (aNum < bNum) return -1;
14342
+ if (aNum > bNum) return 1;
14343
+ } else if (aIsNum) {
14344
+ return -1;
14345
+ } else if (bIsNum) {
14346
+ return 1;
14347
+ } else {
14348
+ if (ap < bp) return -1;
14349
+ if (ap > bp) return 1;
14350
+ }
14351
+ }
14352
+ return 0;
14353
+ }
14354
+ function _compareSemverRangeVersionsMcp(a, b) {
14355
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
14356
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
14357
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
14358
+ return _compareSemverPrereleaseMcp(a.prerelease, b.prerelease);
14359
+ }
14360
+ function _tightenSemverLowerMcp(a, b) {
14361
+ if (a.value === null) return b;
14362
+ if (b.value === null) return a;
14363
+ const cmp = _compareSemverRangeVersionsMcp(a.value, b.value);
14364
+ if (cmp > 0) return a;
14365
+ if (cmp < 0) return b;
14366
+ return { value: a.value, inclusive: a.inclusive && b.inclusive };
14367
+ }
14368
+ function _tightenSemverUpperMcp(a, b) {
14369
+ if (a.value === null) return b;
14370
+ if (b.value === null) return a;
14371
+ const cmp = _compareSemverRangeVersionsMcp(a.value, b.value);
14372
+ if (cmp < 0) return a;
14373
+ if (cmp > 0) return b;
14374
+ return { value: a.value, inclusive: a.inclusive && b.inclusive };
14375
+ }
14376
+ function _isValidSemverIntervalMcp(lower, upper) {
14377
+ if (lower.value === null || upper.value === null) return true;
14378
+ const cmp = _compareSemverRangeVersionsMcp(lower.value, upper.value);
14379
+ if (cmp < 0) return true;
14380
+ if (cmp > 0) return false;
14381
+ return lower.inclusive && upper.inclusive;
14382
+ }
14383
+ function _formatSemverRangeVersionMcp(v) {
14384
+ let s = `${v.major}.${v.minor}.${v.patch}`;
14385
+ if (v.prerelease) s += `-${v.prerelease}`;
14386
+ if (v.build) s += `+${v.build}`;
14387
+ return s;
14388
+ }
14389
+ function _semverIntervalToRangeStringMcp(interval) {
14390
+ const { lower, upper } = interval;
14391
+ if (lower.value === null && upper.value === null) return "*";
14392
+ if (lower.value !== null && upper.value !== null) {
14393
+ const cmp = _compareSemverRangeVersionsMcp(lower.value, upper.value);
14394
+ if (cmp === 0 && lower.inclusive && upper.inclusive) return _formatSemverRangeVersionMcp(lower.value);
14395
+ }
14396
+ const parts = [];
14397
+ if (lower.value !== null) parts.push(`${lower.inclusive ? ">=" : ">"}${_formatSemverRangeVersionMcp(lower.value)}`);
14398
+ if (upper.value !== null) parts.push(`${upper.inclusive ? "<=" : "<"}${_formatSemverRangeVersionMcp(upper.value)}`);
14399
+ return parts.length ? parts.join(" ") : "*";
14400
+ }
14401
+ function _parseSemverSingleConstraintMcp(range) {
14402
+ if (range === "*" || range === "") return { lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } };
14403
+ const caretMatch = range.match(/^\^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+))?$/);
14404
+ if (caretMatch) {
14405
+ const major = parseInt(caretMatch[1], 10);
14406
+ const minor = parseInt(caretMatch[2], 10);
14407
+ const patch = parseInt(caretMatch[3], 10);
14408
+ const pre = caretMatch[4];
14409
+ const lowerVal = { major, minor, patch, prerelease: pre };
14410
+ let upperVal;
14411
+ if (major !== 0) upperVal = { major: major + 1, minor: 0, patch: 0 };
14412
+ else if (minor !== 0) upperVal = { major: 0, minor: minor + 1, patch: 0 };
14413
+ else upperVal = { major: 0, minor: 0, patch: patch + 1 };
14414
+ return { lower: { value: lowerVal, inclusive: true }, upper: { value: upperVal, inclusive: false } };
14415
+ }
14416
+ const tildeMatch = range.match(/^~v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+))?$/);
14417
+ if (tildeMatch) {
14418
+ const major = parseInt(tildeMatch[1], 10);
14419
+ const minor = parseInt(tildeMatch[2], 10);
14420
+ const patch = parseInt(tildeMatch[3], 10);
14421
+ const pre = tildeMatch[4];
14422
+ const lowerVal = { major, minor, patch, prerelease: pre };
14423
+ const upperVal = { major, minor: minor + 1, patch: 0 };
14424
+ return { lower: { value: lowerVal, inclusive: true }, upper: { value: upperVal, inclusive: false } };
14425
+ }
14426
+ const opMatch = range.match(/^(>=|<=|>|<|=)v?(.+)$/);
14427
+ if (opMatch) {
14428
+ const op = opMatch[1];
14429
+ const parsed = _parseSemverRangeMcp(opMatch[2]);
14430
+ if ("error" in parsed) return parsed;
14431
+ if (op === ">=") return { lower: { value: parsed, inclusive: true }, upper: { value: null, inclusive: true } };
14432
+ if (op === "<=") return { lower: { value: null, inclusive: true }, upper: { value: parsed, inclusive: true } };
14433
+ if (op === ">") return { lower: { value: parsed, inclusive: false }, upper: { value: null, inclusive: true } };
14434
+ if (op === "<") return { lower: { value: null, inclusive: true }, upper: { value: parsed, inclusive: false } };
14435
+ return { lower: { value: parsed, inclusive: true }, upper: { value: parsed, inclusive: true } };
14436
+ }
14437
+ const plain = _parseSemverRangeMcp(range);
14438
+ if (!("error" in plain)) return { lower: { value: plain, inclusive: true }, upper: { value: plain, inclusive: true } };
14439
+ return { error: `Unsupported range: '${range}'` };
14440
+ }
14441
+ function _semverRangeToIntervalsMcp(range) {
14442
+ const trimmed = range.trim();
14443
+ if (trimmed === "") return [{ lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } }];
14444
+ const orParts = trimmed.split("||").map((s) => s.trim());
14445
+ const intervals = [];
14446
+ for (const orPart of orParts) {
14447
+ const andTokens = orPart.split(/\s+/).filter(Boolean);
14448
+ let combined = { lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } };
14449
+ for (const token of andTokens) {
14450
+ const parsed = _parseSemverSingleConstraintMcp(token);
14451
+ if ("error" in parsed) return parsed;
14452
+ combined = { lower: _tightenSemverLowerMcp(combined.lower, parsed.lower), upper: _tightenSemverUpperMcp(combined.upper, parsed.upper) };
14453
+ }
14454
+ intervals.push(combined);
14455
+ }
14456
+ return intervals;
14457
+ }
14458
+ function _intersectSemverRangesMcp(rangeA, rangeB) {
14459
+ const intervalsA = _semverRangeToIntervalsMcp(rangeA);
14460
+ if (!Array.isArray(intervalsA)) return intervalsA;
14461
+ const intervalsB = _semverRangeToIntervalsMcp(rangeB);
14462
+ if (!Array.isArray(intervalsB)) return intervalsB;
14463
+ const results = [];
14464
+ for (const a of intervalsA) {
14465
+ for (const b of intervalsB) {
14466
+ const lower = _tightenSemverLowerMcp(a.lower, b.lower);
14467
+ const upper = _tightenSemverUpperMcp(a.upper, b.upper);
14468
+ if (_isValidSemverIntervalMcp(lower, upper)) results.push({ lower, upper });
14469
+ }
14470
+ }
14471
+ if (results.length === 0) return { overlaps: false, intersection: null };
14472
+ const formatted = Array.from(new Set(results.map(_semverIntervalToRangeStringMcp)));
14473
+ return { overlaps: true, intersection: formatted.join(" || ") };
14474
+ }
14475
+ server.tool(
14476
+ "intersect_semver_ranges",
14477
+ "Determine whether two semver ranges overlap and, if so, the intersecting range. Supports the same caret/tilde/comparison-operator/exact-version syntax (combined with || or space-separated AND) as the semver tool.",
14478
+ {
14479
+ rangeA: z.string().describe('The first semver range, e.g. "^1.2.0"'),
14480
+ rangeB: z.string().describe('The second semver range, e.g. ">=1.5.0 <2.0.0"')
14481
+ },
14482
+ async ({ rangeA, rangeB }) => {
14483
+ const result = _intersectSemverRangesMcp(rangeA, rangeB);
14484
+ if ("error" in result) return errText(result);
14485
+ return text(JSON.stringify(result, null, 2));
14486
+ }
14487
+ );
14488
+ function _stripInlineMarkdownForLinksMcp(text2) {
14489
+ return text2.replace(/(\*\*\*|___)(.+?)\1/g, "$2").replace(/(\*\*|__)(.+?)\1/g, "$2").replace(/(\*|_)(.+?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").trim();
14490
+ }
14491
+ function _slugifyHeadingForLinksMcp(text2, seen) {
14492
+ let slug = text2.toLowerCase().trim().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
14493
+ const count = seen.get(slug) ?? 0;
14494
+ seen.set(slug, count + 1);
14495
+ if (count > 0) slug = `${slug}-${count}`;
14496
+ return slug;
14497
+ }
14498
+ var _ANCHOR_LINK_RE_MCP = /\[([^\]]*)\]\(#([^)\s]+)\)/g;
14499
+ function _checkMarkdownInternalLinksMcp(markdown) {
14500
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
14501
+ const headings = [];
14502
+ const seen = /* @__PURE__ */ new Map();
14503
+ let inFence = false;
14504
+ let fenceMarker = "";
14505
+ for (let i = 0; i < lines.length; i++) {
14506
+ const line = lines[i];
14507
+ const fenceMatch = line.match(/^\s*(```+|~~~+)/);
14508
+ if (fenceMatch) {
14509
+ if (!inFence) {
14510
+ inFence = true;
14511
+ fenceMarker = fenceMatch[1][0];
14512
+ } else if (fenceMatch[1][0] === fenceMarker) {
14513
+ inFence = false;
14514
+ }
14515
+ continue;
14516
+ }
14517
+ if (inFence) continue;
14518
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
14519
+ if (!headingMatch) continue;
14520
+ const level = headingMatch[1].length;
14521
+ const text2 = _stripInlineMarkdownForLinksMcp(headingMatch[2]);
14522
+ if (text2 === "") continue;
14523
+ const slug = _slugifyHeadingForLinksMcp(text2, seen);
14524
+ headings.push({ level, text: text2, slug, line: i + 1 });
14525
+ }
14526
+ const validSlugs = new Set(headings.map((h) => h.slug));
14527
+ const links = [];
14528
+ inFence = false;
14529
+ fenceMarker = "";
14530
+ for (let i = 0; i < lines.length; i++) {
14531
+ const line = lines[i];
14532
+ const fenceMatch = line.match(/^\s*(```+|~~~+)/);
14533
+ if (fenceMatch) {
14534
+ if (!inFence) {
14535
+ inFence = true;
14536
+ fenceMarker = fenceMatch[1][0];
14537
+ } else if (fenceMatch[1][0] === fenceMarker) {
14538
+ inFence = false;
14539
+ }
14540
+ continue;
14541
+ }
14542
+ if (inFence) continue;
14543
+ const re = new RegExp(_ANCHOR_LINK_RE_MCP);
14544
+ let m;
14545
+ while ((m = re.exec(line)) !== null) {
14546
+ const target = m[2];
14547
+ links.push({ text: m[1], target, line: i + 1, valid: validSlugs.has(target) });
14548
+ }
14549
+ }
14550
+ return { headings, links, brokenLinks: links.filter((l) => !l.valid) };
14551
+ }
14552
+ server.tool(
14553
+ "check_markdown_internal_links",
14554
+ "Scan a Markdown document's ATX headings, compute the GitHub-style anchor slug for each, and check every [text](#slug) internal link against them, flagging links that point at a heading that doesn't exist (or was renamed).",
14555
+ {
14556
+ markdown: z.string().describe("The Markdown document to scan")
14557
+ },
14558
+ async ({ markdown }) => {
14559
+ const result = _checkMarkdownInternalLinksMcp(markdown);
14560
+ return text(JSON.stringify(result, null, 2));
14561
+ }
14562
+ );
14192
14563
  var transport = new StdioServerTransport();
14193
14564
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
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.",
3
+ "version": "0.91.0",
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",
7
7
  "homepage": "https://utilix.tech/mcp",