@utilix-tech/mcp 0.90.0 → 0.92.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.92.0
6
+ - 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
+ - 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.
8
+
9
+ ## 0.91.0
10
+ - 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.
11
+ - 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.
12
+
5
13
  ## 0.90.0
6
14
  - 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
15
  - 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
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.
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 (49 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,10 +220,11 @@ 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 |
224
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 |
225
226
 
226
- ### API / Network (51 tools)
227
+ ### API / Network (53 tools)
227
228
  | Tool | Description |
228
229
  |------|-------------|
229
230
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -280,6 +281,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
280
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 |
281
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 |
282
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 |
283
286
 
284
287
  ### Color (4 tools)
285
288
  | Tool | Description |
@@ -321,11 +324,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
321
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 |
322
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 |
323
326
 
324
- ### Data (19 tools)
327
+ ### Data (21 tools)
325
328
  | Tool | Description |
326
329
  |------|-------------|
327
330
  | `validate_yaml` | Validate YAML syntax |
328
331
  | `resolve_yaml_anchors` | Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused |
332
+ | `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 |
329
333
  | `toml_to_json` | Convert TOML to JSON |
330
334
  | `xml_to_json` | Convert XML to JSON |
331
335
  | `parse_csv` | Parse a CSV string and return the data as JSON |
@@ -343,6 +347,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
343
347
  | `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 |
344
348
  | `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
349
  | `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 |
350
+ | `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 |
346
351
 
347
352
  ### CSS (2 tools)
348
353
  | 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",
@@ -14437,5 +14560,177 @@ server.tool(
14437
14560
  return text(JSON.stringify(result, null, 2));
14438
14561
  }
14439
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
+ );
14440
14735
  var transport = new StdioServerTransport();
14441
14736
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
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.",
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.",
5
5
  "author": "Utilix <hello@utilix.tech>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://utilix.tech/mcp",