@utilix-tech/mcp 0.56.0 → 0.58.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.58.0
6
+ - Add `apply_json_merge_patch` tool: apply an RFC 7396 JSON Merge Patch document to a target JSON document, with null-valued patch keys removing the matching target key.
7
+ - Add `generate_csp_nonce` tool: generate a cryptographically random, base64-encoded Content-Security-Policy nonce with ready-to-paste header directive and script/style tag snippets.
8
+
9
+ ## 0.57.0
10
+ - 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.
11
+ - 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.
12
+
5
13
  ## 0.56.0
6
14
  - 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
15
  - 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 205 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
 
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
75
75
 
76
76
  ## Tools
77
77
 
78
- ### JSON (15 tools)
78
+ ### JSON (16 tools)
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
81
  | `format_json` | Pretty-print and validate a JSON string |
@@ -85,6 +85,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
85
85
  | `csv_to_json` | Convert CSV text to a JSON array |
86
86
  | `jsonpath` | Evaluate a JSONPath expression |
87
87
  | `resolve_json_pointer` | Resolve an RFC 6901 JSON Pointer against a JSON document |
88
+ | `apply_json_merge_patch` | Apply an RFC 7396 JSON Merge Patch document to a target JSON document |
88
89
  | `validate_json` | Validate JSON against a JSON Schema |
89
90
  | `yaml_to_json` | Convert YAML to JSON |
90
91
  | `json_to_yaml` | Convert JSON to YAML |
@@ -120,7 +121,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
120
121
  | `generate_sri_hash` | Compute a Subresource Integrity hash (SHA-256/384/512) for a script or stylesheet's content, formatted for an HTML `integrity` attribute |
121
122
  | `generate_etag` | Compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator prefix |
122
123
 
123
- ### Generators (6 tools)
124
+ ### Generators (7 tools)
124
125
  | Tool | Description |
125
126
  |------|-------------|
126
127
  | `generate_uuid` | Generate UUID v4 or v7 |
@@ -129,6 +130,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
129
130
  | `estimate_password_entropy` | Estimate a password's entropy in bits, strength label, and crack-time estimates |
130
131
  | `generate_qr` | Generate a QR code as an SVG string |
131
132
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
133
+ | `generate_csp_nonce` | Generate a cryptographically random CSP nonce with ready-to-paste header directive and script/style tag snippets |
132
134
 
133
135
  ### Text (16 tools)
134
136
  | Tool | Description |
@@ -160,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
160
162
  | `convert_timezone` | Convert a date/time between timezones |
161
163
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
162
164
 
163
- ### Units (34 tools)
165
+ ### Units (35 tools)
164
166
  | Tool | Description |
165
167
  |------|-------------|
166
168
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -197,8 +199,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
197
199
  | `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
200
  | `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
201
  | `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 |
202
+ | `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
203
 
201
- ### API / Network (29 tools)
204
+ ### API / Network (31 tools)
202
205
  | Tool | Description |
203
206
  |------|-------------|
204
207
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -230,6 +233,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
230
233
  | `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
234
  | `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
