@utilix-tech/mcp 0.55.0 → 0.57.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.57.0
6
+ - Add `build_accept_language`, `parse_accept_language` tools: build an Accept-Language request-header value from locale/quality pairs sorted by descending preference, or parse an existing one into its ranked entries.
7
+ - Add `calculate_blended_loan_rate` tool: compute the single weighted-average interest rate across a list of loan balances, plus combined monthly and annual interest accrual.
8
+
9
+ ## 0.56.0
10
+ - Add `calculate_debt_to_income_ratio` tool: compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance.
11
+ - Add `infer_csv_column_types` tool: 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.
12
+
5
13
  ## 0.55.0
6
14
  - Add `validate_package_exports` tool: parse a package.json's exports field and flag common dual-package (ESM/CJS) misconfigurations, a types condition not listed first, a missing default fallback, mismatched require/import extensions, mixed subpath/condition keys, and pattern mismatches.
7
15
  - Add `build_x_content_type_options_header` tool: build the fixed X-Content-Type-Options: nosniff header, validate a pasted value, and assess MIME-sniffing risk for a given Content-Type.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 198 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 203 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
 
@@ -160,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
160
160
  | `convert_timezone` | Convert a date/time between timezones |
161
161
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
162
162
 
163
- ### Units (33 tools)
163
+ ### Units (35 tools)
164
164
  | Tool | Description |
165
165
  |------|-------------|
166
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -196,8 +196,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
196
196
  | `calculate_moving_averages` | Compute simple moving average (SMA) and exponential moving average (EMA) series from a list of numbers and a window size |
197
197
  | `calculate_gpa` | Compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale |
198
198
  | `calculate_credit_utilization` | Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance |
199
+ | `calculate_debt_to_income_ratio` | Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance |
200
+ | `calculate_blended_loan_rate` | Compute the single weighted-average interest rate across a list of loan balances, weighted by each loan's share of the combined balance, plus combined monthly and annual interest accrual |
199
201
 
200
- ### API / Network (29 tools)
202
+ ### API / Network (31 tools)
201
203
  | Tool | Description |
202
204
  |------|-------------|
203
205
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -229,6 +231,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
229
231
  | `build_content_disposition` | Build a Content-Disposition response header for a file download or inline display, with an RFC 5987 filename* parameter for non-ASCII filenames |
230
232
  | `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
231
233
  | `build_x_content_type_options_header` | Build the fixed X-Content-Type-Options: nosniff header, validate a pasted value, or assess MIME-sniffing risk for a given Content-Type |
234
+ | `build_accept_language` | Build an Accept-Language request-header value from locale/quality pairs, sorted by descending preference |
235
+ | `parse_accept_language` | Parse an existing Accept-Language header value into its ranked locale/quality pairs |
232
236
 
233
237
  ### Color (4 tools)
234
238
  | Tool | Description |
@@ -257,7 +261,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
257
261
  | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
258
262
  | `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
259
263
 
260
- ### Data (13 tools)
264
+ ### Data (14 tools)
261
265
  | Tool | Description |
262
266
  |------|-------------|
263
267
  | `validate_yaml` | Validate YAML syntax |
@@ -273,6 +277,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
273
277
  | `find_csv_duplicate_rows` | Group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row |
274
278
  | `calculate_markdown_task_list_progress` | Parse a Markdown document's checkbox task list and report completion percentage overall and per section, grouped by the nearest preceding heading |
275
279
  | `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
280
+ | `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 |
276
281
 
277
282
  ### CSS (2 tools)
278
283
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7271,6 +7271,71 @@ server.tool(
7271
7271
  return text(JSON.stringify(result, null, 2));
7272
7272
  }
7273
7273
  );
