@utilix-tech/mcp 0.91.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 +8 -0
- package/README.md +9 -4
- package/dist/index.js +393 -0
- package/package.json +2 -2
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.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
|
+
|
|
9
|
+
## 0.92.0
|
|
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.
|
|
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.
|
|
12
|
+
|
|
5
13
|
## 0.91.0
|
|
6
14
|
- 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
15
|
- 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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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
|
|
|
@@ -224,7 +224,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
224
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
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 |
|
|
226
226
|
|
|
227
|
-
### API / Network (
|
|
227
|
+
### API / Network (53 tools)
|
|
228
228
|
| Tool | Description |
|
|
229
229
|
|------|-------------|
|
|
230
230
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -281,6 +281,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
281
281
|
| `calculate_cookie_size_budget` | Parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's 4096-byte per-cookie guarantee and a configurable budget |
|
|
282
282
|
| `verify_webhook_signature` | Recompute an HMAC over a raw webhook payload and compare it to a provided signature, accepting a `sha256=` prefix style and matching against hex or base64 encoding |
|
|
283
283
|
| `score_security_headers` | Score a pasted block of raw HTTP response headers against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*), with an overall 0-100 score and A-F letter grade |
|
|
284
|
+
| `build_clear_site_data_header` | Build a Clear-Site-Data response header from directives (cache, cookies, storage, executionContexts, or * for all of them), flagging redundant directives alongside the wildcard |
|
|
285
|
+
| `parse_clear_site_data_header` | Parse an existing Clear-Site-Data header value into its individual directives |
|
|
284
286
|
|
|
285
287
|
### Color (4 tools)
|
|
286
288
|
| Tool | Description |
|
|
@@ -290,7 +292,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
290
292
|
| `color_palette` | Generate a color palette from a base color |
|
|
291
293
|
| `color_shades` | Generate shades or tints from a base color |
|
|
292
294
|
|
|
293
|
-
### Code (
|
|
295
|
+
### Code (29 tools)
|
|
294
296
|
| Tool | Description |
|
|
295
297
|
|------|-------------|
|
|
296
298
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -321,8 +323,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
321
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 |
|
|
322
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 |
|
|
323
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 |
|
|
324
328
|
|
|
325
|
-
### Data (
|
|
329
|
+
### Data (21 tools)
|
|
326
330
|
| Tool | Description |
|
|
327
331
|
|------|-------------|
|
|
328
332
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -345,6 +349,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
345
349
|
| `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 |
|
|
346
350
|
| `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
351
|
| `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 |
|
|
352
|
+
| `summarize_csv_column_stats` | Parse CSV/TSV data and compute per-column summary statistics: null/unique counts for every column, plus min/max/mean/median/standard deviation for numeric columns or the most common value for categorical ones |
|
|
348
353
|
|
|
349
354
|
### CSS (2 tools)
|
|
350
355
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -14560,5 +14560,398 @@ server.tool(
|
|
|
14560
14560
|
return text(JSON.stringify(result, null, 2));
|
|
14561
14561
|
}
|
|
14562
14562
|
);
|
|
14563
|
+
var _CLEAR_SITE_DATA_DIRECTIVES_MCP = /* @__PURE__ */ new Set(["cache", "cookies", "storage", "executionContexts", "*"]);
|
|
14564
|
+
function _buildClearSiteDataMcp(directives) {
|
|
14565
|
+
if (!Array.isArray(directives) || directives.length === 0) {
|
|
14566
|
+
return { error: "directives must be a non-empty array" };
|
|
14567
|
+
}
|
|
14568
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14569
|
+
const normalized = [];
|
|
14570
|
+
for (const raw of directives) {
|
|
14571
|
+
if (!_CLEAR_SITE_DATA_DIRECTIVES_MCP.has(raw)) {
|
|
14572
|
+
return { error: `Unrecognized directive "${raw}". Valid directives: cache, cookies, storage, executionContexts, *` };
|
|
14573
|
+
}
|
|
14574
|
+
if (seen.has(raw)) continue;
|
|
14575
|
+
seen.add(raw);
|
|
14576
|
+
normalized.push(raw);
|
|
14577
|
+
}
|
|
14578
|
+
const warnings = [];
|
|
14579
|
+
if (normalized.includes("*") && normalized.length > 1) {
|
|
14580
|
+
warnings.push('"*" already clears cache, cookies, storage, and executionContexts (plus any future data types the spec adds), so listing other directives alongside it is redundant.');
|
|
14581
|
+
}
|
|
14582
|
+
const header = normalized.map((d) => `"${d}"`).join(", ");
|
|
14583
|
+
return {
|
|
14584
|
+
header,
|
|
14585
|
+
headerName: "Clear-Site-Data",
|
|
14586
|
+
snippet: `Clear-Site-Data: ${header}`,
|
|
14587
|
+
directives: normalized,
|
|
14588
|
+
warnings
|
|
14589
|
+
};
|
|
14590
|
+
}
|
|
14591
|
+
function _parseClearSiteDataMcp(raw) {
|
|
14592
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
14593
|
+
const withoutLabel = raw.trim().replace(/^clear-site-data:\s*/i, "");
|
|
14594
|
+
const tokens = withoutLabel.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
14595
|
+
if (tokens.length === 0) return { error: "No directives found" };
|
|
14596
|
+
const directives = [];
|
|
14597
|
+
for (const token of tokens) {
|
|
14598
|
+
const match = token.match(/^"(.*)"$/);
|
|
14599
|
+
const value = match ? match[1] : token;
|
|
14600
|
+
if (!_CLEAR_SITE_DATA_DIRECTIVES_MCP.has(value)) {
|
|
14601
|
+
return { error: `Unrecognized directive "${token}"; each directive should be a quoted string like "cookies"` };
|
|
14602
|
+
}
|
|
14603
|
+
directives.push(value);
|
|
14604
|
+
}
|
|
14605
|
+
return _buildClearSiteDataMcp(directives);
|
|
14606
|
+
}
|
|
14607
|
+
server.tool(
|
|
14608
|
+
"build_clear_site_data_header",
|
|
14609
|
+
"Build a Clear-Site-Data response-header value from a list of directives (cache, cookies, storage, executionContexts, or * for all of them), flagging redundant directives alongside the wildcard.",
|
|
14610
|
+
{
|
|
14611
|
+
directives: z.array(z.enum(["cache", "cookies", "storage", "executionContexts", "*"])).min(1).describe("Directives to clear")
|
|
14612
|
+
},
|
|
14613
|
+
async ({ directives }) => {
|
|
14614
|
+
const result = _buildClearSiteDataMcp(directives);
|
|
14615
|
+
if ("error" in result) return errText(result);
|
|
14616
|
+
return text(JSON.stringify(result, null, 2));
|
|
14617
|
+
}
|
|
14618
|
+
);
|
|
14619
|
+
server.tool(
|
|
14620
|
+
"parse_clear_site_data_header",
|
|
14621
|
+
'Parse an existing Clear-Site-Data header value (with or without a leading "Clear-Site-Data:" label, quoted or unquoted) into its individual directives.',
|
|
14622
|
+
{
|
|
14623
|
+
header: z.string().min(1).describe("The Clear-Site-Data header value to parse")
|
|
14624
|
+
},
|
|
14625
|
+
async ({ header }) => {
|
|
14626
|
+
const result = _parseClearSiteDataMcp(header);
|
|
14627
|
+
if ("error" in result) return errText(result);
|
|
14628
|
+
return text(JSON.stringify(result, null, 2));
|
|
14629
|
+
}
|
|
14630
|
+
);
|
|
14631
|
+
function _parseCsvForStatsMcp(input, delimiter, hasHeader) {
|
|
14632
|
+
if (!input || input.trim() === "") return { error: "Input is empty" };
|
|
14633
|
+
const lines = input.replace(/\r\n/g, "\n").split("\n").filter((l) => l.trim() !== "");
|
|
14634
|
+
if (lines.length === 0) return { error: "No data found" };
|
|
14635
|
+
function parseRow(line) {
|
|
14636
|
+
const fields = [];
|
|
14637
|
+
let cur = "";
|
|
14638
|
+
let inQuote = false;
|
|
14639
|
+
for (let i = 0; i < line.length; i++) {
|
|
14640
|
+
if (line[i] === '"') {
|
|
14641
|
+
if (inQuote && line[i + 1] === '"') {
|
|
14642
|
+
cur += '"';
|
|
14643
|
+
i++;
|
|
14644
|
+
} else {
|
|
14645
|
+
inQuote = !inQuote;
|
|
14646
|
+
}
|
|
14647
|
+
} else if (line[i] === delimiter && !inQuote) {
|
|
14648
|
+
fields.push(cur);
|
|
14649
|
+
cur = "";
|
|
14650
|
+
} else {
|
|
14651
|
+
cur += line[i];
|
|
14652
|
+
}
|
|
14653
|
+
}
|
|
14654
|
+
fields.push(cur);
|
|
14655
|
+
return fields;
|
|
14656
|
+
}
|
|
14657
|
+
const allRows = lines.map(parseRow);
|
|
14658
|
+
const headers = hasHeader ? allRows[0] : allRows[0].map((_, idx) => `Column ${idx + 1}`);
|
|
14659
|
+
const rows = hasHeader ? allRows.slice(1) : allRows;
|
|
14660
|
+
if (rows.length === 0) return { error: "No data rows found" };
|
|
14661
|
+
return { headers, rows };
|
|
14662
|
+
}
|
|
14663
|
+
var _CSV_STATS_NUMERIC_RE_MCP = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
14664
|
+
function _parseCsvStatsNumericMcp(raw) {
|
|
14665
|
+
if (!_CSV_STATS_NUMERIC_RE_MCP.test(raw)) return null;
|
|
14666
|
+
const n = Number(raw);
|
|
14667
|
+
return Number.isFinite(n) ? n : null;
|
|
14668
|
+
}
|
|
14669
|
+
function _summarizeCsvColumnStatsMcp(input, delimiter, hasHeader) {
|
|
14670
|
+
const parsed = _parseCsvForStatsMcp(input, delimiter, hasHeader);
|
|
14671
|
+
if ("error" in parsed) return parsed;
|
|
14672
|
+
const columns = parsed.headers.map((name, colIdx) => {
|
|
14673
|
+
let nullCount = 0;
|
|
14674
|
+
let nonNumericCount = 0;
|
|
14675
|
+
const numericValues = [];
|
|
14676
|
+
const freq = /* @__PURE__ */ new Map();
|
|
14677
|
+
for (const row of parsed.rows) {
|
|
14678
|
+
const raw = (row[colIdx] ?? "").trim();
|
|
14679
|
+
if (raw === "") {
|
|
14680
|
+
nullCount++;
|
|
14681
|
+
continue;
|
|
14682
|
+
}
|
|
14683
|
+
freq.set(raw, (freq.get(raw) ?? 0) + 1);
|
|
14684
|
+
const n = _parseCsvStatsNumericMcp(raw);
|
|
14685
|
+
if (n !== null) numericValues.push(n);
|
|
14686
|
+
else nonNumericCount++;
|
|
14687
|
+
}
|
|
14688
|
+
const nonEmptyCount = parsed.rows.length - nullCount;
|
|
14689
|
+
const isNumeric = nonEmptyCount > 0 && nonNumericCount === 0;
|
|
14690
|
+
let mostCommon;
|
|
14691
|
+
for (const [value, count] of freq) {
|
|
14692
|
+
if (!mostCommon || count > mostCommon.count) mostCommon = { value, count };
|
|
14693
|
+
}
|
|
14694
|
+
const stats = {
|
|
14695
|
+
name,
|
|
14696
|
+
totalCount: parsed.rows.length,
|
|
14697
|
+
nullCount,
|
|
14698
|
+
uniqueCount: freq.size,
|
|
14699
|
+
isNumeric,
|
|
14700
|
+
numericCount: numericValues.length
|
|
14701
|
+
};
|
|
14702
|
+
if (isNumeric && numericValues.length > 0) {
|
|
14703
|
+
const sorted = [...numericValues].sort((a, b) => a - b);
|
|
14704
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
14705
|
+
const mean = sum / sorted.length;
|
|
14706
|
+
const mid = Math.floor(sorted.length / 2);
|
|
14707
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
14708
|
+
const variance = sorted.reduce((acc, v) => acc + (v - mean) ** 2, 0) / sorted.length;
|
|
14709
|
+
stats.min = sorted[0];
|
|
14710
|
+
stats.max = sorted[sorted.length - 1];
|
|
14711
|
+
stats.mean = mean;
|
|
14712
|
+
stats.median = median;
|
|
14713
|
+
stats.stdDev = Math.sqrt(variance);
|
|
14714
|
+
} else {
|
|
14715
|
+
stats.mostCommon = mostCommon;
|
|
14716
|
+
}
|
|
14717
|
+
return stats;
|
|
14718
|
+
});
|
|
14719
|
+
return { columns, rowCount: parsed.rows.length };
|
|
14720
|
+
}
|
|
14721
|
+
server.tool(
|
|
14722
|
+
"summarize_csv_column_stats",
|
|
14723
|
+
"Parse CSV/TSV data and compute per-column summary statistics: null and unique-value counts for every column, plus min/max/mean/median/standard deviation for any column where every non-empty value parses as a plain number. A column with any non-numeric non-empty value is treated as categorical, reporting the most frequent value and its count instead.",
|
|
14724
|
+
{
|
|
14725
|
+
csv: z.string().min(1).describe("The CSV/TSV data, with a header row by default"),
|
|
14726
|
+
delimiter: z.string().optional().describe('Field delimiter, default ","'),
|
|
14727
|
+
hasHeader: z.boolean().optional().describe("Whether the first row is a header row, default true")
|
|
14728
|
+
},
|
|
14729
|
+
async ({ csv, delimiter, hasHeader }) => {
|
|
14730
|
+
const result = _summarizeCsvColumnStatsMcp(csv, delimiter?.trim() || ",", hasHeader !== false);
|
|
14731
|
+
if ("error" in result) return errText(result);
|
|
14732
|
+
return text(JSON.stringify(result, null, 2));
|
|
14733
|
+
}
|
|
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
|
+
);
|
|
14563
14956
|
var transport = new StdioServerTransport();
|
|
14564
14957
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
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",
|