@utilix-tech/mcp 0.52.0 → 0.53.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.53.0
6
+ - Add `validate_editorconfig` tool: parse an .editorconfig file into its root flag and glob sections, and flag structural mistakes against the EditorConfig spec.
7
+ - Add `validate_tsconfig` tool: parse a tsconfig.json file as JSONC and flag unknown keys, unknown compilerOptions, and type mismatches against a curated list of known options.
8
+
5
9
  ## 0.52.0
6
10
  - Add `calculate_gpa` tool: compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale.
7
11
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 192 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 194 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
 
@@ -236,7 +236,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
236
236
  | `color_palette` | Generate a color palette from a base color |
237
237
  | `color_shades` | Generate shades or tints from a base color |
238
238
 
239
- ### Code (12 tools)
239
+ ### Code (14 tools)
240
240
  | Tool | Description |
241
241
  |------|-------------|
242
242
  | `format_sql` | Format a SQL query with proper indentation |
@@ -251,6 +251,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
251
251
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
252
252
  | `lint_conventional_commit` | Lint a commit message against the Conventional Commits v1.0.0 spec: header format, blank-line separation, footer tokens, and BREAKING CHANGE casing |
253
253
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
254
+ | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
255
+ | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
254
256
 
255
257
  ### Data (12 tools)
256
258
  | Tool | Description |
package/dist/index.js CHANGED
@@ -8506,5 +8506,363 @@ server.tool(
8506
8506
  return text(JSON.stringify(result, null, 2));
8507
8507
  }
8508
8508
  );
