@utilix-tech/mcp 0.64.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 +8 -0
- package/README.md +10 -3
- package/dist/index.js +429 -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.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
|
+
|
|
9
|
+
## 0.65.0
|
|
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.
|
|
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.
|
|
12
|
+
|
|
5
13
|
## 0.64.0
|
|
6
14
|
- Add `calculate_loan_refinance` tool: 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.
|
|
7
15
|
- Add `parse_multipart_form_data` tool: parse a raw multipart/form-data request body into its individual parts (field name, filename, headers, decoded value) per RFC 7578, with an optional Content-Type header value to extract the boundary from.
|
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 |
|
|
@@ -248,6 +248,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
248
248
|
| `lookup_close_code` | Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges |
|
|
249
249
|
| `list_close_codes` | List every well-known WebSocket close code (1000-1015) |
|
|
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
|
+
| `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
|
+
| `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) |
|
|
251
257
|
|
|
252
258
|
### Color (4 tools)
|
|
253
259
|
| Tool | Description |
|
|
@@ -277,7 +283,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
277
283
|
| `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 |
|
|
278
284
|
| `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 |
|
|
279
285
|
|
|
280
|
-
### Data (
|
|
286
|
+
### Data (16 tools)
|
|
281
287
|
| Tool | Description |
|
|
282
288
|
|------|-------------|
|
|
283
289
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -295,6 +301,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
295
301
|
| `calculate_markdown_task_list_progress` | Parse a Markdown document's checkbox task list and report completion percentage overall and per section, grouped by the nearest preceding heading |
|
|
296
302
|
| `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
|
|
297
303
|
| `infer_csv_column_types` | Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string), flagging nullable columns and empty-cell counts |
|
|
304
|
+
| `align_markdown_tables` | Reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, preserving left/center/right alignment markers |
|
|
298
305
|
|
|
299
306
|
### CSS (2 tools)
|
|
300
307
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -6120,6 +6120,153 @@ server.tool(
|
|
|
6120
6120
|
return text(JSON.stringify(result, null, 2));
|
|
6121
6121
|
}
|
|
6122
6122
|
);
|
|
6123
|
+
var _MD_TABLE_DELIM_CELL_RE_MCP = /^:?-+:?$/;
|
|
6124
|
+
function _splitMarkdownTableRowMcp(line) {
|
|
6125
|
+
const raw = [];
|
|
6126
|
+
let current = "";
|
|
6127
|
+
for (let i = 0; i < line.length; i++) {
|
|
6128
|
+
const ch = line[i];
|
|
6129
|
+
if (ch === "\\" && line[i + 1] === "|") {
|
|
6130
|
+
current += "\\|";
|
|
6131
|
+
i++;
|
|
6132
|
+
continue;
|
|
6133
|
+
}
|
|
6134
|
+
if (ch === "|") {
|
|
6135
|
+
raw.push(current);
|
|
6136
|
+
current = "";
|
|
6137
|
+
continue;
|
|
6138
|
+
}
|
|
6139
|
+
current += ch;
|
|
6140
|
+
}
|
|
6141
|
+
raw.push(current);
|
|
6142
|
+
const cells = raw.map((c) => c.trim());
|
|
6143
|
+
if (cells.length > 1 && cells[0] === "") cells.shift();
|
|
6144
|
+
if (cells.length > 1 && cells[cells.length - 1] === "") cells.pop();
|
|
6145
|
+
return cells;
|
|
6146
|
+
}
|
|
6147
|
+
function _isMarkdownTableDelimiterRowMcp(line) {
|
|
6148
|
+
const trimmed = line.trim();
|
|
6149
|
+
if (trimmed === "" || !trimmed.includes("-")) return false;
|
|
6150
|
+
const cells = _splitMarkdownTableRowMcp(trimmed);
|
|
6151
|
+
if (cells.length === 0) return false;
|
|
6152
|
+
return cells.every((c) => _MD_TABLE_DELIM_CELL_RE_MCP.test(c));
|
|
6153
|
+
}
|
|
6154
|
+
function _markdownTableCellAlignmentMcp(delimCell) {
|
|
6155
|
+
const left = delimCell.startsWith(":");
|
|
6156
|
+
const right = delimCell.endsWith(":");
|
|
6157
|
+
if (left && right) return "center";
|
|
6158
|
+
if (right) return "right";
|
|
6159
|
+
if (left) return "left";
|
|
6160
|
+
return "none";
|
|
6161
|
+
}
|
|
6162
|
+
function _padMarkdownTableCellMcp(text2, width, alignment) {
|
|
6163
|
+
const diff = Math.max(0, width - [...text2].length);
|
|
6164
|
+
if (alignment === "right") return " ".repeat(diff) + text2;
|
|
6165
|
+
if (alignment === "center") {
|
|
6166
|
+
const left = Math.floor(diff / 2);
|
|
6167
|
+
const right = diff - left;
|
|
6168
|
+
return " ".repeat(left) + text2 + " ".repeat(right);
|
|
6169
|
+
}
|
|
6170
|
+
return text2 + " ".repeat(diff);
|
|
6171
|
+
}
|
|
6172
|
+
function _markdownTableDelimiterCellMcp(width, alignment) {
|
|
6173
|
+
switch (alignment) {
|
|
6174
|
+
case "left":
|
|
6175
|
+
return ":" + "-".repeat(Math.max(1, width - 1));
|
|
6176
|
+
case "right":
|
|
6177
|
+
return "-".repeat(Math.max(1, width - 1)) + ":";
|
|
6178
|
+
case "center":
|
|
6179
|
+
return ":" + "-".repeat(Math.max(1, width - 2)) + ":";
|
|
6180
|
+
default:
|
|
6181
|
+
return "-".repeat(width);
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
function _alignMarkdownTablesMcp(markdown) {
|
|
6185
|
+
if (typeof markdown !== "string") return { error: "markdown must be a string" };
|
|
6186
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
6187
|
+
const outputLines = [...lines];
|
|
6188
|
+
const tables = [];
|
|
6189
|
+
const warnings = [];
|
|
6190
|
+
let inFence = false;
|
|
6191
|
+
let fenceMarker = "";
|
|
6192
|
+
let i = 0;
|
|
6193
|
+
while (i < lines.length) {
|
|
6194
|
+
const line = lines[i];
|
|
6195
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
|
6196
|
+
if (fenceMatch) {
|
|
6197
|
+
if (!inFence) {
|
|
6198
|
+
inFence = true;
|
|
6199
|
+
fenceMarker = fenceMatch[1][0];
|
|
6200
|
+
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
6201
|
+
inFence = false;
|
|
6202
|
+
}
|
|
6203
|
+
i++;
|
|
6204
|
+
continue;
|
|
6205
|
+
}
|
|
6206
|
+
if (inFence) {
|
|
6207
|
+
i++;
|
|
6208
|
+
continue;
|
|
6209
|
+
}
|
|
6210
|
+
const nextLine = lines[i + 1];
|
|
6211
|
+
if (line.includes("|") && nextLine !== void 0 && _isMarkdownTableDelimiterRowMcp(nextLine)) {
|
|
6212
|
+
const headerCells = _splitMarkdownTableRowMcp(line);
|
|
6213
|
+
const delimCells = _splitMarkdownTableRowMcp(nextLine);
|
|
6214
|
+
if (headerCells.length > 0 && headerCells.length === delimCells.length) {
|
|
6215
|
+
const columnCount = headerCells.length;
|
|
6216
|
+
const alignments = delimCells.map(_markdownTableCellAlignmentMcp);
|
|
6217
|
+
let j = i + 2;
|
|
6218
|
+
const bodyRows = [];
|
|
6219
|
+
while (j < lines.length && lines[j].trim() !== "" && lines[j].includes("|")) {
|
|
6220
|
+
bodyRows.push(_splitMarkdownTableRowMcp(lines[j]));
|
|
6221
|
+
j++;
|
|
6222
|
+
}
|
|
6223
|
+
const normalizedBody = [];
|
|
6224
|
+
for (const row of bodyRows) {
|
|
6225
|
+
if (row.length !== columnCount) {
|
|
6226
|
+
warnings.push(`Table at line ${i + 1}: a row has ${row.length} cell(s) but the header has ${columnCount}; it was padded/truncated to match.`);
|
|
6227
|
+
}
|
|
6228
|
+
normalizedBody.push(Array.from({ length: columnCount }, (_, c) => row[c] ?? ""));
|
|
6229
|
+
}
|
|
6230
|
+
const allRows = [headerCells, ...normalizedBody];
|
|
6231
|
+
const widths = Array.from(
|
|
6232
|
+
{ length: columnCount },
|
|
6233
|
+
(_, c) => Math.max(3, ...allRows.map((r) => [...r[c] ?? ""].length))
|
|
6234
|
+
);
|
|
6235
|
+
const formattedHeader = "| " + headerCells.map((c, idx) => _padMarkdownTableCellMcp(c, widths[idx], alignments[idx])).join(" | ") + " |";
|
|
6236
|
+
const formattedDelim = "| " + widths.map((w, idx) => _markdownTableDelimiterCellMcp(w, alignments[idx])).join(" | ") + " |";
|
|
6237
|
+
const formattedBody = normalizedBody.map((row) => "| " + row.map((c, idx) => _padMarkdownTableCellMcp(c, widths[idx], alignments[idx])).join(" | ") + " |");
|
|
6238
|
+
const formattedLines = [formattedHeader, formattedDelim, ...formattedBody];
|
|
6239
|
+
for (let k = 0; k < formattedLines.length; k++) {
|
|
6240
|
+
outputLines[i + k] = formattedLines[k];
|
|
6241
|
+
}
|
|
6242
|
+
tables.push({
|
|
6243
|
+
startLine: i + 1,
|
|
6244
|
+
endLine: i + formattedLines.length,
|
|
6245
|
+
columnCount,
|
|
6246
|
+
rowCount: normalizedBody.length,
|
|
6247
|
+
alignments
|
|
6248
|
+
});
|
|
6249
|
+
i = j;
|
|
6250
|
+
continue;
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
i++;
|
|
6254
|
+
}
|
|
6255
|
+
if (tables.length === 0) return { error: "No Markdown tables found" };
|
|
6256
|
+
return { output: outputLines.join("\n"), tables, warnings };
|
|
6257
|
+
}
|
|
6258
|
+
server.tool(
|
|
6259
|
+
"align_markdown_tables",
|
|
6260
|
+
"Reformat every GitHub-Flavored-Markdown pipe table in a document so each column's pipes line up visually, padding cells to the widest content in that column and rebuilding the delimiter row to match, while preserving each column's declared alignment. Tables inside fenced code blocks are left untouched.",
|
|
6261
|
+
{
|
|
6262
|
+
markdown: z.string().describe("The Markdown document containing one or more pipe tables")
|
|
6263
|
+
},
|
|
6264
|
+
async ({ markdown }) => {
|
|
6265
|
+
const result = _alignMarkdownTablesMcp(markdown);
|
|
6266
|
+
if ("error" in result) return errText(result);
|
|
6267
|
+
return text(JSON.stringify(result, null, 2));
|
|
6268
|
+
}
|
|
6269
|
+
);
|
|
6123
6270
|
var _COMMIT_DEFAULT_TYPES_MCP = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"];
|
|
6124
6271
|
var _COMMIT_HEADER_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*)(\(([^()]*)\))?(!)?: (.+)$/;
|
|
6125
6272
|
var _COMMIT_FOOTER_LINE_RE_MCP = /^([A-Za-z][A-Za-z0-9-]*|BREAKING CHANGE)(: ?| #)(.*)$/;
|
|
@@ -9968,6 +10115,288 @@ server.tool(
|
|
|
9968
10115
|
return text(JSON.stringify({ codes }, null, 2));
|
|
9969
10116
|
}
|
|
9970
10117
|
);
|
|
10118
|
+
var _HSTS_ONE_YEAR_SECONDS_MCP = 31536e3;
|
|
10119
|
+
function _humanizeHstsMaxAgeMcp(seconds) {
|
|
10120
|
+
if (seconds === 0) return "0 seconds (clears HSTS)";
|
|
10121
|
+
if (seconds % _HSTS_ONE_YEAR_SECONDS_MCP === 0) {
|
|
10122
|
+
const years = seconds / _HSTS_ONE_YEAR_SECONDS_MCP;
|
|
10123
|
+
return `${years} year${years === 1 ? "" : "s"}`;
|
|
10124
|
+
}
|
|
10125
|
+
if (seconds % 86400 === 0) {
|
|
10126
|
+
const days = seconds / 86400;
|
|
10127
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
10128
|
+
}
|
|
10129
|
+
if (seconds % 3600 === 0) {
|
|
10130
|
+
const hours = seconds / 3600;
|
|
10131
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
10132
|
+
}
|
|
10133
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
10134
|
+
}
|
|
10135
|
+
function _buildHstsMcp(options) {
|
|
10136
|
+
const { maxAge, includeSubDomains = false, preload = false } = options;
|
|
10137
|
+
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
10138
|
+
return { error: "maxAge must be a non-negative integer number of seconds" };
|
|
10139
|
+
}
|
|
10140
|
+
const warnings = [];
|
|
10141
|
+
const preloadEligible = maxAge >= _HSTS_ONE_YEAR_SECONDS_MCP && includeSubDomains && preload;
|
|
10142
|
+
if (preload && !includeSubDomains) {
|
|
10143
|
+
warnings.push("Browser HSTS preload lists require includeSubDomains whenever preload is set; this header will not qualify for preloading as configured.");
|
|
10144
|
+
}
|
|
10145
|
+
if (preload && maxAge < _HSTS_ONE_YEAR_SECONDS_MCP) {
|
|
10146
|
+
warnings.push(`Browser HSTS preload lists require max-age of at least ${_HSTS_ONE_YEAR_SECONDS_MCP} (1 year); ${maxAge} is too low to qualify.`);
|
|
10147
|
+
}
|
|
10148
|
+
if (maxAge === 0) {
|
|
10149
|
+
warnings.push("max-age=0 tells the browser to forget this host supports HSTS immediately, this clears HSTS rather than enabling it.");
|
|
10150
|
+
} else if (maxAge < 86400) {
|
|
10151
|
+
warnings.push("A max-age under a day gives very little protection; most real deployments use at least 15552000 (180 days) once confident HTTPS is fully working.");
|
|
10152
|
+
}
|
|
10153
|
+
const parts = [`max-age=${maxAge}`];
|
|
10154
|
+
if (includeSubDomains) parts.push("includeSubDomains");
|
|
10155
|
+
if (preload) parts.push("preload");
|
|
10156
|
+
const header = parts.join("; ");
|
|
10157
|
+
return {
|
|
10158
|
+
header,
|
|
10159
|
+
headerName: "Strict-Transport-Security",
|
|
10160
|
+
snippet: `Strict-Transport-Security: ${header}`,
|
|
10161
|
+
maxAge,
|
|
10162
|
+
maxAgeHuman: _humanizeHstsMaxAgeMcp(maxAge),
|
|
10163
|
+
includeSubDomains,
|
|
10164
|
+
preload,
|
|
10165
|
+
preloadEligible,
|
|
10166
|
+
warnings
|
|
10167
|
+
};
|
|
10168
|
+
}
|
|
10169
|
+
function _parseHstsMcp(raw) {
|
|
10170
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
10171
|
+
const withoutLabel = raw.trim().replace(/^strict-transport-security:\s*/i, "");
|
|
10172
|
+
const tokens = withoutLabel.split(";").map((t) => t.trim()).filter((t) => t !== "");
|
|
10173
|
+
if (tokens.length === 0) return { error: "No directives found" };
|
|
10174
|
+
let maxAge = null;
|
|
10175
|
+
let includeSubDomains = false;
|
|
10176
|
+
let preload = false;
|
|
10177
|
+
for (const token of tokens) {
|
|
10178
|
+
const eq = token.indexOf("=");
|
|
10179
|
+
const name = (eq === -1 ? token : token.slice(0, eq)).trim().toLowerCase();
|
|
10180
|
+
if (name === "max-age") {
|
|
10181
|
+
const rawValue = eq === -1 ? "" : token.slice(eq + 1).trim();
|
|
10182
|
+
const n = Number(rawValue);
|
|
10183
|
+
if (rawValue === "" || !Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
|
10184
|
+
return { error: `max-age value "${rawValue}" is not a valid non-negative integer` };
|
|
10185
|
+
}
|
|
10186
|
+
maxAge = n;
|
|
10187
|
+
} else if (name === "includesubdomains") {
|
|
10188
|
+
includeSubDomains = true;
|
|
10189
|
+
} else if (name === "preload") {
|
|
10190
|
+
preload = true;
|
|
10191
|
+
} else {
|
|
10192
|
+
return { error: `Unrecognized directive "${token}"` };
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
if (maxAge === null) return { error: "max-age directive is required" };
|
|
10196
|
+
return _buildHstsMcp({ maxAge, includeSubDomains, preload });
|
|
10197
|
+
}
|
|
10198
|
+
server.tool(
|
|
10199
|
+
"build_hsts_header",
|
|
10200
|
+
"Build a Strict-Transport-Security response-header value from a max-age (in seconds) plus optional includeSubDomains and preload directives, flagging combinations that will not qualify for browser HSTS preload lists (which require max-age >= 1 year, includeSubDomains, and preload all together).",
|
|
10201
|
+
{
|
|
10202
|
+
maxAge: z.number().int().min(0).describe("How long, in seconds, the browser should remember this host is HTTPS-only, e.g. 31536000 for 1 year"),
|
|
10203
|
+
includeSubDomains: z.boolean().optional().describe("Whether the policy also applies to all subdomains, default false"),
|
|
10204
|
+
preload: z.boolean().optional().describe("Whether to opt into browser HSTS preload lists, default false")
|
|
10205
|
+
},
|
|
10206
|
+
async ({ maxAge, includeSubDomains, preload }) => {
|
|
10207
|
+
const result = _buildHstsMcp({ maxAge, includeSubDomains, preload });
|
|
10208
|
+
if ("error" in result) return errText(result);
|
|
10209
|
+
return text(JSON.stringify(result, null, 2));
|
|
10210
|
+
}
|
|
10211
|
+
);
|
|
10212
|
+
server.tool(
|
|
10213
|
+
"parse_hsts_header",
|
|
10214
|
+
'Parse an existing Strict-Transport-Security header value (with or without a leading "Strict-Transport-Security:" label) into its max-age, includeSubDomains, and preload fields, flagging whether it qualifies for browser HSTS preload lists.',
|
|
10215
|
+
{
|
|
10216
|
+
header: z.string().min(1).describe("The Strict-Transport-Security header value to parse")
|
|
10217
|
+
},
|
|
10218
|
+
async ({ header }) => {
|
|
10219
|
+
const result = _parseHstsMcp(header);
|
|
10220
|
+
if ("error" in result) return errText(result);
|
|
10221
|
+
return text(JSON.stringify(result, null, 2));
|
|
10222
|
+
}
|
|
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
|
+
);
|
|
9971
10400
|
var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
|
|
9972
10401
|
function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
|
|
9973
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",
|