@utilix-tech/mcp 0.94.0 → 0.95.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,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.95.0
6
+ - Add `read_png_chunks` tool: walk a PNG's chunk stream directly from its bytes to report every chunk's type, offset, size, and CRC-32 validity, plus the decoded IHDR header and common ancillary chunks (tEXt, pHYs, gAMA, tIME, acTL).
7
+ - Add `estimate_audio_file_size` tool: estimate an audio file's size before recording or exporting it, uncompressed PCM size from sample rate/bit depth/channels or compressed size from a target bitrate.
8
+
5
9
  ## 0.94.0
6
10
  - Add `build_alt_svc_header`/`parse_alt_svc_header` tools: build an `Alt-Svc` response header (RFC 7838) from one or more alternative services with a plain-English note per protocol-id, flagging outdated HTTP/3 draft protocol-ids, or parse an existing header back into its alternatives, including the special `clear` value.
7
11
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 281 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 283 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
 
@@ -400,13 +400,14 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
400
400
  | `calculate_retry_backoff_schedule` | Compute the full sequence of delays an exponential-backoff retry policy would actually produce, given a base delay, multiplier, and max retry count, optionally capped at a max delay and with full/equal jitter ranges |
401
401
  | `extract_prompt_template_variables` | Extract every {{variable}}-style placeholder from a prompt template, report occurrence counts, flag duplicates, and optionally cross-check against a list of provided variable names |
402
402
 
403
- ### Image (4 tools)
403
+ ### Image (5 tools)
404
404
  | Tool | Description |
405
405
  |------|-------------|
406
406
  | `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
407
407
  | `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
408
408
  | `read_ico_info` | Read how many images a .ico file embeds and each one's width, height, color depth, and size from its ICONDIR header |
409
409
  | `inspect_gif_frames` | Walk a GIF87a/89a file's block structure to report frame count, total animation duration, and loop count, without decoding any LZW-compressed pixel data |
410
+ | `read_png_chunks` | Walk a PNG's chunk stream directly from its bytes to report every chunk's type, offset, size, and CRC-32 validity, plus the decoded IHDR header and common ancillary chunks (tEXt, pHYs, gAMA, tIME, acTL) |
410
411
 
411
412
  ### PDF (3 tools)
412
413
  | Tool | Description |
@@ -415,13 +416,14 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
415
416
  | `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 |
416
417
  | `read_pdf_outline` | Walk a PDF's /Outlines bookmark tree (Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order: no PDF rendering library required |
417
418
 
418
- ### Audio (4 tools)
419
+ ### Audio (5 tools)
419
420
  | Tool | Description |
420
421
  |------|-------------|
421
422
  | `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 |
422
423
  | `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
423
424
  | `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 |