8509
+ var _EDITORCONFIG_ENUM_PROPERTIES_MCP = {
8510
+ indent_style: ["tab", "space"],
8511
+ end_of_line: ["lf", "cr", "crlf"],
8512
+ charset: ["latin1", "utf-8", "utf-8-bom", "utf-16be", "utf-16le"],
8513
+ trim_trailing_whitespace: ["true", "false"],
8514
+ insert_final_newline: ["true", "false"]
8515
+ };
8516
+ var _EDITORCONFIG_KNOWN_PROPERTIES_MCP = /* @__PURE__ */ new Set([
8517
+ "indent_style",
8518
+ "indent_size",
8519
+ "tab_width",
8520
+ "end_of_line",
8521
+ "charset",
8522
+ "trim_trailing_whitespace",
8523
+ "insert_final_newline",
8524
+ "max_line_length",
8525
+ "root"
8526
+ ]);
8527
+ var _EC_POSITIVE_INT_RE_MCP = /^[1-9][0-9]*$/;
8528
+ function _validateEditorConfigMcp(text2) {
8529
+ if (!text2 || !text2.trim()) return { error: "Input is empty" };
8530
+ const lines = text2.split("\n");
8531
+ const issues = [];
8532
+ const sections = [];
8533
+ let root = false;
8534
+ let rootSeen = false;
8535
+ let currentSection = null;
8536
+ for (let i = 0; i < lines.length; i++) {
8537
+ const lineNum = i + 1;
8538
+ const trimmed = lines[i].trim();
8539
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
8540
+ const sectionMatch = trimmed.match(/^\[(.*)\]$/);
8541
+ if (sectionMatch) {
8542
+ const glob = sectionMatch[1];
8543
+ if (!glob) {
8544
+ issues.push({ severity: "warning", line: lineNum, message: 'Section header is empty ("[]" matches no files)' });
8545
+ }
8546
+ if (sections.some((s) => s.glob === glob)) {
8547
+ issues.push({ severity: "warning", line: lineNum, message: `Duplicate section "[${glob}]" (an earlier section already uses this exact glob)` });
8548
+ }
8549
+ currentSection = { glob, line: lineNum, properties: [] };
8550
+ sections.push(currentSection);
8551
+ continue;
8552
+ }
8553
+ const eqIdx = trimmed.indexOf("=");
8554
+ if (eqIdx === -1) {
8555
+ issues.push({ severity: "error", line: lineNum, message: `Malformed line (missing '='): "${trimmed}"` });
8556
+ continue;
8557
+ }
8558
+ const rawKey = trimmed.slice(0, eqIdx).trim();
8559
+ const key = rawKey.toLowerCase();
8560
+ const rawValue = trimmed.slice(eqIdx + 1).trim();
8561
+ const normalizedValue = rawValue.toLowerCase();
8562
+ if (!rawKey) {
8563
+ issues.push({ severity: "error", line: lineNum, message: "Property is missing a name" });
8564
+ continue;
8565
+ }
8566
+ if (key === "root") {
8567
+ if (sections.length > 0) {
8568
+ issues.push({ severity: "error", line: lineNum, message: `"root" must appear before any section, in the file's preamble` });
8569
+ } else if (rootSeen) {
8570
+ issues.push({ severity: "warning", line: lineNum, message: 'Duplicate "root" declaration' });
8571
+ } else if (normalizedValue !== "true" && normalizedValue !== "false") {
8572
+ issues.push({ severity: "error", line: lineNum, message: `"root" must be "true" or "false", got "${rawValue}"` });
8573
+ } else {
8574
+ root = normalizedValue === "true";
8575
+ }
8576
+ rootSeen = true;
8577
+ continue;
8578
+ }
8579
+ if (!currentSection) {
8580
+ issues.push({ severity: "error", line: lineNum, message: `Property "${rawKey}" found before any section header` });
8581
+ continue;
8582
+ }
8583
+ if (currentSection.properties.some((p) => p.key === key)) {
8584
+ issues.push({ severity: "warning", line: lineNum, message: `Duplicate property "${key}" in section "[${currentSection.glob}]"` });
8585
+ }
8586
+ currentSection.properties.push({ key, value: rawValue, line: lineNum });
8587
+ if (!_EDITORCONFIG_KNOWN_PROPERTIES_MCP.has(key)) {
8588
+ issues.push({ severity: "warning", line: lineNum, message: `Unknown property "${key}"` });
8589
+ continue;
8590
+ }
8591
+ if (key in _EDITORCONFIG_ENUM_PROPERTIES_MCP) {
8592
+ if (!_EDITORCONFIG_ENUM_PROPERTIES_MCP[key].includes(normalizedValue)) {
8593
+ issues.push({ severity: "error", line: lineNum, message: `"${key}" must be one of ${_EDITORCONFIG_ENUM_PROPERTIES_MCP[key].join(", ")}, got "${rawValue}"` });
8594
+ }
8595
+ } else if (key === "indent_size") {
8596
+ if (normalizedValue !== "tab" && !_EC_POSITIVE_INT_RE_MCP.test(rawValue)) {
8597
+ issues.push({ severity: "error", line: lineNum, message: `"indent_size" must be a positive integer or "tab", got "${rawValue}"` });
8598
+ }
8599
+ } else if (key === "tab_width") {
8600
+ if (!_EC_POSITIVE_INT_RE_MCP.test(rawValue)) {
8601
+ issues.push({ severity: "error", line: lineNum, message: `"tab_width" must be a positive integer, got "${rawValue}"` });
8602
+ }
8603
+ } else if (key === "max_line_length") {
8604
+ if (normalizedValue !== "off" && !_EC_POSITIVE_INT_RE_MCP.test(rawValue)) {
8605
+ issues.push({ severity: "error", line: lineNum, message: `"max_line_length" must be a positive integer or "off", got "${rawValue}"` });
8606
+ }
8607
+ }
8608
+ }
8609
+ if (sections.length === 0 && !rootSeen) {
8610
+ issues.push({ severity: "warning", line: 1, message: "File has no section headers and no root declaration" });
8611
+ }
8612
+ const valid = !issues.some((i) => i.severity === "error");
8613
+ return { root, sections, issues, valid };
8614
+ }
8615
+ server.tool(
8616
+ "validate_editorconfig",
8617
+ "Validate an .editorconfig file: parse it into its root flag and glob sections, and flag structural mistakes against the EditorConfig spec \u2014 unknown properties, invalid values for known properties (indent_style, end_of_line, charset, indent_size, tab_width, max_line_length), a root declaration outside the file's preamble, and duplicate properties within the same section.",
8618
+ {
8619
+ text: z.string().describe("The .editorconfig file contents")
8620
+ },
8621
+ async ({ text: editorconfigText }) => {
8622
+ const result = _validateEditorConfigMcp(editorconfigText);
8623
+ if ("error" in result) return errText(result);
8624
+ return text(JSON.stringify(result, null, 2));
8625
+ }
8626
+ );
8627
+ function _stripJsonCommentsMcp(input) {
8628
+ let out = "";
8629
+ let i = 0;
8630
+ const n = input.length;
8631
+ let inString = false;
8632
+ while (i < n) {
8633
+ const c = input[i];
8634
+ const next = input[i + 1];
8635
+ if (inString) {
8636
+ out += c;
8637
+ if (c === "\\") {
8638
+ out += next ?? "";
8639
+ i += 2;
8640
+ continue;
8641
+ }
8642
+ if (c === '"') inString = false;
8643
+ i++;
8644
+ continue;
8645
+ }
8646
+ if (c === '"') {
8647
+ inString = true;
8648
+ out += c;
8649
+ i++;
8650
+ continue;
8651
+ }
8652
+ if (c === "/" && next === "/") {
8653
+ while (i < n && input[i] !== "\n") i++;
8654
+ continue;
8655
+ }
8656
+ if (c === "/" && next === "*") {
8657
+ i += 2;
8658
+ while (i < n && !(input[i] === "*" && input[i + 1] === "/")) {
8659
+ out += input[i] === "\n" ? "\n" : "";
8660
+ i++;
8661
+ }
8662
+ i += 2;
8663
+ continue;
8664
+ }
8665
+ out += c;
8666
+ i++;
8667
+ }
8668
+ return out;
8669
+ }
8670
+ function _stripTrailingCommasMcp(input) {
8671
+ return input.replace(/,(\s*[}\]])/g, "$1");
8672
+ }
8673
+ var _TSCONFIG_TOP_LEVEL_KEYS_MCP = /* @__PURE__ */ new Set([
8674
+ "compilerOptions",
8675
+ "files",
8676
+ "include",
8677
+ "exclude",
8678
+ "extends",
8679
+ "references",
8680
+ "compileOnSave",
8681
+ "typeAcquisition",
8682
+ "watchOptions",
8683
+ "buildOptions",
8684
+ "$schema"
8685
+ ]);
8686
+ var _TSCONFIG_ENUM_OPTIONS_MCP = {
8687
+ target: ["es3", "es5", "es6", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "es2021", "es2022", "es2023", "esnext"],
8688
+ module: ["none", "commonjs", "amd", "umd", "system", "es6", "es2015", "es2020", "es2022", "esnext", "node16", "nodenext", "node18", "preserve"],
8689
+ moduleResolution: ["classic", "node", "node10", "node16", "nodenext", "bundler"],
8690
+ moduleDetection: ["legacy", "auto", "force"],
8691
+ jsx: ["preserve", "react", "react-jsx", "react-jsxdev", "react-native"],
8692
+ newLine: ["crlf", "lf"],
8693
+ importsNotUsedAsValues: ["remove", "preserve", "error"]
8694
+ };
8695
+ var _TSCONFIG_BOOLEAN_OPTIONS_MCP = /* @__PURE__ */ new Set([
8696
+ "allowJs",
8697
+ "checkJs",
8698
+ "strict",
8699
+ "noImplicitAny",
8700
+ "strictNullChecks",
8701
+ "strictFunctionTypes",
8702
+ "strictBindCallApply",
8703
+ "strictPropertyInitialization",
8704
+ "noImplicitThis",
8705
+ "useUnknownInCatchVariables",
8706
+ "alwaysStrict",
8707
+ "noUnusedLocals",
8708
+ "noUnusedParameters",
8709
+ "exactOptionalPropertyTypes",
8710
+ "noImplicitReturns",
8711
+ "noFallthroughCasesInSwitch",
8712
+ "noUncheckedIndexedAccess",
8713
+ "noImplicitOverride",
8714
+ "noPropertyAccessFromIndexSignature",
8715
+ "esModuleInterop",
8716
+ "allowSyntheticDefaultImports",
8717
+ "forceConsistentCasingInFileNames",
8718
+ "isolatedModules",
8719
+ "verbatimModuleSyntax",
8720
+ "declaration",
8721
+ "declarationMap",
8722
+ "emitDeclarationOnly",
8723
+ "sourceMap",
8724
+ "inlineSourceMap",
8725
+ "inlineSources",
8726
+ "noEmit",
8727
+ "noEmitOnError",
8728
+ "preserveConstEnums",
8729
+ "removeComments",
8730
+ "skipLibCheck",
8731
+ "composite",
8732
+ "incremental",
8733
+ "experimentalDecorators",
8734
+ "emitDecoratorMetadata",
8735
+ "resolveJsonModule",
8736
+ "allowUmdGlobalAccess",
8737
+ "downlevelIteration",
8738
+ "importHelpers",
8739
+ "preserveSymlinks",
8740
+ "allowUnreachableCode",
8741
+ "allowUnusedLabels",
8742
+ "disableSizeLimit",
8743
+ "noResolve",
8744
+ "skipDefaultLibCheck",
8745
+ "suppressImplicitAnyIndexErrors"
8746
+ ]);
8747
+ var _TSCONFIG_STRING_OPTIONS_MCP = /* @__PURE__ */ new Set([
8748
+ "outDir",
8749
+ "rootDir",
8750
+ "baseUrl",
8751
+ "outFile",
8752
+ "declarationDir",
8753
+ "tsBuildInfoFile",
8754
+ "jsxFactory",
8755
+ "jsxFragmentFactory",
8756
+ "jsxImportSource"
8757
+ ]);
8758
+ var _TSCONFIG_ARRAY_OPTIONS_MCP = /* @__PURE__ */ new Set(["lib", "types", "typeRoots", "rootDirs"]);
8759
+ function _validateTsconfigMcp(text2) {
8760
+ if (!text2 || !text2.trim()) return { error: "Input is empty" };
8761
+ const stripped = _stripTrailingCommasMcp(_stripJsonCommentsMcp(text2));
8762
+ let parsed;
8763
+ try {
8764
+ parsed = JSON.parse(stripped);
8765
+ } catch (e) {
8766
+ return { error: `Invalid JSON: ${e.message}` };
8767
+ }
8768
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8769
+ return { error: "Root of tsconfig.json must be an object" };
8770
+ }
8771
+ const obj = parsed;
8772
+ const issues = [];
8773
+ const topLevelKeys = Object.keys(obj);
8774
+ for (const key of topLevelKeys) {
8775
+ if (!_TSCONFIG_TOP_LEVEL_KEYS_MCP.has(key)) {
8776
+ issues.push({ severity: "warning", path: key, message: `Unknown top-level key "${key}"` });
8777
+ }
8778
+ }
8779
+ for (const key of ["files", "include", "exclude"]) {
8780
+ if (key in obj) {
8781
+ const v = obj[key];
8782
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
8783
+ issues.push({ severity: "error", path: key, message: `"${key}" must be an array of strings` });
8784
+ }
8785
+ }
8786
+ }
8787
+ if ("extends" in obj) {
8788
+ const v = obj.extends;
8789
+ const validExtends = typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string");
8790
+ if (!validExtends) {
8791
+ issues.push({ severity: "error", path: "extends", message: '"extends" must be a string or an array of strings' });
8792
+ }
8793
+ }
8794
+ if ("references" in obj) {
8795
+ const v = obj.references;
8796
+ const validRefs = Array.isArray(v) && v.every((r) => typeof r === "object" && r !== null && typeof r.path === "string");
8797
+ if (!validRefs) {
8798
+ issues.push({ severity: "error", path: "references", message: '"references" must be an array of objects with a "path" string' });
8799
+ }
8800
+ }
8801
+ let compilerOptionKeys = [];
8802
+ if ("compilerOptions" in obj) {
8803
+ const co = obj.compilerOptions;
8804
+ if (typeof co !== "object" || co === null || Array.isArray(co)) {
8805
+ issues.push({ severity: "error", path: "compilerOptions", message: '"compilerOptions" must be an object' });
8806
+ } else {
8807
+ const coObj = co;
8808
+ compilerOptionKeys = Object.keys(coObj);
8809
+ for (const key of compilerOptionKeys) {
8810
+ const value = coObj[key];
8811
+ const path = `compilerOptions.${key}`;
8812
+ if (key === "paths") {
8813
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
8814
+ issues.push({ severity: "error", path, message: '"paths" must be an object mapping patterns to arrays of strings' });
8815
+ } else {
8816
+ for (const [k, v] of Object.entries(value)) {
8817
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
8818
+ issues.push({ severity: "error", path: `${path}.${k}`, message: `"paths.${k}" must be an array of strings` });
8819
+ }
8820
+ }
8821
+ }
8822
+ continue;
8823
+ }
8824
+ if (key in _TSCONFIG_ENUM_OPTIONS_MCP) {
8825
+ if (typeof value !== "string" || !_TSCONFIG_ENUM_OPTIONS_MCP[key].includes(value.toLowerCase())) {
8826
+ issues.push({ severity: "error", path, message: `"${key}" must be one of ${_TSCONFIG_ENUM_OPTIONS_MCP[key].join(", ")}, got ${JSON.stringify(value)}` });
8827
+ }
8828
+ continue;
8829
+ }
8830
+ if (_TSCONFIG_BOOLEAN_OPTIONS_MCP.has(key)) {
8831
+ if (typeof value !== "boolean") {
8832
+ issues.push({ severity: "error", path, message: `"${key}" must be a boolean, got ${JSON.stringify(value)}` });
8833
+ }
8834
+ continue;
8835
+ }
8836
+ if (_TSCONFIG_STRING_OPTIONS_MCP.has(key)) {
8837
+ if (typeof value !== "string") {
8838
+ issues.push({ severity: "error", path, message: `"${key}" must be a string, got ${JSON.stringify(value)}` });
8839
+ }
8840
+ continue;
8841
+ }
8842
+ if (_TSCONFIG_ARRAY_OPTIONS_MCP.has(key)) {
8843
+ if (!Array.isArray(value) || !value.every((x) => typeof x === "string")) {
8844
+ issues.push({ severity: "error", path, message: `"${key}" must be an array of strings, got ${JSON.stringify(value)}` });
8845
+ }
8846
+ continue;
8847
+ }
8848
+ issues.push({ severity: "warning", path, message: `Unknown compilerOption "${key}"` });
8849
+ }
8850
+ }
8851
+ }
8852
+ const valid = !issues.some((i) => i.severity === "error");
8853
+ return { parsed: true, topLevelKeys, compilerOptionKeys, issues, valid };
8854
+ }
8855
+ server.tool(
8856
+ "validate_tsconfig",
8857
+ "Validate a tsconfig.json file: parse it as JSONC (// and block comments plus trailing commas are stripped, matching how tsc itself reads the file) and flag structural mistakes \u2014 unknown top-level keys, unknown compilerOptions keys, wrong types, and malformed files/include/exclude/references/paths.",
8858
+ {
8859
+ text: z.string().describe("The tsconfig.json file contents (JSONC allowed)")
8860
+ },
8861
+ async ({ text: tsconfigText }) => {
8862
+ const result = _validateTsconfigMcp(tsconfigText);
8863
+ if ("error" in result) return errText(result);
8864
+ return text(JSON.stringify(result, null, 2));
8865
+ }
8866
+ );
8509
8867
  var transport = new StdioServerTransport();
8510
8868
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "MCP server exposing 188 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",