@utilix-tech/mcp 0.46.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 +8 -0
- package/README.md +9 -4
- package/dist/index.js +358 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.47.0
|
|
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.
|
|
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.
|
|
12
|
+
|
|
5
13
|
## 0.46.0
|
|
6
14
|
- Add `calculate_cd_maturity` tool: 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 payout if withdrawn early.
|
|
7
15
|
- Add `build_client_hints`, `parse_client_hint_headers` tools: build an `Accept-CH`/`Critical-CH` response header from a list of client hint names with a plain-English note per hint, or decode raw Client Hints request-header text into structured values.
|
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
130
130
|
| `generate_qr` | Generate a QR code as an SVG string |
|
|
131
131
|
| `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
|
|
132
132
|
|
|
133
|
-
### Text (
|
|
133
|
+
### Text (16 tools)
|
|
134
134
|
| Tool | Description |
|
|
135
135
|
|------|-------------|
|
|
136
136
|
| `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
|
|
@@ -148,6 +148,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
148
148
|
| `calculate_string_distance` | Compute the Levenshtein (or Damerau) edit distance and normalized similarity between two strings |
|
|
149
149
|
| `analyze_sentence_lengths` | Split text into sentences and report a histogram of sentence lengths, plus mean, median, standard deviation, and a consistency label |
|
|
150
150
|
| `lookup_emoji` | Search a curated table of ~280 common emoji by name, keyword, or GitHub-style shortcode, or look one up exactly by shortcode, character, or category |
|
|
151
|
+
| `analyze_keyword_density` | Count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes |
|
|
151
152
|
|
|
152
153
|
### Time (6 tools)
|
|
153
154
|
| Tool | Description |
|
|
@@ -159,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
159
160
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
160
161
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
161
162
|
|
|
162
|
-
### Units (
|
|
163
|
+
### Units (29 tools)
|
|
163
164
|
| Tool | Description |
|
|
164
165
|
|------|-------------|
|
|
165
166
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -190,8 +191,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
190
191
|
| `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
|
|
191
192
|
| `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
|
|
192
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
|
+
| `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 |
|
|
193
195
|
|
|
194
|
-
### API / Network (
|
|
196
|
+
### API / Network (27 tools)
|
|
195
197
|
| Tool | Description |
|
|
196
198
|
|------|-------------|
|
|
197
199
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -219,6 +221,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
219
221
|
| `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
|
|
220
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 |
|
|
221
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 |
|
|
222
226
|
|
|
223
227
|
### Color (4 tools)
|
|
224
228
|
| Tool | Description |
|
|
@@ -303,12 +307,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
303
307
|
| `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
|
|
304
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 |
|
|
305
309
|
|
|
306
|
-
### Image (
|
|
310
|
+
### Image (4 tools)
|
|
307
311
|
| Tool | Description |
|
|
308
312
|
|------|-------------|
|
|
309
313
|
| `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
|
|
310
314
|
| `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
|
|
311
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 |
|
|
312
317
|
|
|
313
318
|
### PDF (2 tools)
|
|
314
319
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -7811,5 +7811,363 @@ server.tool(
|
|
|
7811
7811
|
return text(JSON.stringify(result, null, 2));
|
|
7812
7812
|
}
|
|
7813
7813
|
);
|
|
7814
|
+
var _DEPRECIATION_METHODS_MCP = ["straight-line", "double-declining-balance", "sum-of-years-digits"];
|
|
7815
|
+
function _calculateDepreciationScheduleMcp(cost, salvageValue, usefulLifeYears, method) {
|
|
7816
|
+
if (!Number.isFinite(cost) || cost <= 0) return { error: "Cost must be a positive number" };
|
|
7817
|
+
if (!Number.isFinite(salvageValue) || salvageValue < 0) return { error: "Salvage value must be zero or a positive number" };
|
|
7818
|
+
if (salvageValue >= cost) return { error: "Salvage value must be less than cost" };
|
|
7819
|
+
if (!Number.isInteger(usefulLifeYears) || usefulLifeYears <= 0) {
|
|
7820
|
+
return { error: "Useful life must be a positive whole number of years" };
|
|
7821
|
+
}
|
|
7822
|
+
if (!_DEPRECIATION_METHODS_MCP.includes(method)) {
|
|
7823
|
+
return { error: `Method must be one of: ${_DEPRECIATION_METHODS_MCP.join(", ")}` };
|
|
7824
|
+
}
|
|
7825
|
+
const depreciableBase = _roundMoneyMcp(cost - salvageValue);
|
|
7826
|
+
const schedule = [];
|
|
7827
|
+
let accumulated = 0;
|
|
7828
|
+
if (method === "straight-line") {
|
|
7829
|
+
const annual = _roundMoneyMcp(depreciableBase / usefulLifeYears);
|
|
7830
|
+
for (let year = 1; year <= usefulLifeYears; year++) {
|
|
7831
|
+
const expense = year === usefulLifeYears ? _roundMoneyMcp(depreciableBase - accumulated) : annual;
|
|
7832
|
+
accumulated = _roundMoneyMcp(accumulated + expense);
|
|
7833
|
+
schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: _roundMoneyMcp(cost - accumulated) });
|
|
7834
|
+
}
|
|
7835
|
+
} else if (method === "double-declining-balance") {
|
|
7836
|
+
const rate = 2 / usefulLifeYears;
|
|
7837
|
+
let bookValue = cost;
|
|
7838
|
+
for (let year = 1; year <= usefulLifeYears; year++) {
|
|
7839
|
+
let expense = _roundMoneyMcp(bookValue * rate);
|
|
7840
|
+
if (bookValue - expense < salvageValue) expense = _roundMoneyMcp(bookValue - salvageValue);
|
|
7841
|
+
if (expense < 0) expense = 0;
|
|
7842
|
+
bookValue = _roundMoneyMcp(bookValue - expense);
|
|
7843
|
+
accumulated = _roundMoneyMcp(accumulated + expense);
|
|
7844
|
+
schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue });
|
|
7845
|
+
}
|
|
7846
|
+
} else {
|
|
7847
|
+
const sumOfYears = usefulLifeYears * (usefulLifeYears + 1) / 2;
|
|
7848
|
+
for (let year = 1; year <= usefulLifeYears; year++) {
|
|
7849
|
+
const fraction = (usefulLifeYears - year + 1) / sumOfYears;
|
|
7850
|
+
const expense = year === usefulLifeYears ? _roundMoneyMcp(depreciableBase - accumulated) : _roundMoneyMcp(fraction * depreciableBase);
|
|
7851
|
+
accumulated = _roundMoneyMcp(accumulated + expense);
|
|
7852
|
+
schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: _roundMoneyMcp(cost - accumulated) });
|
|
7853
|
+
}
|
|
7854
|
+
}
|
|
7855
|
+
return { cost, salvageValue, usefulLifeYears, method, totalDepreciation: accumulated, schedule };
|
|
7856
|
+
}
|
|
7857
|
+
server.tool(
|
|
7858
|
+
"calculate_depreciation_schedule",
|
|
7859
|
+
"Build a year-by-year depreciation schedule for a fixed asset from its cost, salvage value, and useful life in years, using straight-line, double-declining-balance, or sum-of-years-digits. The double-declining-balance method clamps at salvage value rather than auto-switching to straight-line partway through.",
|
|
7860
|
+
{
|
|
7861
|
+
cost: z.number().describe("The asset's original cost"),
|
|
7862
|
+
salvageValue: z.number().describe("Estimated residual value at the end of its useful life; must be less than cost"),
|
|
7863
|
+
usefulLifeYears: z.number().int().describe("Useful life in whole years"),
|
|
7864
|
+
method: z.enum(["straight-line", "double-declining-balance", "sum-of-years-digits"]).describe("Depreciation method to use")
|
|
7865
|
+
},
|
|
7866
|
+
async ({ cost, salvageValue, usefulLifeYears, method }) => {
|
|
7867
|
+
const result = _calculateDepreciationScheduleMcp(cost, salvageValue, usefulLifeYears, method);
|
|
7868
|
+
if ("error" in result) return errText(result);
|
|
7869
|
+
return text(JSON.stringify(result, null, 2));
|
|
7870
|
+
}
|
|
7871
|
+
);
|
|
7872
|
+
var _KEYWORD_DENSITY_STOPWORDS_MCP = /* @__PURE__ */ new Set([
|
|
7873
|
+
"a",
|
|
7874
|
+
"an",
|
|
7875
|
+
"the",
|
|
7876
|
+
"and",
|
|
7877
|
+
"or",
|
|
7878
|
+
"but",
|
|
7879
|
+
"if",
|
|
7880
|
+
"then",
|
|
7881
|
+
"else",
|
|
7882
|
+
"for",
|
|
7883
|
+
"nor",
|
|
7884
|
+
"so",
|
|
7885
|
+
"yet",
|
|
7886
|
+
"of",
|
|
7887
|
+
"in",
|
|
7888
|
+
"on",
|
|
7889
|
+
"at",
|
|
7890
|
+
"to",
|
|
7891
|
+
"from",
|
|
7892
|
+
"by",
|
|
7893
|
+
"with",
|
|
7894
|
+
"about",
|
|
7895
|
+
"against",
|
|
7896
|
+
"between",
|
|
7897
|
+
"into",
|
|
7898
|
+
"through",
|
|
7899
|
+
"during",
|
|
7900
|
+
"before",
|
|
7901
|
+
"after",
|
|
7902
|
+
"above",
|
|
7903
|
+
"below",
|
|
7904
|
+
"up",
|
|
7905
|
+
"down",
|
|
7906
|
+
"out",
|
|
7907
|
+
"off",
|
|
7908
|
+
"over",
|
|
7909
|
+
"under",
|
|
7910
|
+
"again",
|
|
7911
|
+
"further",
|
|
7912
|
+
"once",
|
|
7913
|
+
"is",
|
|
7914
|
+
"are",
|
|
7915
|
+
"was",
|
|
7916
|
+
"were",
|
|
7917
|
+
"be",
|
|
7918
|
+
"been",
|
|
7919
|
+
"being",
|
|
7920
|
+
"am",
|
|
7921
|
+
"do",
|
|
7922
|
+
"does",
|
|
7923
|
+
"did",
|
|
7924
|
+
"doing",
|
|
7925
|
+
"have",
|
|
7926
|
+
"has",
|
|
7927
|
+
"had",
|
|
7928
|
+
"having",
|
|
7929
|
+
"i",
|
|
7930
|
+
"me",
|
|
7931
|
+
"my",
|
|
7932
|
+
"we",
|
|
7933
|
+
"our",
|
|
7934
|
+
"you",
|
|
7935
|
+
"your",
|
|
7936
|
+
"he",
|
|
7937
|
+
"him",
|
|
7938
|
+
"his",
|
|
7939
|
+
"she",
|
|
7940
|
+
"her",
|
|
7941
|
+
"it",
|
|
7942
|
+
"its",
|
|
7943
|
+
"they",
|
|
7944
|
+
"them",
|
|
7945
|
+
"their",
|
|
7946
|
+
"this",
|
|
7947
|
+
"that",
|
|
7948
|
+
"these",
|
|
7949
|
+
"those",
|
|
7950
|
+
"as",
|
|
7951
|
+
"not",
|
|
7952
|
+
"no",
|
|
7953
|
+
"can",
|
|
7954
|
+
"will",
|
|
7955
|
+
"just",
|
|
7956
|
+
"than",
|
|
7957
|
+
"too",
|
|
7958
|
+
"very",
|
|
7959
|
+
"would",
|
|
7960
|
+
"should",
|
|
7961
|
+
"could",
|
|
7962
|
+
"there",
|
|
7963
|
+
"here",
|
|
7964
|
+
"what",
|
|
7965
|
+
"which",
|
|
7966
|
+
"who",
|
|
7967
|
+
"whom"
|
|
7968
|
+
]);
|
|
7969
|
+
function _analyzeKeywordDensityMcp(input, topN, minWordLength, ngramSize) {
|
|
7970
|
+
if (!input || !input.trim()) return { error: "Input text is empty" };
|
|
7971
|
+
if (!Number.isInteger(topN) || topN <= 0) return { error: "topN must be a positive whole number" };
|
|
7972
|
+
if (!Number.isInteger(minWordLength) || minWordLength < 1) return { error: "minWordLength must be a positive whole number" };
|
|
7973
|
+
if (!Number.isInteger(ngramSize) || ngramSize < 1 || ngramSize > 3) return { error: "ngramSize must be 1, 2, or 3" };
|
|
7974
|
+
const tokens = input.toLowerCase().match(/[a-z0-9']+/g) ?? [];
|
|
7975
|
+
const totalWords = tokens.length;
|
|
7976
|
+
if (totalWords === 0) return { error: "No words found in input text" };
|
|
7977
|
+
if (totalWords < ngramSize) return { error: "Input text has fewer words than the requested phrase length" };
|
|
7978
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7979
|
+
for (let i = 0; i <= tokens.length - ngramSize; i++) {
|
|
7980
|
+
const phraseWords = tokens.slice(i, i + ngramSize);
|
|
7981
|
+
if (phraseWords.some((w) => w.length < minWordLength)) continue;
|
|
7982
|
+
if (ngramSize === 1 && _KEYWORD_DENSITY_STOPWORDS_MCP.has(phraseWords[0])) continue;
|
|
7983
|
+
const phrase = phraseWords.join(" ");
|
|
7984
|
+
counts.set(phrase, (counts.get(phrase) ?? 0) + 1);
|
|
7985
|
+
}
|
|
7986
|
+
const keywords = Array.from(counts.entries()).map(([keyword, count]) => ({ keyword, count, density: Math.round(count / totalWords * 1e3) / 10 })).sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword)).slice(0, topN);
|
|
7987
|
+
return { totalWords, uniqueKeywords: counts.size, ngramSize, keywords };
|
|
7988
|
+
}
|
|
7989
|
+
server.tool(
|
|
7990
|
+
"analyze_keyword_density",
|
|
7991
|
+
"Count keyword or phrase frequency and density (occurrences per 100 words) across a block of text, the way an SEO keyword-density checker does. With the default ngramSize of 1, common English stopwords and words shorter than minWordLength are excluded; for ngramSize 2 or 3 (phrases), stopwords are kept since useful phrases often contain them.",
|
|
7992
|
+
{
|
|
7993
|
+
text: z.string().describe("The text to analyze"),
|
|
7994
|
+
topN: z.number().int().optional().describe("Number of top keywords/phrases to return (default 10)"),
|
|
7995
|
+
minWordLength: z.number().int().optional().describe("Minimum word length to include (default 3)"),
|
|
7996
|
+
ngramSize: z.number().int().min(1).max(3).optional().describe("Phrase length in words: 1 for single words, 2 or 3 for phrases (default 1)")
|
|
7997
|
+
},
|
|
7998
|
+
async ({ text: input, topN, minWordLength, ngramSize }) => {
|
|
7999
|
+
const result = _analyzeKeywordDensityMcp(input, topN ?? 10, minWordLength ?? 3, ngramSize ?? 1);
|
|
8000
|
+
if ("error" in result) return errText(result);
|
|
8001
|
+
return text(JSON.stringify(result, null, 2));
|
|
8002
|
+
}
|
|
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
|
+
);
|
|
7814
8172
|
var transport = new StdioServerTransport();
|
|
7815
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",
|