@utilix-tech/mcp 0.8.0 → 0.9.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,21 @@
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.9.0
6
+ - Added ID3 Tag Reader and .env.example Diff Checker.
7
+
8
+ ## 0.8.0
9
+ - Added WAV Audio Info and Readability Score Calculator.
10
+
11
+ ## 0.7.0
12
+ - Added EXIF Viewer and Commit Message Generator (AI).
13
+
14
+ ## 0.6.0
15
+ - Added PDF Metadata Reader and JWK Viewer.
16
+
17
+ ## 0.5.0
18
+ - Added HAR File Viewer and NDJSON Formatter/Validator.
19
+
5
20
  ## 0.4.1
6
21
  - Bumped the `@utilix-tech/sdk` dependency from `^0.2.0` to `^0.3.0` to match the SDK version actually in use, closing a gap where a future tool addition could have silently pinned to a stale SDK release.
7
22
  - Corrected repository/bug tracker links.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 104 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 106 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
 
@@ -180,7 +180,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
180
180
  | `parse_env` | Parse a .env file and return all key-value pairs |
181
181
  | `parse_docker_image` | Parse a Docker image reference into its components |
182
182
 
183
- ### Data (5 tools)
183
+ ### Data (6 tools)
184
184
  | Tool | Description |
185
185
  |------|-------------|
186
186
  | `validate_yaml` | Validate YAML syntax |
@@ -188,6 +188,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
188
188
  | `xml_to_json` | Convert XML to JSON |
189
189
  | `parse_csv` | Parse a CSV string and return the data as JSON |
190
190
  | `format_ndjson` | Validate and pretty-print newline-delimited JSON (NDJSON) |
191
+ | `diff_env_example` | Diff a .env file against its .env.example template: flags keys missing from either side, plus empty placeholder values |
191
192
 
192
193
  ### CSS (2 tools)
193
194
  | Tool | Description |
@@ -242,10 +243,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
242
243
  |------|-------------|
243
244
  | `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
244
245
 
245
- ### Audio (1 tool)
246
+ ### Audio (2 tools)
246
247
  | Tool | Description |
247
248
  |------|-------------|
248
249
  | `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 |
