@utilix-tech/mcp 0.41.0 → 0.43.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 +7 -0
- package/README.md +8 -3
- package/dist/index.js +267 -0
- package/package.json +1 -1
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.43.0
|
|
6
|
+
- Add `build_vary_header`, `parse_vary_header` tools: build a `Vary` response header from request-header names with a plain-English caching note per header, or parse an existing Vary header into the same shape.
|
|
7
|
+
|
|
8
|
+
## 0.42.0
|
|
9
|
+
- Add `build_range_header`, `parse_content_range_header` tools: build an HTTP `Range` request header from explicit, open-ended, or suffix ("last N bytes") byte ranges, and parse a server's `Content-Range` response to verify what it actually sent back.
|
|
10
|
+
- Add `find_csv_duplicate_rows` tool: group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row, and report each duplicate group's occurrence count and data row numbers.
|
|
11
|
+
|
|
5
12
|
## 0.41.0
|
|
6
13
|
- Add `calculate_cash_breakdown`, `calculate_change` tools: break a cash amount down into the minimum count of bills/coins from a set of denominations (largest first), or compute change owed for a payment/tender pair and its breakdown.
|
|
7
14
|
- Add `generate_etag` tool: compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator (W/) prefix.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 173 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
|
|
|
@@ -189,7 +189,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
189
189
|
| `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
|
|
190
190
|
| `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
|
|
191
191
|
|
|
192
|
-
### API / Network (
|
|
192
|
+
### API / Network (20 tools)
|
|
193
193
|
| Tool | Description |
|
|
194
194
|
|------|-------------|
|
|
195
195
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -208,6 +208,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
208
208
|
| `build_link_header` | Build and explain an HTTP Link response header from URL/rel pairs (preload, preconnect, prefetch, canonical, pagination, etc.) |
|
|
209
209
|
| `check_set_cookie_header` | Parse a Set-Cookie header and validate its attribute combination: SameSite=None requires Secure, __Host-/__Secure- prefix rules, missing Secure, overly long Max-Age |
|
|
210
210
|
| `analyze_email_headers` | Parse a raw email header block into structured fields, the Received hop chain, and SPF/DKIM/DMARC results from an Authentication-Results header |
|
|
211
|
+
| `build_range_header` | Build an HTTP Range request header from explicit, open-ended, or suffix ("last N bytes") byte ranges |
|
|
212
|
+
| `parse_content_range_header` | Parse a Content-Range response header into its unit, byte bounds, and total resource size |
|
|
213
|
+
| `build_vary_header` | Build a Vary response header from request-header names, with a plain-English caching note per header |
|
|
214
|
+
| `parse_vary_header` | Parse an existing Vary header value into its individual header names, each with a caching note |
|
|
211
215
|
|
|
212
216
|
### Color (4 tools)
|
|
213
217
|
| Tool | Description |
|
|
@@ -233,7 +237,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
233
237
|
| `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 |
|
|
234
238
|
| `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
|
|
235
239
|
|
|
236
|
-
### Data (
|
|
240
|
+
### Data (10 tools)
|
|
237
241
|
| Tool | Description |
|
|
238
242
|
|------|-------------|
|
|
239
243
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -245,6 +249,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
245
249
|
| `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
|
|
246
250
|
| `generate_markdown_toc` | Scan a Markdown document's ATX headings and build a nested table of contents with GitHub-style anchor slugs |
|
|
247
251
|
| `csv_to_sql_insert` | Convert CSV data with a header row into parameterless SQL INSERT statements, with basic type inference and ANSI/MySQL identifier quoting |
|
|
252
|
+
| `find_csv_duplicate_rows` | Group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row |
|
|
248
253
|
|
|
249
254
|
### CSS (2 tools)
|
|
250
255
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -6931,5 +6931,272 @@ server.tool(
|
|
|
6931
6931
|
return text(JSON.stringify(result, null, 2));
|
|
6932
6932
|
}
|
|
6933
6933
|
);
|
|
6934
|
+
function _round2Mcp(n) {
|
|
6935
|
+
return Math.round(n * 100) / 100;
|
|
6936
|
+
}
|
|
6937
|
+
function _buildRangeHeaderMcp(entries, totalSize, unitRaw) {
|
|
6938
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
6939
|
+
return { error: "At least one range is required" };
|
|
6940
|
+
}
|
|
6941
|
+
const unit = unitRaw?.trim() || "bytes";
|
|
6942
|
+
if (totalSize !== void 0 && (!Number.isFinite(totalSize) || totalSize <= 0)) {
|
|
6943
|
+
return { error: "totalSize must be a positive number when provided" };
|
|
6944
|
+
}
|
|
6945
|
+
const parsed = [];
|
|
6946
|
+
for (const entry of entries) {
|
|
6947
|
+
if (entry.suffixLength !== void 0) {
|
|
6948
|
+
if (!Number.isFinite(entry.suffixLength) || entry.suffixLength <= 0) {
|
|
6949
|
+
return { error: "suffixLength must be a positive number" };
|
|
6950
|
+
}
|
|
6951
|
+
const suffixLength = Math.floor(entry.suffixLength);
|
|
6952
|
+
const byteLength = totalSize !== void 0 ? Math.min(suffixLength, totalSize) : suffixLength;
|
|
6953
|
+
parsed.push({
|
|
6954
|
+
spec: `-${suffixLength}`,
|
|
6955
|
+
start: null,
|
|
6956
|
+
end: null,
|
|
6957
|
+
suffixLength,
|
|
6958
|
+
byteLength,
|
|
6959
|
+
percentOfTotal: totalSize !== void 0 ? _round2Mcp(byteLength / totalSize * 100) : null
|
|
6960
|
+
});
|
|
6961
|
+
continue;
|
|
6962
|
+
}
|
|
6963
|
+
if (entry.start === void 0) {
|
|
6964
|
+
return { error: "Each range requires either a start byte or a suffixLength" };
|
|
6965
|
+
}
|
|
6966
|
+
if (!Number.isFinite(entry.start) || entry.start < 0) {
|
|
6967
|
+
return { error: "start must be a non-negative number" };
|
|
6968
|
+
}
|
|
6969
|
+
const start = Math.floor(entry.start);
|
|
6970
|
+
if (totalSize !== void 0 && start >= totalSize) {
|
|
6971
|
+
return { error: `start byte ${start} is beyond totalSize (${totalSize})` };
|
|
6972
|
+
}
|
|
6973
|
+
if (entry.end !== void 0) {
|
|
6974
|
+
if (!Number.isFinite(entry.end) || entry.end < 0) {
|
|
6975
|
+
return { error: "end must be a non-negative number" };
|
|
6976
|
+
}
|
|
6977
|
+
const end = Math.floor(entry.end);
|
|
6978
|
+
if (end < start) return { error: `range end (${end}) must be >= start (${start})` };
|
|
6979
|
+
if (totalSize !== void 0 && end >= totalSize) {
|
|
6980
|
+
return { error: `end byte ${end} is beyond totalSize (${totalSize})` };
|
|
6981
|
+
}
|
|
6982
|
+
const byteLength = end - start + 1;
|
|
6983
|
+
parsed.push({
|
|
6984
|
+
spec: `${start}-${end}`,
|
|
6985
|
+
start,
|
|
6986
|
+
end,
|
|
6987
|
+
suffixLength: null,
|
|
6988
|
+
byteLength,
|
|
6989
|
+
percentOfTotal: totalSize !== void 0 ? _round2Mcp(byteLength / totalSize * 100) : null
|
|
6990
|
+
});
|
|
6991
|
+
} else {
|
|
6992
|
+
const byteLength = totalSize !== void 0 ? totalSize - start : null;
|
|
6993
|
+
parsed.push({
|
|
6994
|
+
spec: `${start}-`,
|
|
6995
|
+
start,
|
|
6996
|
+
end: null,
|
|
6997
|
+
suffixLength: null,
|
|
6998
|
+
byteLength,
|
|
6999
|
+
percentOfTotal: byteLength !== null && totalSize !== void 0 ? _round2Mcp(byteLength / totalSize * 100) : null
|
|
7000
|
+
});
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
const warnings = [];
|
|
7004
|
+
const resolvable = parsed.map((r, i) => ({ i, start: r.start ?? (totalSize !== void 0 && r.suffixLength !== null ? totalSize - Math.min(r.suffixLength, totalSize) : null), end: r.end ?? (totalSize !== void 0 ? totalSize - 1 : null) })).filter((r) => r.start !== null && r.end !== null).sort((a, b) => a.start - b.start);
|
|
7005
|
+
for (let i = 1; i < resolvable.length; i++) {
|
|
7006
|
+
if (resolvable[i].start <= resolvable[i - 1].end) {
|
|
7007
|
+
warnings.push(`ranges "${parsed[resolvable[i - 1].i].spec}" and "${parsed[resolvable[i].i].spec}" overlap \u2014 servers may reject overlapping multi-range requests`);
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
if (parsed.length > 1 && parsed.some((r) => r.byteLength === null)) {
|
|
7011
|
+
warnings.push("totalSize was not provided, so open-ended ranges cannot be checked for overlap or total byte count");
|
|
7012
|
+
}
|
|
7013
|
+
const totalBytesRequested = parsed.every((r) => r.byteLength !== null) ? parsed.reduce((sum, r) => sum + (r.byteLength ?? 0), 0) : null;
|
|
7014
|
+
const headerValue = `${unit}=${parsed.map((r) => r.spec).join(",")}`;
|
|
7015
|
+
return { header: `Range: ${headerValue}`, headerValue, ranges: parsed, totalBytesRequested, warnings };
|
|
7016
|
+
}
|
|
7017
|
+
function _parseContentRangeHeaderMcp(header) {
|
|
7018
|
+
if (!header || header.trim() === "") return { error: "Input is empty" };
|
|
7019
|
+
const match = header.trim().match(/^(\S+)\s+(\*|\d+-\d+)\/(\*|\d+)$/);
|
|
7020
|
+
if (!match) {
|
|
7021
|
+
return { error: 'Could not parse Content-Range header. Expected a format like "bytes 200-1000/67589" or "bytes */67589"' };
|
|
7022
|
+
}
|
|
7023
|
+
const [, unit, range, totalRaw] = match;
|
|
7024
|
+
const isUnsatisfiable = range === "*";
|
|
7025
|
+
const isTotalUnknown = totalRaw === "*";
|
|
7026
|
+
const total = isTotalUnknown ? null : Number(totalRaw);
|
|
7027
|
+
if (isUnsatisfiable) {
|
|
7028
|
+
return { unit, start: null, end: null, total, isUnsatisfiable, isTotalUnknown, byteLength: null };
|
|
7029
|
+
}
|
|
7030
|
+
const [startRaw, endRaw] = range.split("-");
|
|
7031
|
+
const start = Number(startRaw);
|
|
7032
|
+
const end = Number(endRaw);
|
|
7033
|
+
if (end < start) return { error: `Invalid range: end (${end}) is before start (${start})` };
|
|
7034
|
+
return { unit, start, end, total, isUnsatisfiable, isTotalUnknown, byteLength: end - start + 1 };
|
|
7035
|
+
}
|
|
7036
|
+
server.tool(
|
|
7037
|
+
"build_range_header",
|
|
7038
|
+
'Build an HTTP Range request-header value (RFC 7233) from one or more byte ranges: explicit start-end pairs, open-ended "start to end of file" ranges, or suffix "last N bytes" ranges. Pass totalSize to resolve open-ended/suffix byte lengths and get overlap warnings. Pure string assembly and arithmetic; never makes a network request.',
|
|
7039
|
+
{
|
|
7040
|
+
ranges: z.array(z.object({
|
|
7041
|
+
start: z.number().optional().describe("First byte offset (inclusive)"),
|
|
7042
|
+
end: z.number().optional().describe('Last byte offset (inclusive); omit for "to end of file"'),
|
|
7043
|
+
suffixLength: z.number().optional().describe("Request the last N bytes instead of start/end")
|
|
7044
|
+
})).min(1).describe("One or more byte ranges to request"),
|
|
7045
|
+
totalSize: z.number().optional().describe("Total resource size in bytes, used to resolve open-ended/suffix ranges and detect overlap"),
|
|
7046
|
+
unit: z.string().optional().describe('Range unit (default "bytes")')
|
|
7047
|
+
},
|
|
7048
|
+
async ({ ranges, totalSize, unit }) => {
|
|
7049
|
+
const result = _buildRangeHeaderMcp(ranges, totalSize, unit);
|
|
7050
|
+
if ("error" in result) return errText(result);
|
|
7051
|
+
return text(JSON.stringify(result, null, 2));
|
|
7052
|
+
}
|
|
7053
|
+
);
|
|
7054
|
+
server.tool(
|
|
7055
|
+
"parse_content_range_header",
|
|
7056
|
+
'Parse a Content-Range response header (e.g. "bytes 200-1000/67589" or "bytes */67589" for an unsatisfiable range) into its unit, byte bounds, and total resource size, so you can verify what a server actually sent back for a partial/resumable request.',
|
|
7057
|
+
{
|
|
7058
|
+
header: z.string().min(1).describe("The Content-Range header value to parse")
|
|
7059
|
+
},
|
|
7060
|
+
async ({ header }) => {
|
|
7061
|
+
const result = _parseContentRangeHeaderMcp(header);
|
|
7062
|
+
if ("error" in result) return errText(result);
|
|
7063
|
+
return text(JSON.stringify(result, null, 2));
|
|
7064
|
+
}
|
|
7065
|
+
);
|
|
7066
|
+
function _normalizeDuplicateValueMcp(value, caseSensitive, trimWhitespace) {
|
|
7067
|
+
let v = value;
|
|
7068
|
+
if (trimWhitespace) v = v.trim();
|
|
7069
|
+
if (!caseSensitive) v = v.toLowerCase();
|
|
7070
|
+
return v;
|
|
7071
|
+
}
|
|
7072
|
+
function _findCsvDuplicateRowsMcp(input, delimiter, hasHeader, keyColumns, caseSensitive, trimWhitespace) {
|
|
7073
|
+
if (!input || input.trim() === "") return { error: "Input is empty" };
|
|
7074
|
+
const delim = delimiter?.trim() || ",";
|
|
7075
|
+
const header = hasHeader !== false;
|
|
7076
|
+
const cs = caseSensitive !== false;
|
|
7077
|
+
const tw = trimWhitespace !== false;
|
|
7078
|
+
const lines = input.replace(/\r\n/g, "\n").split("\n").filter((l) => l.trim() !== "");
|
|
7079
|
+
if (lines.length === 0) return { error: "No data found" };
|
|
7080
|
+
const allRows = lines.map((l) => _parseCsvRowForSqlMcp(l, delim));
|
|
7081
|
+
const headers = header ? allRows[0].map((h) => h.trim()) : allRows[0].map((_, idx) => `Column ${idx + 1}`);
|
|
7082
|
+
const dataRows = header ? allRows.slice(1) : allRows;
|
|
7083
|
+
if (dataRows.length === 0) return { error: "No data rows found" };
|
|
7084
|
+
let keyIndexes;
|
|
7085
|
+
if (keyColumns && keyColumns.length > 0) {
|
|
7086
|
+
keyIndexes = [];
|
|
7087
|
+
for (const col of keyColumns) {
|
|
7088
|
+
const idx = headers.indexOf(col);
|
|
7089
|
+
if (idx === -1) return { error: `Column "${col}" not found. Available columns: ${headers.join(", ")}` };
|
|
7090
|
+
keyIndexes.push(idx);
|
|
7091
|
+
}
|
|
7092
|
+
} else {
|
|
7093
|
+
keyIndexes = headers.map((_, idx) => idx);
|
|
7094
|
+
}
|
|
7095
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7096
|
+
dataRows.forEach((row, i) => {
|
|
7097
|
+
const key = keyIndexes.map((idx) => row[idx] ?? "");
|
|
7098
|
+
const normalizedKey = keyIndexes.map((idx) => _normalizeDuplicateValueMcp(row[idx] ?? "", cs, tw));
|
|
7099
|
+
const mapKey = JSON.stringify(normalizedKey);
|
|
7100
|
+
const rowNumber = i + 1;
|
|
7101
|
+
const existing = groups.get(mapKey);
|
|
7102
|
+
if (existing) existing.rowNumbers.push(rowNumber);
|
|
7103
|
+
else groups.set(mapKey, { key, rowNumbers: [rowNumber] });
|
|
7104
|
+
});
|
|
7105
|
+
const duplicateGroups = [];
|
|
7106
|
+
let duplicateRowCount = 0;
|
|
7107
|
+
for (const group of groups.values()) {
|
|
7108
|
+
if (group.rowNumbers.length > 1) {
|
|
7109
|
+
duplicateGroups.push({ key: group.key, rowNumbers: group.rowNumbers, count: group.rowNumbers.length });
|
|
7110
|
+
duplicateRowCount += group.rowNumbers.length;
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
duplicateGroups.sort((a, b) => b.count - a.count);
|
|
7114
|
+
return {
|
|
7115
|
+
headers,
|
|
7116
|
+
totalRows: dataRows.length,
|
|
7117
|
+
duplicateGroups,
|
|
7118
|
+
duplicateRowCount,
|
|
7119
|
+
uniqueRowCount: dataRows.length - duplicateRowCount + duplicateGroups.length
|
|
7120
|
+
};
|
|
7121
|
+
}
|
|
7122
|
+
server.tool(
|
|
7123
|
+
"find_csv_duplicate_rows",
|
|
7124
|
+
"Parse CSV/TSV data and group rows that share the same value across a chosen set of key columns, or the entire row if no key columns are given, so you can spot and clean up accidental duplicates before an import. Row numbers count data rows only (the header row, if any, is not counted).",
|
|
7125
|
+
{
|
|
7126
|
+
csv: z.string().min(1).describe("CSV/TSV data to scan for duplicate rows"),
|
|
7127
|
+
delimiter: z.string().optional().describe('Field delimiter (default ",")'),
|
|
7128
|
+
hasHeader: z.boolean().default(true).describe("Whether the first row is a header"),
|
|
7129
|
+
keyColumns: z.array(z.string()).optional().describe("Column names to compare; omit to compare the entire row"),
|
|
7130
|
+
caseSensitive: z.boolean().default(true).describe("Whether comparison is case-sensitive"),
|
|
7131
|
+
trimWhitespace: z.boolean().default(true).describe("Whether to trim whitespace before comparing")
|
|
7132
|
+
},
|
|
7133
|
+
async ({ csv, delimiter, hasHeader, keyColumns, caseSensitive, trimWhitespace }) => {
|
|
7134
|
+
const result = _findCsvDuplicateRowsMcp(csv, delimiter, hasHeader, keyColumns, caseSensitive, trimWhitespace);
|
|
7135
|
+
if ("error" in result) return errText(result);
|
|
7136
|
+
return text(JSON.stringify(result, null, 2));
|
|
7137
|
+
}
|
|
7138
|
+
);
|
|
7139
|
+
var _VARY_HEADER_NOTES_MCP = {
|
|
7140
|
+
"accept-encoding": "Caches must store a separate response per compression scheme (gzip, br, etc.) the client accepts.",
|
|
7141
|
+
"accept-language": "Caches must store a separate response per requested language, which can fragment a shared cache badly if Accept-Language values are highly varied.",
|
|
7142
|
+
"accept": "Caches must store a separate response per requested content type (e.g. JSON vs HTML).",
|
|
7143
|
+
"user-agent": "Caches must store a separate response per distinct User-Agent string, which is often unique enough to make caching nearly ineffective.",
|
|
7144
|
+
"authorization": "Signals the response is user-specific; most shared caches will refuse to cache it at all rather than key on this header.",
|
|
7145
|
+
"cookie": "Same effect as Authorization: shared caches typically treat the response as private and skip caching rather than key on cookie values.",
|
|
7146
|
+
"origin": "Caches must store a separate response per requesting Origin, needed whenever a response includes CORS headers that differ by origin.",
|
|
7147
|
+
"x-requested-with": "Caches must store a separate response depending on whether the request was made via an XHR/fetch client hint.",
|
|
7148
|
+
"x-forwarded-proto": "Caches must store a separate response for HTTP vs HTTPS requests, relevant behind a proxy or load balancer that terminates TLS."
|
|
7149
|
+
};
|
|
7150
|
+
function _buildVaryHeaderMcp(headers) {
|
|
7151
|
+
if (!Array.isArray(headers) || headers.length === 0) return { error: "At least one header is required" };
|
|
7152
|
+
const trimmed = headers.map((h) => typeof h === "string" ? h.trim() : "").filter((h) => h.length > 0);
|
|
7153
|
+
if (trimmed.length === 0) return { error: "At least one header is required" };
|
|
7154
|
+
const warnings = [];
|
|
7155
|
+
if (trimmed.some((h) => h === "*") && trimmed.length > 1) {
|
|
7156
|
+
warnings.push('"*" makes every request effectively uncacheable; combining it with other header names is redundant, most caches will just skip caching entirely');
|
|
7157
|
+
}
|
|
7158
|
+
const seen = /* @__PURE__ */ new Map();
|
|
7159
|
+
for (const h of trimmed) {
|
|
7160
|
+
const key = h.toLowerCase();
|
|
7161
|
+
if (!seen.has(key)) seen.set(key, h);
|
|
7162
|
+
}
|
|
7163
|
+
if (seen.size < trimmed.length) warnings.push("duplicate header names were removed");
|
|
7164
|
+
const entries = Array.from(seen.values()).map((h) => ({
|
|
7165
|
+
header: h,
|
|
7166
|
+
note: h === "*" ? "Every response is considered unique; most caches will not store it at all." : _VARY_HEADER_NOTES_MCP[h.toLowerCase()] ?? `Caches must store a separate response per distinct ${h} value.`
|
|
7167
|
+
}));
|
|
7168
|
+
const headerValue = entries.map((e) => e.header).join(", ");
|
|
7169
|
+
return { header: `Vary: ${headerValue}`, headerValue, entries, warnings };
|
|
7170
|
+
}
|
|
7171
|
+
function _parseVaryHeaderMcp(raw) {
|
|
7172
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
7173
|
+
const withoutLabel = raw.trim().replace(/^vary:\s*/i, "");
|
|
7174
|
+
const headers = withoutLabel.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
|
|
7175
|
+
return _buildVaryHeaderMcp(headers);
|
|
7176
|
+
}
|
|
7177
|
+
server.tool(
|
|
7178
|
+
"build_vary_header",
|
|
7179
|
+
"Build a Vary response-header value from a list of request-header names (e.g. Accept-Encoding, Accept-Language, Authorization), de-duplicating case-insensitively and attaching a plain-English caching note for each well-known header explaining what it does to shared caches.",
|
|
7180
|
+
{
|
|
7181
|
+
headers: z.array(z.string()).min(1).describe("Request-header names to vary the response by")
|
|
7182
|
+
},
|
|
7183
|
+
async ({ headers }) => {
|
|
7184
|
+
const result = _buildVaryHeaderMcp(headers);
|
|
7185
|
+
if ("error" in result) return errText(result);
|
|
7186
|
+
return text(JSON.stringify(result, null, 2));
|
|
7187
|
+
}
|
|
7188
|
+
);
|
|
7189
|
+
server.tool(
|
|
7190
|
+
"parse_vary_header",
|
|
7191
|
+
'Parse an existing Vary header value (with or without a leading "Vary:" label) into its individual header names, each with a plain-English note on what it does to shared caching.',
|
|
7192
|
+
{
|
|
7193
|
+
header: z.string().min(1).describe("The Vary header value to parse")
|
|
7194
|
+
},
|
|
7195
|
+
async ({ header }) => {
|
|
7196
|
+
const result = _parseVaryHeaderMcp(header);
|
|
7197
|
+
if ("error" in result) return errText(result);
|
|
7198
|
+
return text(JSON.stringify(result, null, 2));
|
|
7199
|
+
}
|
|
7200
|
+
);
|
|
6934
7201
|
var transport = new StdioServerTransport();
|
|
6935
7202
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "MCP server exposing 168 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",
|