7274
+ var _CSV_TYPE_ISO_DATE_RE_MCP = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
7275
+ function _classifyCsvValueMcp(raw) {
7276
+ const v = raw.trim();
7277
+ if (v === "") return "empty";
7278
+ if (/^(true|false)$/i.test(v)) return "boolean";
7279
+ if (/^-?\d+$/.test(v)) return "integer";
7280
+ if (/^-?\d+\.\d+$/.test(v)) return "float";
7281
+ if (_CSV_TYPE_ISO_DATE_RE_MCP.test(v)) return "date";
7282
+ return "string";
7283
+ }
7284
+ function _inferCsvColumnTypesMcp(input, delimiter, hasHeader) {
7285
+ if (!input || input.trim() === "") return { error: "Input is empty" };
7286
+ const delim = delimiter || ",";
7287
+ const header = hasHeader !== false;
7288
+ const lines = input.replace(/\r\n/g, "\n").split("\n").filter((l) => l.trim() !== "");
7289
+ if (lines.length === 0) return { error: "No data found" };
7290
+ const allRows = lines.map((l) => _parseCsvRowForSqlMcp(l, delim));
7291
+ const headers = header ? allRows[0].map((h) => h.trim()) : allRows[0].map((_, idx) => `Column ${idx + 1}`);
7292
+ const rows = header ? allRows.slice(1) : allRows;
7293
+ const columns = headers.map((name, colIdx) => {
7294
+ const seen = /* @__PURE__ */ new Set();
7295
+ let sawString = false;
7296
+ let nullCount = 0;
7297
+ const sampleValues = [];
7298
+ for (const row of rows) {
7299
+ const raw = row[colIdx] ?? "";
7300
+ const kind = _classifyCsvValueMcp(raw);
7301
+ if (kind === "empty") {
7302
+ nullCount++;
7303
+ continue;
7304
+ }
7305
+ if (sampleValues.length < 3) sampleValues.push(raw);
7306
+ if (kind === "string") sawString = true;
7307
+ else seen.add(kind);
7308
+ }
7309
+ let inferredType;
7310
+ if (sawString || seen.size === 0) inferredType = "string";
7311
+ else if (seen.size === 1) inferredType = [...seen][0];
7312
+ else if (seen.has("integer") && seen.has("float") && seen.size === 2) inferredType = "float";
7313
+ else inferredType = "string";
7314
+ return {
7315
+ name,
7316
+ inferredType,
7317
+ nullable: nullCount > 0,
7318
+ nullCount,
7319
+ totalCount: rows.length,
7320
+ sampleValues
7321
+ };
7322
+ });
7323
+ return { columns, rowCount: rows.length };
7324
+ }
7325
+ server.tool(
7326
+ "infer_csv_column_types",
7327
+ "Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string) by classifying every non-empty value and finding the tightest type that fits them all: all-integer values infer as integer, a mix of integers and decimals infers as float, and anything that doesn't fit a narrower type falls back to string. Empty cells mark a column nullable but don't affect its inferred type.",
7328
+ {
7329
+ csv: z.string().min(1).describe("CSV/TSV data to infer column types from"),
7330
+ delimiter: z.string().optional().describe('Field delimiter (default ",")'),
7331
+ hasHeader: z.boolean().default(true).describe("Whether the first row is a header")
7332
+ },
7333
+ async ({ csv, delimiter, hasHeader }) => {
7334
+ const result = _inferCsvColumnTypesMcp(csv, delimiter, hasHeader);
7335
+ if ("error" in result) return errText(result);
7336
+ return text(JSON.stringify(result, null, 2));
7337
+ }
7338
+ );
7274
7339
  var _VARY_HEADER_NOTES_MCP = {
7275
7340
  "accept-encoding": "Caches must store a separate response per compression scheme (gzip, br, etc.) the client accepts.",
7276
7341
  "accept-language": "Caches must store a separate response per requested language, which can fragment a shared cache badly if Accept-Language values are highly varied.",
@@ -8922,6 +8987,59 @@ server.tool(
8922
8987
  return text(JSON.stringify(result, null, 2));
8923
8988
  }
8924
8989
  );
8990
+ function _classifyDtiMcp(percent) {
8991
+ if (percent < 36) return "healthy";
8992
+ if (percent < 43) return "moderate";
8993
+ if (percent < 50) return "high";
8994
+ return "severe";
8995
+ }
8996
+ function _calculateDebtToIncomeRatioMcp(debts, grossMonthlyIncome) {
8997
+ if (!Array.isArray(debts) || debts.length === 0) {
8998
+ return { error: "debts must be a non-empty array" };
8999
+ }
9000
+ if (typeof grossMonthlyIncome !== "number" || !Number.isFinite(grossMonthlyIncome) || grossMonthlyIncome <= 0) {
9001
+ return { error: "grossMonthlyIncome must be a finite number greater than zero" };
9002
+ }
9003
+ const round2 = (n) => Math.round(n * 100) / 100;
9004
+ let totalMonthlyDebt = 0;
9005
+ const results = [];
9006
+ for (let i = 0; i < debts.length; i++) {
9007
+ const debt = debts[i];
9008
+ if (typeof debt.amount !== "number" || !Number.isFinite(debt.amount) || debt.amount < 0) {
9009
+ return { error: `debt ${i + 1}: amount must be a finite, non-negative number` };
9010
+ }
9011
+ totalMonthlyDebt += debt.amount;
9012
+ results.push({
9013
+ label: debt.label?.trim() || `Debt ${i + 1}`,
9014
+ amount: debt.amount,
9015
+ percentOfIncome: round2(debt.amount / grossMonthlyIncome * 100)
9016
+ });
9017
+ }
9018
+ const dtiPercent = round2(totalMonthlyDebt / grossMonthlyIncome * 100);
9019
+ return {
9020
+ debts: results,
9021
+ totalMonthlyDebt: round2(totalMonthlyDebt),
9022
+ grossMonthlyIncome,
9023
+ dtiPercent,
9024
+ tier: _classifyDtiMcp(dtiPercent)
9025
+ };
9026
+ }
9027
+ server.tool(
9028
+ "calculate_debt_to_income_ratio",
9029
+ "Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debt items, plus a qualitative tier matching common mortgage-lending guidance: under 36% is healthy, under 43% is moderate (43% is the back-end ratio ceiling many mortgage lenders use), under 50% is high, and 50%+ is severe. This is a back-end DTI (all monthly debt payments over gross income), not a front-end housing-only ratio.",
9030
+ {
9031
+ debts: z.array(z.object({
9032
+ label: z.string().optional().describe("Optional debt label"),
9033
+ amount: z.number().min(0).describe("Monthly payment amount for this debt")
9034
+ })).min(1).describe("A non-empty list of monthly debt payments"),
9035
+ grossMonthlyIncome: z.number().positive().describe("Gross (pre-tax) monthly income")
9036
+ },
9037
+ async ({ debts, grossMonthlyIncome }) => {
9038
+ const result = _calculateDebtToIncomeRatioMcp(debts, grossMonthlyIncome);
9039
+ if ("error" in result) return errText(result);
9040
+ return text(JSON.stringify(result, null, 2));
9041
+ }
9042
+ );
8925
9043
  var _CAPTION_TIMESTAMP_RE_MCP = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
