@utilix-tech/mcp 0.51.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,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.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
+
9
+ ## 0.52.0
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.
11
+
5
12
  ## 0.51.0
6
13
  - Add `calculate_moving_averages` tool: compute SMA and EMA series from a list of numbers and a window size.
7
14
  - Add `calculate_markdown_task_list_progress` tool: parse a Markdown document's checkbox task list and report completion percentage overall and per section.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 191 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
 
@@ -160,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
160
160
  | `convert_timezone` | Convert a date/time between timezones |
161
161
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
162
162
 
163
- ### Units (31 tools)
163
+ ### Units (32 tools)
164
164
  | Tool | Description |
165
165
  |------|-------------|
166
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -194,6 +194,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
194
194
  | `calculate_depreciation_schedule` | Build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits |
195
195
  | `calculate_cash_rounding` | Round a cashless total to the nearest payable cash denomination ("Swedish rounding"), with round-nearest, round-up, and round-down modes |
196
196
  | `calculate_moving_averages` | Compute simple moving average (SMA) and exponential moving average (EMA) series from a list of numbers and a window size |
197
+ | `calculate_gpa` | Compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale |
197
198
 
198
199
  ### API / Network (28 tools)
199
200
  | Tool | Description |
@@ -235,7 +236,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
235
236
  | `color_palette` | Generate a color palette from a base color |
236
237
  | `color_shades` | Generate shades or tints from a base color |
237
238
 
238
- ### Code (12 tools)
239
+ ### Code (14 tools)
239
240
  | Tool | Description |
240
241
  |------|-------------|
241
242
  | `format_sql` | Format a SQL query with proper indentation |
@@ -250,6 +251,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
250
251
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
251
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 |
252
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 |
253
256
 
254
257
  ### Data (12 tools)
255
258
  | Tool | Description |
package/dist/index.js CHANGED
@@ -8440,5 +8440,429 @@ server.tool(
8440
8440
  return text(JSON.stringify(result, null, 2));
8441
8441
  }
8442
8442
  );
8443
+ var GPA_GRADE_POINTS_MCP = {
8444
+ "A+": 4,
8445
+ A: 4,
8446
+ "A-": 3.7,
8447
+ "B+": 3.3,
8448
+ B: 3,
8449
+ "B-": 2.7,
8450
+ "C+": 2.3,
8451
+ C: 2,
8452
+ "C-": 1.7,
8453
+ "D+": 1.3,
8454
+ D: 1,
8455
+ "D-": 0.7,
8456
+ F: 0
8457
+ };
8458
+ function _calculateGpaMcp(courses) {
8459
+ if (!Array.isArray(courses) || courses.length === 0) {
8460
+ return { error: "courses must be a non-empty array" };
8461
+ }
8462
+ const round2 = (n) => Math.round(n * 100) / 100;
8463
+ let totalCredits = 0;
8464
+ let totalQualityPoints = 0;
8465
+ const results = [];
8466
+ for (let i = 0; i < courses.length; i++) {
8467
+ const course = courses[i];
8468
+ if (typeof course.credits !== "number" || !Number.isFinite(course.credits) || course.credits <= 0) {
8469
+ return { error: `course ${i + 1}: credits must be a finite number greater than zero` };
8470
+ }
8471
+ const gradeKey = typeof course.grade === "string" ? course.grade.trim().toUpperCase() : "";
8472
+ const points = GPA_GRADE_POINTS_MCP[gradeKey];
8473
+ if (points === void 0) {
8474
+ return { error: `course ${i + 1}: unrecognized grade "${course.grade}" (expected one of ${Object.keys(GPA_GRADE_POINTS_MCP).join(", ")})` };
8475
+ }
8476
+ const qualityPoints = round2(points * course.credits);
8477
+ totalCredits += course.credits;
8478
+ totalQualityPoints += qualityPoints;
8479
+ results.push({
8480
+ name: course.name?.trim() || `Course ${i + 1}`,
8481
+ grade: gradeKey,
8482
+ credits: course.credits,
8483
+ qualityPoints
8484
+ });
8485
+ }
8486
+ return {
8487
+ courses: results,
8488
+ totalCredits: round2(totalCredits),
8489
+ totalQualityPoints: round2(totalQualityPoints),
8490
+ gpa: round2(totalQualityPoints / totalCredits)
8491
+ };
8492
+ }
8493
+ server.tool(
8494
+ "calculate_gpa",
8495
+ "Compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale (A+ counts the same as A).",
8496
+ {
8497
+ courses: z.array(z.object({
8498
+ name: z.string().optional().describe("Optional course name/label"),
8499
+ grade: z.string().describe('Letter grade, e.g. "A", "B+", "C-"'),
8500
+ credits: z.number().positive().describe("Credit hours for this course, must be greater than zero")
8501
+ })).min(1).describe("A non-empty list of courses to average")
8502
+ },
8503
+ async ({ courses }) => {
8504
+ const result = _calculateGpaMcp(courses);
8505
+ if ("error" in result) return errText(result);
8506
+ return text(JSON.stringify(result, null, 2));
8507
+ }
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
+ );
8443
8867
  var transport = new StdioServerTransport();
8444
8868
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.51.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",