@utilix-tech/mcp 0.66.0 → 0.68.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,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.68.0
6
+ - Add `build_robots_meta_tag`, `parse_robots_meta_tag` tools: build a `<meta name="robots" content="...">` tag from the same directive vocabulary as X-Robots-Tag, optionally scoped to a crawler via the name attribute, or parse an existing tag into the same shape.
7
+ - Add `extract_svg_sprite_symbols` tool: parse an SVG sprite sheet's `<symbol>` definitions and list each one's id, viewBox, width, and height, alongside a ready-to-paste `<use href="#id"></use>` usage snippet.
8
+
9
+ ## 0.67.0
10
+ - Add `analyze_regex_groups` tool: parse a regex pattern's capture groups (numbered and named) and backreferences, flagging any backreference pointing at a group that doesn't exist or hasn't finished being defined yet at that point in the pattern.
11
+
5
12
  ## 0.66.0
6
13
  - Add `build_x_robots_tag`, `parse_x_robots_tag` tools: build an X-Robots-Tag response header from flag directives (noindex, nofollow, etc.) plus max-snippet/max-image-preview/max-video-preview/unavailable_after value directives, optionally scoped to a crawler user-agent, or parse an existing header into the same shape.
7
14
  - Add `lookup_websocket_opcode`, `list_websocket_opcodes` tools: look up what a WebSocket frame opcode means per RFC 6455's 4-bit opcode space, or list every opcode.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 227 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 231 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
 
@@ -205,7 +205,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
205
205
  | `plan_debt_payoff` | Simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest |
206
206
  | `calculate_loan_refinance` | Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs |
207
207
 
208
- ### API / Network (44 tools)
208
+ ### API / Network (48 tools)
209
209
  | Tool | Description |
210
210
  |------|-------------|
211
211
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -254,6 +254,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
254
254
  | `parse_x_robots_tag` | Parse an existing X-Robots-Tag header value into the same shape |
255
255
  | `lookup_websocket_opcode` | Look up what a WebSocket frame opcode means, per RFC 6455's 4-bit opcode space |
256
256
  | `list_websocket_opcodes` | List every WebSocket frame opcode (0x0-0xF) |
257
+ | `build_robots_meta_tag` | Build a `<meta name="robots" content="...">` tag from directives (noindex, nofollow, max-snippet, etc.), the same vocabulary as X-Robots-Tag, optionally scoped to a crawler via the name attribute |
258
+ | `parse_robots_meta_tag` | Parse an existing robots meta tag (or bare content value) into the same shape |
257
259
 
258
260
  ### Color (4 tools)
259
261
  | Tool | Description |
@@ -263,7 +265,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
263
265
  | `color_palette` | Generate a color palette from a base color |
264
266
  | `color_shades` | Generate shades or tints from a base color |
265
267
 
266
- ### Code (16 tools)
268
+ ### Code (18 tools)
267
269
  | Tool | Description |
268
270
  |------|-------------|
269
271
  | `format_sql` | Format a SQL query with proper indentation |
@@ -276,12 +278,14 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
276
278
  | `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
277
279
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
278
280
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
281
+ | `analyze_regex_groups` | Parse a regex's capture groups (numbered and named) and backreferences, flagging a backreference pointing at a group that doesn't exist or hasn't finished being defined yet |
279
282
  | `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 |
280
283
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
281
284
  | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
282
285
  | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
