@utilix-tech/mcp 0.65.0 → 0.67.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.67.0
6
+ - 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.
7
+
8
+ ## 0.66.0
9
+ - 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.
10
+ - 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.
11
+
5
12
  ## 0.65.0
6
13
  - Add `build_hsts_header`, `parse_hsts_header` tools: build a Strict-Transport-Security response header from a max-age plus includeSubDomains/preload flags, flagging combinations that won't qualify for browser HSTS preload lists, or parse an existing header into the same shape.
7
14
  - Add `align_markdown_tables` tool: reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, preserving left/center/right alignment markers and skipping tables inside fenced code blocks.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 223 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 228 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 (40 tools)
208
+ ### API / Network (44 tools)
209
209
  | Tool | Description |
210
210
  |------|-------------|
211
211
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -250,6 +250,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
250
250
  | `parse_multipart_form_data` | Parse a raw multipart/form-data request body into its individual parts: field name, filename, headers, and decoded value, per RFC 7578 |
251
251
  | `build_hsts_header` | Build a Strict-Transport-Security response header from a max-age plus includeSubDomains/preload flags, flagging combinations that won't qualify for browser HSTS preload lists |
252
252
  | `parse_hsts_header` | Parse an existing Strict-Transport-Security header value into the same shape, with preload-list eligibility flagged |
253
+ | `build_x_robots_tag` | Build an X-Robots-Tag response header from directives (noindex, nofollow, max-snippet, etc.), optionally scoped to a crawler user-agent |
254
+ | `parse_x_robots_tag` | Parse an existing X-Robots-Tag header value into the same shape |
255
+ | `lookup_websocket_opcode` | Look up what a WebSocket frame opcode means, per RFC 6455's 4-bit opcode space |
256
+ | `list_websocket_opcodes` | List every WebSocket frame opcode (0x0-0xF) |
253
257
 
254
258
  ### Color (4 tools)
255
259
  | Tool | Description |
