@utilix-tech/mcp 0.77.0 → 0.79.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,13 @@
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.79.0
6
+ - Add `calculate_real_rate_of_return` tool: 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.
7
+ - Add `calculate_retry_backoff_schedule` tool: compute the full sequence of delays an exponential-backoff retry policy would actually produce, optionally capped and with full/equal jitter ranges, plus the total best-case and worst-case wait across all retries.
8
+
9
+ ## 0.78.0
10
+ - Add `ndjson_to_csv` tool: convert newline-delimited JSON (one flat object per line) into CSV, inferring the header row from the union of keys across all lines.
11
+
5
12
  ## 0.77.0
6
13
  - Add `read_pdf_outline` tool: walk a PDF's /Outlines bookmark tree (the Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order.
7
14
  - Add `score_security_headers` tool: 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 249 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 250 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
 
@@ -164,7 +164,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
164
164
  | `convert_timezone` | Convert a date/time between timezones |
165
165
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
166
166
 
167
- ### Units (45 tools)
167
+ ### Units (46 tools)
168
168
  | Tool | Description |
169
169
  |------|-------------|
170
170
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -212,6 +212,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
212
212
  | `calculate_mortgage_points` | Compare a loan with no discount points against the same loan at a reduced rate bought with points, and compute the monthly savings and the break-even month where cumulative savings recoup the points cost |
213
213
  | `calculate_loan_prepayment_penalty` | Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate |
214
214
  | `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 |
215
+ | `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 |
215
216
 
216
217
  ### API / Network (51 tools)
217
218
  | Tool | Description |
@@ -313,6 +314,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
313
314
  | `parse_csv` | Parse a CSV string and return the data as JSON |
314
315
  | `format_ndjson` | Validate and pretty-print newline-delimited JSON (NDJSON) |
315
316
  | `dedupe_ndjson` | Remove duplicate records from NDJSON, by exact match or by a key field |
317
+ | `ndjson_to_csv` | Convert NDJSON into CSV, inferring the header row from the union of keys |
316
318
  | `diff_env_example` | Diff a .env file against its .env.example template: flags keys missing from either side, plus empty placeholder values |
317
319
  | `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
318
320
  | `generate_markdown_toc` | Scan a Markdown document's ATX headings and build a nested table of contents with GitHub-style anchor slugs |
@@ -336,7 +338,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
336
338
  | `unicode_inspect` | Inspect Unicode code points of a string |
337
339
  | `optimize_svg` | Optimize and minify an SVG string |
338
340
 
339
- ### AI / Agent (28 tools)
341
+ ### AI / Agent (29 tools)
340
342
  | Tool | Description |
341
343
  |------|-------------|
342
344
  | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
@@ -367,6 +369,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
367
369
  | `vector_similarity` | Cosine similarity, dot product, and Euclidean / Manhattan distance between two numeric vectors |
368
370
  | `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
369
371
  | `lint_tool_schema` | Lint an OpenAI, Anthropic, or MCP tool/function-calling definition for missing descriptions, invalid names, empty enums, and required/properties mismatches |
372
+ | `calculate_retry_backoff_schedule` | Compute the full sequence of delays an exponential-backoff retry policy would actually produce, given a base delay, multiplier, and max retry count, optionally capped at a max delay and with full/equal jitter ranges |
370
373
 
371
374
  ### Image (4 tools)
372
375
  | Tool | Description |
package/dist/index.js CHANGED
@@ -1243,6 +1243,64 @@ server.tool("vector_similarity", "Compute cosine similarity, dot product, Euclid
1243
1243
  if ("error" in result) return errText(result);
1244
1244
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1245
1245
  });
