@utilix-tech/mcp 0.40.0 → 0.42.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 +11 -5
- package/dist/index.js +306 -0
- package/package.json +2 -2
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.42.0
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.41.0
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
5
13
|
## 0.40.0
|
|
6
14
|
- Add `generate_sri_hash` tool: compute SHA-256/384/512 Subresource Integrity hashes for a script or stylesheet's content, formatted as the `<algorithm>-<base64>` string an HTML `integrity` attribute expects.
|
|
7
15
|
- Add `analyze_email_headers` tool: parse a raw email header block into structured From/To/Subject fields, the Received hop chain in order, and SPF/DKIM/DMARC results from an Authentication-Results header.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 171 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
|
|
|
@@ -110,7 +110,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
110
110
|
| `base62_encode` | Encode text as Base62 (alphanumeric, URL-safe) |
|
|
111
111
|
| `base62_decode` | Decode a Base62 string back to text |
|
|
112
112
|
|
|
113
|
-
### Hashing (
|
|
113
|
+
### Hashing (6 tools)
|
|
114
114
|
| Tool | Description |
|
|
115
115
|
|------|-------------|
|
|
116
116
|
| `hash` | Hash a string with MD5, SHA-1, SHA-256, SHA-384, or SHA-512 |
|
|
@@ -118,6 +118,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
118
118
|
| `bcrypt_hash` | Hash a password using bcrypt |
|
|
119
119
|
| `bcrypt_verify` | Verify a password against a bcrypt hash |
|
|
120
120
|
| `generate_sri_hash` | Compute a Subresource Integrity hash (SHA-256/384/512) for a script or stylesheet's content, formatted for an HTML `integrity` attribute |
|
|
121
|
+
| `generate_etag` | Compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator prefix |
|
|
121
122
|
|
|
122
123
|
### Generators (6 tools)
|
|
123
124
|
| Tool | Description |
|
|
@@ -158,7 +159,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
158
159
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
159
160
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
160
161
|
|
|
161
|
-
### Units (
|
|
162
|
+
### Units (26 tools)
|
|
162
163
|
| Tool | Description |
|
|
163
164
|
|------|-------------|
|
|
164
165
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -185,8 +186,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
185
186
|
| `calculate_savings_goal` | Solve for months to reach a savings goal at a fixed monthly contribution, or the monthly contribution required to hit a goal by a target month count |
|
|
186
187
|
| `calculate_roi` | Compute return on investment percent from an initial and final value, plus the annualized (CAGR) return given an optional holding period in years |
|
|
187
188
|
| `compare_unit_prices` | Compare two or more package options by price and quantity to find the true cost per unit, flag the cheapest, and report how much more expensive each other option is |
|
|
189
|
+
| `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
|
|
190
|
+
| `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
|
|
188
191
|
|
|
189
|
-
### API / Network (
|
|
192
|
+
### API / Network (18 tools)
|
|
190
193
|
| Tool | Description |
|
|
191
194
|
|------|-------------|
|
|
192
195
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -205,6 +208,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
205
208
|
| `build_link_header` | Build and explain an HTTP Link response header from URL/rel pairs (preload, preconnect, prefetch, canonical, pagination, etc.) |
|
|
206
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 |
|
|
207
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 |
|
|
208
213
|
|
|
209
214
|
### Color (4 tools)
|
|
210
215
|
| Tool | Description |
|
|
@@ -230,7 +235,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
230
235
|
| `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 |
|
|
231
236
|
| `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
|
|
232
237
|
|
|
233
|
-
### Data (
|
|
238
|
+
### Data (10 tools)
|
|
234
239
|
| Tool | Description |
|
|
235
240
|
|------|-------------|
|
|
236
241
|
| `validate_yaml` | Validate YAML syntax |
|
|
@@ -242,6 +247,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
242
247
|
| `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
|
|
243
248
|
| `generate_markdown_toc` | Scan a Markdown document's ATX headings and build a nested table of contents with GitHub-style anchor slugs |
|
|
244
249
|
| `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 |
|
|
250
|
+
| `find_csv_duplicate_rows` | Group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row |
|
|
245
251
|
|
|
246
252
|
### CSS (2 tools)
|
|
247
253
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -6830,5 +6830,311 @@ server.tool(
|
|
|
6830
6830
|
return text(JSON.stringify(result, null, 2));
|
|
6831
6831
|
}
|
|
6832
6832
|
);
|
|
6833
|
+
var _USD_DENOMINATIONS_MCP = [100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01];
|
|
6834
|
+
function _cashToCentsMcp(n) {
|
|
6835
|
+
return Math.round(n * 100);
|
|
6836
|
+
}
|
|
6837
|
+
function _calculateCashBreakdownMcp(amount, denominations) {
|
|
6838
|
+
if (!Number.isFinite(amount) || amount < 0) return { error: "Amount must be zero or a positive number" };
|
|
6839
|
+
if (denominations.length === 0) return { error: "At least one denomination is required" };
|
|
6840
|
+
for (const d of denominations) {
|
|
6841
|
+
if (!Number.isFinite(d) || d <= 0) return { error: "Each denomination must be a positive number" };
|
|
6842
|
+
}
|
|
6843
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
6844
|
+
const sorted = Array.from(new Set(denominations)).sort((a, b) => b - a);
|
|
6845
|
+
let remainingCents = _cashToCentsMcp(amount);
|
|
6846
|
+
const breakdown = [];
|
|
6847
|
+
let totalCount = 0;
|
|
6848
|
+
for (const d of sorted) {
|
|
6849
|
+
const dCents = _cashToCentsMcp(d);
|
|
6850
|
+
const count = Math.floor(remainingCents / dCents);
|
|
6851
|
+
if (count > 0) {
|
|
6852
|
+
breakdown.push({ denomination: d, count, subtotal: round2(count * d) });
|
|
6853
|
+
remainingCents -= count * dCents;
|
|
6854
|
+
totalCount += count;
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
return {
|
|
6858
|
+
amount: round2(amount),
|
|
6859
|
+
denominations: sorted,
|
|
6860
|
+
breakdown,
|
|
6861
|
+
totalCount,
|
|
6862
|
+
remainder: round2(remainingCents / 100)
|
|
6863
|
+
};
|
|
6864
|
+
}
|
|
6865
|
+
server.tool(
|
|
6866
|
+
"calculate_cash_breakdown",
|
|
6867
|
+
"Break a cash amount down into the minimum count of bills/coins from a set of denominations, using the standard greedy largest-first approach (optimal for canonical currency denomination sets like USD, EUR, and GBP). Any amount that can't be represented exactly with the given denominations is reported as a remainder rather than rounded away.",
|
|
6868
|
+
{
|
|
6869
|
+
amount: z.number().describe("The cash amount to break down"),
|
|
6870
|
+
denominations: z.array(z.number()).min(1).optional().describe("Available bill/coin denominations, largest to smallest (defaults to US denominations: 100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01)")
|
|
6871
|
+
},
|
|
6872
|
+
async ({ amount, denominations }) => {
|
|
6873
|
+
const result = _calculateCashBreakdownMcp(amount, denominations && denominations.length > 0 ? denominations : _USD_DENOMINATIONS_MCP);
|
|
6874
|
+
if ("error" in result) return errText(result);
|
|
6875
|
+
return text(JSON.stringify(result, null, 2));
|
|
6876
|
+
}
|
|
6877
|
+
);
|
|
6878
|
+
server.tool(
|
|
6879
|
+
"calculate_change",
|
|
6880
|
+
"Compute the change owed for a payment/tender pair (amount tendered minus amount due) and break that change down into the minimum count of bills/coins from the given denominations.",
|
|
6881
|
+
{
|
|
6882
|
+
amountDue: z.number().describe("The amount owed"),
|
|
6883
|
+
amountTendered: z.number().describe("The amount paid, must be greater than or equal to amountDue"),
|
|
6884
|
+
denominations: z.array(z.number()).min(1).optional().describe("Available bill/coin denominations, largest to smallest (defaults to US denominations: 100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01)")
|
|
6885
|
+
},
|
|
6886
|
+
async ({ amountDue, amountTendered, denominations }) => {
|
|
6887
|
+
if (!Number.isFinite(amountDue) || amountDue < 0) return errText({ error: "Amount due must be zero or a positive number" });
|
|
6888
|
+
if (!Number.isFinite(amountTendered) || amountTendered < 0) return errText({ error: "Amount tendered must be zero or a positive number" });
|
|
6889
|
+
if (_cashToCentsMcp(amountTendered) < _cashToCentsMcp(amountDue)) {
|
|
6890
|
+
return errText({ error: "Amount tendered must be greater than or equal to the amount due" });
|
|
6891
|
+
}
|
|
6892
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
6893
|
+
const denoms = denominations && denominations.length > 0 ? denominations : _USD_DENOMINATIONS_MCP;
|
|
6894
|
+
const changeDue = round2(amountTendered - amountDue);
|
|
6895
|
+
const breakdown = _calculateCashBreakdownMcp(changeDue, denoms);
|
|
6896
|
+
if ("error" in breakdown) return errText(breakdown);
|
|
6897
|
+
return text(JSON.stringify({
|
|
6898
|
+
amountDue: round2(amountDue),
|
|
6899
|
+
amountTendered: round2(amountTendered),
|
|
6900
|
+
changeDue,
|
|
6901
|
+
denominations: breakdown.denominations,
|
|
6902
|
+
breakdown: breakdown.breakdown,
|
|
6903
|
+
totalCount: breakdown.totalCount,
|
|
6904
|
+
remainder: breakdown.remainder
|
|
6905
|
+
}, null, 2));
|
|
6906
|
+
}
|
|
6907
|
+
);
|
|
6908
|
+
var _ETAG_ALGORITHMS_MCP = ["MD5", "SHA-1", "SHA-256"];
|
|
6909
|
+
var _ETAG_NODE_ALG_MAP_MCP = { "MD5": "md5", "SHA-1": "sha1", "SHA-256": "sha256" };
|
|
6910
|
+
function _generateEtagMcp(content, algorithm, weak) {
|
|
6911
|
+
if (content.length === 0) return { error: "Content is required" };
|
|
6912
|
+
if (!_ETAG_ALGORITHMS_MCP.includes(algorithm)) {
|
|
6913
|
+
return { error: `Unknown algorithm: ${algorithm}. Must be one of ${_ETAG_ALGORITHMS_MCP.join(", ")}` };
|
|
6914
|
+
}
|
|
6915
|
+
const hash = createHash(_ETAG_NODE_ALG_MAP_MCP[algorithm]).update(content, "utf8").digest("hex");
|
|
6916
|
+
const quoted = `"${hash}"`;
|
|
6917
|
+
const etag = weak ? `W/${quoted}` : quoted;
|
|
6918
|
+
return { algorithm, weak, hash, etag, headerName: "ETag", snippet: `ETag: ${etag}` };
|
|
6919
|
+
}
|
|
6920
|
+
server.tool(
|
|
6921
|
+
"generate_etag",
|
|
6922
|
+
"Compute a content-hash ETag for the given content: digest it with MD5, SHA-1, or SHA-256, hex-encode it, and quote it into the exact string an ETag response header expects, with an optional weak-validator (W/) prefix. Hashes whatever content you give it; does not replicate any specific web server's own size/modification-time-based ETag algorithm.",
|
|
6923
|
+
{
|
|
6924
|
+
content: z.string().min(1).describe("The content to hash into an ETag"),
|
|
6925
|
+
algorithm: z.enum(_ETAG_ALGORITHMS_MCP).default("SHA-1").describe("Hash algorithm to use"),
|
|
6926
|
+
weak: z.boolean().default(false).describe("Whether to produce a weak validator (W/ prefix)")
|
|
6927
|
+
},
|
|
6928
|
+
async ({ content, algorithm, weak }) => {
|
|
6929
|
+
const result = _generateEtagMcp(content, algorithm, weak);
|
|
6930
|
+
if ("error" in result) return errText(result);
|
|
6931
|
+
return text(JSON.stringify(result, null, 2));
|
|
6932
|
+
}
|
|
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
|
+
);
|
|
6833
7139
|
var transport = new StdioServerTransport();
|
|
6834
7140
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
3
|
+
"version": "0.42.0",
|
|
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",
|
|
7
7
|
"homepage": "https://utilix.tech/mcp",
|