232
235
  | `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 |
236
+ | `build_accept_language` | Build an Accept-Language request-header value from locale/quality pairs, sorted by descending preference |
237
+ | `parse_accept_language` | Parse an existing Accept-Language header value into its ranked locale/quality pairs |
233
238
 
234
239
  ### Color (4 tools)
235
240
  | Tool | Description |
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
- import { createHash } from "crypto";
7
+ import { createHash, randomBytes } from "crypto";
8
8
  import * as yaml from "js-yaml";
9
9
  import {
10
10
  parseJson,
@@ -4655,6 +4655,45 @@ server.tool(
4655
4655
  return text(JSON.stringify(result, null, 2));
4656
4656
  }
4657
4657
  );
4658
+ function _isMergePatchObjectMcp(value) {
4659
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4660
+ }
4661
+ function _mergeJsonPatchMcp(target, patch) {
4662
+ if (!_isMergePatchObjectMcp(patch)) return patch;
4663
+ const result = _isMergePatchObjectMcp(target) ? { ...target } : {};
4664
+ for (const key of Object.keys(patch)) {
4665
+ const value = patch[key];
4666
+ if (value === null) {
4667
+ delete result[key];
4668
+ } else {
4669
+ result[key] = _mergeJsonPatchMcp(result[key], value);
4670
+ }
4671
+ }
4672
+ return result;
4673
+ }
4674
+ server.tool(
4675
+ "apply_json_merge_patch",
4676
+ "Apply an RFC 7396 JSON Merge Patch document to a target JSON document and return the merged result. A null-valued key in the patch removes that key from the target; a patch that is not itself a JSON object replaces the target wholesale, which is how arrays are replaced rather than merged element by element.",
4677
+ {
4678
+ target: z.string().describe("The target JSON document, as a JSON string"),
4679
+ patch: z.string().describe("The RFC 7396 JSON Merge Patch document, as a JSON string")
4680
+ },
4681
+ async ({ target, patch }) => {
4682
+ let targetParsed;
4683
+ try {
4684
+ targetParsed = JSON.parse(target);
4685
+ } catch {
4686
+ return errText({ error: "Target is not valid JSON" });
4687
+ }
4688
+ let patchParsed;
4689
+ try {
4690
+ patchParsed = JSON.parse(patch);
4691
+ } catch {
4692
+ return errText({ error: "Patch is not valid JSON" });
4693
+ }
4694
+ return text(JSON.stringify({ result: _mergeJsonPatchMcp(targetParsed, patchParsed) }, null, 2));
4695
+ }
4696
+ );
4658
4697
  function _convertAprApyMcp(rate, compoundsPerYear, direction) {
4659
4698
  if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
4660
4699
  if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) return { error: "Compounding frequency must be a positive whole number of periods per year" };
@@ -9327,5 +9366,158 @@ server.tool(
9327
9366
  }, null, 2));
9328
9367
  }
9329
9368
  );
9369
+ function _formatAcceptLanguageQualityMcp(q) {
9370
+ let s = q.toFixed(3);
9371
+ s = s.replace(/0+$/, "");
9372
+ s = s.replace(/\.$/, "");
9373
+ return s;
9374
+ }
9375
+ function _formatAcceptLanguageEntriesMcp(entries) {
9376
+ return entries.map((e) => e.quality === 1 ? e.locale : `${e.locale};q=${_formatAcceptLanguageQualityMcp(e.quality)}`).join(", ");
9377
+ }
9378
+ function _sortAcceptLanguageEntriesMcp(entries) {
9379
+ return entries.map((e, i) => ({ e, i })).sort((a, b) => b.e.quality - a.e.quality || a.i - b.i).map((x) => x.e);
9380
+ }
9381
+ function _buildAcceptLanguageMcp(locales) {
9382
+ if (!Array.isArray(locales) || locales.length === 0) return { error: "At least one locale is required" };
9383
+ const entries = [];
9384
+ for (let i = 0; i < locales.length; i++) {
9385
+ const { locale, quality } = locales[i];
9386
+ if (typeof locale !== "string" || locale.trim() === "") return { error: `locale ${i + 1}: locale is required` };
9387
+ const q = quality === void 0 ? 1 : quality;
9388
+ if (typeof q !== "number" || !Number.isFinite(q) || q < 0 || q > 1) {
9389
+ return { error: `locale ${i + 1}: quality must be between 0 and 1` };
9390
+ }
9391
+ entries.push({ locale: locale.trim(), quality: Math.round(q * 1e3) / 1e3 });
9392
+ }
9393
+ const sorted = _sortAcceptLanguageEntriesMcp(entries);
9394
+ const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
9395
+ return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
9396
+ }
9397
+ function _parseAcceptLanguageMcp(raw) {
9398
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
9399
+ const withoutLabel = raw.trim().replace(/^accept-language:\s*/i, "");
9400
+ const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
9401
+ if (parts.length === 0) return { error: "Input is empty" };
9402
+ const entries = [];
9403
+ for (const part of parts) {
9404
+ const [localeRaw, ...paramsRaw] = part.split(";").map((s) => s.trim());
9405
+ if (!localeRaw) return { error: `Invalid entry: "${part}"` };
9406
+ let quality = 1;
9407
+ for (const param of paramsRaw) {
9408
+ const m = /^q=([\d.]+)$/i.exec(param);
9409
+ if (!m) return { error: `Invalid parameter in "${part}"` };
9410
+ quality = Number(m[1]);
9411
+ if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
9412
+ return { error: `q value must be between 0 and 1 in "${part}"` };
9413
+ }
9414
+ }
9415
+ entries.push({ locale: localeRaw, quality: Math.round(quality * 1e3) / 1e3 });
9416
+ }
9417
+ const sorted = _sortAcceptLanguageEntriesMcp(entries);
9418
+ const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
9419
+ return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
9420
+ }
9421
+ server.tool(
9422
+ "build_accept_language",
9423
+ '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.',
9424
+ {
9425
+ locales: z.array(z.object({
9426
+ locale: z.string().describe('A language range, e.g. "en-US" or "*"'),
9427
+ quality: z.number().min(0).max(1).optional().describe("Preference weight from 0 to 1 (default 1)")
9428
+ })).min(1).describe("Locale/quality pairs to build the header from")
9429
+ },
9430
+ async ({ locales }) => {
9431
+ const result = _buildAcceptLanguageMcp(locales);
9432
+ if ("error" in result) return errText(result);
9433
+ return text(JSON.stringify(result, null, 2));
9434
+ }
9435
+ );
9436
+ server.tool(
9437
+ "parse_accept_language",
9438
+ '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).',
9439
+ {
9440
+ header: z.string().min(1).describe("The Accept-Language header value to parse")
9441
+ },
9442
+ async ({ header }) => {
9443
+ const result = _parseAcceptLanguageMcp(header);
9444
+ if ("error" in result) return errText(result);
9445
+ return text(JSON.stringify(result, null, 2));
9446
+ }
9447
+ );
9448
+ function _calculateBlendedLoanRateMcp(loans) {
9449
+ if (!Array.isArray(loans) || loans.length === 0) {
9450
+ return { error: "loans must be a non-empty array" };
9451
+ }
9452
+ const round2 = (n) => Math.round(n * 100) / 100;
9453
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
9454
+ let totalBalance = 0;
9455
+ for (let i = 0; i < loans.length; i++) {
9456
+ const loan = loans[i];
9457
+ if (typeof loan.balance !== "number" || !Number.isFinite(loan.balance) || loan.balance <= 0) {
9458
+ return { error: `loan ${i + 1}: balance must be a finite number greater than zero` };
9459
+ }
9460
+ if (typeof loan.ratePercent !== "number" || !Number.isFinite(loan.ratePercent) || loan.ratePercent < 0) {
9461
+ return { error: `loan ${i + 1}: ratePercent must be a finite, non-negative number` };
9462
+ }
9463
+ totalBalance += loan.balance;
9464
+ }
9465
+ let totalMonthlyInterest = 0;
9466
+ let weightedRateSum = 0;
9467
+ const results = loans.map((loan, i) => {
9468
+ const monthlyInterest = round2(loan.balance * loan.ratePercent / 100 / 12);
9469
+ totalMonthlyInterest += monthlyInterest;
9470
+ weightedRateSum += loan.balance * loan.ratePercent;
9471
+ return {
9472
+ label: loan.label?.trim() || `Loan ${i + 1}`,
9473
+ balance: loan.balance,
9474
+ ratePercent: loan.ratePercent,
9475
+ monthlyInterest,
9476
+ shareOfTotalPercent: round2(loan.balance / totalBalance * 100)
9477
+ };
9478
+ });
9479
+ return {
9480
+ loans: results,
9481
+ totalBalance: round2(totalBalance),
9482
+ weightedAverageRatePercent: round4(weightedRateSum / totalBalance),
9483
+ totalMonthlyInterest: round2(totalMonthlyInterest),
9484
+ totalAnnualInterest: round2(totalMonthlyInterest * 12)
9485
+ };
9486
+ }
9487
+ server.tool(
9488
+ "calculate_blended_loan_rate",
9489
+ "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.",
9490
+ {
9491
+ loans: z.array(z.object({
9492
+ label: z.string().optional().describe("Optional loan label"),
9493
+ balance: z.number().positive().describe("Outstanding balance for this loan"),
9494
+ ratePercent: z.number().min(0).describe("Annual interest rate for this loan, as a percent")
9495
+ })).min(1).describe("A non-empty list of loan balances and rates")
9496
+ },
9497
+ async ({ loans }) => {
9498
+ const result = _calculateBlendedLoanRateMcp(loans);
9499
+ if ("error" in result) return errText(result);
9500
+ return text(JSON.stringify(result, null, 2));
9501
+ }
9502
+ );
9503
+ server.tool(
9504
+ "generate_csp_nonce",
9505
+ 'Generate a cryptographically random, base64-encoded nonce for a Content-Security-Policy script-src or style-src directive, along with ready-to-paste header directive and <script>/<style nonce="..."> tag snippets. Generates a fresh, unique value on every call.',
9506
+ {
9507
+ byteLength: z.number().int().min(8).max(64).optional().describe("Number of random bytes to encode (default 16 = 128 bits, the CSP spec's recommended minimum)")
9508
+ },
9509
+ async ({ byteLength }) => {
9510
+ const length = byteLength ?? 16;
9511
+ const nonce = randomBytes(length).toString("base64");
9512
+ return text(JSON.stringify({
9513
+ nonce,
9514
+ byteLength: length,
9515
+ scriptSrcDirective: `script-src 'nonce-${nonce}'`,
9516
+ styleSrcDirective: `style-src 'nonce-${nonce}'`,
9517
+ scriptTag: `<script nonce="${nonce}">`,
9518
+ styleTag: `<style nonce="${nonce}">`
9519
+ }, null, 2));
9520
+ }
9521
+ );
9330
9522
  var transport = new StdioServerTransport();
9331
9523
  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.58.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",