8926
9044
  var _CAPTION_SKIP_BLOCK_KEYWORDS_MCP = ["NOTE", "STYLE", "REGION"];
8927
9045
  function _parseCaptionTimestampMcp(raw) {
@@ -9209,5 +9327,139 @@ server.tool(
9209
9327
  }, null, 2));
9210
9328
  }
9211
9329
  );
9330
+ function _formatAcceptLanguageQualityMcp(q) {
9331
+ let s = q.toFixed(3);
9332
+ s = s.replace(/0+$/, "");
9333
+ s = s.replace(/\.$/, "");
9334
+ return s;
9335
+ }
9336
+ function _formatAcceptLanguageEntriesMcp(entries) {
9337
+ return entries.map((e) => e.quality === 1 ? e.locale : `${e.locale};q=${_formatAcceptLanguageQualityMcp(e.quality)}`).join(", ");
9338
+ }
9339
+ function _sortAcceptLanguageEntriesMcp(entries) {
9340
+ return entries.map((e, i) => ({ e, i })).sort((a, b) => b.e.quality - a.e.quality || a.i - b.i).map((x) => x.e);
9341
+ }
9342
+ function _buildAcceptLanguageMcp(locales) {
9343
+ if (!Array.isArray(locales) || locales.length === 0) return { error: "At least one locale is required" };
9344
+ const entries = [];
9345
+ for (let i = 0; i < locales.length; i++) {
9346
+ const { locale, quality } = locales[i];
9347
+ if (typeof locale !== "string" || locale.trim() === "") return { error: `locale ${i + 1}: locale is required` };
9348
+ const q = quality === void 0 ? 1 : quality;
9349
+ if (typeof q !== "number" || !Number.isFinite(q) || q < 0 || q > 1) {
9350
+ return { error: `locale ${i + 1}: quality must be between 0 and 1` };
9351
+ }
9352
+ entries.push({ locale: locale.trim(), quality: Math.round(q * 1e3) / 1e3 });
9353
+ }
9354
+ const sorted = _sortAcceptLanguageEntriesMcp(entries);
9355
+ const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
9356
+ return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
9357
+ }
9358
+ function _parseAcceptLanguageMcp(raw) {
9359
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
9360
+ const withoutLabel = raw.trim().replace(/^accept-language:\s*/i, "");
9361
+ const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
9362
+ if (parts.length === 0) return { error: "Input is empty" };
9363
+ const entries = [];
9364
+ for (const part of parts) {
9365
+ const [localeRaw, ...paramsRaw] = part.split(";").map((s) => s.trim());
9366
+ if (!localeRaw) return { error: `Invalid entry: "${part}"` };
9367
+ let quality = 1;
9368
+ for (const param of paramsRaw) {
9369
+ const m = /^q=([\d.]+)$/i.exec(param);
9370
+ if (!m) return { error: `Invalid parameter in "${part}"` };
9371
+ quality = Number(m[1]);
9372
+ if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
9373
+ return { error: `q value must be between 0 and 1 in "${part}"` };
9374
+ }
9375
+ }
9376
+ entries.push({ locale: localeRaw, quality: Math.round(quality * 1e3) / 1e3 });
9377
+ }
9378
+ const sorted = _sortAcceptLanguageEntriesMcp(entries);
9379
+ const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
9380
+ return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
9381
+ }
9382
+ server.tool(
9383
+ "build_accept_language",
9384
+ 'Build an Accept-Language request-header value from a list of locale/quality pairs, sorting by descending quality (ties keep their original order) and omitting ";q=1" for the default weight, per RFC 9110 section 12.5.4.',
9385
+ {
9386
+ locales: z.array(z.object({
9387
+ locale: z.string().describe('A language range, e.g. "en-US" or "*"'),
9388
+ quality: z.number().min(0).max(1).optional().describe("Preference weight from 0 to 1 (default 1)")
9389
+ })).min(1).describe("Locale/quality pairs to build the header from")
9390
+ },
9391
+ async ({ locales }) => {
9392
+ const result = _buildAcceptLanguageMcp(locales);
9393
+ if ("error" in result) return errText(result);
9394
+ return text(JSON.stringify(result, null, 2));
9395
+ }
9396
+ );
9397
+ server.tool(
9398
+ "parse_accept_language",
9399
+ 'Parse an existing Accept-Language header value (with or without a leading "Accept-Language:" label) into its ranked locale/quality pairs, sorted by descending quality per RFC 9110 section 12.5.4 (a missing ";q=" defaults to 1).',
9400
+ {
9401
+ header: z.string().min(1).describe("The Accept-Language header value to parse")
9402
+ },
9403
+ async ({ header }) => {
9404
+ const result = _parseAcceptLanguageMcp(header);
9405
+ if ("error" in result) return errText(result);
9406
+ return text(JSON.stringify(result, null, 2));
9407
+ }
9408
+ );
9409
+ function _calculateBlendedLoanRateMcp(loans) {
9410
+ if (!Array.isArray(loans) || loans.length === 0) {
9411
+ return { error: "loans must be a non-empty array" };
9412
+ }
9413
+ const round2 = (n) => Math.round(n * 100) / 100;
9414
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
9415
+ let totalBalance = 0;
9416
+ for (let i = 0; i < loans.length; i++) {
9417
+ const loan = loans[i];
9418
+ if (typeof loan.balance !== "number" || !Number.isFinite(loan.balance) || loan.balance <= 0) {
9419
+ return { error: `loan ${i + 1}: balance must be a finite number greater than zero` };
9420
+ }
9421
+ if (typeof loan.ratePercent !== "number" || !Number.isFinite(loan.ratePercent) || loan.ratePercent < 0) {
9422
+ return { error: `loan ${i + 1}: ratePercent must be a finite, non-negative number` };
9423
+ }
9424
+ totalBalance += loan.balance;
9425
+ }
9426
+ let totalMonthlyInterest = 0;
9427
+ let weightedRateSum = 0;
9428
+ const results = loans.map((loan, i) => {
9429
+ const monthlyInterest = round2(loan.balance * loan.ratePercent / 100 / 12);
9430
+ totalMonthlyInterest += monthlyInterest;
9431
+ weightedRateSum += loan.balance * loan.ratePercent;
9432
+ return {
9433
+ label: loan.label?.trim() || `Loan ${i + 1}`,
9434
+ balance: loan.balance,
9435
+ ratePercent: loan.ratePercent,
9436
+ monthlyInterest,
9437
+ shareOfTotalPercent: round2(loan.balance / totalBalance * 100)
9438
+ };
9439
+ });
9440
+ return {
9441
+ loans: results,
9442
+ totalBalance: round2(totalBalance),
9443
+ weightedAverageRatePercent: round4(weightedRateSum / totalBalance),
9444
+ totalMonthlyInterest: round2(totalMonthlyInterest),
9445
+ totalAnnualInterest: round2(totalMonthlyInterest * 12)
9446
+ };
9447
+ }
9448
+ server.tool(
9449
+ "calculate_blended_loan_rate",
9450
+ "Compute the single weighted-average interest rate across a list of loan balances (e.g. multiple credit cards or student loans being weighed for consolidation), weighting each loan's rate by its share of the combined balance, plus the combined monthly and annual interest accrual.",
9451
+ {
9452
+ loans: z.array(z.object({
9453
+ label: z.string().optional().describe("Optional loan label"),
9454
+ balance: z.number().positive().describe("Outstanding balance for this loan"),
9455
+ ratePercent: z.number().min(0).describe("Annual interest rate for this loan, as a percent")
9456
+ })).min(1).describe("A non-empty list of loan balances and rates")
9457
+ },
9458
+ async ({ loans }) => {
9459
+ const result = _calculateBlendedLoanRateMcp(loans);
9460
+ if ("error" in result) return errText(result);
9461
+ return text(JSON.stringify(result, null, 2));
9462
+ }
9463
+ );
9212
9464
  var transport = new StdioServerTransport();
9213
9465
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "MCP server exposing 198 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",