@utilix-tech/mcp 0.47.0 → 0.49.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,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.49.0
6
+ - Add `resolve_yaml_anchors` tool: expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.
7
+ - Add `split_sitemap` tool: split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.
8
+
9
+ ## 0.48.0
10
+ - 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.
11
+ - 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.
12
+
5
13
  ## 0.47.0
6
14
  - 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
15
  - 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
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 181 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 188 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
 
@@ -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 (25 tools)
196
+ ### API / Network (28 tools)
197
197
  | Tool | Description |
198
198
  |------|-------------|
199
199
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -204,6 +204,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
204
204
  | `cors_headers` | Generate CORS response headers for a configuration |
205
205
  | `parse_har` | Parse a DevTools .har network export into requests and summary stats |
206
206
  | `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
207
+ | `split_sitemap` | Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file |
207
208
  | `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
208
209
  | `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
209
210
  | `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
@@ -221,6 +222,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
221
222
  | `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
222
223
  | `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
224
  | `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
225
+ | `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 |
226
+ | `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
224
227
 
225
228
  ### Color (4 tools)
226
229
  | Tool | Description |
@@ -246,10 +249,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
246
249
  | `lint_conventional_commit` | Lint a commit message against the Conventional Commits v1.0.0 spec: header format, blank-line separation, footer tokens, and BREAKING CHANGE casing |
247
250
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
248
251
 
249
- ### Data (10 tools)
252
+ ### Data (11 tools)
250
253
  | Tool | Description |
251
254
  |------|-------------|
252
255
  | `validate_yaml` | Validate YAML syntax |
256
+ | `resolve_yaml_anchors` | Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused |
253
257
  | `toml_to_json` | Convert TOML to JSON |
254
258
  | `xml_to_json` | Convert XML to JSON |
255
259
  | `parse_csv` | Parse a CSV string and return the data as JSON |
@@ -305,12 +309,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
305
309
  | `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
306
310
  | `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
311
 
308
- ### Image (3 tools)
312
+ ### Image (4 tools)
309
313
  | Tool | Description |
310
314
  |------|-------------|
311
315
  | `read_image_info` | Read format, dimensions, bit depth, and alpha channel from image bytes: no decoding required. Supports PNG, JPEG, GIF, WebP, BMP |
312
316
  | `read_exif_data` | Read camera make/model, orientation, timestamps, exposure settings, and GPS coordinates from a JPEG's EXIF data |
313
317
  | `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 |
318
+ | `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
319
 
315
320
  ### PDF (2 tools)
316
321
  | Tool | Description |
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import { createHash } from "crypto";
8
+ import * as yaml from "js-yaml";
8
9
  import {
9
10
  parseJson,
10
11
  minifyJson,
@@ -730,6 +731,78 @@ Documents: ${result.documentCount}
730
731
  Keys: ${result.keyCount}`);
731
732
  }
732
733
  );
