@utilix-tech/mcp 0.52.0 → 0.54.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.54.0
6
+ - Add `calculate_credit_utilization` tool: compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance.
7
+ - Add `analyze_caption_speed` tool: parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines.
8
+
9
+ ## 0.53.0
10
+ - Add `validate_editorconfig` tool: parse an .editorconfig file into its root flag and glob sections, and flag structural mistakes against the EditorConfig spec.
11
+ - 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.
12
+
5
13
  ## 0.52.0
6
14
  - 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
15
 
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 196 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 (32 tools)
163
+ ### Units (33 tools)
164
164
  | Tool | Description |
165
165
  |------|-------------|
166
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -195,6 +195,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
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
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 |
198
+ | `calculate_credit_utilization` | Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance |
198
199
 
199
200
  ### API / Network (28 tools)
200
201
  | Tool | Description |
@@ -236,7 +237,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
236
237
  | `color_palette` | Generate a color palette from a base color |
237
238
  | `color_shades` | Generate shades or tints from a base color |
238
239
 
239
- ### Code (12 tools)
240
+ ### Code (14 tools)
240
241
  | Tool | Description |
241
242
  |------|-------------|
242
243
  | `format_sql` | Format a SQL query with proper indentation |
@@ -251,8 +252,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
251
252
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
252
253
  | `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
254
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
255
+ | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
256
+ | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
254
257
 
255
- ### Data (12 tools)
258
+ ### Data (13 tools)
256
259
  | Tool | Description |
257
260
  |------|-------------|
258
261
  | `validate_yaml` | Validate YAML syntax |
@@ -267,6 +270,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
267
270
  | `csv_to_sql_insert` | Convert CSV data with a header row into parameterless SQL INSERT statements, with basic type inference and ANSI/MySQL identifier quoting |
268
271
  | `find_csv_duplicate_rows` | Group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row |
269
272
  | `calculate_markdown_task_list_progress` | Parse a Markdown document's checkbox task list and report completion percentage overall and per section, grouped by the nearest preceding heading |
273
+ | `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
270
274
 
271
275
  ### CSS (2 tools)
272
276
  | Tool | Description |
