@utilix-tech/mcp 0.23.0 → 0.25.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 +7 -0
- package/README.md +5 -2
- package/dist/index.js +284 -0
- package/package.json +1 -1
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.25.0
|
|
6
|
+
- Add `calculate_discount` tool: solve a sale price, amount saved, or discount percent from any one of them, or stack successive discounts where 20% then 10% is an effective 28% off.
|
|
7
|
+
- Add `convert_subtitles` tool: convert between SubRip (.srt) and WebVTT (.vtt) with an optional time shift to resync every cue.
|
|
8
|
+
|
|
9
|
+
## 0.24.0
|
|
10
|
+
- Add `calculate_margin_markup` tool: given exactly two of cost, price, margin percent, and markup percent, solve for the other two plus the gross profit.
|
|
11
|
+
|
|
5
12
|
## 0.23.0
|
|
6
13
|
- Add `convert_apr_apy` tool: convert between a nominal APR and effective APY for a given compounding frequency, in both directions.
|
|
7
14
|
- Add `convert_iso8601_duration` tool: parse an ISO 8601 duration into components and total seconds, or format seconds into a canonical duration string.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 137 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
|
|
|
@@ -152,7 +152,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
152
152
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
153
153
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
154
154
|
|
|
155
|
-
### Units (
|
|
155
|
+
### Units (15 tools)
|
|
156
156
|
| Tool | Description |
|
|
157
157
|
|------|-------------|
|
|
158
158
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -169,6 +169,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
169
169
|
| `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
|
|
170
170
|
| `calculate_percentage_change` | Compute the percentage change between two numbers, with absolute change, direction, and multiplier |
|
|
171
171
|
| `convert_apr_apy` | Convert between a nominal APR and effective APY for a given compounding frequency, in both directions |
|
|
172
|
+
| `calculate_margin_markup` | Given any two of cost, price, margin percent, and markup percent, solve for the other two plus gross profit |
|
|
173
|
+
| `calculate_discount` | Solve a sale price, amount saved, or discount percent from any one of them, or stack successive discounts in order |
|
|
174
|
+
| `convert_subtitles` | Convert subtitles between SubRip (.srt) and WebVTT (.vtt), with an optional time shift to resync every cue |
|
|
172
175
|
|
|
173
176
|
### API / Network (10 tools)
|
|
174
177
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -4058,5 +4058,289 @@ server.tool(
|
|
|
4058
4058
|
return text(JSON.stringify(result, null, 2));
|
|
4059
4059
|
}
|
|
4060
4060
|
);
|
|
4061
|
+
var _MARGIN_FIELDS_MCP = ["cost", "price", "marginPercent", "markupPercent"];
|
|
4062
|
+
function _calculateMarginMarkupMcp(input) {
|
|
4063
|
+
const provided = (v) => typeof v === "number" && Number.isFinite(v);
|
|
4064
|
+
const given = _MARGIN_FIELDS_MCP.filter((f) => provided(input[f]));
|
|
4065
|
+
if (given.length !== 2) {
|
|
4066
|
+
return { error: `Provide exactly two of cost, price, marginPercent, markupPercent (got ${given.length})` };
|
|
4067
|
+
}
|
|
4068
|
+
const has = (f) => given.includes(f);
|
|
4069
|
+
const cost0 = input.cost;
|
|
4070
|
+
const price0 = input.price;
|
|
4071
|
+
const margin0 = input.marginPercent;
|
|
4072
|
+
const markup0 = input.markupPercent;
|
|
4073
|
+
if (has("cost") && cost0 <= 0) return { error: "Cost must be greater than zero" };
|
|
4074
|
+
if (has("price") && price0 <= 0) return { error: "Price must be greater than zero" };
|
|
4075
|
+
if (has("marginPercent") && margin0 >= 100) return { error: "Margin percent must be below 100 (a 100% margin implies a zero cost)" };
|
|
4076
|
+
if (has("markupPercent") && markup0 <= -100) return { error: "Markup percent must be greater than -100" };
|
|
4077
|
+
let cost;
|
|
4078
|
+
let price;
|
|
4079
|
+
if (has("cost") && has("price")) {
|
|
4080
|
+
cost = cost0;
|
|
4081
|
+
price = price0;
|
|
4082
|
+
} else if (has("cost") && has("marginPercent")) {
|
|
4083
|
+
cost = cost0;
|
|
4084
|
+
price = cost0 / (1 - margin0 / 100);
|
|
4085
|
+
} else if (has("cost") && has("markupPercent")) {
|
|
4086
|
+
cost = cost0;
|
|
4087
|
+
price = cost0 * (1 + markup0 / 100);
|
|
4088
|
+
} else if (has("price") && has("marginPercent")) {
|
|
4089
|
+
price = price0;
|
|
4090
|
+
cost = price0 * (1 - margin0 / 100);
|
|
4091
|
+
} else if (has("price") && has("markupPercent")) {
|
|
4092
|
+
price = price0;
|
|
4093
|
+
cost = price0 / (1 + markup0 / 100);
|
|
4094
|
+
} else {
|
|
4095
|
+
return { error: "Margin and markup alone cannot be solved: also provide a cost or a price" };
|
|
4096
|
+
}
|
|
4097
|
+
if (!Number.isFinite(cost) || !Number.isFinite(price)) return { error: "Inputs produce a non-finite result" };
|
|
4098
|
+
if (cost <= 0) return { error: "Those inputs imply a cost of zero or less" };
|
|
4099
|
+
if (price <= 0) return { error: "Those inputs imply a price of zero or less" };
|
|
4100
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
4101
|
+
const profit = price - cost;
|
|
4102
|
+
return {
|
|
4103
|
+
cost: round4(cost),
|
|
4104
|
+
price: round4(price),
|
|
4105
|
+
profit: round4(profit),
|
|
4106
|
+
marginPercent: round4(profit / price * 100),
|
|
4107
|
+
markupPercent: round4(profit / cost * 100),
|
|
4108
|
+
given,
|
|
4109
|
+
solvedFor: _MARGIN_FIELDS_MCP.filter((f) => !given.includes(f))
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
server.tool(
|
|
4113
|
+
"calculate_margin_markup",
|
|
4114
|
+
"Solve a pricing triangle: given exactly two of cost, price, gross margin percent, and markup percent, derive the other two plus the gross profit. Margin is profit as a share of the selling price (profit / price) while markup is profit as a share of the cost (profit / cost), so a 20% margin equals a 25% markup. Margin plus markup alone cannot be solved, since neither carries an absolute amount.",
|
|
4115
|
+
{
|
|
4116
|
+
cost: z.number().optional().describe("Unit cost (what you pay). Provide exactly two of the four fields."),
|
|
4117
|
+
price: z.number().optional().describe("Selling price (what the customer pays). Provide exactly two of the four fields."),
|
|
4118
|
+
marginPercent: z.number().optional().describe("Gross margin as a percent of the selling price (must be below 100)"),
|
|
4119
|
+
markupPercent: z.number().optional().describe("Markup as a percent of the cost (must be greater than -100)")
|
|
4120
|
+
},
|
|
4121
|
+
async ({ cost, price, marginPercent, markupPercent }) => {
|
|
4122
|
+
const result = _calculateMarginMarkupMcp({ cost, price, marginPercent, markupPercent });
|
|
4123
|
+
if ("error" in result) return errText(result);
|
|
4124
|
+
return text(JSON.stringify(result, null, 2));
|
|
4125
|
+
}
|
|
4126
|
+
);
|
|
4127
|
+
var _DISCOUNT_FIELDS_MCP = ["discountPercent", "discountAmount", "finalPrice"];
|
|
4128
|
+
function _calculateDiscountMcp(input) {
|
|
4129
|
+
const provided = (v) => typeof v === "number" && Number.isFinite(v);
|
|
4130
|
+
if (!provided(input.originalPrice)) return { error: "originalPrice is required and must be a finite number" };
|
|
4131
|
+
const originalPrice = input.originalPrice;
|
|
4132
|
+
if (originalPrice <= 0) return { error: "Original price must be greater than zero" };
|
|
4133
|
+
const given = _DISCOUNT_FIELDS_MCP.filter((f) => provided(input[f]));
|
|
4134
|
+
if (given.length !== 1) {
|
|
4135
|
+
return { error: `Provide exactly one of discountPercent, discountAmount, finalPrice (got ${given.length})` };
|
|
4136
|
+
}
|
|
4137
|
+
const field = given[0];
|
|
4138
|
+
let discountAmount;
|
|
4139
|
+
if (field === "discountPercent") {
|
|
4140
|
+
const pct = input.discountPercent;
|
|
4141
|
+
if (pct < 0) return { error: "Discount percent cannot be negative" };
|
|
4142
|
+
if (pct > 100) return { error: "Discount percent cannot exceed 100" };
|
|
4143
|
+
discountAmount = originalPrice * (pct / 100);
|
|
4144
|
+
} else if (field === "discountAmount") {
|
|
4145
|
+
const amt = input.discountAmount;
|
|
4146
|
+
if (amt < 0) return { error: "Discount amount cannot be negative" };
|
|
4147
|
+
if (amt > originalPrice) return { error: "Discount amount cannot exceed the original price" };
|
|
4148
|
+
discountAmount = amt;
|
|
4149
|
+
} else {
|
|
4150
|
+
const fin = input.finalPrice;
|
|
4151
|
+
if (fin < 0) return { error: "Final price cannot be negative" };
|
|
4152
|
+
if (fin > originalPrice) return { error: "Final price cannot exceed the original price: that is a price increase, not a discount" };
|
|
4153
|
+
discountAmount = originalPrice - fin;
|
|
4154
|
+
}
|
|
4155
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
4156
|
+
return {
|
|
4157
|
+
originalPrice: round4(originalPrice),
|
|
4158
|
+
finalPrice: round4(originalPrice - discountAmount),
|
|
4159
|
+
discountAmount: round4(discountAmount),
|
|
4160
|
+
discountPercent: round4(discountAmount / originalPrice * 100),
|
|
4161
|
+
given: field,
|
|
4162
|
+
solvedFor: _DISCOUNT_FIELDS_MCP.filter((f) => f !== field)
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
function _applySuccessiveDiscountsMcp(originalPrice, discountPercents) {
|
|
4166
|
+
if (!Number.isFinite(originalPrice) || originalPrice <= 0) {
|
|
4167
|
+
return { error: "Original price must be a finite number greater than zero" };
|
|
4168
|
+
}
|
|
4169
|
+
if (discountPercents.length === 0) return { error: "Provide at least one discount percent" };
|
|
4170
|
+
if (discountPercents.length > 20) return { error: "Provide at most 20 successive discounts" };
|
|
4171
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
4172
|
+
const steps = [];
|
|
4173
|
+
let running = originalPrice;
|
|
4174
|
+
let sumOfDiscountPercents = 0;
|
|
4175
|
+
for (const pct of discountPercents) {
|
|
4176
|
+
if (!Number.isFinite(pct)) return { error: "Every discount percent must be a finite number" };
|
|
4177
|
+
if (pct < 0) return { error: "Discount percent cannot be negative" };
|
|
4178
|
+
if (pct > 100) return { error: "Discount percent cannot exceed 100" };
|
|
4179
|
+
const priceBefore = running;
|
|
4180
|
+
const saved = priceBefore * (pct / 100);
|
|
4181
|
+
running = priceBefore - saved;
|
|
4182
|
+
sumOfDiscountPercents += pct;
|
|
4183
|
+
steps.push({ discountPercent: round4(pct), priceBefore: round4(priceBefore), priceAfter: round4(running), saved: round4(saved) });
|
|
4184
|
+
}
|
|
4185
|
+
const totalSaved = originalPrice - running;
|
|
4186
|
+
return {
|
|
4187
|
+
originalPrice: round4(originalPrice),
|
|
4188
|
+
finalPrice: round4(running),
|
|
4189
|
+
totalSaved: round4(totalSaved),
|
|
4190
|
+
effectiveDiscountPercent: round4(totalSaved / originalPrice * 100),
|
|
4191
|
+
sumOfDiscountPercents: round4(sumOfDiscountPercents),
|
|
4192
|
+
steps
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
server.tool(
|
|
4196
|
+
"calculate_discount",
|
|
4197
|
+
"Discount / markdown calculator. Give an original price plus exactly one of discountPercent, discountAmount, or finalPrice and the other two are derived. Pass discountPercents instead to stack discounts in sequence, where 20% then a further 10% is an effective 28% off rather than 30%. Pure arithmetic, no live price feed and no tax or shipping.",
|
|
4198
|
+
{
|
|
4199
|
+
originalPrice: z.number().describe("The price before any discount (must be greater than zero)"),
|
|
4200
|
+
discountPercent: z.number().optional().describe("Discount as a percent of the original price (0-100). Provide exactly one of discountPercent, discountAmount, finalPrice."),
|
|
4201
|
+
discountAmount: z.number().optional().describe("Discount as an absolute amount off the original price"),
|
|
4202
|
+
finalPrice: z.number().optional().describe("The price after the discount, to solve for the percent and amount"),
|
|
4203
|
+
discountPercents: z.array(z.number()).optional().describe("Successive discount percents applied in order, each off the running price (max 20). Overrides the single-discount fields when present.")
|
|
4204
|
+
},
|
|
4205
|
+
async ({ originalPrice, discountPercent, discountAmount, finalPrice, discountPercents }) => {
|
|
4206
|
+
const result = discountPercents ? _applySuccessiveDiscountsMcp(originalPrice, discountPercents) : _calculateDiscountMcp({ originalPrice, discountPercent, discountAmount, finalPrice });
|
|
4207
|
+
if ("error" in result) return errText(result);
|
|
4208
|
+
return text(JSON.stringify(result, null, 2));
|
|
4209
|
+
}
|
|
4210
|
+
);
|
|
4211
|
+
var _SUBTITLE_TIMESTAMP_RE_MCP = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
|
|
4212
|
+
var _VTT_BLOCK_KEYWORDS_MCP = ["NOTE", "STYLE", "REGION"];
|
|
4213
|
+
function _parseSubtitleTimestampMcp(raw) {
|
|
4214
|
+
const m = _SUBTITLE_TIMESTAMP_RE_MCP.exec(raw.trim());
|
|
4215
|
+
if (!m || m.index !== 0) return null;
|
|
4216
|
+
let hours;
|
|
4217
|
+
let minutes;
|
|
4218
|
+
let seconds;
|
|
4219
|
+
let millis;
|
|
4220
|
+
if (m[1] !== void 0) {
|
|
4221
|
+
hours = Number(m[1]);
|
|
4222
|
+
minutes = Number(m[2]);
|
|
4223
|
+
seconds = Number(m[3]);
|
|
4224
|
+
millis = m[4];
|
|
4225
|
+
} else {
|
|
4226
|
+
hours = 0;
|
|
4227
|
+
minutes = Number(m[5]);
|
|
4228
|
+
seconds = Number(m[6]);
|
|
4229
|
+
millis = m[7];
|
|
4230
|
+
}
|
|
4231
|
+
if (minutes > 59 || seconds > 59) return null;
|
|
4232
|
+
return hours * 3600 + minutes * 60 + seconds + Number(millis.padEnd(3, "0")) / 1e3;
|
|
4233
|
+
}
|
|
4234
|
+
function _formatSubtitleTimestampMcp(totalSeconds, format) {
|
|
4235
|
+
const totalMs = Math.round(Math.max(0, totalSeconds) * 1e3);
|
|
4236
|
+
const ms = totalMs % 1e3;
|
|
4237
|
+
const totalSecs = Math.floor(totalMs / 1e3);
|
|
4238
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
4239
|
+
return `${pad(Math.floor(totalSecs / 3600))}:${pad(Math.floor(totalSecs / 60) % 60)}:${pad(totalSecs % 60)}${format === "srt" ? "," : "."}${pad(ms, 3)}`;
|
|
4240
|
+
}
|
|
4241
|
+
function _detectSubtitleFormatMcp(input) {
|
|
4242
|
+
const t = (input.charCodeAt(0) === 65279 ? input.slice(1) : input).trimStart();
|
|
4243
|
+
if (/^WEBVTT/.test(t)) return "vtt";
|
|
4244
|
+
const arrow = /(\d{1,3}):(\d{1,2})(?::(\d{1,2}))?([.,])(\d{1,3})\s*-->/.exec(t);
|
|
4245
|
+
if (arrow && arrow[4] === ".") return "vtt";
|
|
4246
|
+
return "srt";
|
|
4247
|
+
}
|
|
4248
|
+
function _convertSubtitlesMcp(input, target, offsetSeconds) {
|
|
4249
|
+
if (typeof input !== "string" || input.trim().length === 0) return { error: "Subtitle input is empty" };
|
|
4250
|
+
if (!Number.isFinite(offsetSeconds)) return { error: "offsetSeconds must be a finite number" };
|
|
4251
|
+
const sourceFormat = _detectSubtitleFormatMcp(input);
|
|
4252
|
+
const warnings = [];
|
|
4253
|
+
let body = (input.charCodeAt(0) === 65279 ? input.slice(1) : input).replace(/\r\n?/g, "\n");
|
|
4254
|
+
if (sourceFormat === "vtt") {
|
|
4255
|
+
const headerMatch = /^WEBVTT[^\n]*\n?/.exec(body.trimStart());
|
|
4256
|
+
if (headerMatch) body = body.trimStart().slice(headerMatch[0].length);
|
|
4257
|
+
else warnings.push('WebVTT input is missing its "WEBVTT" header line');
|
|
4258
|
+
}
|
|
4259
|
+
const blocks = body.split(/\n[ \t]*\n+/).map((b) => b.trim()).filter((b) => b.length > 0);
|
|
4260
|
+
const cues = [];
|
|
4261
|
+
for (const block of blocks) {
|
|
4262
|
+
const lines = block.split("\n");
|
|
4263
|
+
const arrowIdx = lines.findIndex((l) => l.includes("-->"));
|
|
4264
|
+
if (arrowIdx === -1) {
|
|
4265
|
+
const keyword = _VTT_BLOCK_KEYWORDS_MCP.find((k) => lines[0] === k || lines[0].startsWith(`${k} `));
|
|
4266
|
+
if (sourceFormat === "vtt" && keyword) warnings.push(`Dropped a WebVTT ${keyword} block, which has no SRT equivalent`);
|
|
4267
|
+
else warnings.push(`Skipped a block with no "-->" timing line: ${JSON.stringify(lines[0].slice(0, 40))}`);
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
const timingLine = lines[arrowIdx];
|
|
4271
|
+
const parts = timingLine.split("-->");
|
|
4272
|
+
const start = _parseSubtitleTimestampMcp(parts[0] ?? "");
|
|
4273
|
+
const rightTrimmed = (parts[1] ?? "").trim();
|
|
4274
|
+
const end = _parseSubtitleTimestampMcp(rightTrimmed);
|
|
4275
|
+
if (start === null || end === null) {
|
|
4276
|
+
warnings.push(`Skipped a cue with an unparseable timing line: ${JSON.stringify(timingLine.slice(0, 60))}`);
|
|
4277
|
+
continue;
|
|
4278
|
+
}
|
|
4279
|
+
const endMatch = _SUBTITLE_TIMESTAMP_RE_MCP.exec(rightTrimmed);
|
|
4280
|
+
const settings = endMatch ? rightTrimmed.slice(endMatch[0].length).trim() : "";
|
|
4281
|
+
const idLines = lines.slice(0, arrowIdx).filter((l) => l.trim().length > 0);
|
|
4282
|
+
const cueText = lines.slice(arrowIdx + 1).join("\n").trim();
|
|
4283
|
+
const cue = { index: cues.length + 1, start, end, text: cueText };
|
|
4284
|
+
if (sourceFormat === "vtt" && idLines.length > 0) cue.id = idLines[idLines.length - 1].trim();
|
|
4285
|
+
if (settings.length > 0) cue.settings = settings;
|
|
4286
|
+
if (end < start) warnings.push(`Cue ${cue.index} ends before it starts`);
|
|
4287
|
+
if (cueText.length === 0) warnings.push(`Cue ${cue.index} has no text`);
|
|
4288
|
+
if (cues.length > 0 && start < cues[cues.length - 1].end) {
|
|
4289
|
+
warnings.push(`Cue ${cue.index} starts before cue ${cue.index - 1} ends (overlapping cues)`);
|
|
4290
|
+
}
|
|
4291
|
+
cues.push(cue);
|
|
4292
|
+
}
|
|
4293
|
+
if (cues.length === 0) return { error: "No subtitle cues found: check that the input is a valid SRT or WebVTT file" };
|
|
4294
|
+
let clamped = 0;
|
|
4295
|
+
const shifted = cues.map((cue) => {
|
|
4296
|
+
const start = cue.start + offsetSeconds;
|
|
4297
|
+
if (start < 0) clamped += 1;
|
|
4298
|
+
return { ...cue, start: Math.max(0, start), end: Math.max(0, cue.end + offsetSeconds) };
|
|
4299
|
+
});
|
|
4300
|
+
if (clamped > 0) warnings.push(`${clamped} cue(s) would start before zero and were clamped to 00:00:00`);
|
|
4301
|
+
if (target === "srt") {
|
|
4302
|
+
const droppedSettings = shifted.filter((c) => c.settings).length;
|
|
4303
|
+
const droppedIds = shifted.filter((c) => c.id).length;
|
|
4304
|
+
if (droppedSettings > 0) warnings.push(`Dropped WebVTT cue settings on ${droppedSettings} cue(s): SRT has no equivalent`);
|
|
4305
|
+
if (droppedIds > 0) warnings.push(`Dropped ${droppedIds} WebVTT cue identifier(s): SRT numbers cues sequentially instead`);
|
|
4306
|
+
}
|
|
4307
|
+
const serialised = shifted.map((cue, i) => {
|
|
4308
|
+
const time = `${_formatSubtitleTimestampMcp(cue.start, target)} --> ${_formatSubtitleTimestampMcp(cue.end, target)}`;
|
|
4309
|
+
if (target === "srt") return `${i + 1}
|
|
4310
|
+
${time}
|
|
4311
|
+
${cue.text}`;
|
|
4312
|
+
const timing = cue.settings ? `${time} ${cue.settings}` : time;
|
|
4313
|
+
return cue.id ? `${cue.id}
|
|
4314
|
+
${timing}
|
|
4315
|
+
${cue.text}` : `${timing}
|
|
4316
|
+
${cue.text}`;
|
|
4317
|
+
}).join("\n\n");
|
|
4318
|
+
return {
|
|
4319
|
+
format: target,
|
|
4320
|
+
sourceFormat,
|
|
4321
|
+
output: target === "vtt" ? `WEBVTT
|
|
4322
|
+
|
|
4323
|
+
${serialised}
|
|
4324
|
+
` : `${serialised}
|
|
4325
|
+
`,
|
|
4326
|
+
cueCount: shifted.length,
|
|
4327
|
+
duration: Math.round(Math.max(...shifted.map((c) => c.end)) * 1e3) / 1e3,
|
|
4328
|
+
warnings
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
server.tool(
|
|
4332
|
+
"convert_subtitles",
|
|
4333
|
+
"Convert a subtitle file between SubRip (.srt) and WebVTT (.vtt), optionally shifting every cue by a fixed number of seconds to resync it. The source format is auto-detected. Converting WebVTT to SRT drops cue identifiers, cue settings, and NOTE/STYLE/REGION blocks, since SubRip has no equivalent; every drop is reported as a warning.",
|
|
4334
|
+
{
|
|
4335
|
+
input: z.string().describe("The full SRT or WebVTT file contents"),
|
|
4336
|
+
target: z.enum(["srt", "vtt"]).default("vtt").describe("Output format"),
|
|
4337
|
+
offsetSeconds: z.number().default(0).describe("Shift every cue by this many seconds (may be negative; results below zero are clamped to 00:00:00)")
|
|
4338
|
+
},
|
|
4339
|
+
async ({ input, target, offsetSeconds }) => {
|
|
4340
|
+
const result = _convertSubtitlesMcp(input, target, offsetSeconds);
|
|
4341
|
+
if ("error" in result) return errText(result);
|
|
4342
|
+
return text(JSON.stringify(result, null, 2));
|
|
4343
|
+
}
|
|
4344
|
+
);
|
|
4061
4345
|
var transport = new StdioServerTransport();
|
|
4062
4346
|
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.25.0",
|
|
4
4
|
"description": "MCP server exposing 125 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",
|