734
+ var _YAML_ANCHOR_RE = /(?:^|\s)&([A-Za-z0-9_.-]+)/g;
735
+ var _YAML_ALIAS_RE = /(?:^|\s)\*([A-Za-z0-9_.-]+)/g;
736
+ function _stripYamlLineComment(line) {
737
+ let inSingle = false;
738
+ let inDouble = false;
739
+ for (let i = 0; i < line.length; i++) {
740
+ const ch = line[i];
741
+ if (ch === "'" && !inDouble) {
742
+ inSingle = !inSingle;
743
+ } else if (ch === '"' && !inSingle) {
744
+ inDouble = !inDouble;
745
+ } else if (ch === "#" && !inSingle && !inDouble && (i === 0 || /\s/.test(line[i - 1]))) {
746
+ return line.slice(0, i);
747
+ }
748
+ }
749
+ return line;
750
+ }
751
+ function _resolveYamlAnchors(yamlText) {
752
+ if (!yamlText || !yamlText.trim()) return { error: "Input is empty" };
753
+ const anchorDefs = [];
754
+ const referencedNames = /* @__PURE__ */ new Set();
755
+ let aliasCount = 0;
756
+ const lines = yamlText.split("\n");
757
+ for (let i = 0; i < lines.length; i++) {
758
+ const line = _stripYamlLineComment(lines[i]);
759
+ const lineNum = i + 1;
760
+ _YAML_ANCHOR_RE.lastIndex = 0;
761
+ let m;
762
+ while ((m = _YAML_ANCHOR_RE.exec(line)) !== null) {
763
+ anchorDefs.push({ name: m[1], line: lineNum });
764
+ }
765
+ _YAML_ALIAS_RE.lastIndex = 0;
766
+ while ((m = _YAML_ALIAS_RE.exec(line)) !== null) {
767
+ referencedNames.add(m[1]);
768
+ aliasCount++;
769
+ }
770
+ }
771
+ let parsed;
772
+ try {
773
+ parsed = yaml.load(yamlText);
774
+ } catch (e) {
775
+ const err = e;
776
+ return { error: err.reason ?? err.message };
777
+ }
778
+ let resolvedYaml;
779
+ try {
780
+ resolvedYaml = yaml.dump(parsed, { noRefs: true, lineWidth: -1 });
781
+ } catch (e) {
782
+ return { error: e instanceof Error ? e.message : String(e) };
783
+ }
784
+ const anchors = anchorDefs.map((a) => ({
785
+ name: a.name,
786
+ line: a.line,
787
+ referenced: referencedNames.has(a.name)
788
+ }));
789
+ const seenAnchorNames = [];
790
+ for (const a of anchorDefs) {
791
+ if (!seenAnchorNames.includes(a.name)) seenAnchorNames.push(a.name);
792
+ }
793
+ const unusedAnchors = seenAnchorNames.filter((name) => !referencedNames.has(name));
794
+ return { resolvedYaml, anchors, unusedAnchors, aliasCount };
795
+ }
796
+ server.tool(
797
+ "resolve_yaml_anchors",
798
+ "Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.",
799
+ { yaml: z.string().describe("The YAML document to resolve") },
800
+ async ({ yaml: yamlText }) => {
801
+ const result = _resolveYamlAnchors(yamlText);
802
+ if ("error" in result) return errText(result);
803
+ return text(JSON.stringify(result, null, 2));
804
+ }
805
+ );
733
806
  server.tool(
734
807
  "toml_to_json",
735
808
  "Convert a TOML string to JSON.",
@@ -1609,6 +1682,68 @@ server.tool(
1609
1682
  return text(JSON.stringify(result, null, 2));
1610
1683
  }
1611
1684
  );
