@utilix-tech/mcp 0.89.0 → 0.90.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,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.90.0
6
+ - 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
+ - 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.
8
+
5
9
  ## 0.89.0
6
10
  - 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
11
 
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 272 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 (49 tools)
173
173
  | Tool | Description |
174
174
  |------|-------------|
175
175
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -221,6 +221,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
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
223
  | `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
+ | `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
225
 
225
226
  ### API / Network (51 tools)
226
227
  | Tool | Description |
@@ -320,7 +321,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
320
321
  | `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
322
  | `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
323
 
323
- ### Data (18 tools)
324
+ ### Data (19 tools)
324
325
  | Tool | Description |
325
326
  |------|-------------|
326
327
  | `validate_yaml` | Validate YAML syntax |
@@ -341,6 +342,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
341
342
  | `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
343
  | `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
344
  | `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 |
345
+ | `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
346
 
345
347
  ### CSS (2 tools)
346
348
  | Tool | Description |
package/dist/index.js CHANGED
@@ -14189,5 +14189,253 @@ server.tool(
14189
14189
  return text(JSON.stringify(result, null, 2));
14190
14190
  }
14191
14191
  );
14192
+ var _SEMVER_RANGE_RE_MCP = /^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+?))?(?:\+([a-zA-Z0-9._-]+))?$/;
14193
+ function _parseSemverRangeMcp(version) {
14194
+ const m = version.trim().match(_SEMVER_RANGE_RE_MCP);
14195
+ if (!m) return { error: `Invalid semver: '${version}'` };
14196
+ const result = { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10) };
14197
+ if (m[4] !== void 0) result.prerelease = m[4];
14198
+ if (m[5] !== void 0) result.build = m[5];
14199
+ return result;
14200
+ }
14201
+ function _compareSemverPrereleaseMcp(a, b) {
14202
+ if (a === void 0 && b === void 0) return 0;
14203
+ if (a === void 0) return 1;
14204
+ if (b === void 0) return -1;
14205
+ const aParts = a.split(".");
14206
+ const bParts = b.split(".");
14207
+ const len = Math.max(aParts.length, bParts.length);
14208
+ for (let i = 0; i < len; i++) {
14209
+ const ap = aParts[i];
14210
+ const bp = bParts[i];
14211
+ if (ap === void 0) return -1;
14212
+ if (bp === void 0) return 1;
14213
+ const aNum = parseInt(ap, 10);
14214
+ const bNum = parseInt(bp, 10);
14215
+ const aIsNum = !isNaN(aNum) && String(aNum) === ap;
14216
+ const bIsNum = !isNaN(bNum) && String(bNum) === bp;
14217
+ if (aIsNum && bIsNum) {
14218
+ if (aNum < bNum) return -1;
14219
+ if (aNum > bNum) return 1;
14220
+ } else if (aIsNum) {
14221
+ return -1;
14222
+ } else if (bIsNum) {
14223
+ return 1;
14224
+ } else {
14225
+ if (ap < bp) return -1;
14226
+ if (ap > bp) return 1;
14227
+ }
14228
+ }
14229
+ return 0;
14230
+ }
14231
+ function _compareSemverRangeVersionsMcp(a, b) {
14232
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
14233
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
14234
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
14235
+ return _compareSemverPrereleaseMcp(a.prerelease, b.prerelease);
14236
+ }
14237
+ function _tightenSemverLowerMcp(a, b) {
14238
+ if (a.value === null) return b;
14239
+ if (b.value === null) return a;
14240
+ const cmp = _compareSemverRangeVersionsMcp(a.value, b.value);
14241
+ if (cmp > 0) return a;
14242
+ if (cmp < 0) return b;
14243
+ return { value: a.value, inclusive: a.inclusive && b.inclusive };
14244
+ }
14245
+ function _tightenSemverUpperMcp(a, b) {
14246
+ if (a.value === null) return b;
14247
+ if (b.value === null) return a;
14248
+ const cmp = _compareSemverRangeVersionsMcp(a.value, b.value);
14249
+ if (cmp < 0) return a;
14250
+ if (cmp > 0) return b;
14251
+ return { value: a.value, inclusive: a.inclusive && b.inclusive };
14252
+ }
14253
+ function _isValidSemverIntervalMcp(lower, upper) {
14254
+ if (lower.value === null || upper.value === null) return true;
14255
+ const cmp = _compareSemverRangeVersionsMcp(lower.value, upper.value);
14256
+ if (cmp < 0) return true;
14257
+ if (cmp > 0) return false;
14258
+ return lower.inclusive && upper.inclusive;
14259
+ }
14260
+ function _formatSemverRangeVersionMcp(v) {
14261
+ let s = `${v.major}.${v.minor}.${v.patch}`;
14262
+ if (v.prerelease) s += `-${v.prerelease}`;
14263
+ if (v.build) s += `+${v.build}`;
14264
+ return s;
14265
+ }
14266
+ function _semverIntervalToRangeStringMcp(interval) {
14267
+ const { lower, upper } = interval;
14268
+ if (lower.value === null && upper.value === null) return "*";
14269
+ if (lower.value !== null && upper.value !== null) {
14270
+ const cmp = _compareSemverRangeVersionsMcp(lower.value, upper.value);
14271
+ if (cmp === 0 && lower.inclusive && upper.inclusive) return _formatSemverRangeVersionMcp(lower.value);
14272
+ }
14273
+ const parts = [];
14274
+ if (lower.value !== null) parts.push(`${lower.inclusive ? ">=" : ">"}${_formatSemverRangeVersionMcp(lower.value)}`);
14275
+ if (upper.value !== null) parts.push(`${upper.inclusive ? "<=" : "<"}${_formatSemverRangeVersionMcp(upper.value)}`);
14276
+ return parts.length ? parts.join(" ") : "*";
14277
+ }
14278
+ function _parseSemverSingleConstraintMcp(range) {
14279
+ if (range === "*" || range === "") return { lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } };
14280
+ const caretMatch = range.match(/^\^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+))?$/);
14281
+ if (caretMatch) {
14282
+ const major = parseInt(caretMatch[1], 10);
14283
+ const minor = parseInt(caretMatch[2], 10);
14284
+ const patch = parseInt(caretMatch[3], 10);
14285
+ const pre = caretMatch[4];
14286
+ const lowerVal = { major, minor, patch, prerelease: pre };
14287
+ let upperVal;
14288
+ if (major !== 0) upperVal = { major: major + 1, minor: 0, patch: 0 };
14289
+ else if (minor !== 0) upperVal = { major: 0, minor: minor + 1, patch: 0 };
14290
+ else upperVal = { major: 0, minor: 0, patch: patch + 1 };
14291
+ return { lower: { value: lowerVal, inclusive: true }, upper: { value: upperVal, inclusive: false } };
14292
+ }
14293
+ const tildeMatch = range.match(/^~v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9._-]+))?$/);
14294
+ if (tildeMatch) {
14295
+ const major = parseInt(tildeMatch[1], 10);
14296
+ const minor = parseInt(tildeMatch[2], 10);
14297
+ const patch = parseInt(tildeMatch[3], 10);
14298
+ const pre = tildeMatch[4];
14299
+ const lowerVal = { major, minor, patch, prerelease: pre };
14300
+ const upperVal = { major, minor: minor + 1, patch: 0 };
14301
+ return { lower: { value: lowerVal, inclusive: true }, upper: { value: upperVal, inclusive: false } };
14302
+ }
14303
+ const opMatch = range.match(/^(>=|<=|>|<|=)v?(.+)$/);
14304
+ if (opMatch) {
14305
+ const op = opMatch[1];
14306
+ const parsed = _parseSemverRangeMcp(opMatch[2]);
14307
+ if ("error" in parsed) return parsed;
14308
+ if (op === ">=") return { lower: { value: parsed, inclusive: true }, upper: { value: null, inclusive: true } };
14309
+ if (op === "<=") return { lower: { value: null, inclusive: true }, upper: { value: parsed, inclusive: true } };
14310
+ if (op === ">") return { lower: { value: parsed, inclusive: false }, upper: { value: null, inclusive: true } };
14311
+ if (op === "<") return { lower: { value: null, inclusive: true }, upper: { value: parsed, inclusive: false } };
14312
+ return { lower: { value: parsed, inclusive: true }, upper: { value: parsed, inclusive: true } };
14313
+ }
14314
+ const plain = _parseSemverRangeMcp(range);
14315
+ if (!("error" in plain)) return { lower: { value: plain, inclusive: true }, upper: { value: plain, inclusive: true } };
14316
+ return { error: `Unsupported range: '${range}'` };
14317
+ }
14318
+ function _semverRangeToIntervalsMcp(range) {
14319
+ const trimmed = range.trim();
14320
+ if (trimmed === "") return [{ lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } }];
14321
+ const orParts = trimmed.split("||").map((s) => s.trim());
14322
+ const intervals = [];
14323
+ for (const orPart of orParts) {
14324
+ const andTokens = orPart.split(/\s+/).filter(Boolean);
14325
+ let combined = { lower: { value: null, inclusive: true }, upper: { value: null, inclusive: true } };
14326
+ for (const token of andTokens) {
14327
+ const parsed = _parseSemverSingleConstraintMcp(token);
14328
+ if ("error" in parsed) return parsed;
14329
+ combined = { lower: _tightenSemverLowerMcp(combined.lower, parsed.lower), upper: _tightenSemverUpperMcp(combined.upper, parsed.upper) };
14330
+ }
14331
+ intervals.push(combined);
14332
+ }
14333
+ return intervals;
14334
+ }
14335
+ function _intersectSemverRangesMcp(rangeA, rangeB) {
14336
+ const intervalsA = _semverRangeToIntervalsMcp(rangeA);
14337
+ if (!Array.isArray(intervalsA)) return intervalsA;
14338
+ const intervalsB = _semverRangeToIntervalsMcp(rangeB);
14339
+ if (!Array.isArray(intervalsB)) return intervalsB;
14340
+ const results = [];
14341
+ for (const a of intervalsA) {
14342
+ for (const b of intervalsB) {
14343
+ const lower = _tightenSemverLowerMcp(a.lower, b.lower);
14344
+ const upper = _tightenSemverUpperMcp(a.upper, b.upper);
14345
+ if (_isValidSemverIntervalMcp(lower, upper)) results.push({ lower, upper });
14346
+ }
14347
+ }
14348
+ if (results.length === 0) return { overlaps: false, intersection: null };
14349
+ const formatted = Array.from(new Set(results.map(_semverIntervalToRangeStringMcp)));
14350
+ return { overlaps: true, intersection: formatted.join(" || ") };
14351
+ }
14352
+ server.tool(
14353
+ "intersect_semver_ranges",
14354
+ "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.",
14355
+ {
14356
+ rangeA: z.string().describe('The first semver range, e.g. "^1.2.0"'),
14357
+ rangeB: z.string().describe('The second semver range, e.g. ">=1.5.0 <2.0.0"')
14358
+ },
14359
+ async ({ rangeA, rangeB }) => {
14360
+ const result = _intersectSemverRangesMcp(rangeA, rangeB);
14361
+ if ("error" in result) return errText(result);
14362
+ return text(JSON.stringify(result, null, 2));
14363
+ }
14364
+ );
14365
+ function _stripInlineMarkdownForLinksMcp(text2) {
14366
+ return text2.replace(/(\*\*\*|___)(.+?)\1/g, "$2").replace(/(\*\*|__)(.+?)\1/g, "$2").replace(/(\*|_)(.+?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").trim();
14367
+ }
14368
+ function _slugifyHeadingForLinksMcp(text2, seen) {
14369
+ let slug = text2.toLowerCase().trim().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
14370
+ const count = seen.get(slug) ?? 0;
14371
+ seen.set(slug, count + 1);
14372
+ if (count > 0) slug = `${slug}-${count}`;
14373
+ return slug;
14374
+ }
14375
+ var _ANCHOR_LINK_RE_MCP = /\[([^\]]*)\]\(#([^)\s]+)\)/g;
14376
+ function _checkMarkdownInternalLinksMcp(markdown) {
14377
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
14378
+ const headings = [];
14379
+ const seen = /* @__PURE__ */ new Map();
14380
+ let inFence = false;
14381
+ let fenceMarker = "";
14382
+ for (let i = 0; i < lines.length; i++) {
14383
+ const line = lines[i];
14384
+ const fenceMatch = line.match(/^\s*(```+|~~~+)/);
14385
+ if (fenceMatch) {
14386
+ if (!inFence) {
14387
+ inFence = true;
14388
+ fenceMarker = fenceMatch[1][0];
14389
+ } else if (fenceMatch[1][0] === fenceMarker) {
14390
+ inFence = false;
14391
+ }
14392
+ continue;
14393
+ }
14394
+ if (inFence) continue;
14395
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
14396
+ if (!headingMatch) continue;
14397
+ const level = headingMatch[1].length;
14398
+ const text2 = _stripInlineMarkdownForLinksMcp(headingMatch[2]);
14399
+ if (text2 === "") continue;
14400
+ const slug = _slugifyHeadingForLinksMcp(text2, seen);
14401
+ headings.push({ level, text: text2, slug, line: i + 1 });
14402
+ }
14403
+ const validSlugs = new Set(headings.map((h) => h.slug));
14404
+ const links = [];
14405
+ inFence = false;
14406
+ fenceMarker = "";
14407
+ for (let i = 0; i < lines.length; i++) {
14408
+ const line = lines[i];
14409
+ const fenceMatch = line.match(/^\s*(```+|~~~+)/);
14410
+ if (fenceMatch) {
14411
+ if (!inFence) {
14412
+ inFence = true;
14413
+ fenceMarker = fenceMatch[1][0];
14414
+ } else if (fenceMatch[1][0] === fenceMarker) {
14415
+ inFence = false;
14416
+ }
14417
+ continue;
14418
+ }
14419
+ if (inFence) continue;
14420
+ const re = new RegExp(_ANCHOR_LINK_RE_MCP);
14421
+ let m;
14422
+ while ((m = re.exec(line)) !== null) {
14423
+ const target = m[2];
14424
+ links.push({ text: m[1], target, line: i + 1, valid: validSlugs.has(target) });
14425
+ }
14426
+ }
14427
+ return { headings, links, brokenLinks: links.filter((l) => !l.valid) };
14428
+ }
14429
+ server.tool(
14430
+ "check_markdown_internal_links",
14431
+ "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).",
14432
+ {
14433
+ markdown: z.string().describe("The Markdown document to scan")
14434
+ },
14435
+ async ({ markdown }) => {
14436
+ const result = _checkMarkdownInternalLinksMcp(markdown);
14437
+ return text(JSON.stringify(result, null, 2));
14438
+ }
14439
+ );
14192
14440
  var transport = new StdioServerTransport();
14193
14441
  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.90.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",