@utilix-tech/mcp 0.92.0 → 0.93.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.93.0
6
+ - Add `find_duplicate_lockfile_versions` tool: parse an npm, yarn, or pnpm lockfile and report every package installed at more than one resolved version, with a count of install locations per version.
7
+ - Add `summarize_git_diff_stat` tool: parse `git diff --stat` / `git log --stat` output into structured per-file insertion/deletion counts plus overall totals, flagging a scaled-down bar's split as approximate.
8
+
5
9
  ## 0.92.0
6
10
  - Add `build_clear_site_data_header`/`parse_clear_site_data_header` tools: build a `Clear-Site-Data` response header from directives (cache, cookies, storage, executionContexts, or `*` for all of them), flagging redundant directives alongside the wildcard, or parse an existing header back into its directives.
7
11
  - Add `summarize_csv_column_stats` tool: parse CSV/TSV data and compute per-column null/unique counts plus min/max/mean/median/standard deviation for numeric columns, or the most common value for categorical ones.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 277 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 279 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
 
@@ -292,7 +292,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
292
292
  | `color_palette` | Generate a color palette from a base color |
293
293
  | `color_shades` | Generate shades or tints from a base color |
294
294
 
295
- ### Code (27 tools)
295
+ ### Code (29 tools)
296
296
  | Tool | Description |
297
297
  |------|-------------|
298
298
  | `format_sql` | Format a SQL query with proper indentation |
@@ -323,6 +323,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
323
323
  | `count_sql_placeholders` | Count positional (`?`, `$1`) and named (`:name`, `@name`) parameter placeholders in a SQL query, flagging mixed styles and numbered-sequence gaps or duplicates |
324
324
  | `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 |
325
325
  | `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 |
326
+ | `find_duplicate_lockfile_versions` | Parse an npm, yarn, or pnpm lockfile and report every package installed at more than one resolved version, with a count of install locations per version |
327
+ | `summarize_git_diff_stat` | Parse `git diff --stat` / `git log --stat` output into structured per-file insertion/deletion counts plus overall totals |
326
328
 
327
329
  ### Data (21 tools)
328
330
  | Tool | Description |
package/dist/index.js CHANGED
@@ -14732,5 +14732,226 @@ server.tool(
14732
14732
  return text(JSON.stringify(result, null, 2));
14733
14733
  }
14734
14734
  );
