@utilix-tech/mcp 0.56.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,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.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
+
5
9
  ## 0.56.0
6
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.
7
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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 200 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 (34 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 |
@@ -197,8 +197,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
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
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 |
200
201
 
201
- ### API / Network (29 tools)
202
+ ### API / Network (31 tools)
202
203
  | Tool | Description |
203
204
  |------|-------------|
204
205
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -230,6 +231,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
230
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 |
231
232
  | `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
232
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 |
233
236
 
234
237
  ### Color (4 tools)
235
238
  | Tool | Description |
package/dist/index.js CHANGED
@@ -9327,5 +9327,139 @@ server.tool(
9327
9327
  }, null, 2));
9328
9328
  }
9329
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
+ );
9330
9464
  var transport = new StdioServerTransport();
9331
9465
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.56.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",