@utilix-tech/mcp 0.93.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 +7 -0
- package/README.md +8 -4
- package/dist/index.js +301 -0
- package/package.json +2 -2
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.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
|
+
|
|
9
|
+
## 0.94.0
|
|
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.
|
|
11
|
+
|
|
5
12
|
## 0.93.0
|
|
6
13
|
- Add `find_duplicate_lockfile_versions` tool: parse an npm, yarn, or pnpm lockfile and report every package installed at more than one resolved version, with a count of install locations per version.
|
|
7
14
|
- Add `summarize_git_diff_stat` tool: parse `git diff --stat` / `git log --stat` output into structured per-file insertion/deletion counts plus overall totals, flagging a scaled-down bar's split as approximate.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
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
|
|
|
@@ -224,7 +224,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
224
224
|
| `calculate_sales_commission` | Compute a sales commission from either a flat percent rate applied to the whole sale amount, or a tiered (marginal) rate structure where each tier applies only to the slice of sales within it, plus an optional base salary added to the total payout |
|
|
225
225
|
| `intersect_semver_ranges` | Determine whether two semver ranges overlap and, if so, the intersecting range, supporting the same caret/tilde/comparison-operator/exact-version syntax as the semver tool |
|
|
226
226
|
|
|
227
|
-
### API / Network (
|
|
227
|
+
### API / Network (55 tools)
|
|
228
228
|
| Tool | Description |
|
|
229
229
|
|------|-------------|
|
|
230
230
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -283,6 +283,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
283
283
|
| `score_security_headers` | Score a pasted block of raw HTTP response headers against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*), with an overall 0-100 score and A-F letter grade |
|
|
284
284
|
| `build_clear_site_data_header` | Build a Clear-Site-Data response header from directives (cache, cookies, storage, executionContexts, or * for all of them), flagging redundant directives alongside the wildcard |
|
|
285
285
|
| `parse_clear_site_data_header` | Parse an existing Clear-Site-Data header value into its individual directives |
|
|
286
|
+
| `build_alt_svc_header` | Build an Alt-Svc response header (RFC 7838) from one or more alternative services (protocol-id, authority, max-age, persist), with a plain-English note per protocol-id |
|
|
287
|
+
| `parse_alt_svc_header` | Parse an existing Alt-Svc header value into its individual alternatives, including the special "clear" value |
|
|
286
288
|
|
|
287
289
|
### Color (4 tools)
|
|
288
290
|
| Tool | Description |
|
|
@@ -398,13 +400,14 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
398
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 |
|
|
399
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 |
|
|
400
402
|
|
|
401
|
-
### Image (
|
|
403
|
+
### Image (5 tools)
|
|
402
404
|
| Tool | Description |
|
|
403
405
|
|------|-------------|
|
|
404
406
|
| `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
|
|
405
407
|
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
406
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 |
|
|
407
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) |
|
|
408
411
|
|
|
409
412
|
### PDF (3 tools)
|
|
410
413
|
| Tool | Description |
|
|
@@ -413,13 +416,14 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
413
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 |
|
|
414
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 |
|
|
415
418
|
|
|
416
|
-
### Audio (
|
|
419
|
+
### Audio (5 tools)
|
|
417
420
|
| Tool | Description |
|
|
418
421
|
|------|-------------|
|
|
419
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 |
|
|
420
423
|
| `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
|
|
421
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 |
|
|
422
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 |
|
|
423
427
|
|
|
424
428
|
### Video (1 tool)
|
|
425
429
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -14953,5 +14953,306 @@ server.tool(
|
|
|
14953
14953
|
return text(JSON.stringify(result, null, 2));
|
|
14954
14954
|
}
|
|
14955
14955
|
);
|
|
14956
|
+
var _ALT_SVC_DEPRECATED_H3_DRAFTS_MCP = /* @__PURE__ */ new Set(["h3-29", "h3-27", "h3-25", "h3-24", "h3-23", "h3-22"]);
|
|
14957
|
+
var _ALT_SVC_PROTOCOL_NOTES_MCP = {
|
|
14958
|
+
"h3": "HTTP/3 over QUIC, the current protocol ID clients should be offered.",
|
|
14959
|
+
"h3-29": 'An outdated HTTP/3 draft version (draft-29); modern clients only match "h3".',
|
|
14960
|
+
"h3-27": 'An outdated HTTP/3 draft version (draft-27); modern clients only match "h3".',
|
|
14961
|
+
"h3-25": 'An outdated HTTP/3 draft version (draft-25); modern clients only match "h3".',
|
|
14962
|
+
"h2": "HTTP/2 over TLS.",
|
|
14963
|
+
"h2c": "HTTP/2 over cleartext TCP; rarely advertised via Alt-Svc since it needs an out-of-band upgrade rather than ALPN negotiation.",
|
|
14964
|
+
"http/1.1": "Plain HTTP/1.1; advertising it via Alt-Svc is unusual since it is already the universal fallback."
|
|
14965
|
+
};
|
|
14966
|
+
function _buildAltSvcHeaderMcp(alternatives) {
|
|
14967
|
+
if (!Array.isArray(alternatives) || alternatives.length === 0) {
|
|
14968
|
+
return { error: "At least one alternative service is required" };
|
|
14969
|
+
}
|
|
14970
|
+
const warnings = [];
|
|
14971
|
+
const entries = [];
|
|
14972
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14973
|
+
for (const alt of alternatives) {
|
|
14974
|
+
const protocolId = typeof alt.protocolId === "string" ? alt.protocolId.trim() : "";
|
|
14975
|
+
if (!protocolId) return { error: 'Each alternative needs a protocol-id (e.g. "h3")' };
|
|
14976
|
+
if (!Number.isInteger(alt.port) || alt.port < 1 || alt.port > 65535) {
|
|
14977
|
+
return { error: `Each alternative needs a valid port (1-65535), got ${JSON.stringify(alt.port)}` };
|
|
14978
|
+
}
|
|
14979
|
+
const host = alt.host?.trim();
|
|
14980
|
+
const authority = `${host ?? ""}:${alt.port}`;
|
|
14981
|
+
const maxAge = alt.maxAge !== void 0 ? alt.maxAge : 86400;
|
|
14982
|
+
if (!Number.isFinite(maxAge) || maxAge < 0) return { error: "maxAge must be a non-negative number of seconds" };
|
|
14983
|
+
const persist = alt.persist === true;
|
|
14984
|
+
const dedupeKey = `${protocolId.toLowerCase()}|${authority.toLowerCase()}`;
|
|
14985
|
+
if (seen.has(dedupeKey)) {
|
|
14986
|
+
warnings.push(`duplicate alternative "${protocolId}=${JSON.stringify(authority)}" was removed`);
|
|
14987
|
+
continue;
|
|
14988
|
+
}
|
|
14989
|
+
seen.add(dedupeKey);
|
|
14990
|
+
if (maxAge === 0) warnings.push(`"${protocolId}" has ma=0, so clients should treat it as already expired`);
|
|
14991
|
+
if (persist && maxAge === 0) warnings.push(`"${protocolId}" sets persist=1 with ma=0, which is contradictory: the entry expires immediately`);
|
|
14992
|
+
if (_ALT_SVC_DEPRECATED_H3_DRAFTS_MCP.has(protocolId.toLowerCase())) {
|
|
14993
|
+
warnings.push(`"${protocolId}" is an outdated HTTP/3 draft protocol-id; modern clients only match "h3"`);
|
|
14994
|
+
}
|
|
14995
|
+
const note = _ALT_SVC_PROTOCOL_NOTES_MCP[protocolId.toLowerCase()] ?? `Advertises "${protocolId}" as an alternative protocol clients may switch to for this authority.`;
|
|
14996
|
+
entries.push({ protocolId, authority, maxAge, persist, note });
|
|
14997
|
+
}
|
|
14998
|
+
if (entries.length === 0) return { error: "At least one alternative service is required" };
|
|
14999
|
+
const headerValue = entries.map((e) => {
|
|
15000
|
+
let s = `${e.protocolId}="${e.authority}"; ma=${e.maxAge}`;
|
|
15001
|
+
if (e.persist) s += "; persist=1";
|
|
15002
|
+
return s;
|
|
15003
|
+
}).join(", ");
|
|
15004
|
+
return { header: `Alt-Svc: ${headerValue}`, headerValue, entries, cleared: false, warnings };
|
|
15005
|
+
}
|
|
15006
|
+
function _altSvcSplitTopLevelMcp(s, sep) {
|
|
15007
|
+
const out = [];
|
|
15008
|
+
let inQuotes = false;
|
|
15009
|
+
let current = "";
|
|
15010
|
+
for (const ch of s) {
|
|
15011
|
+
if (ch === '"') inQuotes = !inQuotes;
|
|
15012
|
+
if (ch === sep && !inQuotes) {
|
|
15013
|
+
out.push(current);
|
|
15014
|
+
current = "";
|
|
15015
|
+
} else {
|
|
15016
|
+
current += ch;
|
|
15017
|
+
}
|
|
15018
|
+
}
|
|
15019
|
+
out.push(current);
|
|
15020
|
+
return out;
|
|
15021
|
+
}
|
|
15022
|
+
function _parseAltSvcHeaderMcp(raw) {
|
|
15023
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
15024
|
+
const withoutLabel = raw.trim().replace(/^alt-svc:\s*/i, "");
|
|
15025
|
+
if (withoutLabel.toLowerCase() === "clear") {
|
|
15026
|
+
return { header: "Alt-Svc: clear", headerValue: "clear", entries: [], cleared: true, warnings: [] };
|
|
15027
|
+
}
|
|
15028
|
+
const parts = _altSvcSplitTopLevelMcp(withoutLabel, ",");
|
|
15029
|
+
const alternatives = [];
|
|
15030
|
+
for (const part of parts) {
|
|
15031
|
+
const trimmedPart = part.trim();
|
|
15032
|
+
if (!trimmedPart) continue;
|
|
15033
|
+
const eq = trimmedPart.indexOf("=");
|
|
15034
|
+
if (eq === -1) return { error: `Could not parse alternative: "${trimmedPart}"` };
|
|
15035
|
+
const protocolId = trimmedPart.slice(0, eq).trim();
|
|
15036
|
+
const rest = trimmedPart.slice(eq + 1).trim();
|
|
15037
|
+
const segments = _altSvcSplitTopLevelMcp(rest, ";").map((s) => s.trim());
|
|
15038
|
+
const authorityMatch = segments[0]?.match(/^"(.*)"$/);
|
|
15039
|
+
if (!authorityMatch) return { error: `Alternative "${protocolId}" is missing a quoted authority (e.g. ":443")` };
|
|
15040
|
+
const authority = authorityMatch[1];
|
|
15041
|
+
const colonIdx = authority.lastIndexOf(":");
|
|
15042
|
+
if (colonIdx === -1) return { error: `Alternative "${protocolId}" has an authority with no port: "${authority}"` };
|
|
15043
|
+
const host = authority.slice(0, colonIdx) || void 0;
|
|
15044
|
+
const portStr = authority.slice(colonIdx + 1);
|
|
15045
|
+
const port = Number(portStr);
|
|
15046
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { error: `Alternative "${protocolId}" has an invalid port: "${portStr}"` };
|
|
15047
|
+
let maxAge;
|
|
15048
|
+
let persist = false;
|
|
15049
|
+
for (const seg of segments.slice(1)) {
|
|
15050
|
+
const sepIdx = seg.indexOf("=");
|
|
15051
|
+
if (sepIdx === -1) continue;
|
|
15052
|
+
const key = seg.slice(0, sepIdx).trim().toLowerCase();
|
|
15053
|
+
const value = seg.slice(sepIdx + 1).trim();
|
|
15054
|
+
if (key === "ma") maxAge = Number(value);
|
|
15055
|
+
if (key === "persist") persist = value === "1";
|
|
15056
|
+
}
|
|
15057
|
+
if (maxAge !== void 0 && !Number.isFinite(maxAge)) return { error: `Alternative "${protocolId}" has a non-numeric ma value` };
|
|
15058
|
+
alternatives.push({ protocolId, host, port, maxAge, persist });
|
|
15059
|
+
}
|
|
15060
|
+
if (alternatives.length === 0) return { error: "No alternative services found" };
|
|
15061
|
+
return _buildAltSvcHeaderMcp(alternatives);
|
|
15062
|
+
}
|
|
15063
|
+
server.tool(
|
|
15064
|
+
"build_alt_svc_header",
|
|
15065
|
+
'Build an Alt-Svc response-header value (RFC 7838) from one or more alternative services, each a protocol-id (e.g. "h3"), optional host, port, max-age in seconds, and an optional persist flag, attaching a plain-English note for each well-known protocol-id.',
|
|
15066
|
+
{
|
|
15067
|
+
alternatives: z.array(z.object({
|
|
15068
|
+
protocolId: z.string().min(1).describe('ALPN protocol-id, e.g. "h3" or "h2"'),
|
|
15069
|
+
host: z.string().optional().describe("Alternative host; omit to mean the same host"),
|
|
15070
|
+
port: z.number().int().min(1).max(65535),
|
|
15071
|
+
maxAge: z.number().min(0).optional().describe("Seconds the alternative stays valid (default 86400)"),
|
|
15072
|
+
persist: z.boolean().optional().describe("Whether the entry should survive a network configuration change")
|
|
15073
|
+
})).min(1).describe("Alternative services to advertise")
|
|
15074
|
+
},
|
|
15075
|
+
async ({ alternatives }) => {
|
|
15076
|
+
const result = _buildAltSvcHeaderMcp(alternatives);
|
|
15077
|
+
if ("error" in result) return errText(result);
|
|
15078
|
+
return text(JSON.stringify(result, null, 2));
|
|
15079
|
+
}
|
|
15080
|
+
);
|
|
15081
|
+
server.tool(
|
|
15082
|
+
"parse_alt_svc_header",
|
|
15083
|
+
'Parse an existing Alt-Svc header value (with or without a leading "Alt-Svc:" label), including the special "clear" value, into its individual alternative services with plain-English protocol notes.',
|
|
15084
|
+
{
|
|
15085
|
+
header: z.string().min(1).describe("The Alt-Svc header value to parse")
|
|
15086
|
+
},
|
|
15087
|
+
async ({ header }) => {
|
|
15088
|
+
const result = _parseAltSvcHeaderMcp(header);
|
|
15089
|
+
if ("error" in result) return errText(result);
|
|
15090
|
+
return text(JSON.stringify(result, null, 2));
|
|
15091
|
+
}
|
|
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
|
+
);
|
|
14956
15257
|
var transport = new StdioServerTransport();
|
|
14957
15258
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
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",
|