@utilix-tech/mcp 0.58.0 → 0.59.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.59.0
6
+ - Add `build_prefer_header`, `parse_prefer_header` tools: build an HTTP Prefer request-header value from preferences with optional values and parameters per RFC 7240, or parse an existing one into the same shape.
7
+ - Add `dedupe_ndjson` tool: remove duplicate records from NDJSON input, either by exact structural equality or by a chosen key field, keeping the first occurrence of each duplicate.
8
+
5
9
  ## 0.58.0
6
10
  - 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
11
  - 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
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.
3
+ MCP server exposing 208 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
 
@@ -235,6 +235,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
235
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
236
  | `build_accept_language` | Build an Accept-Language request-header value from locale/quality pairs, sorted by descending preference |
237
237
  | `parse_accept_language` | Parse an existing Accept-Language header value into its ranked locale/quality pairs |
238
+ | `build_prefer_header` | Build an HTTP Prefer request-header value from preferences with optional values/params, per RFC 7240 |
239
+ | `parse_prefer_header` | Parse an existing Prefer header value into its list of preferences, values, and parameters |
238
240
 
239
241
  ### Color (4 tools)
240
242
  | Tool | Description |
@@ -272,6 +274,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
272
274
  | `xml_to_json` | Convert XML to JSON |
273
275
  | `parse_csv` | Parse a CSV string and return the data as JSON |
274
276
  | `format_ndjson` | Validate and pretty-print newline-delimited JSON (NDJSON) |
277
+ | `dedupe_ndjson` | Remove duplicate records from NDJSON, by exact match or by a key field |
275
278
  | `diff_env_example` | Diff a .env file against its .env.example template: flags keys missing from either side, plus empty placeholder values |