1246
+ function _calculateRetryBackoffSchedule(input) {
1247
+ const { baseDelayMs, multiplier, maxRetries, maxDelayMs, jitter = "none" } = input;
1248
+ if (!Number.isFinite(baseDelayMs) || baseDelayMs <= 0) {
1249
+ return { error: "baseDelayMs must be a finite number greater than zero" };
1250
+ }
1251
+ if (!Number.isFinite(multiplier) || multiplier < 1) {
1252
+ return { error: "multiplier must be a finite number of at least 1" };
1253
+ }
1254
+ if (!Number.isInteger(maxRetries) || maxRetries < 1 || maxRetries > 50) {
1255
+ return { error: "maxRetries must be a whole number between 1 and 50" };
1256
+ }
1257
+ if (maxDelayMs !== void 0 && (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0)) {
1258
+ return { error: "maxDelayMs must be a finite number greater than zero when provided" };
1259
+ }
1260
+ if (jitter !== "none" && jitter !== "full" && jitter !== "equal") {
1261
+ return { error: "jitter must be one of 'none', 'full', or 'equal'" };
1262
+ }
1263
+ const round0 = (n) => Math.round(n);
1264
+ const attempts = [];
1265
+ let totalBestCaseMs = 0;
1266
+ let totalWorstCaseMs = 0;
1267
+ for (let i = 1; i <= maxRetries; i++) {
1268
+ let nominal = baseDelayMs * Math.pow(multiplier, i - 1);
1269
+ if (maxDelayMs !== void 0) nominal = Math.min(nominal, maxDelayMs);
1270
+ let minDelay;
1271
+ let maxDelay;
1272
+ if (jitter === "full") {
1273
+ minDelay = 0;
1274
+ maxDelay = nominal;
1275
+ } else if (jitter === "equal") {
1276
+ minDelay = nominal / 2;
1277
+ maxDelay = nominal;
1278
+ } else {
1279
+ minDelay = nominal;
1280
+ maxDelay = nominal;
1281
+ }
1282
+ attempts.push({ attempt: i, nominalDelayMs: round0(nominal), minDelayMs: round0(minDelay), maxDelayMs: round0(maxDelay) });
1283
+ totalBestCaseMs += minDelay;
1284
+ totalWorstCaseMs += maxDelay;
1285
+ }
1286
+ return { attempts, jitter, totalBestCaseMs: round0(totalBestCaseMs), totalWorstCaseMs: round0(totalWorstCaseMs) };
1287
+ }
1288
+ server.tool(
1289
+ "calculate_retry_backoff_schedule",
1290
+ "Compute the full sequence of delays an exponential-backoff retry policy would actually produce, given a base delay, multiplier, and max retry count, optionally capped at a max delay and with full/equal jitter ranges, plus the total best-case and worst-case wait across all retries. Distinct from build_retry_after_header, which builds a single HTTP Retry-After response header.",
1291
+ {
1292
+ baseDelayMs: z.number().describe("Delay before the first retry, in milliseconds"),
1293
+ multiplier: z.number().describe("Factor the delay is multiplied by after each attempt"),
1294
+ maxRetries: z.number().int().describe("Number of retry attempts to compute (1-50)"),
1295
+ maxDelayMs: z.number().optional().describe("Optional cap applied to every computed delay"),
1296
+ jitter: z.enum(["none", "full", "equal"]).default("none").describe('"full" randomizes uniformly between 0 and the nominal delay; "equal" randomizes between half the nominal delay and the full value')
1297
+ },
1298
+ async ({ baseDelayMs, multiplier, maxRetries, maxDelayMs, jitter }) => {
1299
+ const result = _calculateRetryBackoffSchedule({ baseDelayMs, multiplier, maxRetries, maxDelayMs, jitter });
1300
+ if ("error" in result) return errText(result);
1301
+ return text(JSON.stringify(result, null, 2));
1302
+ }
1303
+ );
1246
1304
  server.tool("summarize_for_llm", "Extractive summarization: pick key sentences to fit a token budget.", { text: z.string(), max_tokens: z.number().int().min(10).default(200), strategy: z.enum(["extractive", "first", "last"]).default("extractive") }, async ({ text: text2, max_tokens, strategy }) => {
1247
1305
  return { content: [{ type: "text", text: JSON.stringify(summarizeForLlm(text2, max_tokens, strategy), null, 2) }] };
1248
1306
  });
@@ -2259,6 +2317,64 @@ server.tool(
2259
2317
  return text(JSON.stringify(result, null, 2));
2260
2318
  }
2261
2319
  );