250
+ | `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
249
251
 
250
252
  ## Requirements
251
253
 
package/dist/index.js CHANGED
@@ -2007,5 +2007,291 @@ function _scoreReadability(input) {
2007
2007
  server.tool("score_readability", "Score text with Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index, using a heuristic syllable counter.", { text: z.string() }, async ({ text: input }) => {
2008
2008
  return { content: [{ type: "text", text: JSON.stringify(_scoreReadability(input), null, 2) }] };
2009
2009
  });
2010
+ var _ID3V1_GENRES = [
2011
+ "Blues",
2012
+ "Classic Rock",
2013
+ "Country",
2014
+ "Dance",
2015
+ "Disco",
2016
+ "Funk",
2017
+ "Grunge",
2018
+ "Hip-Hop",
2019
+ "Jazz",
2020
+ "Metal",
2021
+ "New Age",
2022
+ "Oldies",
2023
+ "Other",
2024
+ "Pop",
2025
+ "R&B",
2026
+ "Rap",
2027
+ "Reggae",
2028
+ "Rock",
2029
+ "Techno",
2030
+ "Industrial",
2031
+ "Alternative",
2032
+ "Ska",
2033
+ "Death Metal",
2034
+ "Pranks",
2035
+ "Soundtrack",
2036
+ "Euro-Techno",
2037
+ "Ambient",
2038
+ "Trip-Hop",
2039
+ "Vocal",
2040
+ "Jazz+Funk",
2041
+ "Fusion",
2042
+ "Trance",
2043
+ "Classical",
2044
+ "Instrumental",
2045
+ "Acid",
2046
+ "House",
2047
+ "Game",
2048
+ "Sound Clip",
2049
+ "Gospel",
2050
+ "Noise",
2051
+ "AlternRock",
2052
+ "Bass",
2053
+ "Soul",
2054
+ "Punk",
2055
+ "Space",
2056
+ "Meditative",
2057
+ "Instrumental Pop",
2058
+ "Instrumental Rock",
2059
+ "Ethnic",
2060
+ "Gothic",
2061
+ "Darkwave",
2062
+ "Techno-Industrial",
2063
+ "Electronic",
2064
+ "Pop-Folk",
2065
+ "Eurodance",
2066
+ "Dream",
2067
+ "Southern Rock",
2068
+ "Comedy",
2069
+ "Cult",
2070
+ "Gangsta",
2071
+ "Top 40",
2072
+ "Christian Rap",
2073
+ "Pop/Funk",
2074
+ "Jungle",
2075
+ "Native American",
2076
+ "Cabaret",
2077
+ "New Wave",
2078
+ "Psychedelic",
2079
+ "Rave",
2080
+ "Showtunes",
2081
+ "Trailer",
2082
+ "Lo-Fi",
2083
+ "Tribal",
2084
+ "Acid Punk",
2085
+ "Acid Jazz",
2086
+ "Polka",
2087
+ "Retro",
2088
+ "Musical",
2089
+ "Rock & Roll",
2090
+ "Hard Rock"
2091
+ ];
2092
+ function _id3ReadSynchsafeUInt32(bytes, offset) {
2093
+ return (bytes[offset] & 127) << 21 | (bytes[offset + 1] & 127) << 14 | (bytes[offset + 2] & 127) << 7 | bytes[offset + 3] & 127;
2094
+ }
2095
+ function _id3ReadUInt32BE(bytes, offset) {
2096
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
2097
+ }
2098
+ function _decodeId3Text(frameBytes) {
2099
+ if (frameBytes.length === 0) return "";
2100
+ const encodingByte = frameBytes[0];
2101
+ const content = frameBytes.subarray(1);
2102
+ let out = "";
2103
+ if (encodingByte === 3) {
2104
+ out = Buffer.from(content).toString("utf8");
2105
+ } else if (encodingByte === 1 || encodingByte === 2) {
2106
+ let little = encodingByte === 1;
2107
+ let start = 0;
2108
+ if (encodingByte === 1 && content.length >= 2) {
2109
+ if (content[0] === 255 && content[1] === 254) {
2110
+ little = true;
2111
+ start = 2;
2112
+ } else if (content[0] === 254 && content[1] === 255) {
2113
+ little = false;
2114
+ start = 2;
2115
+ }
2116
+ }
2117
+ for (let i = start; i + 1 < content.length; i += 2) {
2118
+ const code = little ? content[i] | content[i + 1] << 8 : content[i] << 8 | content[i + 1];
2119
+ if (code === 0) break;
2120
+ out += String.fromCharCode(code);
2121
+ }
2122
+ } else {
2123
+ for (const b of content) {
2124
+ if (b === 0) break;
2125
+ out += String.fromCharCode(b);
2126
+ }
2127
+ }
2128
+ return out.replace(/\0+$/, "").trim();
2129
+ }
2130
+ function _readId3v2(bytes) {
2131
+ if (bytes.length < 10 || bytes[0] !== 73 || bytes[1] !== 68 || bytes[2] !== 51) return null;
2132
+ const majorVersion = bytes[3];
2133
+ const revision = bytes[4];
2134
+ const tagSize = _id3ReadSynchsafeUInt32(bytes, 6);
2135
+ const version = `ID3v2.${majorVersion}.${revision}`;
2136
+ if (majorVersion !== 3 && majorVersion !== 4) {
2137
+ return { version, frames: {}, frameCount: 0 };
2138
+ }
2139
+ const frames = {};
2140
+ let offset = 10;
2141
+ const end = Math.min(10 + tagSize, bytes.length);
2142
+ let frameCount = 0;
2143
+ while (offset + 10 <= end) {
2144
+ const frameId = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
2145
+ if (frameId === "\0\0\0\0") break;
2146
+ const frameSize = majorVersion === 4 ? _id3ReadSynchsafeUInt32(bytes, offset + 4) : _id3ReadUInt32BE(bytes, offset + 4);
2147
+ const frameStart = offset + 10;
2148
+ if (frameSize <= 0 || frameStart + frameSize > bytes.length) break;
2149
+ const frameBytes = bytes.subarray(frameStart, frameStart + frameSize);
2150
+ if (frameId[0] === "T" && frameId !== "TXXX") {
2151
+ frames[frameId] = _decodeId3Text(frameBytes);
2152
+ } else if (frameId === "COMM" && frameBytes.length > 4) {
2153
+ const rebuilt = new Uint8Array(frameBytes.length - 3);
2154
+ rebuilt[0] = frameBytes[0];
2155
+ rebuilt.set(frameBytes.subarray(4), 1);
2156
+ const decoded = _decodeId3Text(rebuilt);
2157
+ const parts = decoded.split("\0");
2158
+ frames["COMM"] = (parts[parts.length - 1] || decoded).trim();
2159
+ }
2160
+ frameCount++;
2161
+ offset = frameStart + frameSize;
2162
+ }
2163
+ return { version, frames, frameCount };
2164
+ }
2165
+ function _readId3v1(bytes) {
2166
+ if (bytes.length < 128) return null;
2167
+ const start = bytes.length - 128;
2168
+ if (String.fromCharCode(bytes[start], bytes[start + 1], bytes[start + 2]) !== "TAG") return null;
2169
+ const readField = (offset, length) => {
2170
+ let s = "";
2171
+ for (let i = 0; i < length; i++) {
2172
+ const b = bytes[start + offset + i];
2173
+ if (b === 0) break;
2174
+ s += String.fromCharCode(b);
2175
+ }
2176
+ return s.trim();
2177
+ };
2178
+ const title = readField(3, 30);
2179
+ const artist = readField(33, 30);
2180
+ const album = readField(63, 30);
2181
+ const year = readField(93, 4);
2182
+ const isV11 = bytes[start + 125] === 0 && bytes[start + 126] !== 0;
2183
+ const comment = readField(97, isV11 ? 28 : 30);
2184
+ const genreIndex = bytes[start + 127];
2185
+ const genre = _ID3V1_GENRES[genreIndex] ?? `Unknown (${genreIndex})`;
2186
+ const result = { genre };
2187
+ if (title) result.title = title;
2188
+ if (artist) result.artist = artist;
2189
+ if (album) result.album = album;
2190
+ if (year) result.year = year;
2191
+ if (comment) result.comment = comment;
2192
+ if (isV11) result.trackNumber = String(bytes[start + 126]);
2193
+ return result;
2194
+ }
2195
+ function _readId3Tags(bytes) {
2196
+ const v2 = _readId3v2(bytes);
2197
+ const v1 = _readId3v1(bytes);
2198
+ if (v2 && Object.keys(v2.frames).length > 0) {
2199
+ const f = v2.frames;
2200
+ const result = { version: v2.version, frameCount: v2.frameCount };
2201
+ if (f["TIT2"]) result.title = f["TIT2"];
2202
+ if (f["TPE1"]) result.artist = f["TPE1"];
2203
+ if (f["TALB"]) result.album = f["TALB"];
2204
+ const year = f["TYER"] || f["TDRC"];
2205
+ if (year) result.year = year;
2206
+ if (f["TCON"]) result.genre = f["TCON"];
2207
+ if (f["COMM"]) result.comment = f["COMM"];
2208
+ if (f["TRCK"]) result.trackNumber = f["TRCK"];
2209
+ return result;
2210
+ }
2211
+ if (v1) {
2212
+ return { version: v1.trackNumber !== void 0 ? "ID3v1.1" : "ID3v1", ...v1 };
2213
+ }
2214
+ if (v2) {
2215
+ return { error: `Unsupported ID3 tag version: ${v2.version} (only ID3v2.3, ID3v2.4, and ID3v1/1.1 are supported)` };
2216
+ }
2217
+ return { error: "No ID3v1 or ID3v2.3/2.4 tags found in this file" };
2218
+ }
2219
+ server.tool("read_id3_tags", "Read ID3 tags (title, artist, album, year, genre, comment, track number) directly from an MP3 file's bytes. Prefers an ID3v2.3/2.4 header; falls back to a classic ID3v1/1.1 128-byte trailer. No audio library required.", { mp3_base64: z.string().describe("Base64-encoded MP3 file bytes, optionally prefixed with a data: URL scheme") }, async ({ mp3_base64 }) => {
2220
+ const bytes = new Uint8Array(Buffer.from(mp3_base64.replace(/^data:.*;base64,/, ""), "base64"));
2221
+ const result = _readId3Tags(bytes);
2222
+ if ("error" in result) return errText(result);
2223
+ return text(JSON.stringify(result, null, 2));
2224
+ });
2225
+ function _parseEnvForDiff(input) {
2226
+ const entries = [];
2227
+ const errors = [];
2228
+ const rawLines = input.split("\n");
2229
+ const lines = [];
2230
+ let i = 0;
2231
+ while (i < rawLines.length) {
2232
+ let line = rawLines[i];
2233
+ while (line.endsWith("\\") && i + 1 < rawLines.length) {
2234
+ line = line.slice(0, -1) + rawLines[i + 1];
2235
+ i++;
2236
+ }
2237
+ lines.push(line);
2238
+ i++;
2239
+ }
2240
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
2241
+ const lineNum = lineIdx + 1;
2242
+ const trimmed = lines[lineIdx].trim();
2243
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2244
+ const eqIdx = trimmed.indexOf("=");
2245
+ if (eqIdx === -1) {
2246
+ errors.push(`Line ${lineNum}: missing '='`);
2247
+ continue;
2248
+ }
2249
+ const key = trimmed.slice(0, eqIdx).trim();
2250
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2251
+ errors.push(`Line ${lineNum}: Invalid key: '${key}'`);
2252
+ continue;
2253
+ }
2254
+ const rest = trimmed.slice(eqIdx + 1);
2255
+ let value = "";
2256
+ if (rest.startsWith('"')) {
2257
+ const closeIdx = rest.indexOf('"', 1);
2258
+ if (closeIdx === -1) {
2259
+ errors.push(`Line ${lineNum}: Unclosed double quote for key '${key}'`);
2260
+ continue;
2261
+ }
2262
+ value = rest.slice(1, closeIdx);
2263
+ } else if (rest.startsWith("'")) {
2264
+ const closeIdx = rest.indexOf("'", 1);
2265
+ if (closeIdx === -1) {
2266
+ errors.push(`Line ${lineNum}: Unclosed single quote for key '${key}'`);
2267
+ continue;
2268
+ }
2269
+ value = rest.slice(1, closeIdx);
2270
+ } else {
2271
+ const hashIdx = rest.indexOf("#");
2272
+ value = (hashIdx !== -1 ? rest.slice(0, hashIdx) : rest).trim();
2273
+ }
2274
+ entries.push({ key, value });
2275
+ }
2276
+ return { entries, errors };
2277
+ }
2278
+ function _diffEnvExample(envInput, exampleInput) {
2279
+ const envParsed = _parseEnvForDiff(envInput);
2280
+ if (envParsed.errors.length > 0) return { error: `.env: ${envParsed.errors.join("; ")}` };
2281
+ const exampleParsed = _parseEnvForDiff(exampleInput);
2282
+ if (exampleParsed.errors.length > 0) return { error: `.env.example: ${exampleParsed.errors.join("; ")}` };
2283
+ const envKeys = new Set(envParsed.entries.map((e) => e.key));
2284
+ const exampleValues = new Map(exampleParsed.entries.map((e) => [e.key, e.value]));
2285
+ const missingInExample = [...envKeys].filter((k) => !exampleValues.has(k)).sort();
2286
+ const missingInEnv = [...exampleValues.keys()].filter((k) => !envKeys.has(k)).sort();
2287
+ const emptyValueInExample = [...exampleValues.entries()].filter(([k, v]) => envKeys.has(k) && v === "").map(([k]) => k).sort();
2288
+ const keysInBoth = [...envKeys].filter((k) => exampleValues.has(k)).length;
2289
+ return { missingInExample, missingInEnv, emptyValueInExample, keysInBoth };
2290
+ }
2291
+ server.tool("diff_env_example", "Diff a .env file against its .env.example template: flags keys missing from either side, plus keys present in both with an empty placeholder value in the example.", { env: z.string().describe("The .env file contents"), example: z.string().describe("The .env.example file contents") }, async ({ env, example }) => {
2292
+ const result = _diffEnvExample(env, example);
2293
+ if ("error" in result) return errText(result);
2294
+ return text(JSON.stringify(result, null, 2));
2295
+ });
2010
2296
  var transport = new StdioServerTransport();
2011
2297
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.8.0",
4
- "description": "MCP server exposing 104 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.9.0",
4
+ "description": "MCP server exposing 106 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",