276
279
  | `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
277
280
  | `generate_markdown_toc` | Scan a Markdown document's ATX headings and build a nested table of contents with GitHub-style anchor slugs |
package/dist/index.js CHANGED
@@ -2606,6 +2606,53 @@ server.tool(
2606
2606
  return text(formatted);
2607
2607
  }
2608
2608
  );
2609
+ function _dedupeNdjsonMcp(input, keyField) {
2610
+ const validated = _validateNdjson(input);
2611
+ if (!validated.valid) {
2612
+ const firstError = validated.lines.find((l) => !l.valid);
2613
+ return { error: firstError ? `Line ${firstError.line}: ${firstError.error}` : "Invalid NDJSON" };
2614
+ }
2615
+ const seen = /* @__PURE__ */ new Set();
2616
+ const keptRaw = [];
2617
+ const removedLines = [];
2618
+ for (const l of validated.lines) {
2619
+ const parsed = JSON.parse(l.raw);
2620
+ let key;
2621
+ if (keyField) {
2622
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !(keyField in parsed)) {
2623
+ return { error: `Line ${l.line}: missing key field "${keyField}"` };
2624
+ }
2625
+ key = JSON.stringify(parsed[keyField]);
2626
+ } else {
2627
+ key = JSON.stringify(parsed);
2628
+ }
2629
+ if (seen.has(key)) removedLines.push(l.line);
2630
+ else {
2631
+ seen.add(key);
2632
+ keptRaw.push(l.raw.trim());
2633
+ }
2634
+ }
2635
+ return {
2636
+ totalLines: validated.totalLines,
2637
+ uniqueLines: keptRaw.length,
2638
+ duplicatesRemoved: removedLines.length,
2639
+ output: keptRaw.join("\n"),
2640
+ removedLines
2641
+ };
2642
+ }
2643
+ server.tool(
2644
+ "dedupe_ndjson",
2645
+ "Remove duplicate records from NDJSON (newline-delimited JSON) input. With no keyField, two lines are duplicates when their parsed JSON values are structurally identical. With a keyField, two lines are duplicates when that field's value matches, keeping the first occurrence of each key.",
2646
+ {
2647
+ input: z.string().describe("NDJSON content: one JSON value per line"),
2648
+ keyField: z.string().optional().describe("Optional field name to dedupe by instead of exact structural equality")
2649
+ },
2650
+ async ({ input, keyField }) => {
2651
+ const result = _dedupeNdjsonMcp(input, keyField);
2652
+ if ("error" in result) return errText(result);
2653
+ return text(JSON.stringify(result, null, 2));
2654
+ }
2655
+ );
2609
2656
  function _decodePdfString(raw, isHex) {
2610
2657
  const bytes = [];
2611
2658
  if (isHex) {
@@ -9445,6 +9492,132 @@ server.tool(
9445
9492
  return text(JSON.stringify(result, null, 2));
9446
9493
  }
9447
9494
  );
9495
+ var _PREFER_TOKEN_RE_MCP = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
9496
+ function _isPreferTokenMcp(s) {
9497
+ return _PREFER_TOKEN_RE_MCP.test(s);
9498
+ }
9499
+ function _splitPreferTopLevelMcp(input, delim) {
9500
+ const parts = [];
9501
+ let current = "";
9502
+ let inQuotes = false;
9503
+ for (const ch of input) {
9504
+ if (ch === '"') inQuotes = !inQuotes;
9505
+ if (ch === delim && !inQuotes) {
9506
+ parts.push(current);
9507
+ current = "";
9508
+ } else {
9509
+ current += ch;
9510
+ }
9511
+ }
9512
+ parts.push(current);
9513
+ return parts;
9514
+ }
9515
+ function _parsePreferTokenValueMcp(part) {
9516
+ const trimmed = part.trim();
9517
+ if (!trimmed) return null;
9518
+ const eq = trimmed.indexOf("=");
9519
+ if (eq === -1) return { token: trimmed };
9520
+ const token = trimmed.slice(0, eq).trim();
9521
+ let value = trimmed.slice(eq + 1).trim();
9522
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
9523
+ return { token, value };
9524
+ }
9525
+ function _formatPreferWordMcp(value) {
9526
+ return _isPreferTokenMcp(value) ? value : `"${value.replace(/"/g, '\\"')}"`;
9527
+ }
9528
+ function _formatPreferPreferencesMcp(preferences) {
9529
+ return preferences.map((p) => {
9530
+ let s = p.token;
9531
+ if (p.value !== void 0) s += `=${_formatPreferWordMcp(p.value)}`;
9532
+ for (const param of p.params ?? []) {
9533
+ s += `;${param.name}`;
9534
+ if (param.value !== void 0) s += `=${_formatPreferWordMcp(param.value)}`;
9535
+ }
9536
+ return s;
9537
+ }).join(", ");
9538
+ }
9539
+ function _buildPreferHeaderMcp(preferences) {
9540
+ if (!Array.isArray(preferences) || preferences.length === 0) {
9541
+ return { error: "At least one preference is required" };
9542
+ }
9543
+ const built = [];
9544
+ for (let i = 0; i < preferences.length; i++) {
9545
+ const { token, value, params } = preferences[i];
9546
+ if (typeof token !== "string" || !_isPreferTokenMcp(token.trim())) {
9547
+ return { error: `preference ${i + 1}: token is required and must be a valid HTTP token` };
9548
+ }
9549
+ const cleanParams = [];
9550
+ if (params !== void 0) {
9551
+ if (!Array.isArray(params)) return { error: `preference ${i + 1}: params must be an array` };
9552
+ for (const param of params) {
9553
+ if (typeof param?.name !== "string" || !_isPreferTokenMcp(param.name.trim())) {
9554
+ return { error: `preference ${i + 1}: parameter name is required and must be a valid HTTP token` };
9555
+ }
9556
+ cleanParams.push({ name: param.name.trim(), value: param.value !== void 0 ? String(param.value) : void 0 });
9557
+ }
9558
+ }
9559
+ built.push({
9560
+ token: token.trim(),
9561
+ value: value !== void 0 ? String(value) : void 0,
9562
+ params: cleanParams.length > 0 ? cleanParams : void 0
9563
+ });
9564
+ }
9565
+ const headerValue = _formatPreferPreferencesMcp(built);
9566
+ return { header: `Prefer: ${headerValue}`, headerValue, preferences: built };
9567
+ }
9568
+ function _parsePreferHeaderMcp(raw) {
9569
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
9570
+ const withoutLabel = raw.trim().replace(/^prefer:\s*/i, "");
9571
+ const parts = _splitPreferTopLevelMcp(withoutLabel, ",").map((p) => p.trim()).filter((p) => p.length > 0);
9572
+ if (parts.length === 0) return { error: "Input is empty" };
9573
+ const preferences = [];
9574
+ for (const part of parts) {
9575
+ const segments = _splitPreferTopLevelMcp(part, ";").map((s) => s.trim()).filter((s) => s.length > 0);
9576
+ if (segments.length === 0) return { error: `Invalid preference: "${part}"` };
9577
+ const main = _parsePreferTokenValueMcp(segments[0]);
9578
+ if (!main || !_isPreferTokenMcp(main.token)) return { error: `Invalid preference token in "${part}"` };
9579
+ const params = [];
9580
+ for (const seg of segments.slice(1)) {
9581
+ const p = _parsePreferTokenValueMcp(seg);
9582
+ if (!p || !_isPreferTokenMcp(p.token)) return { error: `Invalid parameter in "${part}"` };
9583
+ params.push({ name: p.token, value: p.value });
9584
+ }
9585
+ preferences.push({ token: main.token, value: main.value, params: params.length > 0 ? params : void 0 });
9586
+ }
9587
+ const headerValue = _formatPreferPreferencesMcp(preferences);
9588
+ return { header: `Prefer: ${headerValue}`, headerValue, preferences };
9589
+ }
9590
+ server.tool(
9591
+ "build_prefer_header",
9592
+ "Build a Prefer request-header value from a list of preferences, each with an optional value and optional parameters, per RFC 7240. A value or parameter value that is not a valid HTTP token is quoted automatically.",
9593
+ {
9594
+ preferences: z.array(z.object({
9595
+ token: z.string().describe('The preference token, e.g. "respond-async" or "return"'),
9596
+ value: z.string().optional().describe("Optional value for the preference"),
9597
+ params: z.array(z.object({
9598
+ name: z.string(),
9599
+ value: z.string().optional()
9600
+ })).optional().describe("Optional parameters following the preference")
9601
+ })).min(1).describe("Preferences to build the header from")
9602
+ },
9603
+ async ({ preferences }) => {
9604
+ const result = _buildPreferHeaderMcp(preferences);
9605
+ if ("error" in result) return errText(result);
9606
+ return text(JSON.stringify(result, null, 2));
9607
+ }
9608
+ );
9609
+ server.tool(
9610
+ "parse_prefer_header",
9611
+ 'Parse an existing Prefer header value (with or without a leading "Prefer:" label) into its list of preferences, each with an optional value and optional parameters, per RFC 7240.',
9612
+ {
9613
+ header: z.string().min(1).describe("The Prefer header value to parse")
9614
+ },
9615
+ async ({ header }) => {
9616
+ const result = _parsePreferHeaderMcp(header);
9617
+ if ("error" in result) return errText(result);
9618
+ return text(JSON.stringify(result, null, 2));
9619
+ }
9620
+ );
9448
9621
  function _calculateBlendedLoanRateMcp(loans) {
9449
9622
  if (!Array.isArray(loans) || loans.length === 0) {
9450
9623
  return { error: "loans must be a non-empty array" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.58.0",
3
+ "version": "0.59.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",