package/dist/index.js CHANGED
@@ -8506,5 +8506,519 @@ 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
+ );
8867
+ function _classifyUtilizationMcp(percent) {
8868
+ if (percent < 10) return "excellent";
8869
+ if (percent < 30) return "good";
8870
+ if (percent < 50) return "fair";
8871
+ return "poor";
8872
+ }
8873
+ function _calculateCreditUtilizationMcp(accounts) {
8874
+ if (!Array.isArray(accounts) || accounts.length === 0) {
8875
+ return { error: "accounts must be a non-empty array" };
8876
+ }
8877
+ const round2 = (n) => Math.round(n * 100) / 100;
8878
+ let totalBalance = 0;
8879
+ let totalLimit = 0;
8880
+ const results = [];
8881
+ for (let i = 0; i < accounts.length; i++) {
8882
+ const account = accounts[i];
8883
+ if (typeof account.balance !== "number" || !Number.isFinite(account.balance) || account.balance < 0) {
8884
+ return { error: `account ${i + 1}: balance must be a finite, non-negative number` };
8885
+ }
8886
+ if (typeof account.limit !== "number" || !Number.isFinite(account.limit) || account.limit <= 0) {
8887
+ return { error: `account ${i + 1}: limit must be a finite number greater than zero` };
8888
+ }
8889
+ const utilizationPercent = round2(account.balance / account.limit * 100);
8890
+ totalBalance += account.balance;
8891
+ totalLimit += account.limit;
8892
+ results.push({
8893
+ label: account.label?.trim() || `Account ${i + 1}`,
8894
+ balance: account.balance,
8895
+ limit: account.limit,
8896
+ utilizationPercent,
8897
+ tier: _classifyUtilizationMcp(utilizationPercent)
8898
+ });
8899
+ }
8900
+ const overallUtilizationPercent = round2(totalBalance / totalLimit * 100);
8901
+ return {
8902
+ accounts: results,
8903
+ totalBalance: round2(totalBalance),
8904
+ totalLimit: round2(totalLimit),
8905
+ overallUtilizationPercent,
8906
+ overallTier: _classifyUtilizationMcp(overallUtilizationPercent)
8907
+ };
8908
+ }
8909
+ server.tool(
8910
+ "calculate_credit_utilization",
8911
+ "Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance: under 10% is excellent, under 30% is good (the generally recommended ceiling), under 50% is fair, and 50%+ is poor. Overall utilization is total balance over total limit, not an average of the per-account percentages.",
8912
+ {
8913
+ accounts: z.array(z.object({
8914
+ label: z.string().optional().describe("Optional account label"),
8915
+ balance: z.number().min(0).describe("Current balance on the account"),
8916
+ limit: z.number().positive().describe("Credit limit on the account")
8917
+ })).min(1).describe("A non-empty list of credit accounts")
8918
+ },
8919
+ async ({ accounts }) => {
8920
+ const result = _calculateCreditUtilizationMcp(accounts);
8921
+ if ("error" in result) return errText(result);
8922
+ return text(JSON.stringify(result, null, 2));
8923
+ }
8924
+ );
8925
+ var _CAPTION_TIMESTAMP_RE_MCP = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
8926
+ var _CAPTION_SKIP_BLOCK_KEYWORDS_MCP = ["NOTE", "STYLE", "REGION"];
8927
+ function _parseCaptionTimestampMcp(raw) {
8928
+ const m = _CAPTION_TIMESTAMP_RE_MCP.exec(raw.trim());
8929
+ if (!m || m.index !== 0) return null;
8930
+ let hours, minutes, seconds, millis;
8931
+ if (m[1] !== void 0) {
8932
+ hours = Number(m[1]);
8933
+ minutes = Number(m[2]);
8934
+ seconds = Number(m[3]);
8935
+ millis = m[4];
8936
+ } else {
8937
+ hours = 0;
8938
+ minutes = Number(m[5]);
8939
+ seconds = Number(m[6]);
8940
+ millis = m[7];
8941
+ }
8942
+ if (minutes > 59 || seconds > 59) return null;
8943
+ return hours * 3600 + minutes * 60 + seconds + Number(millis.padEnd(3, "0")) / 1e3;
8944
+ }
8945
+ function _extractCaptionCuesMcp(input) {
8946
+ const stripped = input.charCodeAt(0) === 65279 ? input.slice(1) : input;
8947
+ let body = stripped.replace(/\r\n?/g, "\n");
8948
+ const headerMatch = /^\s*WEBVTT[^\n]*\n?/.exec(body);
8949
+ if (headerMatch) body = body.slice(headerMatch[0].length);
8950
+ const blocks = body.split(/\n[ \t]*\n+/).map((b) => b.trim()).filter((b) => b.length > 0);
8951
+ const cues = [];
8952
+ for (const block of blocks) {
8953
+ const lines = block.split("\n");
8954
+ const arrowIdx = lines.findIndex((l) => l.includes("-->"));
8955
+ if (arrowIdx === -1) continue;
8956
+ if (_CAPTION_SKIP_BLOCK_KEYWORDS_MCP.some((k) => lines[0] === k || lines[0].startsWith(`${k} `))) continue;
8957
+ const [leftRaw, rightRaw] = lines[arrowIdx].split("-->");
8958
+ const start = _parseCaptionTimestampMcp(leftRaw ?? "");
8959
+ const end = _parseCaptionTimestampMcp((rightRaw ?? "").trim());
8960
+ if (start === null || end === null || end <= start) continue;
8961
+ const text2 = lines.slice(arrowIdx + 1).join(" ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
8962
+ if (text2.length === 0) continue;
8963
+ cues.push({ start, end, text: text2 });
8964
+ }
8965
+ return cues;
8966
+ }
8967
+ function _analyzeCaptionSpeedMcp(input, wpmThreshold, cpsThreshold) {
8968
+ if (typeof input !== "string" || input.trim().length === 0) return { error: "Subtitle input is empty" };
8969
+ if (!Number.isFinite(wpmThreshold) || wpmThreshold <= 0) {
8970
+ return { error: "wpmThreshold must be a finite number greater than zero" };
8971
+ }
8972
+ if (!Number.isFinite(cpsThreshold) || cpsThreshold <= 0) {
8973
+ return { error: "cpsThreshold must be a finite number greater than zero" };
8974
+ }
8975
+ const rawCues = _extractCaptionCuesMcp(input);
8976
+ if (rawCues.length === 0) return { error: "No subtitle cues with timing and text were found" };
8977
+ const round1 = (n) => Math.round(n * 10) / 10;
8978
+ const cues = rawCues.map((cue, i) => {
8979
+ const durationSeconds = cue.end - cue.start;
8980
+ const wordCount = cue.text.split(" ").filter(Boolean).length;
8981
+ const charCount = cue.text.length;
8982
+ const wpm = wordCount / durationSeconds * 60;
8983
+ const charsPerSecond = charCount / durationSeconds;
8984
+ return {
8985
+ index: i + 1,
8986
+ start: round1(cue.start),
8987
+ end: round1(cue.end),
8988
+ text: cue.text,
8989
+ durationSeconds: round1(durationSeconds),
8990
+ wordCount,
8991
+ charCount,
8992
+ wpm: round1(wpm),
8993
+ charsPerSecond: round1(charsPerSecond),
8994
+ exceedsWpm: wpm > wpmThreshold,
8995
+ exceedsCps: charsPerSecond > cpsThreshold
8996
+ };
8997
+ });
8998
+ const wpmValues = cues.map((c) => c.wpm);
8999
+ return {
9000
+ cueCount: cues.length,
9001
+ cues,
9002
+ averageWpm: round1(wpmValues.reduce((a, b) => a + b, 0) / wpmValues.length),
9003
+ maxWpm: round1(Math.max(...wpmValues)),
9004
+ flaggedCount: cues.filter((c) => c.exceedsWpm || c.exceedsCps).length,
9005
+ wpmThreshold,
9006
+ cpsThreshold
9007
+ };
9008
+ }
9009
+ server.tool(
9010
+ "analyze_caption_speed",
9011
+ "Parse an SRT or WebVTT file and report the words-per-minute reading speed implied by each cue's word count and duration, flagging cues that exceed common broadcast accessibility guidelines (default: ~180 WPM, ~20 characters/second). HTML-style cue tags like <i> are stripped before counting words and characters.",
9012
+ {
9013
+ input: z.string().describe("The SRT or WebVTT file contents"),
9014
+ wpmThreshold: z.number().positive().optional().describe("Words-per-minute above which a cue is flagged (default 180)"),
9015
+ cpsThreshold: z.number().positive().optional().describe("Characters-per-second above which a cue is flagged (default 20)")
9016
+ },
9017
+ async ({ input, wpmThreshold, cpsThreshold }) => {
9018
+ const result = _analyzeCaptionSpeedMcp(input, wpmThreshold ?? 180, cpsThreshold ?? 20);
9019
+ if ("error" in result) return errText(result);
9020
+ return text(JSON.stringify(result, null, 2));
9021
+ }
9022
+ );
8509
9023
  var transport = new StdioServerTransport();
8510
9024
  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.54.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",