2320
+ var _REAL_RATE_FIELDS_MCP = ["nominalRatePercent", "inflationRatePercent", "realRatePercent"];
2321
+ function _calculateRealRateOfReturn(input) {
2322
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
2323
+ const given = _REAL_RATE_FIELDS_MCP.filter((f) => provided(input[f]));
2324
+ if (given.length !== 2) {
2325
+ return { error: `Provide exactly two of nominalRatePercent, inflationRatePercent, realRatePercent (got ${given.length})` };
2326
+ }
2327
+ for (const f of given) {
2328
+ const v = input[f];
2329
+ if (v <= -100) return { error: `${f} must be greater than -100` };
2330
+ }
2331
+ const has = (f) => given.includes(f);
2332
+ const nominal0 = input.nominalRatePercent;
2333
+ const inflation0 = input.inflationRatePercent;
2334
+ const real0 = input.realRatePercent;
2335
+ let nominalRatePercent;
2336
+ let inflationRatePercent;
2337
+ let realRatePercent;
2338
+ let solvedFor;
2339
+ if (has("nominalRatePercent") && has("inflationRatePercent")) {
2340
+ nominalRatePercent = nominal0;
2341
+ inflationRatePercent = inflation0;
2342
+ realRatePercent = ((1 + nominal0 / 100) / (1 + inflation0 / 100) - 1) * 100;
2343
+ solvedFor = "realRatePercent";
2344
+ } else if (has("realRatePercent") && has("inflationRatePercent")) {
2345
+ realRatePercent = real0;
2346
+ inflationRatePercent = inflation0;
2347
+ nominalRatePercent = ((1 + real0 / 100) * (1 + inflation0 / 100) - 1) * 100;
2348
+ solvedFor = "nominalRatePercent";
2349
+ } else {
2350
+ nominalRatePercent = nominal0;
2351
+ realRatePercent = real0;
2352
+ inflationRatePercent = ((1 + nominal0 / 100) / (1 + real0 / 100) - 1) * 100;
2353
+ solvedFor = "inflationRatePercent";
2354
+ }
2355
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
2356
+ return {
2357
+ nominalRatePercent: round4(nominalRatePercent),
2358
+ inflationRatePercent: round4(inflationRatePercent),
2359
+ realRatePercent: round4(realRatePercent),
2360
+ given,
2361
+ solvedFor
2362
+ };
2363
+ }
2364
+ server.tool(
2365
+ "calculate_real_rate_of_return",
2366
+ "Apply the Fisher equation, (1 + nominal) = (1 + real) * (1 + inflation), to solve for whichever of the nominal rate, inflation rate, or inflation-adjusted real rate is missing, given exactly two of the three (all as percents).",
2367
+ {
2368
+ nominalRatePercent: z.number().optional().describe("Nominal (stated) annual rate as a percent"),
2369
+ inflationRatePercent: z.number().optional().describe("Annual inflation rate as a percent"),
2370
+ realRatePercent: z.number().optional().describe("Inflation-adjusted real rate as a percent")
2371
+ },
2372
+ async ({ nominalRatePercent, inflationRatePercent, realRatePercent }) => {
2373
+ const result = _calculateRealRateOfReturn({ nominalRatePercent, inflationRatePercent, realRatePercent });
2374
+ if ("error" in result) return errText(result);
2375
+ return text(JSON.stringify(result, null, 2));
2376
+ }
2377
+ );
2262
2378
  function _computeIsbn10CheckDigitMcp(dataDigits) {
2263
2379
  let sum = 0;
2264
2380
  for (let i = 0; i < 9; i++) sum += dataDigits[i] * (10 - i);
@@ -2702,6 +2818,61 @@ server.tool(
2702
2818
  return text(JSON.stringify(result, null, 2));
2703
2819
  }
2704
2820
  );
2821
+ function _toCsvStringMcp(headers, rows) {
2822
+ const escapeField = (field) => {
2823
+ if (field.includes(",") || field.includes('"') || field.includes("\n") || field.includes("\r")) {
2824
+ return `"${field.replace(/"/g, '""')}"`;
2825
+ }
2826
+ return field;
2827
+ };
2828
+ const lines = [headers.map(escapeField).join(",")];
2829
+ for (const row of rows) lines.push(row.map(escapeField).join(","));
2830
+ return lines.join("\n");
2831
+ }
2832
+ function _stringifyCsvValueMcp(value) {
2833
+ if (value === void 0 || value === null) return "";
2834
+ if (typeof value === "string") return value;
2835
+ if (typeof value === "object") return JSON.stringify(value);
2836
+ return String(value);
2837
+ }
2838
+ function _ndjsonToCsvMcp(input) {
2839
+ const validated = _validateNdjson(input);
2840
+ if (!validated.valid) {
2841
+ const firstError = validated.lines.find((l) => !l.valid);
2842
+ return { error: firstError ? `Line ${firstError.line}: ${firstError.error}` : "Invalid NDJSON" };
2843
+ }
2844
+ if (validated.lines.length === 0) return { error: "No records found" };
2845
+ const records = validated.lines.map((l) => JSON.parse(l.raw));
2846
+ for (const record of records) {
2847
+ if (typeof record !== "object" || record === null || Array.isArray(record)) {
2848
+ return { error: "Every line must be a flat JSON object to convert to CSV" };
2849
+ }
2850
+ }
2851
+ const headers = [];
2852
+ const seen = /* @__PURE__ */ new Set();
2853
+ for (const record of records) {
2854
+ for (const key of Object.keys(record)) {
2855
+ if (!seen.has(key)) {
2856
+ seen.add(key);
2857
+ headers.push(key);
2858
+ }
2859
+ }
2860
+ }
2861
+ const rows = records.map((record) => headers.map((h) => _stringifyCsvValueMcp(record[h])));
2862
+ return { output: _toCsvStringMcp(headers, rows) };
2863
+ }
2864
+ server.tool(
2865
+ "ndjson_to_csv",
2866
+ "Convert NDJSON (newline-delimited JSON, one flat object per line) into CSV, inferring the header row from the union of keys across every line.",
2867
+ {
2868
+ input: z.string().describe("NDJSON content: one flat JSON object per line")
2869
+ },
2870
+ async ({ input }) => {
2871
+ const result = _ndjsonToCsvMcp(input);
2872
+ if ("error" in result) return errText(result);
2873
+ return text(result.output);
2874
+ }
2875
+ );
2705
2876
  function _decodePdfString(raw, isHex) {
2706
2877
  const bytes = [];
2707
2878
  if (isHex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.77.0",
3
+ "version": "0.79.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",