14735
+ function _detectLockfileFormatMcp(input) {
14736
+ const trimmed = input.trim();
14737
+ if (trimmed.startsWith("{")) return "npm";
14738
+ if (/^lockfileVersion:/m.test(trimmed) || /^packages:\s*$/m.test(trimmed)) return "pnpm";
14739
+ if (/yarn lockfile v\d/i.test(trimmed)) return "yarn";
14740
+ if (/^\s*version\s+"[^"]+"/m.test(trimmed) && /@[^\s",]+["']?\s*:?\s*$/m.test(trimmed)) return "yarn";
14741
+ return null;
14742
+ }
14743
+ function _parseNpmLockfileMcp(input) {
14744
+ const data = JSON.parse(input);
14745
+ const entries = [];
14746
+ const packages = data.packages;
14747
+ if (packages && typeof packages === "object" && !Array.isArray(packages)) {
14748
+ for (const [path, metaRaw] of Object.entries(packages)) {
14749
+ if (path === "" || !metaRaw || typeof metaRaw !== "object") continue;
14750
+ const meta = metaRaw;
14751
+ if (typeof meta.version !== "string") continue;
14752
+ const idx = path.lastIndexOf("node_modules/");
14753
+ const name = idx === -1 ? path : path.slice(idx + "node_modules/".length);
14754
+ entries.push({ name, version: meta.version });
14755
+ }
14756
+ return entries;
14757
+ }
14758
+ const dependencies = data.dependencies;
14759
+ if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
14760
+ const walk = (deps) => {
14761
+ for (const [name, metaRaw] of Object.entries(deps)) {
14762
+ if (!metaRaw || typeof metaRaw !== "object") continue;
14763
+ const meta = metaRaw;
14764
+ if (typeof meta.version === "string") entries.push({ name, version: meta.version });
14765
+ if (meta.dependencies && typeof meta.dependencies === "object") walk(meta.dependencies);
14766
+ }
14767
+ };
14768
+ walk(dependencies);
14769
+ }
14770
+ return entries;
14771
+ }
14772
+ function _splitYarnKeyMcp(key) {
14773
+ const trimmedKey = key.trim().replace(/^"|"$/g, "");
14774
+ const idx = trimmedKey.lastIndexOf("@");
14775
+ if (idx <= 0) return null;
14776
+ return { name: trimmedKey.slice(0, idx), range: trimmedKey.slice(idx + 1) };
14777
+ }
14778
+ function _parseYarnLockfileMcp(input) {
14779
+ const entries = [];
14780
+ const blocks = input.replace(/\r\n/g, "\n").split(/\n\n+/);
14781
+ for (const block of blocks) {
14782
+ const lines = block.split("\n");
14783
+ const headerLines = [];
14784
+ let i = 0;
14785
+ while (i < lines.length && lines[i].trim() !== "" && !lines[i].startsWith(" ") && !lines[i].startsWith("#")) {
14786
+ headerLines.push(lines[i]);
14787
+ i++;
14788
+ }
14789
+ if (headerLines.length === 0) continue;
14790
+ const header = headerLines.join(" ").replace(/:\s*$/, "");
14791
+ const keys = header.split(",").map((k) => k.trim()).filter(Boolean);
14792
+ const versionLine = lines.find((l) => /^\s+version\s+"/.test(l));
14793
+ if (!versionLine) continue;
14794
+ const versionMatch = versionLine.match(/version\s+"([^"]+)"/);
14795
+ if (!versionMatch) continue;
14796
+ const version = versionMatch[1];
14797
+ const names = /* @__PURE__ */ new Set();
14798
+ for (const key of keys) {
14799
+ const split = _splitYarnKeyMcp(key);
14800
+ if (split) names.add(split.name);
14801
+ }
14802
+ for (const name of names) entries.push({ name, version });
14803
+ }
14804
+ return entries;
14805
+ }
14806
+ function _parsePnpmPackageKeyMcp(rawKey) {
14807
+ let key = rawKey.trim();
14808
+ if (key.startsWith("/")) key = key.slice(1);
14809
+ const parenIdx = key.indexOf("(");
14810
+ if (parenIdx !== -1) key = key.slice(0, parenIdx);
14811
+ const idx = key.lastIndexOf("@");
14812
+ if (idx <= 0) return null;
14813
+ const name = key.slice(0, idx);
14814
+ const version = key.slice(idx + 1).trim();
14815
+ if (!version) return null;
14816
+ return { name, version };
14817
+ }
14818
+ function _parsePnpmLockfileMcp(input) {
14819
+ const data = yaml.load(input);
14820
+ const entries = [];
14821
+ const packages = data?.packages;
14822
+ if (!packages || typeof packages !== "object" || Array.isArray(packages)) return entries;
14823
+ for (const rawKey of Object.keys(packages)) {
14824
+ const parsed = _parsePnpmPackageKeyMcp(rawKey);
14825
+ if (parsed) entries.push(parsed);
14826
+ }
14827
+ return entries;
14828
+ }
14829
+ function _findDuplicateLockfileVersionsMcp(input, format) {
14830
+ if (!input || !input.trim()) return { error: "Input is empty" };
14831
+ const detected = format ?? _detectLockfileFormatMcp(input);
14832
+ if (!detected) {
14833
+ return { error: "Could not detect lockfile format; expected an npm package-lock.json, a yarn.lock, or a pnpm-lock.yaml" };
14834
+ }
14835
+ let rawEntries;
14836
+ try {
14837
+ if (detected === "npm") rawEntries = _parseNpmLockfileMcp(input);
14838
+ else if (detected === "yarn") rawEntries = _parseYarnLockfileMcp(input);
14839
+ else rawEntries = _parsePnpmLockfileMcp(input);
14840
+ } catch (e) {
14841
+ return { error: `Failed to parse ${detected} lockfile: ${e.message}` };
14842
+ }
14843
+ if (rawEntries.length === 0) {
14844
+ return { error: `No packages found in ${detected} lockfile. If this is a pnpm lockfile older than v6 (slash-separated versions instead of "name@version"), re-generate it with a current pnpm version first.` };
14845
+ }
14846
+ const byName = /* @__PURE__ */ new Map();
14847
+ for (const { name, version } of rawEntries) {
14848
+ let versions = byName.get(name);
14849
+ if (!versions) {
14850
+ versions = /* @__PURE__ */ new Map();
14851
+ byName.set(name, versions);
14852
+ }
14853
+ versions.set(version, (versions.get(version) ?? 0) + 1);
14854
+ }
14855
+ const packages = [];
14856
+ for (const [name, versions] of byName) {
14857
+ const versionList = [...versions.entries()].map(([version, count]) => ({ version, count })).sort((a, b) => b.count - a.count || a.version.localeCompare(b.version));
14858
+ packages.push({ name, versions: versionList, isDuplicate: versionList.length > 1 });
14859
+ }
14860
+ packages.sort((a, b) => a.name.localeCompare(b.name));
14861
+ const duplicatePackages = packages.filter((p) => p.isDuplicate);
14862
+ return { format: detected, totalPackages: packages.length, duplicatePackages, packages };
14863
+ }
14864
+ server.tool(
14865
+ "find_duplicate_lockfile_versions",
14866
+ "Parse an npm (package-lock.json), yarn (yarn.lock), or pnpm (pnpm-lock.yaml) lockfile and report every package installed at more than one resolved version, with a count of how many install locations resolve to each version. Format is auto-detected from content unless given explicitly.",
14867
+ {
14868
+ lockfile: z.string().min(1).describe("The full lockfile contents"),
14869
+ format: z.enum(["npm", "yarn", "pnpm"]).optional().describe("Force a format instead of auto-detecting it from content")
14870
+ },
14871
+ async ({ lockfile, format }) => {
14872
+ const result = _findDuplicateLockfileVersionsMcp(lockfile, format);
14873
+ if ("error" in result) return errText(result);
14874
+ return text(JSON.stringify(result, null, 2));
14875
+ }
14876
+ );
14877
+ var _GIT_DIFF_STAT_FILE_RE_MCP = /^\s*(.+?)\s*\|\s*(Bin|\d+)\s*(.*)$/;
14878
+ var _GIT_DIFF_STAT_SUMMARY_RE_MCP = /^\s*(\d+)\s+files?\s+changed(?:,\s*(\d+)\s+insertions?\(\+\))?(?:,\s*(\d+)\s+deletions?\(-\))?/;
14879
+ function _summarizeGitDiffStatMcp(input) {
14880
+ if (!input || !input.trim()) return { error: "Input is empty" };
14881
+ const lines = input.replace(/\r\n/g, "\n").split("\n");
14882
+ const files = [];
14883
+ let summaryMatch = null;
14884
+ for (const line of lines) {
14885
+ if (!line.trim()) continue;
14886
+ const sm = line.match(_GIT_DIFF_STAT_SUMMARY_RE_MCP);
14887
+ if (sm) {
14888
+ summaryMatch = sm;
14889
+ continue;
14890
+ }
14891
+ const m = line.match(_GIT_DIFF_STAT_FILE_RE_MCP);
14892
+ if (!m) continue;
14893
+ const rawName = m[1];
14894
+ const countRaw = m[2];
14895
+ const bar = m[3];
14896
+ let file = rawName;
14897
+ let oldFile;
14898
+ const renameMatch = rawName.match(/^(.*?)\s*=>\s*(.*)$/);
14899
+ if (renameMatch) {
14900
+ oldFile = renameMatch[1].trim();
14901
+ file = renameMatch[2].trim();
14902
+ }
14903
+ if (countRaw === "Bin") {
14904
+ files.push({ file, oldFile, changes: null, insertions: null, deletions: null, binary: true, approximate: false });
14905
+ continue;
14906
+ }
14907
+ const changes = parseInt(countRaw, 10);
14908
+ const plusCount = (bar.match(/\+/g) ?? []).length;
14909
+ const minusCount = (bar.match(/-/g) ?? []).length;
14910
+ const barTotal = plusCount + minusCount;
14911
+ let insertions;
14912
+ let deletions;
14913
+ let approximate = false;
14914
+ if (barTotal === changes) {
14915
+ insertions = plusCount;
14916
+ deletions = minusCount;
14917
+ } else if (barTotal > 0) {
14918
+ approximate = true;
14919
+ insertions = Math.round(changes * plusCount / barTotal);
14920
+ deletions = changes - insertions;
14921
+ } else if (changes === 0) {
14922
+ insertions = 0;
14923
+ deletions = 0;
14924
+ } else {
14925
+ approximate = true;
14926
+ insertions = null;
14927
+ deletions = null;
14928
+ }
14929
+ files.push({ file, oldFile, changes, insertions, deletions, binary: false, approximate });
14930
+ }
14931
+ if (files.length === 0) {
14932
+ return { error: "No file stat lines found; expected output from `git diff --stat` or `git log --stat`" };
14933
+ }
14934
+ let filesChanged = files.length;
14935
+ let totalInsertions = files.reduce((sum, f) => sum + (f.insertions ?? 0), 0);
14936
+ let totalDeletions = files.reduce((sum, f) => sum + (f.deletions ?? 0), 0);
14937
+ if (summaryMatch) {
14938
+ filesChanged = parseInt(summaryMatch[1], 10);
14939
+ if (summaryMatch[2]) totalInsertions = parseInt(summaryMatch[2], 10);
14940
+ if (summaryMatch[3]) totalDeletions = parseInt(summaryMatch[3], 10);
14941
+ }
14942
+ return { files, filesChanged, totalInsertions, totalDeletions };
14943
+ }
14944
+ server.tool(
14945
+ "summarize_git_diff_stat",
14946
+ "Parse the per-file lines and trailing summary line of `git diff --stat` or `git log --stat` output into structured per-file insertion/deletion counts plus overall totals. Once a diff is wide enough that git scales the bar down, the per-file split is a proportional estimate (flagged via approximate) rather than an exact count, though the overall totals stay exact.",
14947
+ {
14948
+ diffStat: z.string().min(1).describe("Output of `git diff --stat` or `git log --stat`")
14949
+ },
14950
+ async ({ diffStat }) => {
14951
+ const result = _summarizeGitDiffStatMcp(diffStat);
14952
+ if ("error" in result) return errText(result);
14953
+ return text(JSON.stringify(result, null, 2));
14954
+ }
14955
+ );
14735
14956
  var transport = new StdioServerTransport();
14736
14957
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.92.0",
4
- "description": "MCP server exposing 277 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.93.0",
4
+ "description": "MCP server exposing 279 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",