283
286
  | `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
284
287
  | `audit_dependency_ranges` | Classify each package.json dependency's version range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections |
288
+ | `extract_svg_sprite_symbols` | Parse an SVG sprite sheet's `<symbol>` definitions and list each one's id, viewBox, width, and height, alongside a ready-to-paste `<use href="#id"></use>` usage snippet |
285
289
 
286
290
  ### Data (16 tools)
287
291
  | Tool | Description |
package/dist/index.js CHANGED
@@ -4318,6 +4318,123 @@ server.tool(
4318
4318
  return text(JSON.stringify(result, null, 2));
4319
4319
  }
4320
4320
  );
4321
+ function _analyzeRegexGroupsMcp(pattern) {
4322
+ let valid = true;
4323
+ let compileError;
4324
+ try {
4325
+ new RegExp(pattern);
4326
+ } catch (e) {
4327
+ valid = false;
4328
+ compileError = e instanceof Error ? e.message : String(e);
4329
+ }
4330
+ const groups = [];
4331
+ const openStack = [];
4332
+ const backreferences = [];
4333
+ const issues = [];
4334
+ let groupCount = 0;
4335
+ let inClass = false;
4336
+ for (let i = 0; i < pattern.length; i++) {
4337
+ const ch = pattern[i];
4338
+ if (ch === "\\" && i + 1 < pattern.length) {
4339
+ const next = pattern[i + 1];
4340
+ if (!inClass && /[1-9]/.test(next)) {
4341
+ let j = i + 1;
4342
+ while (j < pattern.length && /[0-9]/.test(pattern[j])) j++;
4343
+ const raw = pattern.slice(i, j);
4344
+ const target = parseInt(pattern.slice(i + 1, j), 10);
4345
+ backreferences.push({ raw, target, start: i, resolved: false, definedBeforeUse: false });
4346
+ i = j - 1;
4347
+ continue;
4348
+ }
4349
+ if (!inClass && next === "k" && pattern[i + 2] === "<") {
4350
+ const close = pattern.indexOf(">", i + 3);
4351
+ if (close !== -1) {
4352
+ const name = pattern.slice(i + 3, close);
4353
+ const raw = pattern.slice(i, close + 1);
4354
+ backreferences.push({ raw, target: name, start: i, resolved: false, definedBeforeUse: false });
4355
+ i = close;
4356
+ continue;
4357
+ }
4358
+ }
4359
+ i++;
4360
+ continue;
4361
+ }
4362
+ if (ch === "[" && !inClass) {
4363
+ inClass = true;
4364
+ continue;
4365
+ }
4366
+ if (ch === "]" && inClass) {
4367
+ inClass = false;
4368
+ continue;
4369
+ }
4370
+ if (inClass) continue;
4371
+ if (ch === "(") {
4372
+ if (pattern[i + 1] === "?") {
4373
+ const c2 = pattern[i + 2];
4374
+ if (c2 === ":" || c2 === "=" || c2 === "!") {
4375
+ openStack.push({ index: -1, name: null, start: i });
4376
+ continue;
4377
+ }
4378
+ if (c2 === "<" && (pattern[i + 3] === "=" || pattern[i + 3] === "!")) {
4379
+ openStack.push({ index: -1, name: null, start: i });
4380
+ continue;
4381
+ }
4382
+ if (c2 === "<") {
4383
+ const close = pattern.indexOf(">", i + 3);
4384
+ const name = close !== -1 ? pattern.slice(i + 3, close) : null;
4385
+ groupCount++;
4386
+ openStack.push({ index: groupCount, name, start: i });
4387
+ if (close !== -1) i = close;
4388
+ continue;
4389
+ }
4390
+ openStack.push({ index: -1, name: null, start: i });
4391
+ continue;
4392
+ }
4393
+ groupCount++;
4394
+ openStack.push({ index: groupCount, name: null, start: i });
4395
+ continue;
4396
+ }
4397
+ if (ch === ")") {
4398
+ const open = openStack.pop();
4399
+ if (open && open.index !== -1) {
4400
+ groups.push({ index: open.index, name: open.name, start: open.start, end: i });
4401
+ }
4402
+ continue;
4403
+ }
4404
+ }
4405
+ for (const open of openStack) {
4406
+ if (open.index !== -1) groups.push({ index: open.index, name: open.name, start: open.start, end: -1 });
4407
+ }
4408
+ groups.sort((a, b) => a.index - b.index);
4409
+ const byIndex = new Map(groups.map((g) => [g.index, g]));
4410
+ const byName = new Map(groups.filter((g) => g.name !== null).map((g) => [g.name, g]));
4411
+ for (const b of backreferences) {
4412
+ const g = typeof b.target === "number" ? byIndex.get(b.target) : byName.get(b.target);
4413
+ if (!g) {
4414
+ b.resolved = false;
4415
+ b.definedBeforeUse = false;
4416
+ const label = typeof b.target === "number" ? `group ${b.target}` : `named group "${b.target}"`;
4417
+ issues.push({ severity: "error", message: `Backreference ${b.raw} at position ${b.start} points to ${label}, which doesn't exist in this pattern.` });
4418
+ } else {
4419
+ b.resolved = true;
4420
+ b.definedBeforeUse = g.end !== -1 && g.end < b.start;
4421
+ if (!b.definedBeforeUse) {
4422
+ const label = `group ${g.index}${g.name ? ` ("${g.name}")` : ""}`;
4423
+ issues.push({ severity: "error", message: `Backreference ${b.raw} at position ${b.start} references ${label}, which hasn't finished being defined yet at that point in the pattern.` });
4424
+ }
4425
+ }
4426
+ }
4427
+ return { pattern, valid, ...compileError ? { compileError } : {}, groups, backreferences, issues };
4428
+ }
4429
+ server.tool(
4430
+ "analyze_regex_groups",
4431
+ "Parse a regex pattern's capture groups (numbered and named) and backreferences, and flag any backreference pointing at a group that doesn't exist or hasn't finished being defined yet at that point in the pattern (a forward reference or self-reference). Structural, non-AI check. Pass the pattern body only, no delimiters or flags.",
4432
+ { pattern: z.string().describe("The regex pattern body to analyze, e.g. (?<year>\\d{4})-(?<month>\\d{2})\\k<year>") },
4433
+ async ({ pattern }) => {
4434
+ const result = _analyzeRegexGroupsMcp(pattern);
4435
+ return text(JSON.stringify(result, null, 2));
4436
+ }
4437
+ );
4321
4438
  var _CC_NUM_FIELDS_MCP = ["maxAge", "sMaxAge", "staleWhileRevalidate", "staleIfError"];
