@utilix-tech/mcp 0.79.0 → 0.81.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.81.0
6
+ - Add `validate_fence_language_tags` tool: scan a Markdown document's fenced code blocks and flag a missing, unknown, or inconsistently-cased language tag against a curated list of common syntax-highlighter identifiers.
7
+ - Add `count_sql_placeholders` tool: count positional (`?`, `$1`) and named (`:name`, `@name`) parameter placeholders in a SQL query, flagging mixed styles and numbered-sequence gaps or duplicates.
8
+
9
+ ## 0.80.0
10
+ - 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.
11
+ - 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.
12
+
5
13
  ## 0.79.0
6
14
  - 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
15
  - 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.
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 257 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 |
@@ -279,7 +281,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
279
281
  | `color_palette` | Generate a color palette from a base color |
280
282
  | `color_shades` | Generate shades or tints from a base color |
281
283
 
282
- ### Code (21 tools)
284
+ ### Code (24 tools)
283
285
  | Tool | Description |
284
286
  |------|-------------|
285
287
  | `format_sql` | Format a SQL query with proper indentation |
@@ -303,6 +305,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
303
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 |
304
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 |
305
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 |
309
+ | `validate_fence_language_tags` | Scan a Markdown document's fenced code blocks and flag a missing, unknown, or inconsistently-cased language tag against a curated list of common syntax-highlighter identifiers |
310
+ | `count_sql_placeholders` | Count positional (`?`, `$1`) and named (`:name`, `@name`) parameter placeholders in a SQL query, flagging mixed styles and numbered-sequence gaps or duplicates |
306
311
 
307
312
  ### Data (16 tools)
308
313
  | Tool | Description |
package/dist/index.js CHANGED
@@ -12446,5 +12446,666 @@ server.tool(
12446
12446
  return text(JSON.stringify(result, null, 2));
12447
12447
  }
