@utilix-tech/mcp 0.70.0 → 0.72.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 +6 -3
- package/dist/index.js +241 -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.72.0
|
|
6
|
+
- Add `validate_vin` tool: validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections.
|
|
7
|
+
- Add `validate_isin` tool: validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier.
|
|
8
|
+
|
|
9
|
+
## 0.71.0
|
|
10
|
+
- Add `read_midi_info` tool: parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds accounting for tempo changes.
|
|
11
|
+
|
|
5
12
|
## 0.70.0
|
|
6
13
|
- Add `generate_hotp`, `generate_totp` tools: generate an HOTP code (RFC 4226) for a counter value, or the current TOTP code (RFC 6238) plus seconds until rotation, from a base32-encoded shared secret, supporting SHA-1/SHA-256/SHA-512 and 6-8 digit codes.
|
|
7
14
|
- Add `calculate_tax_equivalent_yield` tool: convert between a tax-free municipal bond yield and its taxable equivalent given a marginal federal (plus optional state) tax rate, in both directions.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 239 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
|
|
|
@@ -162,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
162
162
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
163
163
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
164
164
|
|
|
165
|
-
### Units (
|
|
165
|
+
### Units (42 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -205,6 +205,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
205
205
|
| `calculate_extra_payment_payoff` | Compute how much faster a fixed-rate loan pays off, and how much interest is saved, when a fixed extra amount is added to every monthly payment |
|
|
206
206
|
| `plan_debt_payoff` | Simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest |
|
|
207
207
|
| `calculate_loan_refinance` | Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs |
|
|
208
|
+
| `validate_vin` | Validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections |
|
|
209
|
+
| `validate_isin` | Validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier |
|
|
208
210
|
|
|
209
211
|
### API / Network (48 tools)
|
|
210
212
|
| Tool | Description |
|
|
@@ -371,12 +373,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
371
373
|
| `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
|
|
372
374
|
| `read_pdf_page_dimensions` | List each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree: no PDF rendering library required |
|
|
373
375
|
|
|
374
|
-
### Audio (
|
|
376
|
+
### Audio (4 tools)
|
|
375
377
|
| Tool | Description |
|
|
376
378
|
|------|-------------|
|
|
377
379
|
| `read_wav_info` | Read sample rate, channel count, bit depth, and duration from a WAV file's RIFF/fmt/data chunk headers: no audio library required |
|
|
378
380
|
| `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
|
|
379
381
|
| `read_flac_info` | Read sample rate, channels, bit depth, total samples, and duration from a FLAC file's STREAMINFO block, plus Vorbis comment tags if present: no audio library required |
|
|
382
|
+
| `read_midi_info` | Parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds: no MIDI playback or synthesis |
|
|
380
383
|
|
|
381
384
|
### Video (1 tool)
|
|
382
385
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -8768,6 +8768,139 @@ server.tool(
|
|
|
8768
8768
|
return text(JSON.stringify(result, null, 2));
|
|
8769
8769
|
}
|
|
8770
8770
|
);
|
|
8771
|
+
function _readU16BEMcp(bytes, offset) {
|
|
8772
|
+
return bytes[offset] << 8 | bytes[offset + 1];
|
|
8773
|
+
}
|
|
8774
|
+
function _readMidiVarLenMcp(bytes, pos) {
|
|
8775
|
+
let value = 0;
|
|
8776
|
+
let i = pos;
|
|
8777
|
+
for (let n = 0; n < 4; n++) {
|
|
8778
|
+
if (i >= bytes.length) return null;
|
|
8779
|
+
const b = bytes[i];
|
|
8780
|
+
value = value << 7 | b & 127;
|
|
8781
|
+
i++;
|
|
8782
|
+
if ((b & 128) === 0) return { value, next: i };
|
|
8783
|
+
}
|
|
8784
|
+
return null;
|
|
8785
|
+
}
|
|
8786
|
+
function _readMidiInfoMcp(bytes) {
|
|
8787
|
+
if (bytes.length < 14) return { error: "File is too small to be a valid MIDI file" };
|
|
8788
|
+
if (Buffer.from(bytes.subarray(0, 4)).toString("ascii") !== "MThd") {
|
|
8789
|
+
return { error: 'Not a valid MIDI file (missing "MThd" header chunk)' };
|
|
8790
|
+
}
|
|
8791
|
+
const headerLength = (bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
|
|
8792
|
+
if (headerLength < 6 || 8 + headerLength > bytes.length) return { error: "Truncated or invalid MThd header chunk" };
|
|
8793
|
+
const format = _readU16BEMcp(bytes, 8);
|
|
8794
|
+
if (format !== 0 && format !== 1 && format !== 2) return { error: `Unsupported MIDI format: ${format} (expected 0, 1, or 2)` };
|
|
8795
|
+
const trackCount = _readU16BEMcp(bytes, 10);
|
|
8796
|
+
if (trackCount < 1) return { error: "MIDI file declares zero tracks" };
|
|
8797
|
+
const divisionRaw = _readU16BEMcp(bytes, 12);
|
|
8798
|
+
const division = (divisionRaw & 32768) !== 0 ? { type: "smpte", framesPerSecond: -(divisionRaw >> 8 << 24 >> 24), ticksPerFrame: divisionRaw & 255 } : { type: "ppq", ticksPerQuarterNote: divisionRaw & 32767 };
|
|
8799
|
+
const tracks = [];
|
|
8800
|
+
const tempoChanges = [];
|
|
8801
|
+
let timeSignature = null;
|
|
8802
|
+
let maxEndTick = 0;
|
|
8803
|
+
let offset = 8 + headerLength;
|
|
8804
|
+
let tracksParsed = 0;
|
|
8805
|
+
while (tracksParsed < trackCount && offset + 8 <= bytes.length) {
|
|
8806
|
+
const chunkId = Buffer.from(bytes.subarray(offset, offset + 4)).toString("ascii");
|
|
8807
|
+
const chunkLength = (bytes[offset + 4] << 24 | bytes[offset + 5] << 16 | bytes[offset + 6] << 8 | bytes[offset + 7]) >>> 0;
|
|
8808
|
+
const dataStart = offset + 8;
|
|
8809
|
+
const dataEnd = Math.min(dataStart + chunkLength, bytes.length);
|
|
8810
|
+
if (chunkId !== "MTrk") {
|
|
8811
|
+
offset = dataEnd;
|
|
8812
|
+
continue;
|
|
8813
|
+
}
|
|
8814
|
+
let pos = dataStart;
|
|
8815
|
+
let runningStatus = 0;
|
|
8816
|
+
let absoluteTick = 0;
|
|
8817
|
+
let eventCount = 0;
|
|
8818
|
+
let noteOnCount = 0;
|
|
8819
|
+
let name = null;
|
|
8820
|
+
while (pos < dataEnd) {
|
|
8821
|
+
const delta = _readMidiVarLenMcp(bytes, pos);
|
|
8822
|
+
if (!delta) break;
|
|
8823
|
+
pos = delta.next;
|
|
8824
|
+
absoluteTick += delta.value;
|
|
8825
|
+
if (pos >= dataEnd) break;
|
|
8826
|
+
let statusByte = bytes[pos];
|
|
8827
|
+
if (statusByte & 128) {
|
|
8828
|
+
runningStatus = statusByte;
|
|
8829
|
+
pos++;
|
|
8830
|
+
}
|
|
8831
|
+
statusByte = runningStatus;
|
|
8832
|
+
eventCount++;
|
|
8833
|
+
if (statusByte === 255) {
|
|
8834
|
+
const metaType = bytes[pos];
|
|
8835
|
+
pos++;
|
|
8836
|
+
const len = _readMidiVarLenMcp(bytes, pos);
|
|
8837
|
+
if (!len) break;
|
|
8838
|
+
pos = len.next;
|
|
8839
|
+
const data = bytes.subarray(pos, Math.min(pos + len.value, dataEnd));
|
|
8840
|
+
pos += len.value;
|
|
8841
|
+
if (metaType === 81 && data.length >= 3) {
|
|
8842
|
+
tempoChanges.push({ tick: absoluteTick, usPerQuarter: data[0] << 16 | data[1] << 8 | data[2] });
|
|
8843
|
+
} else if (metaType === 88 && data.length >= 2) {
|
|
8844
|
+
if (!timeSignature) timeSignature = { numerator: data[0], denominator: 2 ** data[1] };
|
|
8845
|
+
} else if (metaType === 3 && name === null) {
|
|
8846
|
+
name = Buffer.from(data).toString("utf8").trim() || null;
|
|
8847
|
+
} else if (metaType === 47) {
|
|
8848
|
+
break;
|
|
8849
|
+
}
|
|
8850
|
+
} else if (statusByte === 240 || statusByte === 247) {
|
|
8851
|
+
const len = _readMidiVarLenMcp(bytes, pos);
|
|
8852
|
+
if (!len) break;
|
|
8853
|
+
pos = len.next + len.value;
|
|
8854
|
+
} else if (statusByte >= 128) {
|
|
8855
|
+
const type = statusByte & 240;
|
|
8856
|
+
if (type === 192 || type === 208) {
|
|
8857
|
+
pos += 1;
|
|
8858
|
+
} else {
|
|
8859
|
+
if (type === 144 && (bytes[pos + 1] ?? 0) > 0) noteOnCount++;
|
|
8860
|
+
pos += 2;
|
|
8861
|
+
}
|
|
8862
|
+
} else {
|
|
8863
|
+
break;
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
tracks.push({ index: tracksParsed, name, eventCount, noteOnCount });
|
|
8867
|
+
maxEndTick = Math.max(maxEndTick, absoluteTick);
|
|
8868
|
+
offset = dataEnd;
|
|
8869
|
+
tracksParsed++;
|
|
8870
|
+
}
|
|
8871
|
+
if (tracks.length === 0) return { error: "No MTrk track chunks found in this MIDI file" };
|
|
8872
|
+
const DEFAULT_US_PER_QUARTER = 5e5;
|
|
8873
|
+
const initialTempoBpm = Math.round(6e7 / (tempoChanges[0]?.usPerQuarter ?? DEFAULT_US_PER_QUARTER) * 100) / 100;
|
|
8874
|
+
let durationSeconds = null;
|
|
8875
|
+
if (division.type === "smpte") {
|
|
8876
|
+
const ticksPerSecond = division.framesPerSecond * division.ticksPerFrame;
|
|
8877
|
+
durationSeconds = ticksPerSecond > 0 ? Math.round(maxEndTick / ticksPerSecond * 1e3) / 1e3 : null;
|
|
8878
|
+
} else {
|
|
8879
|
+
const sorted = [...tempoChanges].sort((a, b) => a.tick - b.tick);
|
|
8880
|
+
let seconds = 0;
|
|
8881
|
+
let prevTick = 0;
|
|
8882
|
+
let currentUsPerQuarter = DEFAULT_US_PER_QUARTER;
|
|
8883
|
+
for (const change of sorted) {
|
|
8884
|
+
seconds += (change.tick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
|
|
8885
|
+
prevTick = change.tick;
|
|
8886
|
+
currentUsPerQuarter = change.usPerQuarter;
|
|
8887
|
+
}
|
|
8888
|
+
seconds += (maxEndTick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
|
|
8889
|
+
durationSeconds = Math.round(seconds * 1e3) / 1e3;
|
|
8890
|
+
}
|
|
8891
|
+
return { format, trackCount: tracks.length, division, initialTempoBpm, timeSignature, durationSeconds, tracks };
|
|
8892
|
+
}
|
|
8893
|
+
server.tool(
|
|
8894
|
+
"read_midi_info",
|
|
8895
|
+
"Parse a Standard MIDI File's header and every track chunk to report format (0/1/2), tick division, initial tempo, time signature, a per-track event/note-on count, and total duration in seconds, without any MIDI playback or synthesis.",
|
|
8896
|
+
{ midi_base64: z.string().describe("Base64-encoded MIDI (.mid/.midi) file bytes, optionally prefixed with a data: URL scheme") },
|
|
8897
|
+
async ({ midi_base64 }) => {
|
|
8898
|
+
const bytes = new Uint8Array(Buffer.from(midi_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
8899
|
+
const result = _readMidiInfoMcp(bytes);
|
|
8900
|
+
if ("error" in result) return errText(result);
|
|
8901
|
+
return text(JSON.stringify(result, null, 2));
|
|
8902
|
+
}
|
|
8903
|
+
);
|
|
8771
8904
|
function _calculateCashRoundingMcp(total, roundingUnit, mode = "nearest") {
|
|
8772
8905
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
8773
8906
|
if (typeof total !== "number" || !Number.isFinite(total)) {
|
|
@@ -11264,5 +11397,113 @@ server.tool(
|
|
|
11264
11397
|
return text(JSON.stringify(result, null, 2));
|
|
11265
11398
|
}
|
|
11266
11399
|
);
|
|
11400
|
+
var _VIN_TRANSLITERATION_MCP = {
|
|
11401
|
+
A: 1,
|
|
11402
|
+
B: 2,
|
|
11403
|
+
C: 3,
|
|
11404
|
+
D: 4,
|
|
11405
|
+
E: 5,
|
|
11406
|
+
F: 6,
|
|
11407
|
+
G: 7,
|
|
11408
|
+
H: 8,
|
|
11409
|
+
J: 1,
|
|
11410
|
+
K: 2,
|
|
11411
|
+
L: 3,
|
|
11412
|
+
M: 4,
|
|
11413
|
+
N: 5,
|
|
11414
|
+
P: 7,
|
|
11415
|
+
R: 9,
|
|
11416
|
+
S: 2,
|
|
11417
|
+
T: 3,
|
|
11418
|
+
U: 4,
|
|
11419
|
+
V: 5,
|
|
11420
|
+
W: 6,
|
|
11421
|
+
X: 7,
|
|
11422
|
+
Y: 8,
|
|
11423
|
+
Z: 9
|
|
11424
|
+
};
|
|
11425
|
+
var _VIN_WEIGHTS_MCP = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
11426
|
+
function _validateVin(input) {
|
|
11427
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
11428
|
+
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
11429
|
+
if (cleaned.length !== 17) return { error: `VIN length (${cleaned.length}) must be exactly 17 characters` };
|
|
11430
|
+
if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(cleaned)) {
|
|
11431
|
+
return { error: "VIN must contain only letters and digits, excluding I, O, and Q" };
|
|
11432
|
+
}
|
|
11433
|
+
let sum = 0;
|
|
11434
|
+
for (let i = 0; i < 17; i++) {
|
|
11435
|
+
const ch = cleaned[i];
|
|
11436
|
+
const value = /[0-9]/.test(ch) ? Number(ch) : _VIN_TRANSLITERATION_MCP[ch];
|
|
11437
|
+
sum += value * _VIN_WEIGHTS_MCP[i];
|
|
11438
|
+
}
|
|
11439
|
+
const remainder = sum % 11;
|
|
11440
|
+
const expectedCheckDigit = remainder === 10 ? "X" : String(remainder);
|
|
11441
|
+
const checkDigit = cleaned[8];
|
|
11442
|
+
return {
|
|
11443
|
+
valid: checkDigit === expectedCheckDigit,
|
|
11444
|
+
vin: cleaned,
|
|
11445
|
+
checkDigit,
|
|
11446
|
+
expectedCheckDigit,
|
|
11447
|
+
wmi: cleaned.slice(0, 3),
|
|
11448
|
+
vds: cleaned.slice(3, 9),
|
|
11449
|
+
vis: cleaned.slice(9, 17)
|
|
11450
|
+
};
|
|
11451
|
+
}
|
|
11452
|
+
server.tool(
|
|
11453
|
+
"validate_vin",
|
|
11454
|
+
"Validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit at position 9, and split it into its World Manufacturer Identifier (WMI), Vehicle Descriptor Section (VDS), and Vehicle Identifier Section (VIS). Does not decode make, model, or model year.",
|
|
11455
|
+
{ vin: z.string().describe("The 17-character VIN, with or without spaces") },
|
|
11456
|
+
async ({ vin }) => {
|
|
11457
|
+
const result = _validateVin(vin);
|
|
11458
|
+
if ("error" in result) return errText(result);
|
|
11459
|
+
return text(JSON.stringify(result, null, 2));
|
|
11460
|
+
}
|
|
11461
|
+
);
|
|
11462
|
+
function _isinLuhnCheckDigitMcp(payloadDigits) {
|
|
11463
|
+
let sum = 0;
|
|
11464
|
+
let shouldDouble = true;
|
|
11465
|
+
for (let i = payloadDigits.length - 1; i >= 0; i--) {
|
|
11466
|
+
let d = parseInt(payloadDigits[i], 10);
|
|
11467
|
+
if (shouldDouble) {
|
|
11468
|
+
d *= 2;
|
|
11469
|
+
if (d > 9) d -= 9;
|
|
11470
|
+
}
|
|
11471
|
+
sum += d;
|
|
11472
|
+
shouldDouble = !shouldDouble;
|
|
11473
|
+
}
|
|
11474
|
+
return (10 - sum % 10) % 10;
|
|
11475
|
+
}
|
|
11476
|
+
function _validateIsin(input) {
|
|
11477
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
11478
|
+
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
11479
|
+
if (cleaned.length !== 12) return { error: `ISIN length (${cleaned.length}) must be exactly 12 characters` };
|
|
11480
|
+
if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(cleaned)) {
|
|
11481
|
+
return { error: "ISIN must be a 2-letter country code, 9 alphanumeric characters, then 1 numeric check digit" };
|
|
11482
|
+
}
|
|
11483
|
+
const countryCode = cleaned.slice(0, 2);
|
|
11484
|
+
const securityIdentifier = cleaned.slice(2, 11);
|
|
11485
|
+
const checkDigit = Number(cleaned[11]);
|
|
11486
|
+
const payload = cleaned.slice(0, 11).split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
|
|
11487
|
+
const expectedCheckDigit = _isinLuhnCheckDigitMcp(payload);
|
|
11488
|
+
return {
|
|
11489
|
+
valid: checkDigit === expectedCheckDigit,
|
|
11490
|
+
isin: cleaned,
|
|
11491
|
+
countryCode,
|
|
11492
|
+
securityIdentifier,
|
|
11493
|
+
checkDigit,
|
|
11494
|
+
expectedCheckDigit,
|
|
11495
|
+
formatted: `${countryCode} ${securityIdentifier} ${checkDigit}`
|
|
11496
|
+
};
|
|
11497
|
+
}
|
|
11498
|
+
server.tool(
|
|
11499
|
+
"validate_isin",
|
|
11500
|
+
"Validate a 12-character ISIN (ISO 6166) via its Luhn-style mod-10 check digit. The 2-letter country code and 9-character alphanumeric security identifier are transliterated (A=10 ... Z=35) into a numeric string before the Luhn algorithm is applied, the same way credit card numbers are checksummed.",
|
|
11501
|
+
{ isin: z.string().describe("The 12-character ISIN, with or without spaces") },
|
|
11502
|
+
async ({ isin }) => {
|
|
11503
|
+
const result = _validateIsin(isin);
|
|
11504
|
+
if ("error" in result) return errText(result);
|
|
11505
|
+
return text(JSON.stringify(result, null, 2));
|
|
11506
|
+
}
|
|
11507
|
+
);
|
|
11267
11508
|
var transport = new StdioServerTransport();
|
|
11268
11509
|
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.72.0",
|
|
4
4
|
"description": "MCP server exposing 198 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",
|