4322
4439
  function _humanizeCacheSecondsMcp(s) {
4323
4440
  if (s === 0) return "0 seconds";
@@ -10751,5 +10868,147 @@ server.tool(
10751
10868
  return text(JSON.stringify(result, null, 2));
10752
10869
  }
10753
10870
  );
10871
+ function _buildRobotsMetaTagMcp(options) {
10872
+ const built = _buildXRobotsTagMcp({
10873
+ flags: options?.flags,
10874
+ maxSnippet: options?.maxSnippet,
10875
+ maxImagePreview: options?.maxImagePreview,
10876
+ maxVideoPreview: options?.maxVideoPreview,
10877
+ unavailableAfter: options?.unavailableAfter
10878
+ });
10879
+ if ("error" in built) return built;
10880
+ const name = (typeof options?.name === "string" ? options.name.trim() : "") || "robots";
10881
+ return {
10882
+ content: built.header,
10883
+ name,
10884
+ tag: `<meta name="${name}" content="${built.header}">`,
10885
+ directives: built.directives,
10886
+ notes: built.notes,
10887
+ warnings: built.warnings
10888
+ };
10889
+ }
10890
+ var _ROBOTS_META_TAG_REGEX_MCP = /<meta\b[^>]*>/i;
10891
+ var _ROBOTS_META_NAME_ATTR_REGEX_MCP = /\bname\s*=\s*["']([^"']*)["']/i;
10892
+ var _ROBOTS_META_CONTENT_ATTR_REGEX_MCP = /\bcontent\s*=\s*["']([^"']*)["']/i;
10893
+ function _parseRobotsMetaTagMcp(raw) {
10894
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
10895
+ const trimmed = raw.trim();
10896
+ let name = "robots";
10897
+ let content = trimmed;
10898
+ const tagMatch = trimmed.match(_ROBOTS_META_TAG_REGEX_MCP);
10899
+ if (tagMatch) {
10900
+ const nameMatch = tagMatch[0].match(_ROBOTS_META_NAME_ATTR_REGEX_MCP);
10901
+ const contentMatch = tagMatch[0].match(_ROBOTS_META_CONTENT_ATTR_REGEX_MCP);
10902
+ if (!contentMatch) return { error: "Meta tag is missing a content attribute" };
10903
+ if (nameMatch) name = nameMatch[1].trim() || "robots";
10904
+ content = contentMatch[1];
10905
+ }
10906
+ const tokens = content.split(",").map((t) => t.trim()).filter((t) => t !== "");
10907
+ if (tokens.length === 0) return { error: "No directives found" };
10908
+ const flags = [];
10909
+ let maxSnippet;
10910
+ let maxImagePreview;
10911
+ let maxVideoPreview;
10912
+ let unavailableAfter;
10913
+ for (const token of tokens) {
10914
+ const eq = token.indexOf(":");
10915
+ const directiveName = (eq === -1 ? token : token.slice(0, eq)).trim().toLowerCase();
10916
+ const value = eq === -1 ? "" : token.slice(eq + 1).trim();
10917
+ if (eq === -1) {
10918
+ if (!_isXRobotsTagFlagMcp(directiveName)) return { error: `Unrecognized directive "${token}"` };
10919
+ flags.push(directiveName);
10920
+ continue;
10921
+ }
10922
+ if (directiveName === "max-snippet") {
10923
+ const n = Number(value);
10924
+ if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-snippet value "${value}" is not a valid integer >= -1` };
10925
+ maxSnippet = n;
10926
+ } else if (directiveName === "max-image-preview") {
10927
+ if (!["none", "standard", "large"].includes(value)) return { error: `max-image-preview value "${value}" must be one of "none", "standard", "large"` };
10928
+ maxImagePreview = value;
10929
+ } else if (directiveName === "max-video-preview") {
10930
+ const n = Number(value);
10931
+ if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-video-preview value "${value}" is not a valid integer >= -1` };
10932
+ maxVideoPreview = n;
10933
+ } else if (directiveName === "unavailable_after") {
10934
+ if (value === "" || Number.isNaN(Date.parse(value))) return { error: `unavailable_after value "${value}" is not a valid, parseable date` };
10935
+ unavailableAfter = value;
10936
+ } else {
10937
+ return { error: `Unrecognized directive "${token}"` };
10938
+ }
10939
+ }
10940
+ return _buildRobotsMetaTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, name });
10941
+ }
10942
+ server.tool(
10943
+ "build_robots_meta_tag",
10944
+ 'Build a <meta name="robots" content="..."> tag from flag directives (noindex, nofollow, noarchive, nosnippet, notranslate, noimageindex, none, all, indexifembedded) plus optional max-snippet, max-image-preview, max-video-preview, and unavailable_after value directives, the exact same directive vocabulary as the X-Robots-Tag header. Scope the tag to a single crawler with name (e.g. "googlebot") instead of the default "robots".',
10945
+ {
10946
+ flags: z.array(z.enum(["noindex", "nofollow", "noarchive", "nosnippet", "notranslate", "noimageindex", "none", "all", "indexifembedded"])).optional().describe("Flag directives to include"),
10947
+ maxSnippet: z.number().int().min(-1).optional().describe("Max text snippet length in characters; 0 shows no snippet, -1 means no limit"),
10948
+ maxImagePreview: z.enum(["none", "standard", "large"]).optional().describe("Max image preview size shown in search results"),
10949
+ maxVideoPreview: z.number().int().min(-1).optional().describe("Max video preview length in seconds; 0 shows no preview, -1 means no limit"),
10950
+ unavailableAfter: z.string().optional().describe("Stop showing this page in search results after this date/time"),
10951
+ name: z.string().optional().describe('Optional crawler name to scope the tag to via its name attribute, e.g. "googlebot" (default "robots")')
10952
+ },
10953
+ async ({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, name }) => {
10954
+ const result = _buildRobotsMetaTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, name });
10955
+ if ("error" in result) return errText(result);
10956
+ return text(JSON.stringify(result, null, 2));
10957
+ }
10958
+ );
10959
+ server.tool(
10960
+ "parse_robots_meta_tag",
10961
+ 'Parse an existing <meta name="..." content="..."> robots tag (or a bare content attribute value on its own) into the same shape build_robots_meta_tag returns.',
10962
+ {
10963
+ tag: z.string().min(1).describe("The meta tag (or bare content value) to parse")
10964
+ },
10965
+ async ({ tag }) => {
10966
+ const result = _parseRobotsMetaTagMcp(tag);
10967
+ if ("error" in result) return errText(result);
10968
+ return text(JSON.stringify(result, null, 2));
10969
+ }
10970
+ );
10971
+ var _SVG_SYMBOL_TAG_REGEX_MCP = /<symbol\b([^>]*)>/gi;
10972
+ function _extractSvgAttrMcp(attrs, name) {
10973
+ const match = attrs.match(new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i"));
10974
+ return match ? match[1] : null;
10975
+ }
10976
+ function _extractSvgSpriteSymbolsMcp(svg) {
10977
+ const trimmed = svg.trim();
10978
+ if (!trimmed) return { error: "Empty input" };
10979
+ const symbols = [];
10980
+ let skippedCount = 0;
10981
+ for (const match of trimmed.matchAll(_SVG_SYMBOL_TAG_REGEX_MCP)) {
10982
+ const attrs = match[1];
10983
+ const id = _extractSvgAttrMcp(attrs, "id");
10984
+ if (!id) {
10985
+ skippedCount++;
10986
+ continue;
10987
+ }
10988
+ symbols.push({
10989
+ id,
10990
+ viewBox: _extractSvgAttrMcp(attrs, "viewBox"),
10991
+ width: _extractSvgAttrMcp(attrs, "width"),
10992
+ height: _extractSvgAttrMcp(attrs, "height"),
10993
+ usageSnippet: `<svg><use href="#${id}"></use></svg>`
10994
+ });
10995
+ }
10996
+ if (symbols.length === 0 && skippedCount === 0) {
10997
+ return { error: "No <symbol> elements found" };
10998
+ }
10999
+ return { symbolCount: symbols.length, symbols, skippedCount };
11000
+ }
11001
+ server.tool(
11002
+ "extract_svg_sprite_symbols",
11003
+ `Parse an SVG sprite sheet's <symbol> definitions and list each one's id, viewBox, width, and height, alongside a ready-to-paste <use href="#id"></use> usage snippet. A symbol with no id is skipped (counted in skippedCount) since it can't be referenced by a <use> element at all.`,
11004
+ {
11005
+ svg: z.string().min(1).describe("The SVG sprite sheet markup")
11006
+ },
11007
+ async ({ svg }) => {
11008
+ const result = _extractSvgSpriteSymbolsMcp(svg);
11009
+ if ("error" in result) return errText(result);
11010
+ return text(JSON.stringify(result, null, 2));
11011
+ }
11012
+ );
10754
11013
  var transport = new StdioServerTransport();
10755
11014
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "MCP server exposing 198 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",