@utilix-tech/mcp 0.53.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +156 -0
- package/package.json +1 -1
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.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
|
+
|
|
5
9
|
## 0.53.0
|
|
6
10
|
- Add `validate_editorconfig` tool: parse an .editorconfig file into its root flag and glob sections, and flag structural mistakes against the EditorConfig spec.
|
|
7
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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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 (
|
|
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 |
|
|
@@ -254,7 +255,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
254
255
|
| `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
|
|
255
256
|
| `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
|
|
256
257
|
|
|
257
|
-
### Data (
|
|
258
|
+
### Data (13 tools)
|
|
258
259
|
| Tool | Description |
|
|
259
260
|
|------|-------------|
|
|
260
261
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -269,6 +270,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
269
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 |
|
|
270
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 |
|
|
271
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 |
|
|
272
274
|
|
|
273
275
|
### CSS (2 tools)
|
|
274
276
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -8864,5 +8864,161 @@ server.tool(
|
|
|
8864
8864
|
return text(JSON.stringify(result, null, 2));
|
|
8865
8865
|
}
|
|
8866
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
|
+
);
|
|
8867
9023
|
var transport = new StdioServerTransport();
|
|
8868
9024
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "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",
|