@utilix-tech/mcp 0.88.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,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.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
+
9
+ ## 0.89.0
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.
11
+
5
12
  ## 0.88.0
6
13
  - 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
14
  - 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
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
 
@@ -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 |
@@ -168,7 +169,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
168
169
  | `convert_timezone` | Convert a date/time between timezones |
169
170
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
170
171
 
171
- ### Units (48 tools)
172
+ ### Units (49 tools)
172
173
  | Tool | Description |
173
174
  |------|-------------|
174
175
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -220,6 +221,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
220
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 |
221
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 |
222
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 |
223
225
 
224
226
  ### API / Network (51 tools)
225
227
  | Tool | Description |
@@ -319,7 +321,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
319
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 |
320
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 |
321
323
 
322
- ### Data (18 tools)
324
+ ### Data (19 tools)
323
325
  | Tool | Description |
324
326
  |------|-------------|
325
327
  | `validate_yaml` | Validate YAML syntax |
@@ -340,6 +342,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
340
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 |
341
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 |
342
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 |
343
346
 
344
347
  ### CSS (2 tools)
345
348
  | Tool | Description |
package/dist/index.js CHANGED
@@ -13944,6 +13944,76 @@ server.tool(
13944
13944
  return text(JSON.stringify(_checkDashUsageMcp(input), null, 2));
13945
13945
  }
13946
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
+ );
13947
14017
  var _CommissionTierSchema = z.object({
13948
14018
  upTo: z.number().nullable().describe("Upper bound of this tier (exclusive). null means unbounded and is only valid on the last tier."),
13949
14019
  ratePercent: z.number().min(0).describe("Commission rate as a percent applied to the slice of salesAmount within this tier")
@@ -14119,5 +14189,253 @@ server.tool(
14119
14189
  return text(JSON.stringify(result, null, 2));
14120
14190
  }
14121
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
+ );
14122
14440
  var transport = new StdioServerTransport();
14123
14441
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.88.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.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",