424
425
  | `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 |
426
+ | `estimate_audio_file_size` | Estimate an audio file's size before recording or exporting it: uncompressed PCM size from sample rate/bit depth/channels, or compressed size from a target bitrate |
425
427
 
426
428
  ### Video (1 tool)
427
429
  | Tool | Description |
package/dist/index.js CHANGED
@@ -15090,5 +15090,169 @@ server.tool(
15090
15090
  return text(JSON.stringify(result, null, 2));
15091
15091
  }
15092
15092
  );
15093
+ var _PNG_SIGNATURE_MCP = [137, 80, 78, 71, 13, 10, 26, 10];
15094
+ function _readU32BEMcp(bytes, offset) {
15095
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
15096
+ }
15097
+ var _pngCrcTableMcp = null;
15098
+ function _pngCrc32Mcp(bytes) {
15099
+ if (!_pngCrcTableMcp) {
15100
+ const table = new Uint32Array(256);
15101
+ for (let n = 0; n < 256; n++) {
15102
+ let c = n;
15103
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
15104
+ table[n] = c;
15105
+ }
15106
+ _pngCrcTableMcp = table;
15107
+ }
15108
+ let crc = 4294967295;
15109
+ for (let i = 0; i < bytes.length; i++) crc = _pngCrcTableMcp[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
15110
+ return (crc ^ 4294967295) >>> 0;
15111
+ }
15112
+ var _PNG_COLOR_TYPES_MCP = { 0: "grayscale", 2: "rgb", 3: "palette", 4: "grayscale+alpha", 6: "rgba" };
15113
+ function _pad2Mcp(n) {
15114
+ return n < 10 ? `0${n}` : `${n}`;
15115
+ }
15116
+ function _readPngChunksMcp(bytes) {
15117
+ if (bytes.length < 8) return { error: "File too small to be a valid PNG file" };
15118
+ for (let i = 0; i < 8; i++) {
15119
+ if (bytes[i] !== _PNG_SIGNATURE_MCP[i]) return { error: "Not a valid PNG file (bad 8-byte signature)" };
15120
+ }
15121
+ const chunks = [];
15122
+ const textChunks = [];
15123
+ let header = null;
15124
+ let physicalDimensions = null;
15125
+ let gamma = null;
15126
+ let lastModified = null;
15127
+ let isAnimated = false;
15128
+ let offset = 8;
15129
+ while (offset + 8 <= bytes.length) {
15130
+ const dataLength = _readU32BEMcp(bytes, offset);
15131
+ const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
15132
+ const dataStart = offset + 8;
15133
+ const dataEnd = dataStart + dataLength;
15134
+ const crcOffset = dataEnd;
15135
+ if (crcOffset + 4 > bytes.length) return { error: `Truncated chunk "${type}" at offset ${offset}` };
15136
+ const data = bytes.subarray(dataStart, dataEnd);
15137
+ const expectedCrc = _readU32BEMcp(bytes, crcOffset);
15138
+ const actualCrc = _pngCrc32Mcp(bytes.subarray(offset + 4, dataEnd));
15139
+ const firstChar = type.charCodeAt(0);
15140
+ const critical = firstChar >= 65 && firstChar <= 90;
15141
+ chunks.push({ type, offset, dataLength, crcValid: actualCrc === expectedCrc, critical });
15142
+ if (type === "IHDR" && data.length >= 13) {
15143
+ const colorTypeNum = data[9];
15144
+ header = {
15145
+ width: _readU32BEMcp(data, 0),
15146
+ height: _readU32BEMcp(data, 4),
15147
+ bitDepth: data[8],
15148
+ colorType: _PNG_COLOR_TYPES_MCP[colorTypeNum] ?? "unknown",
15149
+ hasAlpha: colorTypeNum === 4 || colorTypeNum === 6,
15150
+ interlace: data[12] === 1
15151
+ };
15152
+ } else if (type === "tEXt") {
15153
+ const nullIdx = data.indexOf(0);
15154
+ if (nullIdx !== -1) {
15155
+ textChunks.push({
15156
+ keyword: Buffer.from(data.subarray(0, nullIdx)).toString("latin1"),
15157
+ text: Buffer.from(data.subarray(nullIdx + 1)).toString("latin1"),
15158
+ compressed: false
15159
+ });
15160
+ }
15161
+ } else if (type === "zTXt" || type === "iTXt") {
15162
+ const nullIdx = data.indexOf(0);
15163
+ if (nullIdx !== -1) {
15164
+ textChunks.push({ keyword: Buffer.from(data.subarray(0, nullIdx)).toString("latin1"), text: null, compressed: true });
15165
+ }
15166
+ } else if (type === "pHYs" && data.length >= 9) {
15167
+ physicalDimensions = { pixelsPerUnitX: _readU32BEMcp(data, 0), pixelsPerUnitY: _readU32BEMcp(data, 4), unit: data[8] === 1 ? "meter" : "unknown" };
15168
+ } else if (type === "gAMA" && data.length >= 4) {
15169
+ gamma = _readU32BEMcp(data, 0) / 1e5;
15170
+ } else if (type === "tIME" && data.length >= 7) {
15171
+ const year = data[0] << 8 | data[1];
15172
+ lastModified = `${year}-${_pad2Mcp(data[2])}-${_pad2Mcp(data[3])}T${_pad2Mcp(data[4])}:${_pad2Mcp(data[5])}:${_pad2Mcp(data[6])}Z`;
15173
+ } else if (type === "acTL") {
15174
+ isAnimated = true;
15175
+ }
15176
+ offset = crcOffset + 4;
15177
+ if (type === "IEND") break;
15178
+ }
15179
+ if (chunks.length === 0) return { error: "No chunks found after the PNG signature" };
15180
+ if (!header) return { error: "Missing required IHDR chunk" };
15181
+ return { valid: true, fileSize: bytes.length, chunkCount: chunks.length, chunks, header, textChunks, physicalDimensions, gamma, lastModified, isAnimated };
15182
+ }
15183
+ server.tool(
15184
+ "read_png_chunks",
15185
+ "Walk a PNG file's chunk stream directly from its bytes to report every chunk's type, byte offset, data length, and whether its stored CRC-32 matches the data, plus the decoded IHDR header and a handful of common ancillary chunks (tEXt, pHYs, gAMA, tIME, acTL). No pixel decoding, and zTXt/iTXt text is listed but not zlib-inflated.",
15186
+ { png_base64: z.string().describe("Base64-encoded PNG file bytes, optionally prefixed with a data: URL scheme") },
15187
+ async ({ png_base64 }) => {
15188
+ const bytes = new Uint8Array(Buffer.from(png_base64.replace(/^data:.*;base64,/, ""), "base64"));
15189
+ const result = _readPngChunksMcp(bytes);
15190
+ if ("error" in result) return errText(result);
15191
+ return text(JSON.stringify(result, null, 2));
15192
+ }
15193
+ );
15194
+ function _estimateAudioFileSizeMcp(mode, durationSeconds, sampleRateHz, bitDepth, channels, bitrateKbps) {
15195
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
15196
+ return { error: "durationSeconds must be a positive number" };
15197
+ }
15198
+ const round2 = (n) => Math.round(n * 100) / 100;
15199
+ if (mode === "pcm") {
15200
+ if (typeof sampleRateHz !== "number" || !Number.isFinite(sampleRateHz) || sampleRateHz <= 0) {
15201
+ return { error: 'sampleRateHz must be a positive number for mode "pcm"' };
15202
+ }
15203
+ if (typeof bitDepth !== "number" || !Number.isFinite(bitDepth) || bitDepth <= 0) {
15204
+ return { error: 'bitDepth must be a positive number for mode "pcm"' };
15205
+ }
15206
+ if (typeof channels !== "number" || !Number.isInteger(channels) || channels <= 0) {
15207
+ return { error: 'channels must be a positive whole number for mode "pcm"' };
15208
+ }
15209
+ const dataBytes = sampleRateHz * (bitDepth / 8) * channels * durationSeconds;
15210
+ const fileSizeBytes = dataBytes + 44;
15211
+ return {
15212
+ mode,
15213
+ durationSeconds,
15214
+ sampleRateHz,
15215
+ bitDepth,
15216
+ channels,
15217
+ dataBytes: Math.round(dataBytes),
15218
+ fileSizeBytes: Math.round(fileSizeBytes),
15219
+ fileSizeMB: round2(fileSizeBytes / 1e6),
15220
+ fileSizeMiB: round2(fileSizeBytes / 1048576)
15221
+ };
15222
+ }
15223
+ if (mode === "compressed") {
15224
+ if (typeof bitrateKbps !== "number" || !Number.isFinite(bitrateKbps) || bitrateKbps <= 0) {
15225
+ return { error: 'bitrateKbps must be a positive number for mode "compressed"' };
15226
+ }
15227
+ const dataBytes = bitrateKbps * 1e3 * durationSeconds / 8;
15228
+ return {
15229
+ mode,
15230
+ durationSeconds,
15231
+ bitrateKbps,
15232
+ dataBytes: Math.round(dataBytes),
15233
+ fileSizeBytes: Math.round(dataBytes),
15234
+ fileSizeMB: round2(dataBytes / 1e6),
15235
+ fileSizeMiB: round2(dataBytes / 1048576)
15236
+ };
15237
+ }
15238
+ return { error: 'mode must be "pcm" or "compressed"' };
15239
+ }
15240
+ server.tool(
15241
+ "estimate_audio_file_size",
15242
+ 'Estimate how large an audio file will be before recording or exporting it. Mode "pcm" computes the uncompressed size from sample rate, bit depth, and channel count, plus a canonical 44-byte WAV header. Mode "compressed" computes it from a target bitrate. Pure arithmetic: no file is read, written, or encoded.',
15243
+ {
15244
+ mode: z.enum(["pcm", "compressed"]).describe('"pcm" requires sampleRateHz/bitDepth/channels; "compressed" requires bitrateKbps'),
15245
+ durationSeconds: z.number().describe("Audio duration in seconds"),
15246
+ sampleRateHz: z.number().optional().describe('Required for mode "pcm", e.g. 44100 for CD quality'),
15247
+ bitDepth: z.number().optional().describe('Required for mode "pcm", bits per sample, e.g. 16 or 24'),
15248
+ channels: z.number().optional().describe('Required for mode "pcm", e.g. 1 = mono, 2 = stereo'),
15249
+ bitrateKbps: z.number().optional().describe('Required for mode "compressed", e.g. 192 for a typical MP3')
15250
+ },
15251
+ async ({ mode, durationSeconds, sampleRateHz, bitDepth, channels, bitrateKbps }) => {
15252
+ const result = _estimateAudioFileSizeMcp(mode, durationSeconds, sampleRateHz, bitDepth, channels, bitrateKbps);
15253
+ if ("error" in result) return errText(result);
15254
+ return text(JSON.stringify(result, null, 2));
15255
+ }
15256
+ );
15093
15257
  var transport = new StdioServerTransport();
15094
15258
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.94.0",
4
- "description": "MCP server exposing 279 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.95.0",
4
+ "description": "MCP server exposing 283 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",
7
7
  "homepage": "https://utilix.tech/mcp",