@@ -272,6 +276,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
272
276
  | `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
273
277
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
274
278
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
279
+ | `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 |
275
280
  | `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 |
276
281
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
277
282
  | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
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";
@@ -10221,6 +10338,182 @@ server.tool(
10221
10338
  return text(JSON.stringify(result, null, 2));
10222
10339
  }
10223
10340
  );
10341
+ var _X_ROBOTS_TAG_FLAGS_MCP = /* @__PURE__ */ new Set(["noindex", "nofollow", "noarchive", "nosnippet", "notranslate", "noimageindex", "none", "all", "indexifembedded"]);
10342
+ var _X_ROBOTS_TAG_FLAG_NOTES_MCP = {
10343
+ noindex: "Do not show this page in search results.",
10344
+ nofollow: "Do not follow, or pass ranking credit through, any links on this page.",
10345
+ noarchive: "Do not show a cached copy of this page in search results.",
10346
+ nosnippet: "Do not show a text snippet or video preview for this page in search results.",
10347
+ notranslate: "Do not offer a translation of this page in search results.",
10348
+ noimageindex: "Do not index images on this page.",
10349
+ none: "Shorthand for noindex plus nofollow combined.",
10350
+ all: "Explicitly allow indexing and following, the default with no directives at all.",
10351
+ indexifembedded: "Allow indexing of this page's content when embedded via iframe in another page, even if this page also carries noindex. Only Google supports this directive."
10352
+ };
10353
+ var _X_ROBOTS_TAG_VALUED_DIRECTIVES_MCP = /* @__PURE__ */ new Set(["max-snippet", "max-image-preview", "max-video-preview", "unavailable_after"]);
10354
+ function _isXRobotsTagFlagMcp(value) {
10355
+ return _X_ROBOTS_TAG_FLAGS_MCP.has(value);
10356
+ }
10357
+ function _buildXRobotsTagMcp(options) {
10358
+ const { flags = [], maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent } = options;
10359
+ for (const flag of flags) {
10360
+ if (!_isXRobotsTagFlagMcp(flag)) return { error: `Unknown directive "${flag}"` };
10361
+ }
10362
+ if (maxSnippet !== void 0 && (!Number.isInteger(maxSnippet) || maxSnippet < -1)) return { error: "maxSnippet must be an integer >= -1" };
10363
+ if (maxVideoPreview !== void 0 && (!Number.isInteger(maxVideoPreview) || maxVideoPreview < -1)) return { error: "maxVideoPreview must be an integer >= -1" };
10364
+ if (maxImagePreview !== void 0 && !["none", "standard", "large"].includes(maxImagePreview)) return { error: 'maxImagePreview must be one of "none", "standard", "large"' };
10365
+ if (unavailableAfter !== void 0 && (unavailableAfter.trim() === "" || Number.isNaN(Date.parse(unavailableAfter)))) return { error: "unavailableAfter must be a valid, parseable date string" };
10366
+ const uniqueFlags = Array.from(new Set(flags));
10367
+ if (uniqueFlags.length === 0 && maxSnippet === void 0 && maxImagePreview === void 0 && maxVideoPreview === void 0 && unavailableAfter === void 0) {
10368
+ return { error: "At least one directive is required" };
10369
+ }
10370
+ const warnings = [];
10371
+ if (uniqueFlags.includes("none") && (uniqueFlags.includes("noindex") || uniqueFlags.includes("nofollow"))) {
10372
+ warnings.push('"none" already implies noindex and nofollow; combining it with either is redundant.');
10373
+ }
10374
+ if (uniqueFlags.includes("all") && uniqueFlags.some((f) => f !== "all" && f !== "indexifembedded")) {
10375
+ warnings.push('"all" explicitly allows indexing and following, which contradicts any restrictive directive combined with it.');
10376
+ }
10377
+ const directives = [...uniqueFlags];
10378
+ const notes = uniqueFlags.map((f) => `${f}: ${_X_ROBOTS_TAG_FLAG_NOTES_MCP[f]}`);
10379
+ if (maxSnippet !== void 0) {
10380
+ directives.push(`max-snippet: ${maxSnippet}`);
10381
+ notes.push(`max-snippet: ${maxSnippet === 0 ? "Show no text snippet at all." : maxSnippet === -1 ? "No limit on snippet length." : `Limit the text snippet to at most ${maxSnippet} characters.`}`);
10382
+ }
10383
+ if (maxImagePreview !== void 0) {
10384
+ directives.push(`max-image-preview: ${maxImagePreview}`);
10385
+ notes.push(`max-image-preview: Limit any image preview shown in search results to the "${maxImagePreview}" size.`);
10386
+ }
10387
+ if (maxVideoPreview !== void 0) {
10388
+ directives.push(`max-video-preview: ${maxVideoPreview}`);
10389
+ notes.push(`max-video-preview: ${maxVideoPreview === 0 ? "Show no video preview at all, a static image only." : maxVideoPreview === -1 ? "No limit on video preview length." : `Limit the video preview to at most ${maxVideoPreview} seconds.`}`);
10390
+ }
10391
+ if (unavailableAfter !== void 0) {
10392
+ directives.push(`unavailable_after: ${unavailableAfter}`);
10393
+ notes.push("unavailable_after: Stop showing this page in search results after this date.");
10394
+ }
10395
+ const directivesJoined = directives.join(", ");
10396
+ const header = userAgent ? `${userAgent}: ${directivesJoined}` : directivesJoined;
10397
+ return { header, headerName: "X-Robots-Tag", snippet: `X-Robots-Tag: ${header}`, directives, notes, warnings };
10398
+ }
10399
+ function _parseXRobotsTagMcp(raw) {
10400
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
10401
+ let withoutLabel = raw.trim().replace(/^x-robots-tag:\s*/i, "");
10402
+ let userAgent;
10403
+ const firstColon = withoutLabel.indexOf(":");
10404
+ const firstComma = withoutLabel.indexOf(",");
10405
+ if (firstColon !== -1 && (firstComma === -1 || firstColon < firstComma)) {
10406
+ const candidate = withoutLabel.slice(0, firstColon).trim();
10407
+ const candidateLower = candidate.toLowerCase();
10408
+ if (candidate !== "" && !_X_ROBOTS_TAG_VALUED_DIRECTIVES_MCP.has(candidateLower) && !_isXRobotsTagFlagMcp(candidateLower)) {
10409
+ userAgent = candidate;
10410
+ withoutLabel = withoutLabel.slice(firstColon + 1).trim();
10411
+ }
10412
+ }
10413
+ const tokens = withoutLabel.split(",").map((t) => t.trim()).filter((t) => t !== "");
10414
+ if (tokens.length === 0) return { error: "No directives found" };
10415
+ const flags = [];
10416
+ let maxSnippet;
10417
+ let maxImagePreview;
10418
+ let maxVideoPreview;
10419
+ let unavailableAfter;
10420
+ for (const token of tokens) {
10421
+ const eq = token.indexOf(":");
10422
+ const name = (eq === -1 ? token : token.slice(0, eq)).trim().toLowerCase();
10423
+ const value = eq === -1 ? "" : token.slice(eq + 1).trim();
10424
+ if (eq === -1) {
10425
+ if (!_isXRobotsTagFlagMcp(name)) return { error: `Unrecognized directive "${token}"` };
10426
+ flags.push(name);
10427
+ continue;
10428
+ }
10429
+ if (name === "max-snippet") {
10430
+ const n = Number(value);
10431
+ if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-snippet value "${value}" is not a valid integer >= -1` };
10432
+ maxSnippet = n;
10433
+ } else if (name === "max-image-preview") {
10434
+ if (!["none", "standard", "large"].includes(value)) return { error: `max-image-preview value "${value}" must be one of "none", "standard", "large"` };
10435
+ maxImagePreview = value;
10436
+ } else if (name === "max-video-preview") {
10437
+ const n = Number(value);
10438
+ if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-video-preview value "${value}" is not a valid integer >= -1` };
10439
+ maxVideoPreview = n;
10440
+ } else if (name === "unavailable_after") {
10441
+ if (value === "" || Number.isNaN(Date.parse(value))) return { error: `unavailable_after value "${value}" is not a valid, parseable date` };
10442
+ unavailableAfter = value;
10443
+ } else {
10444
+ return { error: `Unrecognized directive "${token}"` };
10445
+ }
10446
+ }
10447
+ return _buildXRobotsTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent });
10448
+ }
10449
+ server.tool(
10450
+ "build_x_robots_tag",
10451
+ `Build an X-Robots-Tag response-header value 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, per Google's documented robots directive syntax. Optionally scope the header to a single crawler with userAgent (e.g. "googlebot").`,
10452
+ {
10453
+ flags: z.array(z.enum(["noindex", "nofollow", "noarchive", "nosnippet", "notranslate", "noimageindex", "none", "all", "indexifembedded"])).optional().describe("Flag directives to include"),
10454
+ maxSnippet: z.number().int().min(-1).optional().describe("Max text snippet length in characters; 0 shows no snippet, -1 means no limit"),
10455
+ maxImagePreview: z.enum(["none", "standard", "large"]).optional().describe("Max image preview size shown in search results"),
10456
+ maxVideoPreview: z.number().int().min(-1).optional().describe("Max video preview length in seconds; 0 shows no preview, -1 means no limit"),
10457
+ unavailableAfter: z.string().optional().describe("Stop showing this page in search results after this date/time"),
10458
+ userAgent: z.string().optional().describe('Optional crawler name to scope the header to, e.g. "googlebot"')
10459
+ },
10460
+ async ({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent }) => {
10461
+ const result = _buildXRobotsTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent });
10462
+ if ("error" in result) return errText(result);
10463
+ return text(JSON.stringify(result, null, 2));
10464
+ }
10465
+ );
10466
+ server.tool(
10467
+ "parse_x_robots_tag",
10468
+ 'Parse an existing X-Robots-Tag header value (with or without a leading "X-Robots-Tag:" label, and with or without a leading crawler user-agent token) into the same shape build_x_robots_tag returns.',
10469
+ {
10470
+ header: z.string().min(1).describe("The X-Robots-Tag header value to parse")
10471
+ },
10472
+ async ({ header }) => {
10473
+ const result = _parseXRobotsTagMcp(header);
10474
+ if ("error" in result) return errText(result);
10475
+ return text(JSON.stringify(result, null, 2));
10476
+ }
10477
+ );
10478
+ var _WELL_KNOWN_OPCODES_MCP = {
10479
+ 0: { name: "Continuation Frame", description: "Continues a fragmented message whose first frame was a text or binary frame sent with the FIN bit unset.", category: "data" },
10480
+ 1: { name: "Text Frame", description: "Carries a UTF-8 encoded text payload.", category: "data" },
10481
+ 2: { name: "Binary Frame", description: "Carries an arbitrary binary payload.", category: "data" },
10482
+ 8: { name: "Connection Close", description: "Initiates or acknowledges closing the connection, optionally carrying a 2-byte close code and a UTF-8 reason in its payload.", category: "control" },
10483
+ 9: { name: "Ping", description: "A control frame used to check the connection is alive; the receiver should reply with a Pong carrying the same application data.", category: "control" },
10484
+ 10: { name: "Pong", description: "Sent in response to a Ping (or unprompted as a heartbeat), carrying the same application data as the Ping it answers.", category: "control" }
10485
+ };
10486
+ var _RESERVED_NON_CONTROL_OPCODES_MCP = /* @__PURE__ */ new Set([3, 4, 5, 6, 7]);
10487
+ function _describeOpcodeMcp(opcode) {
10488
+ const known = _WELL_KNOWN_OPCODES_MCP[opcode];
10489
+ const hex = `0x${opcode.toString(16).toUpperCase()}`;
10490
+ if (known) return { opcode, hex, name: known.name, description: known.description, category: known.category, isControl: known.category === "control" };
10491
+ if (_RESERVED_NON_CONTROL_OPCODES_MCP.has(opcode)) {
10492
+ return { opcode, hex, name: "Reserved (non-control)", description: "Reserved for future non-control frame types by a later revision of the WebSocket protocol. No meaning is defined yet; a compliant endpoint must fail the connection if it receives one.", category: "reserved-non-control", isControl: false };
10493
+ }
10494
+ return { opcode, hex, name: "Reserved (control)", description: "Reserved for future control frame types by a later revision of the WebSocket protocol. No meaning is defined yet; a compliant endpoint must fail the connection if it receives one.", category: "reserved-control", isControl: true };
10495
+ }
10496
+ server.tool(
10497
+ "lookup_websocket_opcode",
10498
+ "Look up what a WebSocket frame opcode means per RFC 6455: 0x0-0x2 are data frames (continuation, text, binary), 0x8-0xA are control frames (close, ping, pong), and the rest of the 4-bit opcode space is reserved for future protocol revisions.",
10499
+ {
10500
+ opcode: z.number().int().min(0).max(15).describe("The WebSocket frame opcode to look up (0-15 / 0x0-0xF)")
10501
+ },
10502
+ async ({ opcode }) => {
10503
+ const result = _describeOpcodeMcp(opcode);
10504
+ return text(JSON.stringify(result, null, 2));
10505
+ }
10506
+ );
10507
+ server.tool(
10508
+ "list_websocket_opcodes",
10509
+ "List every WebSocket frame opcode (0x0-0xF), the full 4-bit opcode space defined by RFC 6455, in ascending order.",
10510
+ {},
10511
+ async () => {
10512
+ const opcodes = [];
10513
+ for (let opcode = 0; opcode <= 15; opcode++) opcodes.push(_describeOpcodeMcp(opcode));
10514
+ return text(JSON.stringify({ opcodes }, null, 2));
10515
+ }
10516
+ );
10224
10517
  var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
10225
10518
  function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
10226
10519
  const round2 = (n) => Math.round(n * 100) / 100;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.65.0",
3
+ "version": "0.67.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",