@utilix-tech/mcp 0.70.0 → 0.71.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 +3 -0
- package/README.md +3 -2
- package/dist/index.js +133 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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.71.0
|
|
6
|
+
- 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.
|
|
7
|
+
|
|
5
8
|
## 0.70.0
|
|
6
9
|
- 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
10
|
- 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 237 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
|
|
|
@@ -371,12 +371,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
371
371
|
| `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
372
|
| `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
373
|
|
|
374
|
-
### Audio (
|
|
374
|
+
### Audio (4 tools)
|
|
375
375
|
| Tool | Description |
|
|
376
376
|
|------|-------------|
|
|
377
377
|
| `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
378
|
| `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
|
|
379
379
|
| `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 |
|
|
380
|
+
| `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
381
|
|
|
381
382
|
### Video (1 tool)
|
|
382
383
|
| 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)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.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",
|