@utilix-tech/mcp 0.78.0 → 0.80.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.80.0
6
+ - Add `diff_json_patch`, `apply_json_patch` tools: diff two JSON documents into an RFC 6902 JSON Patch operation list (add/remove/replace/move/copy/test), or apply a patch to a target document.
7
+ - Add `parse_connection_string` tool: parse a database connection URI (postgres, mysql, mongodb, redis, amqp, and similar) into its scheme, credentials, hosts, database, and query parameters.
8
+
9
+ ## 0.79.0
10
+ - 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.
11
+ - 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.
12
+
5
13
  ## 0.78.0
6
14
  - 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.
7
15
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
3
+ MCP server exposing 255 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 (16 tools)
78
+ ### JSON (18 tools)
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
81
  | `format_json` | Pretty-print and validate a JSON string |
@@ -86,6 +86,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
86
86
  | `jsonpath` | Evaluate a JSONPath expression |
87
87
  | `resolve_json_pointer` | Resolve an RFC 6901 JSON Pointer against a JSON document |
88
88
  | `apply_json_merge_patch` | Apply an RFC 7396 JSON Merge Patch document to a target JSON document |
89
+ | `diff_json_patch` | Compare two JSON documents and generate an RFC 6902 JSON Patch operation list |
90
+ | `apply_json_patch` | Apply a sequence of RFC 6902 JSON Patch operations (add, remove, replace, move, copy, test) to a target document |
89
91
  | `validate_json` | Validate JSON against a JSON Schema |
90
92
  | `yaml_to_json` | Convert YAML to JSON |
91
93
  | `json_to_yaml` | Convert JSON to YAML |
@@ -164,7 +166,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
164
166
  | `convert_timezone` | Convert a date/time between timezones |
165
167
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
166
168
 
167
- ### Units (45 tools)
169
+ ### Units (46 tools)
168
170
  | Tool | Description |
169
171
  |------|-------------|
170
172
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -212,6 +214,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
212
214
  | `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
215
  | `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
216
  | `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 |
217
+ | `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
218
 
216
219
  ### API / Network (51 tools)
217
220
  | Tool | Description |
@@ -278,7 +281,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
278
281
  | `color_palette` | Generate a color palette from a base color |
279
282
  | `color_shades` | Generate shades or tints from a base color |
280
283
 
281
- ### Code (21 tools)
284
+ ### Code (22 tools)
282
285
  | Tool | Description |
283
286
  |------|-------------|
284
287
  | `format_sql` | Format a SQL query with proper indentation |
@@ -302,6 +305,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
302
305
  | `parse_commit_trailers` | Parse a commit message's trailer block (Co-authored-by, Signed-off-by, Fixes, Reviewed-by, etc.) per git's RFC 822-style trailer convention, returning each trailer's key/value plus the body with the trailer block removed |
303
306
  | `extract_markdown_footnotes` | Extract every Markdown-Extra-style footnote reference and definition from a document, flagging references with no matching definition and definitions that are never referenced |
304
307
  | `validate_aria` | Parse an HTML snippet and check every role and aria-* attribute against a curated table of WAI-ARIA 1.2 roles: unknown role names, an aria-* attribute not permitted on the given role, and a widget role missing its required aria-* state |
308
+ | `parse_connection_string` | Parse a database connection URI (postgres, mysql, mongodb, redis, amqp, and similar) into its scheme, credentials, hosts, database, and query parameters |
305
309
 
306
310
  ### Data (16 tools)
307
311
  | Tool | Description |
@@ -337,7 +341,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
337
341
  | `unicode_inspect` | Inspect Unicode code points of a string |
338
342
  | `optimize_svg` | Optimize and minify an SVG string |
339
343
 
340
- ### AI / Agent (28 tools)
344
+ ### AI / Agent (29 tools)
341
345
  | Tool | Description |
342
346
  |------|-------------|
343
347
  | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
@@ -368,6 +372,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
368
372
  | `vector_similarity` | Cosine similarity, dot product, and Euclidean / Manhattan distance between two numeric vectors |
369
373
  | `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
370
374
  | `lint_tool_schema` | Lint an OpenAI, Anthropic, or MCP tool/function-calling definition for missing descriptions, invalid names, empty enums, and required/properties mismatches |
375
+ | `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 |
371
376
 
