@utilix-tech/mcp 0.56.0 → 0.58.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 -5
- package/dist/index.js +193 -1
- 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.58.0
|
|
6
|
+
- Add `apply_json_merge_patch` tool: apply an RFC 7396 JSON Merge Patch document to a target JSON document, with null-valued patch keys removing the matching target key.
|
|
7
|
+
- Add `generate_csp_nonce` tool: generate a cryptographically random, base64-encoded Content-Security-Policy nonce with ready-to-paste header directive and script/style tag snippets.
|
|
8
|
+
|
|
9
|
+
## 0.57.0
|
|
10
|
+
- Add `build_accept_language`, `parse_accept_language` tools: build an Accept-Language request-header value from locale/quality pairs sorted by descending preference, or parse an existing one into its ranked entries.
|
|
11
|
+
- Add `calculate_blended_loan_rate` tool: compute the single weighted-average interest rate across a list of loan balances, plus combined monthly and annual interest accrual.
|
|
12
|
+
|
|
5
13
|
## 0.56.0
|
|
6
14
|
- Add `calculate_debt_to_income_ratio` tool: compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance.
|
|
7
15
|
- Add `infer_csv_column_types` tool: 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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 205 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
|
|
|
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
75
75
|
|
|
76
76
|
## Tools
|
|
77
77
|
|
|
78
|
-
### JSON (
|
|
78
|
+
### JSON (16 tools)
|
|
79
79
|
| Tool | Description |
|
|
80
80
|
|------|-------------|
|
|
81
81
|
| `format_json` | Pretty-print and validate a JSON string |
|
|
@@ -85,6 +85,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
85
85
|
| `csv_to_json` | Convert CSV text to a JSON array |
|
|
86
86
|
| `jsonpath` | Evaluate a JSONPath expression |
|
|
87
87
|
| `resolve_json_pointer` | Resolve an RFC 6901 JSON Pointer against a JSON document |
|
|
88
|
+
| `apply_json_merge_patch` | Apply an RFC 7396 JSON Merge Patch document to a target JSON document |
|
|
88
89
|
| `validate_json` | Validate JSON against a JSON Schema |
|
|
89
90
|
| `yaml_to_json` | Convert YAML to JSON |
|
|
90
91
|
| `json_to_yaml` | Convert JSON to YAML |
|
|
@@ -120,7 +121,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
120
121
|
| `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
122
|
| `generate_etag` | Compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator prefix |
|
|
122
123
|
|
|
123
|
-
### Generators (
|
|
124
|
+
### Generators (7 tools)
|
|
124
125
|
| Tool | Description |
|
|
125
126
|
|------|-------------|
|
|
126
127
|
| `generate_uuid` | Generate UUID v4 or v7 |
|
|
@@ -129,6 +130,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
129
130
|
| `estimate_password_entropy` | Estimate a password's entropy in bits, strength label, and crack-time estimates |
|
|
130
131
|
| `generate_qr` | Generate a QR code as an SVG string |
|
|
131
132
|
| `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
|
|
133
|
+
| `generate_csp_nonce` | Generate a cryptographically random CSP nonce with ready-to-paste header directive and script/style tag snippets |
|
|
132
134
|
|
|
133
135
|
### Text (16 tools)
|
|
134
136
|
| Tool | Description |
|
|
@@ -160,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
160
162
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
161
163
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
162
164
|
|
|
163
|
-
### Units (
|
|
165
|
+
### Units (35 tools)
|
|
164
166
|
| Tool | Description |
|
|
165
167
|
|------|-------------|
|
|
166
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -197,8 +199,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
197
199
|
| `calculate_gpa` | Compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale |
|
|
198
200
|
| `calculate_credit_utilization` | Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance |
|
|
199
201
|
| `calculate_debt_to_income_ratio` | Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance |
|
|
202
|
+
| `calculate_blended_loan_rate` | Compute the single weighted-average interest rate across a list of loan balances, weighted by each loan's share of the combined balance, plus combined monthly and annual interest accrual |
|
|
200
203
|
|
|
201
|
-
### API / Network (
|
|
204
|
+
### API / Network (31 tools)
|
|
202
205
|
| Tool | Description |
|
|
203
206
|
|------|-------------|
|
|
204
207
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -230,6 +233,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
230
233
|
| `build_content_disposition` | Build a Content-Disposition response header for a file download or inline display, with an RFC 5987 filename* parameter for non-ASCII filenames |
|
|
231
234
|
| `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
|
|
232
235
|
| `build_x_content_type_options_header` | Build the fixed X-Content-Type-Options: nosniff header, validate a pasted value, or assess MIME-sniffing risk for a given Content-Type |
|
|
236
|
+
| `build_accept_language` | Build an Accept-Language request-header value from locale/quality pairs, sorted by descending preference |
|
|
237
|
+
| `parse_accept_language` | Parse an existing Accept-Language header value into its ranked locale/quality pairs |
|
|
233
238
|
|
|
234
239
|
### Color (4 tools)
|
|
235
240
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { createHash } from "crypto";
|
|
7
|
+
import { createHash, randomBytes } from "crypto";
|
|
8
8
|
import * as yaml from "js-yaml";
|
|
9
9
|
import {
|
|
10
10
|
parseJson,
|
|
@@ -4655,6 +4655,45 @@ server.tool(
|
|
|
4655
4655
|
return text(JSON.stringify(result, null, 2));
|
|
4656
4656
|
}
|
|
4657
4657
|
);
|
|
4658
|
+
function _isMergePatchObjectMcp(value) {
|
|
4659
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4660
|
+
}
|
|
4661
|
+
function _mergeJsonPatchMcp(target, patch) {
|
|
4662
|
+
if (!_isMergePatchObjectMcp(patch)) return patch;
|
|
4663
|
+
const result = _isMergePatchObjectMcp(target) ? { ...target } : {};
|
|
4664
|
+
for (const key of Object.keys(patch)) {
|
|
4665
|
+
const value = patch[key];
|
|
4666
|
+
if (value === null) {
|
|
4667
|
+
delete result[key];
|
|
4668
|
+
} else {
|
|
4669
|
+
result[key] = _mergeJsonPatchMcp(result[key], value);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
return result;
|
|
4673
|
+
}
|
|
4674
|
+
server.tool(
|
|
4675
|
+
"apply_json_merge_patch",
|
|
4676
|
+
"Apply an RFC 7396 JSON Merge Patch document to a target JSON document and return the merged result. A null-valued key in the patch removes that key from the target; a patch that is not itself a JSON object replaces the target wholesale, which is how arrays are replaced rather than merged element by element.",
|
|
4677
|
+
{
|
|
4678
|
+
target: z.string().describe("The target JSON document, as a JSON string"),
|
|
4679
|
+
patch: z.string().describe("The RFC 7396 JSON Merge Patch document, as a JSON string")
|
|
4680
|
+
},
|
|
4681
|
+
async ({ target, patch }) => {
|
|
4682
|
+
let targetParsed;
|
|
4683
|
+
try {
|
|
4684
|
+
targetParsed = JSON.parse(target);
|
|
4685
|
+
} catch {
|
|
4686
|
+
return errText({ error: "Target is not valid JSON" });
|
|
4687
|
+
}
|
|
4688
|
+
let patchParsed;
|
|
4689
|
+
try {
|
|
4690
|
+
patchParsed = JSON.parse(patch);
|
|
4691
|
+
} catch {
|
|
4692
|
+
return errText({ error: "Patch is not valid JSON" });
|
|
4693
|
+
}
|
|
4694
|
+
return text(JSON.stringify({ result: _mergeJsonPatchMcp(targetParsed, patchParsed) }, null, 2));
|
|
4695
|
+
}
|
|
4696
|
+
);
|
|
4658
4697
|
function _convertAprApyMcp(rate, compoundsPerYear, direction) {
|
|
4659
4698
|
if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
|
|
4660
4699
|
if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) return { error: "Compounding frequency must be a positive whole number of periods per year" };
|
|
@@ -9327,5 +9366,158 @@ server.tool(
|
|
|
9327
9366
|
}, null, 2));
|
|
9328
9367
|
}
|
|
9329
9368
|
);
|
|
9369
|
+
function _formatAcceptLanguageQualityMcp(q) {
|
|
9370
|
+
let s = q.toFixed(3);
|
|
9371
|
+
s = s.replace(/0+$/, "");
|
|
9372
|
+
s = s.replace(/\.$/, "");
|
|
9373
|
+
return s;
|
|
9374
|
+
}
|
|
9375
|
+
function _formatAcceptLanguageEntriesMcp(entries) {
|
|
9376
|
+
return entries.map((e) => e.quality === 1 ? e.locale : `${e.locale};q=${_formatAcceptLanguageQualityMcp(e.quality)}`).join(", ");
|
|
9377
|
+
}
|
|
9378
|
+
function _sortAcceptLanguageEntriesMcp(entries) {
|
|
9379
|
+
return entries.map((e, i) => ({ e, i })).sort((a, b) => b.e.quality - a.e.quality || a.i - b.i).map((x) => x.e);
|
|
9380
|
+
}
|
|
9381
|
+
function _buildAcceptLanguageMcp(locales) {
|
|
9382
|
+
if (!Array.isArray(locales) || locales.length === 0) return { error: "At least one locale is required" };
|
|
9383
|
+
const entries = [];
|
|
9384
|
+
for (let i = 0; i < locales.length; i++) {
|
|
9385
|
+
const { locale, quality } = locales[i];
|
|
9386
|
+
if (typeof locale !== "string" || locale.trim() === "") return { error: `locale ${i + 1}: locale is required` };
|
|
9387
|
+
const q = quality === void 0 ? 1 : quality;
|
|
9388
|
+
if (typeof q !== "number" || !Number.isFinite(q) || q < 0 || q > 1) {
|
|
9389
|
+
return { error: `locale ${i + 1}: quality must be between 0 and 1` };
|
|
9390
|
+
}
|
|
9391
|
+
entries.push({ locale: locale.trim(), quality: Math.round(q * 1e3) / 1e3 });
|
|
9392
|
+
}
|
|
9393
|
+
const sorted = _sortAcceptLanguageEntriesMcp(entries);
|
|
9394
|
+
const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
|
|
9395
|
+
return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
|
|
9396
|
+
}
|
|
9397
|
+
function _parseAcceptLanguageMcp(raw) {
|
|
9398
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
9399
|
+
const withoutLabel = raw.trim().replace(/^accept-language:\s*/i, "");
|
|
9400
|
+
const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
9401
|
+
if (parts.length === 0) return { error: "Input is empty" };
|
|
9402
|
+
const entries = [];
|
|
9403
|
+
for (const part of parts) {
|
|
9404
|
+
const [localeRaw, ...paramsRaw] = part.split(";").map((s) => s.trim());
|
|
9405
|
+
if (!localeRaw) return { error: `Invalid entry: "${part}"` };
|
|
9406
|
+
let quality = 1;
|
|
9407
|
+
for (const param of paramsRaw) {
|
|
9408
|
+
const m = /^q=([\d.]+)$/i.exec(param);
|
|
9409
|
+
if (!m) return { error: `Invalid parameter in "${part}"` };
|
|
9410
|
+
quality = Number(m[1]);
|
|
9411
|
+
if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
|
|
9412
|
+
return { error: `q value must be between 0 and 1 in "${part}"` };
|
|
9413
|
+
}
|
|
9414
|
+
}
|
|
9415
|
+
entries.push({ locale: localeRaw, quality: Math.round(quality * 1e3) / 1e3 });
|
|
9416
|
+
}
|
|
9417
|
+
const sorted = _sortAcceptLanguageEntriesMcp(entries);
|
|
9418
|
+
const headerValue = _formatAcceptLanguageEntriesMcp(sorted);
|
|
9419
|
+
return { header: `Accept-Language: ${headerValue}`, headerValue, entries: sorted };
|
|
9420
|
+
}
|
|
9421
|
+
server.tool(
|
|
9422
|
+
"build_accept_language",
|
|
9423
|
+
'Build an Accept-Language request-header value from a list of locale/quality pairs, sorting by descending quality (ties keep their original order) and omitting ";q=1" for the default weight, per RFC 9110 section 12.5.4.',
|
|
9424
|
+
{
|
|
9425
|
+
locales: z.array(z.object({
|
|
9426
|
+
locale: z.string().describe('A language range, e.g. "en-US" or "*"'),
|
|
9427
|
+
quality: z.number().min(0).max(1).optional().describe("Preference weight from 0 to 1 (default 1)")
|
|
9428
|
+
})).min(1).describe("Locale/quality pairs to build the header from")
|
|
9429
|
+
},
|
|
9430
|
+
async ({ locales }) => {
|
|
9431
|
+
const result = _buildAcceptLanguageMcp(locales);
|
|
9432
|
+
if ("error" in result) return errText(result);
|
|
9433
|
+
return text(JSON.stringify(result, null, 2));
|
|
9434
|
+
}
|
|
9435
|
+
);
|
|
9436
|
+
server.tool(
|
|
9437
|
+
"parse_accept_language",
|
|
9438
|
+
'Parse an existing Accept-Language header value (with or without a leading "Accept-Language:" label) into its ranked locale/quality pairs, sorted by descending quality per RFC 9110 section 12.5.4 (a missing ";q=" defaults to 1).',
|
|
9439
|
+
{
|
|
9440
|
+
header: z.string().min(1).describe("The Accept-Language header value to parse")
|
|
9441
|
+
},
|
|
9442
|
+
async ({ header }) => {
|
|
9443
|
+
const result = _parseAcceptLanguageMcp(header);
|
|
9444
|
+
if ("error" in result) return errText(result);
|
|
9445
|
+
return text(JSON.stringify(result, null, 2));
|
|
9446
|
+
}
|
|
9447
|
+
);
|
|
9448
|
+
function _calculateBlendedLoanRateMcp(loans) {
|
|
9449
|
+
if (!Array.isArray(loans) || loans.length === 0) {
|
|
9450
|
+
return { error: "loans must be a non-empty array" };
|
|
9451
|
+
}
|
|
9452
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
9453
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
9454
|
+
let totalBalance = 0;
|
|
9455
|
+
for (let i = 0; i < loans.length; i++) {
|
|
9456
|
+
const loan = loans[i];
|
|
9457
|
+
if (typeof loan.balance !== "number" || !Number.isFinite(loan.balance) || loan.balance <= 0) {
|
|
9458
|
+
return { error: `loan ${i + 1}: balance must be a finite number greater than zero` };
|
|
9459
|
+
}
|
|
9460
|
+
if (typeof loan.ratePercent !== "number" || !Number.isFinite(loan.ratePercent) || loan.ratePercent < 0) {
|
|
9461
|
+
return { error: `loan ${i + 1}: ratePercent must be a finite, non-negative number` };
|
|
9462
|
+
}
|
|
9463
|
+
totalBalance += loan.balance;
|
|
9464
|
+
}
|
|
9465
|
+
let totalMonthlyInterest = 0;
|
|
9466
|
+
let weightedRateSum = 0;
|
|
9467
|
+
const results = loans.map((loan, i) => {
|
|
9468
|
+
const monthlyInterest = round2(loan.balance * loan.ratePercent / 100 / 12);
|
|
9469
|
+
totalMonthlyInterest += monthlyInterest;
|
|
9470
|
+
weightedRateSum += loan.balance * loan.ratePercent;
|
|
9471
|
+
return {
|
|
9472
|
+
label: loan.label?.trim() || `Loan ${i + 1}`,
|
|
9473
|
+
balance: loan.balance,
|
|
9474
|
+
ratePercent: loan.ratePercent,
|
|
9475
|
+
monthlyInterest,
|
|
9476
|
+
shareOfTotalPercent: round2(loan.balance / totalBalance * 100)
|
|
9477
|
+
};
|
|
9478
|
+
});
|
|
9479
|
+
return {
|
|
9480
|
+
loans: results,
|
|
9481
|
+
totalBalance: round2(totalBalance),
|
|
9482
|
+
weightedAverageRatePercent: round4(weightedRateSum / totalBalance),
|
|
9483
|
+
totalMonthlyInterest: round2(totalMonthlyInterest),
|
|
9484
|
+
totalAnnualInterest: round2(totalMonthlyInterest * 12)
|
|
9485
|
+
};
|
|
9486
|
+
}
|
|
9487
|
+
server.tool(
|
|
9488
|
+
"calculate_blended_loan_rate",
|
|
9489
|
+
"Compute the single weighted-average interest rate across a list of loan balances (e.g. multiple credit cards or student loans being weighed for consolidation), weighting each loan's rate by its share of the combined balance, plus the combined monthly and annual interest accrual.",
|
|
9490
|
+
{
|
|
9491
|
+
loans: z.array(z.object({
|
|
9492
|
+
label: z.string().optional().describe("Optional loan label"),
|
|
9493
|
+
balance: z.number().positive().describe("Outstanding balance for this loan"),
|
|
9494
|
+
ratePercent: z.number().min(0).describe("Annual interest rate for this loan, as a percent")
|
|
9495
|
+
})).min(1).describe("A non-empty list of loan balances and rates")
|
|
9496
|
+
},
|
|
9497
|
+
async ({ loans }) => {
|
|
9498
|
+
const result = _calculateBlendedLoanRateMcp(loans);
|
|
9499
|
+
if ("error" in result) return errText(result);
|
|
9500
|
+
return text(JSON.stringify(result, null, 2));
|
|
9501
|
+
}
|
|
9502
|
+
);
|
|
9503
|
+
server.tool(
|
|
9504
|
+
"generate_csp_nonce",
|
|
9505
|
+
'Generate a cryptographically random, base64-encoded nonce for a Content-Security-Policy script-src or style-src directive, along with ready-to-paste header directive and <script>/<style nonce="..."> tag snippets. Generates a fresh, unique value on every call.',
|
|
9506
|
+
{
|
|
9507
|
+
byteLength: z.number().int().min(8).max(64).optional().describe("Number of random bytes to encode (default 16 = 128 bits, the CSP spec's recommended minimum)")
|
|
9508
|
+
},
|
|
9509
|
+
async ({ byteLength }) => {
|
|
9510
|
+
const length = byteLength ?? 16;
|
|
9511
|
+
const nonce = randomBytes(length).toString("base64");
|
|
9512
|
+
return text(JSON.stringify({
|
|
9513
|
+
nonce,
|
|
9514
|
+
byteLength: length,
|
|
9515
|
+
scriptSrcDirective: `script-src 'nonce-${nonce}'`,
|
|
9516
|
+
styleSrcDirective: `style-src 'nonce-${nonce}'`,
|
|
9517
|
+
scriptTag: `<script nonce="${nonce}">`,
|
|
9518
|
+
styleTag: `<style nonce="${nonce}">`
|
|
9519
|
+
}, null, 2));
|
|
9520
|
+
}
|
|
9521
|
+
);
|
|
9330
9522
|
var transport = new StdioServerTransport();
|
|
9331
9523
|
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.58.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",
|