@utilix-tech/mcp 0.65.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +6 -2
- package/dist/index.js +176 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
|
|
4
4
|
|
|
5
|
+
## 0.66.0
|
|
6
|
+
- 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
|
+
- 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.
|
|
8
|
+
|
|
5
9
|
## 0.65.0
|
|
6
10
|
- 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
11
|
- 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
|
|
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.
|
|
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 (
|
|
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 |
|
package/dist/index.js
CHANGED
|
@@ -10221,6 +10221,182 @@ server.tool(
|
|
|
10221
10221
|
return text(JSON.stringify(result, null, 2));
|
|
10222
10222
|
}
|
|
10223
10223
|
);
|
|
10224
|
+
var _X_ROBOTS_TAG_FLAGS_MCP = /* @__PURE__ */ new Set(["noindex", "nofollow", "noarchive", "nosnippet", "notranslate", "noimageindex", "none", "all", "indexifembedded"]);
|
|
10225
|
+
var _X_ROBOTS_TAG_FLAG_NOTES_MCP = {
|
|
10226
|
+
noindex: "Do not show this page in search results.",
|
|
10227
|
+
nofollow: "Do not follow, or pass ranking credit through, any links on this page.",
|
|
10228
|
+
noarchive: "Do not show a cached copy of this page in search results.",
|
|
10229
|
+
nosnippet: "Do not show a text snippet or video preview for this page in search results.",
|
|
10230
|
+
notranslate: "Do not offer a translation of this page in search results.",
|
|
10231
|
+
noimageindex: "Do not index images on this page.",
|
|
10232
|
+
none: "Shorthand for noindex plus nofollow combined.",
|
|
10233
|
+
all: "Explicitly allow indexing and following, the default with no directives at all.",
|
|
10234
|
+
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."
|
|
10235
|
+
};
|
|
10236
|
+
var _X_ROBOTS_TAG_VALUED_DIRECTIVES_MCP = /* @__PURE__ */ new Set(["max-snippet", "max-image-preview", "max-video-preview", "unavailable_after"]);
|
|
10237
|
+
function _isXRobotsTagFlagMcp(value) {
|
|
10238
|
+
return _X_ROBOTS_TAG_FLAGS_MCP.has(value);
|
|
10239
|
+
}
|
|
10240
|
+
function _buildXRobotsTagMcp(options) {
|
|
10241
|
+
const { flags = [], maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent } = options;
|
|
10242
|
+
for (const flag of flags) {
|
|
10243
|
+
if (!_isXRobotsTagFlagMcp(flag)) return { error: `Unknown directive "${flag}"` };
|
|
10244
|
+
}
|
|
10245
|
+
if (maxSnippet !== void 0 && (!Number.isInteger(maxSnippet) || maxSnippet < -1)) return { error: "maxSnippet must be an integer >= -1" };
|
|
10246
|
+
if (maxVideoPreview !== void 0 && (!Number.isInteger(maxVideoPreview) || maxVideoPreview < -1)) return { error: "maxVideoPreview must be an integer >= -1" };
|
|
10247
|
+
if (maxImagePreview !== void 0 && !["none", "standard", "large"].includes(maxImagePreview)) return { error: 'maxImagePreview must be one of "none", "standard", "large"' };
|
|
10248
|
+
if (unavailableAfter !== void 0 && (unavailableAfter.trim() === "" || Number.isNaN(Date.parse(unavailableAfter)))) return { error: "unavailableAfter must be a valid, parseable date string" };
|
|
10249
|
+
const uniqueFlags = Array.from(new Set(flags));
|
|
10250
|
+
if (uniqueFlags.length === 0 && maxSnippet === void 0 && maxImagePreview === void 0 && maxVideoPreview === void 0 && unavailableAfter === void 0) {
|
|
10251
|
+
return { error: "At least one directive is required" };
|
|
10252
|
+
}
|
|
10253
|
+
const warnings = [];
|
|
10254
|
+
if (uniqueFlags.includes("none") && (uniqueFlags.includes("noindex") || uniqueFlags.includes("nofollow"))) {
|
|
10255
|
+
warnings.push('"none" already implies noindex and nofollow; combining it with either is redundant.');
|
|
10256
|
+
}
|
|
10257
|
+
if (uniqueFlags.includes("all") && uniqueFlags.some((f) => f !== "all" && f !== "indexifembedded")) {
|
|
10258
|
+
warnings.push('"all" explicitly allows indexing and following, which contradicts any restrictive directive combined with it.');
|
|
10259
|
+
}
|
|
10260
|
+
const directives = [...uniqueFlags];
|
|
10261
|
+
const notes = uniqueFlags.map((f) => `${f}: ${_X_ROBOTS_TAG_FLAG_NOTES_MCP[f]}`);
|
|
10262
|
+
if (maxSnippet !== void 0) {
|
|
10263
|
+
directives.push(`max-snippet: ${maxSnippet}`);
|
|
10264
|
+
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.`}`);
|
|
10265
|
+
}
|
|
10266
|
+
if (maxImagePreview !== void 0) {
|
|
10267
|
+
directives.push(`max-image-preview: ${maxImagePreview}`);
|
|
10268
|
+
notes.push(`max-image-preview: Limit any image preview shown in search results to the "${maxImagePreview}" size.`);
|
|
10269
|
+
}
|
|
10270
|
+
if (maxVideoPreview !== void 0) {
|
|
10271
|
+
directives.push(`max-video-preview: ${maxVideoPreview}`);
|
|
10272
|
+
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.`}`);
|
|
10273
|
+
}
|
|
10274
|
+
if (unavailableAfter !== void 0) {
|
|
10275
|
+
directives.push(`unavailable_after: ${unavailableAfter}`);
|
|
10276
|
+
notes.push("unavailable_after: Stop showing this page in search results after this date.");
|
|
10277
|
+
}
|
|
10278
|
+
const directivesJoined = directives.join(", ");
|
|
10279
|
+
const header = userAgent ? `${userAgent}: ${directivesJoined}` : directivesJoined;
|
|
10280
|
+
return { header, headerName: "X-Robots-Tag", snippet: `X-Robots-Tag: ${header}`, directives, notes, warnings };
|
|
10281
|
+
}
|
|
10282
|
+
function _parseXRobotsTagMcp(raw) {
|
|
10283
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
10284
|
+
let withoutLabel = raw.trim().replace(/^x-robots-tag:\s*/i, "");
|
|
10285
|
+
let userAgent;
|
|
10286
|
+
const firstColon = withoutLabel.indexOf(":");
|
|
10287
|
+
const firstComma = withoutLabel.indexOf(",");
|
|
10288
|
+
if (firstColon !== -1 && (firstComma === -1 || firstColon < firstComma)) {
|
|
10289
|
+
const candidate = withoutLabel.slice(0, firstColon).trim();
|
|
10290
|
+
const candidateLower = candidate.toLowerCase();
|
|
10291
|
+
if (candidate !== "" && !_X_ROBOTS_TAG_VALUED_DIRECTIVES_MCP.has(candidateLower) && !_isXRobotsTagFlagMcp(candidateLower)) {
|
|
10292
|
+
userAgent = candidate;
|
|
10293
|
+
withoutLabel = withoutLabel.slice(firstColon + 1).trim();
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
const tokens = withoutLabel.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
10297
|
+
if (tokens.length === 0) return { error: "No directives found" };
|
|
10298
|
+
const flags = [];
|
|
10299
|
+
let maxSnippet;
|
|
10300
|
+
let maxImagePreview;
|
|
10301
|
+
let maxVideoPreview;
|
|
10302
|
+
let unavailableAfter;
|
|
10303
|
+
for (const token of tokens) {
|
|
10304
|
+
const eq = token.indexOf(":");
|
|
10305
|
+
const name = (eq === -1 ? token : token.slice(0, eq)).trim().toLowerCase();
|
|
10306
|
+
const value = eq === -1 ? "" : token.slice(eq + 1).trim();
|
|
10307
|
+
if (eq === -1) {
|
|
10308
|
+
if (!_isXRobotsTagFlagMcp(name)) return { error: `Unrecognized directive "${token}"` };
|
|
10309
|
+
flags.push(name);
|
|
10310
|
+
continue;
|
|
10311
|
+
}
|
|
10312
|
+
if (name === "max-snippet") {
|
|
10313
|
+
const n = Number(value);
|
|
10314
|
+
if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-snippet value "${value}" is not a valid integer >= -1` };
|
|
10315
|
+
maxSnippet = n;
|
|
10316
|
+
} else if (name === "max-image-preview") {
|
|
10317
|
+
if (!["none", "standard", "large"].includes(value)) return { error: `max-image-preview value "${value}" must be one of "none", "standard", "large"` };
|
|
10318
|
+
maxImagePreview = value;
|
|
10319
|
+
} else if (name === "max-video-preview") {
|
|
10320
|
+
const n = Number(value);
|
|
10321
|
+
if (value === "" || !Number.isInteger(n) || n < -1) return { error: `max-video-preview value "${value}" is not a valid integer >= -1` };
|
|
10322
|
+
maxVideoPreview = n;
|
|
10323
|
+
} else if (name === "unavailable_after") {
|
|
10324
|
+
if (value === "" || Number.isNaN(Date.parse(value))) return { error: `unavailable_after value "${value}" is not a valid, parseable date` };
|
|
10325
|
+
unavailableAfter = value;
|
|
10326
|
+
} else {
|
|
10327
|
+
return { error: `Unrecognized directive "${token}"` };
|
|
10328
|
+
}
|
|
10329
|
+
}
|
|
10330
|
+
return _buildXRobotsTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent });
|
|
10331
|
+
}
|
|
10332
|
+
server.tool(
|
|
10333
|
+
"build_x_robots_tag",
|
|
10334
|
+
`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").`,
|
|
10335
|
+
{
|
|
10336
|
+
flags: z.array(z.enum(["noindex", "nofollow", "noarchive", "nosnippet", "notranslate", "noimageindex", "none", "all", "indexifembedded"])).optional().describe("Flag directives to include"),
|
|
10337
|
+
maxSnippet: z.number().int().min(-1).optional().describe("Max text snippet length in characters; 0 shows no snippet, -1 means no limit"),
|
|
10338
|
+
maxImagePreview: z.enum(["none", "standard", "large"]).optional().describe("Max image preview size shown in search results"),
|
|
10339
|
+
maxVideoPreview: z.number().int().min(-1).optional().describe("Max video preview length in seconds; 0 shows no preview, -1 means no limit"),
|
|
10340
|
+
unavailableAfter: z.string().optional().describe("Stop showing this page in search results after this date/time"),
|
|
10341
|
+
userAgent: z.string().optional().describe('Optional crawler name to scope the header to, e.g. "googlebot"')
|
|
10342
|
+
},
|
|
10343
|
+
async ({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent }) => {
|
|
10344
|
+
const result = _buildXRobotsTagMcp({ flags, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, userAgent });
|
|
10345
|
+
if ("error" in result) return errText(result);
|
|
10346
|
+
return text(JSON.stringify(result, null, 2));
|
|
10347
|
+
}
|
|
10348
|
+
);
|
|
10349
|
+
server.tool(
|
|
10350
|
+
"parse_x_robots_tag",
|
|
10351
|
+
'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.',
|
|
10352
|
+
{
|
|
10353
|
+
header: z.string().min(1).describe("The X-Robots-Tag header value to parse")
|
|
10354
|
+
},
|
|
10355
|
+
async ({ header }) => {
|
|
10356
|
+
const result = _parseXRobotsTagMcp(header);
|
|
10357
|
+
if ("error" in result) return errText(result);
|
|
10358
|
+
return text(JSON.stringify(result, null, 2));
|
|
10359
|
+
}
|
|
10360
|
+
);
|
|
10361
|
+
var _WELL_KNOWN_OPCODES_MCP = {
|
|
10362
|
+
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" },
|
|
10363
|
+
1: { name: "Text Frame", description: "Carries a UTF-8 encoded text payload.", category: "data" },
|
|
10364
|
+
2: { name: "Binary Frame", description: "Carries an arbitrary binary payload.", category: "data" },
|
|
10365
|
+
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" },
|
|
10366
|
+
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" },
|
|
10367
|
+
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" }
|
|
10368
|
+
};
|
|
10369
|
+
var _RESERVED_NON_CONTROL_OPCODES_MCP = /* @__PURE__ */ new Set([3, 4, 5, 6, 7]);
|
|
10370
|
+
function _describeOpcodeMcp(opcode) {
|
|
10371
|
+
const known = _WELL_KNOWN_OPCODES_MCP[opcode];
|
|
10372
|
+
const hex = `0x${opcode.toString(16).toUpperCase()}`;
|
|
10373
|
+
if (known) return { opcode, hex, name: known.name, description: known.description, category: known.category, isControl: known.category === "control" };
|
|
10374
|
+
if (_RESERVED_NON_CONTROL_OPCODES_MCP.has(opcode)) {
|
|
10375
|
+
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 };
|
|
10376
|
+
}
|
|
10377
|
+
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 };
|
|
10378
|
+
}
|
|
10379
|
+
server.tool(
|
|
10380
|
+
"lookup_websocket_opcode",
|
|
10381
|
+
"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.",
|
|
10382
|
+
{
|
|
10383
|
+
opcode: z.number().int().min(0).max(15).describe("The WebSocket frame opcode to look up (0-15 / 0x0-0xF)")
|
|
10384
|
+
},
|
|
10385
|
+
async ({ opcode }) => {
|
|
10386
|
+
const result = _describeOpcodeMcp(opcode);
|
|
10387
|
+
return text(JSON.stringify(result, null, 2));
|
|
10388
|
+
}
|
|
10389
|
+
);
|
|
10390
|
+
server.tool(
|
|
10391
|
+
"list_websocket_opcodes",
|
|
10392
|
+
"List every WebSocket frame opcode (0x0-0xF), the full 4-bit opcode space defined by RFC 6455, in ascending order.",
|
|
10393
|
+
{},
|
|
10394
|
+
async () => {
|
|
10395
|
+
const opcodes = [];
|
|
10396
|
+
for (let opcode = 0; opcode <= 15; opcode++) opcodes.push(_describeOpcodeMcp(opcode));
|
|
10397
|
+
return text(JSON.stringify({ opcodes }, null, 2));
|
|
10398
|
+
}
|
|
10399
|
+
);
|
|
10224
10400
|
var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
|
|
10225
10401
|
function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
|
|
10226
10402
|
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.
|
|
3
|
+
"version": "0.66.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",
|