372
377
  ### Image (4 tools)
373
378
  | 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);
@@ -12330,5 +12446,303 @@ server.tool(
12330
12446
  return text(JSON.stringify(result, null, 2));
12331
12447
  }
12332
12448
  );
12449
+ function _escapeJsonPointerTokenMcp(token) {
12450
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
12451
+ }
12452
+ function _unescapeJsonPointerTokenMcp(token) {
12453
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
12454
+ }
12455
+ function _parseJsonPointerTokensMcp(pointer) {
12456
+ if (pointer === "") return [];
12457
+ if (!pointer.startsWith("/")) throw new Error(`Invalid JSON Pointer "${pointer}": must start with "/"`);
12458
+ return pointer.slice(1).split("/").map(_unescapeJsonPointerTokenMcp);
12459
+ }
12460
+ function _isJsonPatchObjectMcp(value) {
12461
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12462
+ }
12463
+ function _jsonPatchDeepEqualMcp(a, b) {
12464
+ if (a === b) return true;
12465
+ if (typeof a !== typeof b) return false;
12466
+ if (Array.isArray(a) || Array.isArray(b)) {
12467
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
12468
+ return a.every((v, i) => _jsonPatchDeepEqualMcp(v, b[i]));
12469
+ }
12470
+ if (_isJsonPatchObjectMcp(a) && _isJsonPatchObjectMcp(b)) {
12471
+ const aKeys = Object.keys(a);
12472
+ const bKeys = Object.keys(b);
12473
+ if (aKeys.length !== bKeys.length) return false;
12474
+ return aKeys.every((k) => k in b && _jsonPatchDeepEqualMcp(a[k], b[k]));
12475
+ }
12476
+ return false;
12477
+ }
12478
+ function _jsonPatchGetByTokensMcp(doc, tokens) {
12479
+ let current = doc;
12480
+ for (const token of tokens) {
12481
+ if (Array.isArray(current)) {
12482
+ const index = token === "-" ? current.length - 1 : parseInt(token, 10);
12483
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) return { found: false, value: void 0 };
12484
+ current = current[index];
12485
+ } else if (_isJsonPatchObjectMcp(current)) {
12486
+ if (!(token in current)) return { found: false, value: void 0 };
12487
+ current = current[token];
12488
+ } else {
12489
+ return { found: false, value: void 0 };
12490
+ }
12491
+ }
12492
+ return { found: true, value: current };
12493
+ }
12494
+ function _diffJsonPatchMcp(a, b) {
12495
+ const ops = [];
12496
+ _diffJsonPatchValueMcp(a, b, "", ops);
12497
+ return ops;
12498
+ }
12499
+ function _diffJsonPatchValueMcp(a, b, path, ops) {
12500
+ if (_jsonPatchDeepEqualMcp(a, b)) return;
12501
+ if (_isJsonPatchObjectMcp(a) && _isJsonPatchObjectMcp(b)) {
12502
+ for (const key of Object.keys(a)) {
12503
+ if (!(key in b)) ops.push({ op: "remove", path: `${path}/${_escapeJsonPointerTokenMcp(key)}` });
12504
+ }
12505
+ for (const key of Object.keys(b)) {
12506
+ const childPath = `${path}/${_escapeJsonPointerTokenMcp(key)}`;
12507
+ if (!(key in a)) {
12508
+ ops.push({ op: "add", path: childPath, value: b[key] });
12509
+ } else {
12510
+ _diffJsonPatchValueMcp(a[key], b[key], childPath, ops);
12511
+ }
12512
+ }
12513
+ return;
12514
+ }
12515
+ if (Array.isArray(a) && Array.isArray(b)) {
12516
+ const minLen = Math.min(a.length, b.length);
12517
+ for (let i = 0; i < minLen; i++) _diffJsonPatchValueMcp(a[i], b[i], `${path}/${i}`, ops);
12518
+ if (b.length > a.length) {
12519
+ for (let i = a.length; i < b.length; i++) ops.push({ op: "add", path: `${path}/${i}`, value: b[i] });
12520
+ } else if (a.length > b.length) {
12521
+ for (let i = a.length - 1; i >= b.length; i--) ops.push({ op: "remove", path: `${path}/${i}` });
12522
+ }
12523
+ return;
12524
+ }
12525
+ ops.push({ op: "replace", path, value: b });
12526
+ }
12527
+ function _applyJsonPatchMcp(target, operations) {
12528
+ let doc = JSON.parse(JSON.stringify(target ?? null));
12529
+ const applyOne = (op, index) => {
12530
+ if (op.path === void 0 || op.op === void 0) return `Operation ${index}: missing "op" or "path"`;
12531
+ let tokens;
12532
+ try {
12533
+ tokens = _parseJsonPointerTokensMcp(op.path);
12534
+ } catch (e) {
12535
+ return `Operation ${index}: ${e.message}`;
12536
+ }
12537
+ const setAt = (tgtTokens, value) => {
12538
+ if (tgtTokens.length === 0) {
12539
+ doc = value;
12540
+ return null;
12541
+ }
12542
+ const parentTokens = tgtTokens.slice(0, -1);
12543
+ const lastToken = tgtTokens[tgtTokens.length - 1];
12544
+ const { found, value: parent } = _jsonPatchGetByTokensMcp(doc, parentTokens);
12545
+ if (!found) return `Operation ${index}: path "${op.path}" has no parent`;
12546
+ if (Array.isArray(parent)) {
12547
+ const idx = lastToken === "-" ? parent.length : parseInt(lastToken, 10);
12548
+ if (!Number.isInteger(idx) || idx < 0 || idx > parent.length) return `Operation ${index}: array index "${lastToken}" out of bounds`;
12549
+ parent.splice(idx, 0, value);
12550
+ } else if (_isJsonPatchObjectMcp(parent)) {
12551
+ parent[lastToken] = value;
12552
+ } else {
12553
+ return `Operation ${index}: cannot add into a non-object, non-array value at "${op.path}"`;
12554
+ }
12555
+ return null;
12556
+ };
12557
+ const replaceAt = (tgtTokens, value) => {
12558
+ if (tgtTokens.length === 0) {
12559
+ doc = value;
12560
+ return null;
12561
+ }
12562
+ const parentTokens = tgtTokens.slice(0, -1);
12563
+ const lastToken = tgtTokens[tgtTokens.length - 1];
12564
+ const { found, value: parent } = _jsonPatchGetByTokensMcp(doc, parentTokens);
12565
+ if (!found) return `Operation ${index}: path "${op.path}" has no parent`;
12566
+ if (Array.isArray(parent)) {
12567
+ const idx = lastToken === "-" ? parent.length - 1 : parseInt(lastToken, 10);
12568
+ if (!Number.isInteger(idx) || idx < 0 || idx >= parent.length) return `Operation ${index}: array index "${lastToken}" out of bounds`;
12569
+ parent[idx] = value;
12570
+ } else if (_isJsonPatchObjectMcp(parent)) {
12571
+ if (!(lastToken in parent)) return `Operation ${index}: path "${op.path}" does not exist`;
12572
+ parent[lastToken] = value;
12573
+ } else {
12574
+ return `Operation ${index}: cannot replace into a non-object, non-array value at "${op.path}"`;
12575
+ }
12576
+ return null;
12577
+ };
12578
+ const removeAt = (tgtTokens) => {
12579
+ if (tgtTokens.length === 0) return { error: `Operation ${index}: cannot remove the whole document (path "")` };
12580
+ const parentTokens = tgtTokens.slice(0, -1);
12581
+ const lastToken = tgtTokens[tgtTokens.length - 1];
12582
+ const { found, value: parent } = _jsonPatchGetByTokensMcp(doc, parentTokens);
12583
+ if (!found) return { error: `Operation ${index}: path "${op.path}" has no parent` };
12584
+ if (Array.isArray(parent)) {
12585
+ const idx = lastToken === "-" ? parent.length - 1 : parseInt(lastToken, 10);
12586
+ if (!Number.isInteger(idx) || idx < 0 || idx >= parent.length) return { error: `Operation ${index}: array index "${lastToken}" out of bounds` };
12587
+ const [removed] = parent.splice(idx, 1);
12588
+ return { error: null, removed };
12589
+ }
12590
+ if (_isJsonPatchObjectMcp(parent)) {
12591
+ if (!(lastToken in parent)) return { error: `Operation ${index}: path "${op.path}" does not exist` };
12592
+ const removed = parent[lastToken];
12593
+ delete parent[lastToken];
12594
+ return { error: null, removed };
12595
+ }
12596
+ return { error: `Operation ${index}: cannot remove from a non-object, non-array value at "${op.path}"` };
12597
+ };
12598
+ switch (op.op) {
12599
+ case "add":
12600
+ return setAt(tokens, op.value);
12601
+ case "replace":
12602
+ return replaceAt(tokens, op.value);
12603
+ case "remove":
12604
+ return removeAt(tokens).error;
12605
+ case "test": {
12606
+ const { found, value } = _jsonPatchGetByTokensMcp(doc, tokens);
12607
+ if (!found) return `Operation ${index}: path "${op.path}" does not exist`;
12608
+ if (!_jsonPatchDeepEqualMcp(value, op.value)) return `Operation ${index}: test failed at "${op.path}"`;
12609
+ return null;
12610
+ }
12611
+ case "move": {
12612
+ if (!op.from) return `Operation ${index}: "move" requires "from"`;
12613
+ let fromTokens;
12614
+ try {
12615
+ fromTokens = _parseJsonPointerTokensMcp(op.from);
12616
+ } catch (e) {
12617
+ return `Operation ${index}: ${e.message}`;
12618
+ }
12619
+ const removal = removeAt(fromTokens);
12620
+ if (removal.error) return removal.error;
12621
+ return setAt(tokens, removal.removed);
12622
+ }
12623
+ case "copy": {
12624
+ if (!op.from) return `Operation ${index}: "copy" requires "from"`;
12625
+ let fromTokens;
12626
+ try {
12627
+ fromTokens = _parseJsonPointerTokensMcp(op.from);
12628
+ } catch (e) {
12629
+ return `Operation ${index}: ${e.message}`;
12630
+ }
12631
+ const { found, value } = _jsonPatchGetByTokensMcp(doc, fromTokens);
12632
+ if (!found) return `Operation ${index}: "from" path "${op.from}" does not exist`;
12633
+ return setAt(tokens, JSON.parse(JSON.stringify(value)));
12634
+ }
12635
+ default:
12636
+ return `Operation ${index}: unknown op "${op.op}"`;
12637
+ }
12638
+ };
12639
+ for (let i = 0; i < operations.length; i++) {
12640
+ const err = applyOne(operations[i], i);
12641
+ if (err) return { error: err };
12642
+ }
12643
+ return { result: doc };
12644
+ }
12645
+ server.tool(
12646
+ "diff_json_patch",
12647
+ "Compare two JSON documents and generate an RFC 6902 JSON Patch (a list of add/remove/replace operations) that transforms the first into the second. Object keys produce add/remove/replace; arrays are compared position by position (not a minimal edit-distance diff, so a reordering is not detected as a move).",
12648
+ {
12649
+ a: z.string().describe("The source JSON document, as a JSON string"),
12650
+ b: z.string().describe("The target JSON document, as a JSON string")
12651
+ },
12652
+ async ({ a, b }) => {
12653
+ let aParsed;
12654
+ try {
12655
+ aParsed = JSON.parse(a);
12656
+ } catch {
12657
+ return errText({ error: "First document is not valid JSON" });
12658
+ }
12659
+ let bParsed;
12660
+ try {
12661
+ bParsed = JSON.parse(b);
12662
+ } catch {
12663
+ return errText({ error: "Second document is not valid JSON" });
12664
+ }
12665
+ return text(JSON.stringify({ patch: _diffJsonPatchMcp(aParsed, bParsed) }, null, 2));
12666
+ }
12667
+ );
12668
+ server.tool(
12669
+ "apply_json_patch",
12670
+ "Apply a sequence of RFC 6902 JSON Patch operations (add, remove, replace, move, copy, test) to a target JSON document and return the result, or an error naming the first operation that failed and why.",
12671
+ {
12672
+ target: z.string().describe("The document to patch, as a JSON string"),
12673
+ patch: z.string().describe("The RFC 6902 JSON Patch operation array, as a JSON string")
12674
+ },
12675
+ async ({ target, patch }) => {
12676
+ let targetParsed;
12677
+ try {
12678
+ targetParsed = JSON.parse(target);
12679
+ } catch {
12680
+ return errText({ error: "Target is not valid JSON" });
12681
+ }
12682
+ let operations;
12683
+ try {
12684
+ operations = JSON.parse(patch);
12685
+ } catch {
12686
+ return errText({ error: "Patch is not valid JSON" });
12687
+ }
12688
+ if (!Array.isArray(operations)) return errText({ error: "Patch must be a JSON array of RFC 6902 operations" });
12689
+ const result = _applyJsonPatchMcp(targetParsed, operations);
12690
+ if ("error" in result) return errText(result);
12691
+ return text(JSON.stringify(result, null, 2));
12692
+ }
12693
+ );
12694
+ var _CONNECTION_STRING_RE_MCP = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?:([^@/?#]*)@)?([^/?#]*)(?:\/([^?#]*))?(?:\?([^#]*))?$/;
12695
+ function _parseConnectionHostMcp(segment) {
12696
+ const ipv6Match = segment.match(/^\[([^\]]+)\](?::(\d+))?$/);
12697
+ if (ipv6Match) return { host: ipv6Match[1], port: ipv6Match[2] ? parseInt(ipv6Match[2], 10) : null };
12698
+ const lastColon = segment.lastIndexOf(":");
12699
+ if (lastColon === -1) return { host: segment, port: null };
12700
+ const portPart = segment.slice(lastColon + 1);
12701
+ if (!/^\d+$/.test(portPart)) return { host: segment, port: null };
12702
+ return { host: segment.slice(0, lastColon), port: parseInt(portPart, 10) };
12703
+ }
12704
+ function _parseConnectionStringMcp(input) {
12705
+ if (!input || !input.trim()) return { error: "Connection string is empty" };
12706
+ const trimmed = input.trim();
12707
+ const match = trimmed.match(_CONNECTION_STRING_RE_MCP);
12708
+ if (!match) return { error: "Not a valid scheme://[user[:password]@]host[,host2][/database][?params] connection string" };
12709
+ const [, scheme, userinfo, hostPart, databasePart, queryPart] = match;
12710
+ let user = null;
12711
+ let password = null;
12712
+ if (userinfo) {
12713
+ const sepIndex = userinfo.indexOf(":");
12714
+ if (sepIndex === -1) {
12715
+ user = decodeURIComponent(userinfo);
12716
+ } else {
12717
+ user = decodeURIComponent(userinfo.slice(0, sepIndex));
12718
+ password = decodeURIComponent(userinfo.slice(sepIndex + 1));
12719
+ }
12720
+ }
12721
+ const hosts = hostPart ? hostPart.split(",").filter(Boolean).map(_parseConnectionHostMcp) : [];
12722
+ const database = databasePart ? decodeURIComponent(databasePart) : null;
12723
+ const params = {};
12724
+ if (queryPart) {
12725
+ for (const pair of queryPart.split("&")) {
12726
+ if (!pair) continue;
12727
+ const eqIndex = pair.indexOf("=");
12728
+ const key = eqIndex === -1 ? pair : pair.slice(0, eqIndex);
12729
+ const value = eqIndex === -1 ? "" : pair.slice(eqIndex + 1);
12730
+ params[decodeURIComponent(key)] = decodeURIComponent(value);
12731
+ }
12732
+ }
12733
+ return { scheme, user, password, hosts, database, params };
12734
+ }
12735
+ server.tool(
12736
+ "parse_connection_string",
12737
+ "Parse a database connection URI (postgres://, mysql://, mongodb://, mongodb+srv://, redis://, amqp://, and similar generic scheme:// connection strings) into its scheme, credentials, one or more hosts (mongodb-style comma-separated host lists included), database/path, and query parameters. Pure offline parse: never connects, resolves DNS, or validates the driver/scheme combination.",
12738
+ {
12739
+ connectionString: z.string().describe("The database connection string to parse")
12740
+ },
12741
+ async ({ connectionString }) => {
12742
+ const result = _parseConnectionStringMcp(connectionString);
12743
+ if ("error" in result) return errText(result);
12744
+ return text(JSON.stringify(result, null, 2));
12745
+ }
12746
+ );
12333
12747
  var transport = new StdioServerTransport();
12334
12748
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.78.0",
3
+ "version": "0.80.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",