@utilix-tech/mcp 0.47.0 → 0.48.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 +4 -0
- package/README.md +5 -2
- package/dist/index.js +168 -0
- package/package.json +1 -1
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.48.0
|
|
6
|
+
- Add `build_content_disposition`, `parse_content_disposition` tools: build a `Content-Disposition` response header from a disposition type and filename, with an RFC 5987 `filename*` extended parameter for non-ASCII filenames, or parse an existing header into its type, filename, and raw parameters.
|
|
7
|
+
- Add `inspect_gif_frames` tool: 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.
|
|
8
|
+
|
|
5
9
|
## 0.47.0
|
|
6
10
|
- Add `calculate_depreciation_schedule` tool: build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits.
|
|
7
11
|
- Add `analyze_keyword_density` tool: count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes.
|
package/README.md
CHANGED
|
@@ -193,7 +193,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
193
193
|
| `calculate_cd_maturity` | Compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net early-withdrawal payout |
|
|
194
194
|
| `calculate_depreciation_schedule` | Build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits |
|
|
195
195
|
|
|
196
|
-
### API / Network (
|
|
196
|
+
### API / Network (27 tools)
|
|
197
197
|
| Tool | Description |
|
|
198
198
|
|------|-------------|
|
|
199
199
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -221,6 +221,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
221
221
|
| `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
|
|
222
222
|
| `build_client_hints` | Build an Accept-CH (and optional Critical-CH) response header from a list of client hint names, with a plain-English note on what each hint reveals |
|
|
223
223
|
| `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
|
|
224
|
+
| `build_content_disposition` | Build a Content-Disposition response header for a file download or inline display, with an RFC 5987 filename* parameter for non-ASCII filenames |
|
|
225
|
+
| `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
|
|
224
226
|
|
|
225
227
|
### Color (4 tools)
|
|
226
228
|
| Tool | Description |
|
|
@@ -305,12 +307,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
305
307
|
| `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
|
|
306
308
|
| `lint_tool_schema` | Lint an OpenAI, Anthropic, or MCP tool/function-calling definition for missing descriptions, invalid names, empty enums, and required/properties mismatches |
|
|
307
309
|
|
|
308
|
-
### Image (
|
|
310
|
+
### Image (4 tools)
|
|
309
311
|
| Tool | Description |
|
|
310
312
|
|------|-------------|
|
|
311
313
|
| `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
|
|
312
314
|
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
313
315
|
| `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 |
|
|
316
|
+
| `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 |
|
|
314
317
|
|
|
315
318
|
### PDF (2 tools)
|
|
316
319
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -8001,5 +8001,173 @@ server.tool(
|
|
|
8001
8001
|
return text(JSON.stringify(result, null, 2));
|
|
8002
8002
|
}
|
|
8003
8003
|
);
|
|
8004
|
+
function _encodeRFC5987ValueCharsMcp(value) {
|
|
8005
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => "%" + ch.charCodeAt(0).toString(16).toUpperCase());
|
|
8006
|
+
}
|
|
8007
|
+
function _buildContentDispositionMcp(options) {
|
|
8008
|
+
const type = options.type ?? "attachment";
|
|
8009
|
+
if (type !== "inline" && type !== "attachment") return { error: 'type must be "inline" or "attachment"' };
|
|
8010
|
+
if (!options.filename) return { header: type, type, filenameEncoded: false };
|
|
8011
|
+
const filename = options.filename;
|
|
8012
|
+
if (filename.includes("/") || filename.includes("\\")) return { error: "filename must not contain path separators" };
|
|
8013
|
+
const isAscii = /^[\x20-\x7e]*$/.test(filename);
|
|
8014
|
+
const asciiFallback = filename.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1");
|
|
8015
|
+
let header = `${type}; filename="${asciiFallback}"`;
|
|
8016
|
+
let filenameEncoded = false;
|
|
8017
|
+
if (!isAscii) {
|
|
8018
|
+
header += `; filename*=UTF-8''${_encodeRFC5987ValueCharsMcp(filename)}`;
|
|
8019
|
+
filenameEncoded = true;
|
|
8020
|
+
}
|
|
8021
|
+
return { header, type, filename, filenameEncoded };
|
|
8022
|
+
}
|
|
8023
|
+
function _parseContentDispositionMcp(header) {
|
|
8024
|
+
if (typeof header !== "string" || header.trim() === "") return { error: "header is required" };
|
|
8025
|
+
const parts = header.split(";").map((p) => p.trim()).filter(Boolean);
|
|
8026
|
+
const type = parts[0]?.toLowerCase();
|
|
8027
|
+
if (!type) return { error: "Could not parse a disposition type from the header" };
|
|
8028
|
+
const params = {};
|
|
8029
|
+
for (let i = 1; i < parts.length; i++) {
|
|
8030
|
+
const eq = parts[i].indexOf("=");
|
|
8031
|
+
if (eq === -1) continue;
|
|
8032
|
+
const key = parts[i].slice(0, eq).trim().toLowerCase();
|
|
8033
|
+
let value = parts[i].slice(eq + 1).trim();
|
|
8034
|
+
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
8035
|
+
params[key] = value;
|
|
8036
|
+
}
|
|
8037
|
+
let filename = params.filename;
|
|
8038
|
+
let filenameIsExtended = false;
|
|
8039
|
+
if (params["filename*"]) {
|
|
8040
|
+
const match = params["filename*"].match(/^([^']*)'([^']*)'(.*)$/);
|
|
8041
|
+
if (match) {
|
|
8042
|
+
try {
|
|
8043
|
+
filename = decodeURIComponent(match[3]);
|
|
8044
|
+
filenameIsExtended = true;
|
|
8045
|
+
} catch {
|
|
8046
|
+
}
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
return { type, filename, filenameIsExtended, params };
|
|
8050
|
+
}
|
|
8051
|
+
server.tool(
|
|
8052
|
+
"build_content_disposition",
|
|
8053
|
+
"Build a Content-Disposition response-header value for a file download or inline display, given a disposition type (inline/attachment) and filename. Non-ASCII filenames get an RFC 5987 filename* extended parameter alongside an ASCII-sanitized filename fallback.",
|
|
8054
|
+
{
|
|
8055
|
+
type: z.enum(["inline", "attachment"]).optional().describe('Disposition type (default "attachment")'),
|
|
8056
|
+
filename: z.string().optional().describe("The filename to suggest to the browser")
|
|
8057
|
+
},
|
|
8058
|
+
async ({ type, filename }) => {
|
|
8059
|
+
const result = _buildContentDispositionMcp({ type, filename });
|
|
8060
|
+
if ("error" in result) return errText(result);
|
|
8061
|
+
return text(JSON.stringify(result, null, 2));
|
|
8062
|
+
}
|
|
8063
|
+
);
|
|
8064
|
+
server.tool(
|
|
8065
|
+
"parse_content_disposition",
|
|
8066
|
+
"Parse a Content-Disposition header value into its disposition type, filename (decoding an RFC 5987 filename* parameter if present), and raw parameters.",
|
|
8067
|
+
{
|
|
8068
|
+
header: z.string().describe("The raw Content-Disposition header value")
|
|
8069
|
+
},
|
|
8070
|
+
async ({ header }) => {
|
|
8071
|
+
const result = _parseContentDispositionMcp(header);
|
|
8072
|
+
if ("error" in result) return errText(result);
|
|
8073
|
+
return text(JSON.stringify(result, null, 2));
|
|
8074
|
+
}
|
|
8075
|
+
);
|
|
8076
|
+
function _skipGifSubBlocksMcp(bytes, offset) {
|
|
8077
|
+
while (offset < bytes.length) {
|
|
8078
|
+
const len = bytes[offset];
|
|
8079
|
+
offset += 1;
|
|
8080
|
+
if (len === 0) break;
|
|
8081
|
+
offset += len;
|
|
8082
|
+
}
|
|
8083
|
+
return offset;
|
|
8084
|
+
}
|
|
8085
|
+
function _inspectGifFramesMcp(bytes) {
|
|
8086
|
+
if (bytes.length < 13) return { error: "File too small to be a valid GIF file" };
|
|
8087
|
+
const signature = Buffer.from(bytes.subarray(0, 6)).toString("ascii");
|
|
8088
|
+
if (signature !== "GIF87a" && signature !== "GIF89a") return { error: "Not a valid GIF file (missing GIF87a/GIF89a signature)" };
|
|
8089
|
+
const version = signature.slice(3);
|
|
8090
|
+
const width = _readU16LE(bytes, 6);
|
|
8091
|
+
const height = _readU16LE(bytes, 8);
|
|
8092
|
+
const packed = bytes[10];
|
|
8093
|
+
const hasGlobalColorTable = (packed & 128) !== 0;
|
|
8094
|
+
const globalColorTableSize = hasGlobalColorTable ? 2 << (packed & 7) : 0;
|
|
8095
|
+
let offset = 13;
|
|
8096
|
+
if (hasGlobalColorTable) offset += globalColorTableSize * 3;
|
|
8097
|
+
let frameCount = 0;
|
|
8098
|
+
let loopCount = null;
|
|
8099
|
+
let totalDurationMs = 0;
|
|
8100
|
+
let hasTransparency = false;
|
|
8101
|
+
let pendingDelay = 0;
|
|
8102
|
+
while (offset < bytes.length) {
|
|
8103
|
+
const blockType = bytes[offset];
|
|
8104
|
+
if (blockType === 59) break;
|
|
8105
|
+
if (blockType === 33) {
|
|
8106
|
+
const label = bytes[offset + 1];
|
|
8107
|
+
offset += 2;
|
|
8108
|
+
if (label === 249) {
|
|
8109
|
+
const subLen = bytes[offset] ?? 0;
|
|
8110
|
+
if (subLen >= 4) {
|
|
8111
|
+
const flags = bytes[offset + 1];
|
|
8112
|
+
pendingDelay = _readU16LE(bytes, offset + 2);
|
|
8113
|
+
if (flags & 1) hasTransparency = true;
|
|
8114
|
+
}
|
|
8115
|
+
offset += 1 + subLen;
|
|
8116
|
+
offset = _skipGifSubBlocksMcp(bytes, offset);
|
|
8117
|
+
} else if (label === 255) {
|
|
8118
|
+
const blockSize = bytes[offset] ?? 0;
|
|
8119
|
+
const appId = Buffer.from(bytes.subarray(offset + 1, offset + 1 + Math.min(blockSize, 11))).toString("ascii");
|
|
8120
|
+
offset += 1 + blockSize;
|
|
8121
|
+
if (appId === "NETSCAPE2.0") {
|
|
8122
|
+
const subLen = bytes[offset] ?? 0;
|
|
8123
|
+
if (subLen >= 3 && bytes[offset + 1] === 1) {
|
|
8124
|
+
loopCount = _readU16LE(bytes, offset + 2);
|
|
8125
|
+
}
|
|
8126
|
+
}
|
|
8127
|
+
offset = _skipGifSubBlocksMcp(bytes, offset);
|
|
8128
|
+
} else {
|
|
8129
|
+
offset = _skipGifSubBlocksMcp(bytes, offset);
|
|
8130
|
+
}
|
|
8131
|
+
} else if (blockType === 44) {
|
|
8132
|
+
if (offset + 10 > bytes.length) return { error: "Truncated image descriptor" };
|
|
8133
|
+
frameCount++;
|
|
8134
|
+
totalDurationMs += pendingDelay * 10;
|
|
8135
|
+
pendingDelay = 0;
|
|
8136
|
+
const packedImg = bytes[offset + 9];
|
|
8137
|
+
const hasLocalColorTable = (packedImg & 128) !== 0;
|
|
8138
|
+
const localColorTableSize = hasLocalColorTable ? 2 << (packedImg & 7) : 0;
|
|
8139
|
+
offset += 10;
|
|
8140
|
+
if (hasLocalColorTable) offset += localColorTableSize * 3;
|
|
8141
|
+
offset += 1;
|
|
8142
|
+
offset = _skipGifSubBlocksMcp(bytes, offset);
|
|
8143
|
+
} else {
|
|
8144
|
+
break;
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
if (frameCount === 0) return { error: "No image data blocks found in this GIF file" };
|
|
8148
|
+
return {
|
|
8149
|
+
version,
|
|
8150
|
+
width,
|
|
8151
|
+
height,
|
|
8152
|
+
frameCount,
|
|
8153
|
+
animated: frameCount > 1,
|
|
8154
|
+
loopCount,
|
|
8155
|
+
totalDurationMs,
|
|
8156
|
+
hasGlobalColorTable,
|
|
8157
|
+
globalColorTableSize,
|
|
8158
|
+
hasTransparency
|
|
8159
|
+
};
|
|
8160
|
+
}
|
|
8161
|
+
server.tool(
|
|
8162
|
+
"inspect_gif_frames",
|
|
8163
|
+
"Walk a GIF87a/89a file's block structure (image descriptors, graphic control extensions, the NETSCAPE2.0 application extension) to report frame count, total animation duration, and loop count, without decoding any LZW-compressed pixel data.",
|
|
8164
|
+
{ gif_base64: z.string().describe("Base64-encoded GIF file bytes, optionally prefixed with a data: URL scheme") },
|
|
8165
|
+
async ({ gif_base64 }) => {
|
|
8166
|
+
const bytes = new Uint8Array(Buffer.from(gif_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
8167
|
+
const result = _inspectGifFramesMcp(bytes);
|
|
8168
|
+
if ("error" in result) return errText(result);
|
|
8169
|
+
return text(JSON.stringify(result, null, 2));
|
|
8170
|
+
}
|
|
8171
|
+
);
|
|
8004
8172
|
var transport = new StdioServerTransport();
|
|
8005
8173
|
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.48.0",
|
|
4
4
|
"description": "MCP server exposing 175 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",
|