12448
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
+ );
12747
+ var _KNOWN_LANGUAGE_TAGS_MCP = /* @__PURE__ */ new Set([
12748
+ "js",
12749
+ "jsx",
12750
+ "mjs",
12751
+ "cjs",
12752
+ "javascript",
12753
+ "ts",
12754
+ "tsx",
12755
+ "typescript",
12756
+ "json",
12757
+ "json5",
12758
+ "jsonc",
12759
+ "jsonl",
12760
+ "ndjson",
12761
+ "yaml",
12762
+ "yml",
12763
+ "toml",
12764
+ "ini",
12765
+ "cfg",
12766
+ "conf",
12767
+ "properties",
12768
+ "env",
12769
+ "dotenv",
12770
+ "html",
12771
+ "htm",
12772
+ "xml",
12773
+ "svg",
12774
+ "xhtml",
12775
+ "css",
12776
+ "scss",
12777
+ "sass",
12778
+ "less",
12779
+ "stylus",
12780
+ "styl",
12781
+ "markdown",
12782
+ "md",
12783
+ "mdx",
12784
+ "bash",
12785
+ "sh",
12786
+ "shell",
12787
+ "zsh",
12788
+ "fish",
12789
+ "console",
12790
+ "powershell",
12791
+ "ps1",
12792
+ "bat",
12793
+ "cmd",
12794
+ "python",
12795
+ "py",
12796
+ "py3",
12797
+ "ipynb",
12798
+ "ruby",
12799
+ "rb",
12800
+ "erb",
12801
+ "php",
12802
+ "java",
12803
+ "kotlin",
12804
+ "kt",
12805
+ "kts",
12806
+ "groovy",
12807
+ "gradle",
12808
+ "swift",
12809
+ "objectivec",
12810
+ "objc",
12811
+ "go",
12812
+ "golang",
12813
+ "rust",
12814
+ "rs",
12815
+ "c",
12816
+ "h",
12817
+ "cpp",
12818
+ "cc",
12819
+ "cxx",
12820
+ "hpp",
12821
+ "c++",
12822
+ "csharp",
12823
+ "cs",
12824
+ "fsharp",
12825
+ "fs",
12826
+ "perl",
12827
+ "pl",
12828
+ "lua",
12829
+ "r",
12830
+ "matlab",
12831
+ "m",
12832
+ "scala",
12833
+ "nim",
12834
+ "zig",
12835
+ "julia",
12836
+ "jl",
12837
+ "haskell",
12838
+ "hs",
12839
+ "elixir",
12840
+ "ex",
12841
+ "exs",
12842
+ "erlang",
12843
+ "erl",
12844
+ "clojure",
12845
+ "clj",
12846
+ "cljs",
12847
+ "dart",
12848
+ "ocaml",
12849
+ "ml",
12850
+ "sql",
12851
+ "mysql",
12852
+ "postgresql",
12853
+ "plsql",
12854
+ "sqlite",
12855
+ "graphql",
12856
+ "gql",
12857
+ "dockerfile",
12858
+ "docker",
12859
+ "makefile",
12860
+ "make",
12861
+ "cmake",
12862
+ "nginx",
12863
+ "apache",
12864
+ "diff",
12865
+ "patch",
12866
+ "git",
12867
+ "http",
12868
+ "proto",
12869
+ "protobuf",
12870
+ "csv",
12871
+ "tsv",
12872
+ "vim",
12873
+ "viml",
12874
+ "tex",
12875
+ "latex",
12876
+ "bibtex",
12877
+ "plaintext",
12878
+ "text",
12879
+ "txt",
12880
+ "nohighlight",
12881
+ "none",
12882
+ "asciidoc",
12883
+ "adoc",
12884
+ "rst",
12885
+ "vue",
12886
+ "svelte",
12887
+ "astro",
12888
+ "wasm",
12889
+ "wat",
12890
+ "handlebars",
12891
+ "hbs",
12892
+ "pug",
12893
+ "jade",
12894
+ "coffee",
12895
+ "coffeescript",
12896
+ "actionscript",
12897
+ "terraform",
12898
+ "tf",
12899
+ "hcl",
12900
+ "puppet",
12901
+ "ansible"
12902
+ ]);
12903
+ function _parseFenceLineMcp(line) {
12904
+ const match = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
12905
+ if (!match) return null;
12906
+ const [, , marker, rest] = match;
12907
+ const char = marker[0];
12908
+ if (char === "`" && rest.includes("`")) return null;
12909
+ return { char, length: marker.length, infoString: rest.trim() };
12910
+ }
12911
+ function _isClosingFenceMcp(line, fenceChar, fenceLength) {
12912
+ const match = line.match(/^ {0,3}(`+|~+)\s*$/);
12913
+ if (!match) return false;
12914
+ const marker = match[1];
12915
+ return marker[0] === fenceChar && marker.length >= fenceLength;
12916
+ }
12917
+ function _validateFenceLanguageTagsMcp(markdown) {
12918
+ const lines = markdown.split("\n");
12919
+ const issues = [];
12920
+ let totalFences = 0;
12921
+ let open = null;
12922
+ for (let i = 0; i < lines.length; i++) {
12923
+ const lineNumber = i + 1;
12924
+ const line = lines[i];
12925
+ if (open) {
12926
+ if (_isClosingFenceMcp(line, open.char, open.length)) open = null;
12927
+ continue;
12928
+ }
12929
+ const fence = _parseFenceLineMcp(line);
12930
+ if (!fence) continue;
12931
+ totalFences++;
12932
+ const tag = fence.infoString.split(/\s+/)[0] ?? "";
12933
+ open = { char: fence.char, length: fence.length };
12934
+ if (tag === "") {
12935
+ issues.push({ line: lineNumber, tag, issue: "missing" });
12936
+ continue;
12937
+ }
12938
+ const lower = tag.toLowerCase();
12939
+ if (!_KNOWN_LANGUAGE_TAGS_MCP.has(lower)) {
12940
+ issues.push({ line: lineNumber, tag, issue: "unknown" });
12941
+ continue;
12942
+ }
12943
+ if (tag !== lower) {
12944
+ issues.push({ line: lineNumber, tag, issue: "inconsistent-case", suggestion: lower });
12945
+ }
12946
+ }
12947
+ return { totalFences, issues };
12948
+ }
12949
+ server.tool(
12950
+ "validate_fence_language_tags",
12951
+ "Scan a Markdown document's fenced code blocks (``` or ~~~) and flag ones with a missing, unknown, or inconsistently-cased language tag, checked against a curated list of common syntax-highlighter language identifiers.",
12952
+ {
12953
+ markdown: z.string().describe("The Markdown document to scan")
12954
+ },
12955
+ async ({ markdown }) => {
12956
+ const result = _validateFenceLanguageTagsMcp(markdown);
12957
+ return text(JSON.stringify(result, null, 2));
12958
+ }
12959
+ );
12960
+ var _SQL_IDENT_START_MCP = /[A-Za-z_]/;
12961
+ var _SQL_IDENT_CHAR_MCP = /[A-Za-z0-9_]/;
12962
+ function _readSqlIdentifierMcp(sql, start) {
12963
+ let end = start;
12964
+ while (end < sql.length && _SQL_IDENT_CHAR_MCP.test(sql[end])) end++;
12965
+ return sql.slice(start, end);
12966
+ }
12967
+ function _countSqlPlaceholdersMcp(sql) {
12968
+ const placeholders = [];
12969
+ let state = "normal";
12970
+ let i = 0;
12971
+ while (i < sql.length) {
12972
+ const ch = sql[i];
12973
+ const next = sql[i + 1];
12974
+ if (state === "single") {
12975
+ if (ch === "'" && next === "'") {
12976
+ i += 2;
12977
+ continue;
12978
+ }
12979
+ if (ch === "'") {
12980
+ state = "normal";
12981
+ i++;
12982
+ continue;
12983
+ }
12984
+ i++;
12985
+ continue;
12986
+ }
12987
+ if (state === "double") {
12988
+ if (ch === '"' && next === '"') {
12989
+ i += 2;
12990
+ continue;
12991
+ }
12992
+ if (ch === '"') {
12993
+ state = "normal";
12994
+ i++;
12995
+ continue;
12996
+ }
12997
+ i++;
12998
+ continue;
12999
+ }
13000
+ if (state === "line-comment") {
13001
+ if (ch === "\n") state = "normal";
13002
+ i++;
13003
+ continue;
13004
+ }
13005
+ if (state === "block-comment") {
13006
+ if (ch === "*" && next === "/") {
13007
+ state = "normal";
13008
+ i += 2;
13009
+ continue;
13010
+ }
13011
+ i++;
13012
+ continue;
13013
+ }
13014
+ if (ch === "'") {
13015
+ state = "single";
13016
+ i++;
13017
+ continue;
13018
+ }
13019
+ if (ch === '"') {
13020
+ state = "double";
13021
+ i++;
13022
+ continue;
13023
+ }
13024
+ if (ch === "-" && next === "-") {
13025
+ state = "line-comment";
13026
+ i += 2;
13027
+ continue;
13028
+ }
13029
+ if (ch === "/" && next === "*") {
13030
+ state = "block-comment";
13031
+ i += 2;
13032
+ continue;
13033
+ }
13034
+ if (ch === "?") {
13035
+ placeholders.push({ style: "anonymous", raw: "?", index: i });
13036
+ i++;
13037
+ continue;
13038
+ }
13039
+ if (ch === "$" && /[0-9]/.test(next ?? "")) {
13040
+ let end = i + 1;
13041
+ while (end < sql.length && /[0-9]/.test(sql[end])) end++;
13042
+ const raw = sql.slice(i, end);
13043
+ placeholders.push({ style: "numbered", raw, index: i, number: parseInt(raw.slice(1), 10) });
13044
+ i = end;
13045
+ continue;
13046
+ }
13047
+ if (ch === ":") {
13048
+ if (next === ":") {
13049
+ i += 2;
13050
+ continue;
13051
+ }
13052
+ if (next && _SQL_IDENT_START_MCP.test(next)) {
13053
+ const name = _readSqlIdentifierMcp(sql, i + 1);
13054
+ placeholders.push({ style: "named-colon", raw: `:${name}`, index: i, name });
13055
+ i += 1 + name.length;
13056
+ continue;
13057
+ }
13058
+ i++;
13059
+ continue;
13060
+ }
13061
+ if (ch === "@") {
13062
+ if (next === "@") {
13063
+ i += 2;
13064
+ continue;
13065
+ }
13066
+ if (next && _SQL_IDENT_START_MCP.test(next)) {
13067
+ const name = _readSqlIdentifierMcp(sql, i + 1);
13068
+ placeholders.push({ style: "named-at", raw: `@${name}`, index: i, name });
13069
+ i += 1 + name.length;
13070
+ continue;
13071
+ }
13072
+ i++;
13073
+ continue;
13074
+ }
13075
+ i++;
13076
+ }
13077
+ const counts = { anonymous: 0, numbered: 0, "named-colon": 0, "named-at": 0 };
13078
+ for (const p of placeholders) counts[p.style]++;
13079
+ const stylesUsed = ["anonymous", "numbered", "named-colon", "named-at"].filter((s) => counts[s] > 0);
13080
+ const numbers = placeholders.filter((p) => p.style === "numbered").map((p) => p.number);
13081
+ const maxNumber = numbers.length > 0 ? Math.max(...numbers) : 0;
13082
+ const seen = new Set(numbers);
13083
+ const numberedGaps = [];
13084
+ for (let n = 1; n < maxNumber; n++) {
13085
+ if (!seen.has(n)) numberedGaps.push(n);
13086
+ }
13087
+ const countedMap = /* @__PURE__ */ new Map();
13088
+ for (const n of numbers) countedMap.set(n, (countedMap.get(n) ?? 0) + 1);
13089
+ const numberedDuplicates = [...countedMap.entries()].filter(([, c]) => c > 1).map(([n]) => n).sort((a, b) => a - b);
13090
+ return {
13091
+ placeholders,
13092
+ counts,
13093
+ stylesUsed,
13094
+ mixedStyles: stylesUsed.length > 1,
13095
+ numberedGaps,
13096
+ numberedDuplicates
13097
+ };
13098
+ }
13099
+ server.tool(
13100
+ "count_sql_placeholders",
13101
+ "Count and list positional (?, $1) and named (:name, @name) parameter placeholders in a SQL query string, flagging a mix of placeholder styles in the same query and, for numbered placeholders, a gap or duplicate in the sequence. String literals and comments are skipped.",
13102
+ {
13103
+ sql: z.string().describe("The SQL query string to scan")
13104
+ },
13105
+ async ({ sql }) => {
13106
+ const result = _countSqlPlaceholdersMcp(sql);
13107
+ return text(JSON.stringify(result, null, 2));
13108
+ }
13109
+ );
12449
13110
  var transport = new StdioServerTransport();
12450
13111
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.79.0",
3
+ "version": "0.81.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",