1685
+ var _SITEMAP_SPLITTER_MAX_URLS_PER_FILE = 5e4;
1686
+ function _escapeXmlUrlForSplitter(url) {
1687
+ return url.replace(/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1688
+ }
1689
+ function _splitSitemap(urlsText, baseUrlInput, urlsPerFileInput, filePrefixInput) {
1690
+ const urls = (urlsText ?? "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1691
+ if (urls.length === 0) return { error: "No URLs provided" };
1692
+ for (let i = 0; i < urls.length; i++) {
1693
+ if (!/^https?:\/\//i.test(urls[i])) {
1694
+ return { error: `Line ${i + 1}: "${urls[i]}" is not an absolute URL starting with http:// or https://` };
1695
+ }
1696
+ }
1697
+ if (!baseUrlInput || !/^https?:\/\//i.test(baseUrlInput)) {
1698
+ return { error: "baseUrl must be an absolute URL starting with http:// or https://" };
1699
+ }
1700
+ const baseUrl = baseUrlInput.replace(/\/+$/, "") + "/";
1701
+ let urlsPerFile = urlsPerFileInput ?? _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1702
+ if (urlsPerFile <= 0) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1703
+ if (urlsPerFile > _SITEMAP_SPLITTER_MAX_URLS_PER_FILE) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
1704
+ const filePrefix = (filePrefixInput || "sitemap").replace(/[^A-Za-z0-9_-]/g, "") || "sitemap";
1705
+ const files = [];
1706
+ for (let i = 0; i < urls.length; i += urlsPerFile) {
1707
+ const chunk = urls.slice(i, i + urlsPerFile);
1708
+ const fileIndex = files.length + 1;
1709
+ const filename = `${filePrefix}-${fileIndex}.xml`;
1710
+ const urlLines = chunk.map((url) => ` <url>
1711
+ <loc>${_escapeXmlUrlForSplitter(url)}</loc>
1712
+ </url>`).join("\n");
1713
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
1714
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1715
+ ${urlLines}
1716
+ </urlset>
1717
+ `;
1718
+ files.push({ filename, urlCount: chunk.length, xml });
1719
+ }
1720
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1721
+ const indexEntries = files.map((f) => ` <sitemap>
1722
+ <loc>${_escapeXmlUrlForSplitter(baseUrl + f.filename)}</loc>
1723
+ <lastmod>${today}</lastmod>
1724
+ </sitemap>`).join("\n");
1725
+ const indexXml = `<?xml version="1.0" encoding="UTF-8"?>
1726
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1727
+ ${indexEntries}
1728
+ </sitemapindex>
1729
+ `;
1730
+ return { files, indexXml, totalUrls: urls.length };
1731
+ }
1732
+ server.tool(
1733
+ "split_sitemap",
1734
+ "Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.",
1735
+ {
1736
+ urls: z.string().describe("Newline-separated list of absolute URLs"),
1737
+ baseUrl: z.string().describe("Base URL the generated sitemap files will be hosted at"),
1738
+ urlsPerFile: z.number().optional().describe("URLs per generated file, clamped to 50000 (default 50000)"),
1739
+ filePrefix: z.string().optional().describe('Filename prefix for generated files (default "sitemap")')
1740
+ },
1741
+ async ({ urls, baseUrl, urlsPerFile, filePrefix }) => {
1742
+ const result = _splitSitemap(urls, baseUrl, urlsPerFile, filePrefix);
1743
+ if ("error" in result) return errText(result);
1744
+ return text(JSON.stringify(result, null, 2));
1745
+ }
1746
+ );
1612
1747
  var _KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
1613
1748
  function _validateRobotsTxt(text2) {
1614
1749
  if (!text2 || !text2.trim()) return { error: "Input is empty" };
@@ -8001,5 +8136,173 @@ server.tool(
8001
8136
  return text(JSON.stringify(result, null, 2));
8002
8137
  }
8003
8138
  );
8139
+ function _encodeRFC5987ValueCharsMcp(value) {
8140
+ return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => "%" + ch.charCodeAt(0).toString(16).toUpperCase());
8141
+ }
8142
+ function _buildContentDispositionMcp(options) {
8143
+ const type = options.type ?? "attachment";
8144
+ if (type !== "inline" && type !== "attachment") return { error: 'type must be "inline" or "attachment"' };
8145
+ if (!options.filename) return { header: type, type, filenameEncoded: false };
8146
+ const filename = options.filename;
8147
+ if (filename.includes("/") || filename.includes("\\")) return { error: "filename must not contain path separators" };
8148
+ const isAscii = /^[\x20-\x7e]*$/.test(filename);
8149
+ const asciiFallback = filename.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1");
8150
+ let header = `${type}; filename="${asciiFallback}"`;
8151
+ let filenameEncoded = false;
8152
+ if (!isAscii) {
8153
+ header += `; filename*=UTF-8''${_encodeRFC5987ValueCharsMcp(filename)}`;
8154
+ filenameEncoded = true;
8155
+ }
8156
+ return { header, type, filename, filenameEncoded };
8157
+ }
8158
+ function _parseContentDispositionMcp(header) {
8159
+ if (typeof header !== "string" || header.trim() === "") return { error: "header is required" };
8160
+ const parts = header.split(";").map((p) => p.trim()).filter(Boolean);
8161
+ const type = parts[0]?.toLowerCase();
8162
+ if (!type) return { error: "Could not parse a disposition type from the header" };
8163
+ const params = {};
8164
+ for (let i = 1; i < parts.length; i++) {
8165
+ const eq = parts[i].indexOf("=");
8166
+ if (eq === -1) continue;
8167
+ const key = parts[i].slice(0, eq).trim().toLowerCase();
8168
+ let value = parts[i].slice(eq + 1).trim();
8169
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
8170
+ params[key] = value;
8171
+ }
8172
+ let filename = params.filename;
8173
+ let filenameIsExtended = false;
8174
+ if (params["filename*"]) {
8175
+ const match = params["filename*"].match(/^([^']*)'([^']*)'(.*)$/);
8176
+ if (match) {
8177
+ try {
8178
+ filename = decodeURIComponent(match[3]);
8179
+ filenameIsExtended = true;
8180
+ } catch {
8181
+ }
8182
+ }
8183
+ }
8184
+ return { type, filename, filenameIsExtended, params };
8185
+ }
8186
+ server.tool(
8187
+ "build_content_disposition",
8188
+ "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.",
8189
+ {
8190
+ type: z.enum(["inline", "attachment"]).optional().describe('Disposition type (default "attachment")'),
8191
+ filename: z.string().optional().describe("The filename to suggest to the browser")
8192
+ },
8193
+ async ({ type, filename }) => {
8194
+ const result = _buildContentDispositionMcp({ type, filename });
8195
+ if ("error" in result) return errText(result);
8196
+ return text(JSON.stringify(result, null, 2));
8197
+ }
8198
+ );
8199
+ server.tool(
8200
+ "parse_content_disposition",
8201
+ "Parse a Content-Disposition header value into its disposition type, filename (decoding an RFC 5987 filename* parameter if present), and raw parameters.",
8202
+ {
8203
+ header: z.string().describe("The raw Content-Disposition header value")
8204
+ },
8205
+ async ({ header }) => {
8206
+ const result = _parseContentDispositionMcp(header);
8207
+ if ("error" in result) return errText(result);
8208
+ return text(JSON.stringify(result, null, 2));
8209
+ }
8210
+ );
8211
+ function _skipGifSubBlocksMcp(bytes, offset) {
8212
+ while (offset < bytes.length) {
8213
+ const len = bytes[offset];
8214
+ offset += 1;
8215
+ if (len === 0) break;
8216
+ offset += len;
8217
+ }
8218
+ return offset;
8219
+ }
8220
+ function _inspectGifFramesMcp(bytes) {
8221
+ if (bytes.length < 13) return { error: "File too small to be a valid GIF file" };
8222
+ const signature = Buffer.from(bytes.subarray(0, 6)).toString("ascii");
8223
+ if (signature !== "GIF87a" && signature !== "GIF89a") return { error: "Not a valid GIF file (missing GIF87a/GIF89a signature)" };
8224
+ const version = signature.slice(3);
8225
+ const width = _readU16LE(bytes, 6);
8226
+ const height = _readU16LE(bytes, 8);
8227
+ const packed = bytes[10];
8228
+ const hasGlobalColorTable = (packed & 128) !== 0;
8229
+ const globalColorTableSize = hasGlobalColorTable ? 2 << (packed & 7) : 0;
8230
+ let offset = 13;
8231
+ if (hasGlobalColorTable) offset += globalColorTableSize * 3;
8232
+ let frameCount = 0;
8233
+ let loopCount = null;
8234
+ let totalDurationMs = 0;
8235
+ let hasTransparency = false;
8236
+ let pendingDelay = 0;
8237
+ while (offset < bytes.length) {
8238
+ const blockType = bytes[offset];
8239
+ if (blockType === 59) break;
8240
+ if (blockType === 33) {
8241
+ const label = bytes[offset + 1];
8242
+ offset += 2;
8243
+ if (label === 249) {
8244
+ const subLen = bytes[offset] ?? 0;
8245
+ if (subLen >= 4) {
8246
+ const flags = bytes[offset + 1];
8247
+ pendingDelay = _readU16LE(bytes, offset + 2);
8248
+ if (flags & 1) hasTransparency = true;
8249
+ }
8250
+ offset += 1 + subLen;
8251
+ offset = _skipGifSubBlocksMcp(bytes, offset);
8252
+ } else if (label === 255) {
8253
+ const blockSize = bytes[offset] ?? 0;
8254
+ const appId = Buffer.from(bytes.subarray(offset + 1, offset + 1 + Math.min(blockSize, 11))).toString("ascii");
8255
+ offset += 1 + blockSize;
8256
+ if (appId === "NETSCAPE2.0") {
8257
+ const subLen = bytes[offset] ?? 0;
8258
+ if (subLen >= 3 && bytes[offset + 1] === 1) {
8259
+ loopCount = _readU16LE(bytes, offset + 2);
8260
+ }
8261
+ }
8262
+ offset = _skipGifSubBlocksMcp(bytes, offset);
8263
+ } else {
8264
+ offset = _skipGifSubBlocksMcp(bytes, offset);
8265
+ }
8266
+ } else if (blockType === 44) {
8267
+ if (offset + 10 > bytes.length) return { error: "Truncated image descriptor" };
8268
+ frameCount++;
8269
+ totalDurationMs += pendingDelay * 10;
8270
+ pendingDelay = 0;
8271
+ const packedImg = bytes[offset + 9];
8272
+ const hasLocalColorTable = (packedImg & 128) !== 0;
8273
+ const localColorTableSize = hasLocalColorTable ? 2 << (packedImg & 7) : 0;
8274
+ offset += 10;
8275
+ if (hasLocalColorTable) offset += localColorTableSize * 3;
8276
+ offset += 1;
8277
+ offset = _skipGifSubBlocksMcp(bytes, offset);
8278
+ } else {
8279
+ break;
8280
+ }
8281
+ }
8282
+ if (frameCount === 0) return { error: "No image data blocks found in this GIF file" };
8283
+ return {
8284
+ version,
8285
+ width,
8286
+ height,
8287
+ frameCount,
8288
+ animated: frameCount > 1,
8289
+ loopCount,
8290
+ totalDurationMs,
8291
+ hasGlobalColorTable,
8292
+ globalColorTableSize,
8293
+ hasTransparency
8294
+ };
8295
+ }
8296
+ server.tool(
8297
+ "inspect_gif_frames",
8298
+ "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.",
8299
+ { gif_base64: z.string().describe("Base64-encoded GIF file bytes, optionally prefixed with a data: URL scheme") },
8300
+ async ({ gif_base64 }) => {
8301
+ const bytes = new Uint8Array(Buffer.from(gif_base64.replace(/^data:.*;base64,/, ""), "base64"));
8302
+ const result = _inspectGifFramesMcp(bytes);
8303
+ if ("error" in result) return errText(result);
8304
+ return text(JSON.stringify(result, null, 2));
8305
+ }
8306
+ );
8004
8307
  var transport = new StdioServerTransport();
8005
8308
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.47.0",
4
- "description": "MCP server exposing 175 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.49.0",
4
+ "description": "MCP server exposing 188 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",
@@ -54,9 +54,11 @@
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.12.0",
56
56
  "@utilix-tech/sdk": "^0.3.0",
57
+ "js-yaml": "^4.1.0",
57
58
  "zod": "^3.23.0"
58
59
  },
59
60
  "devDependencies": {
61
+ "@types/js-yaml": "^4.0.9",
60
62
  "@types/node": "^20.0.0",
61
63
  "tsup": "^8.0.0",
62
64
  "typescript": "^5.4.0"