@utilix-tech/mcp 0.72.0 → 0.74.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 +8 -4
- package/dist/index.js +340 -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.74.0
|
|
6
|
+
- Add `calculate_loan_prepayment_penalty` tool: compute the dollar penalty cost of paying a loan off early, given its payoff balance and a prepayment penalty structure (a flat percent of balance, or a fixed number of months of interest).
|
|
7
|
+
- Add `validate_aria` tool: check every role and aria-* attribute in an HTML snippet against a curated WAI-ARIA 1.2 role table, flagging unknown roles, disallowed attributes, and missing required states.
|
|
8
|
+
|
|
9
|
+
## 0.73.0
|
|
10
|
+
- Add `calculate_mortgage_points` tool: compare a loan with no discount points against the same loan at a reduced rate bought with points, and compute the monthly savings and break-even month.
|
|
11
|
+
- Add `calculate_cookie_size_budget` tool: parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's per-cookie limit and a configurable budget.
|
|
12
|
+
|
|
5
13
|
## 0.72.0
|
|
6
14
|
- Add `validate_vin` tool: validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections.
|
|
7
15
|
- Add `validate_isin` tool: validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 243 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
|
|
|
@@ -162,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
162
162
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
163
163
|
| `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
|
|
164
164
|
|
|
165
|
-
### Units (
|
|
165
|
+
### Units (44 tools)
|
|
166
166
|
| Tool | Description |
|
|
167
167
|
|------|-------------|
|
|
168
168
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -207,8 +207,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
207
207
|
| `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 |
|
|
208
208
|
| `validate_vin` | Validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections |
|
|
209
209
|
| `validate_isin` | Validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier |
|
|
210
|
+
| `calculate_mortgage_points` | Compare a loan with no discount points against the same loan at a reduced rate bought with points, and compute the monthly savings and the break-even month where cumulative savings recoup the points cost |
|
|
211
|
+
| `calculate_loan_prepayment_penalty` | Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate |
|
|
210
212
|
|
|
211
|
-
### API / Network (
|
|
213
|
+
### API / Network (49 tools)
|
|
212
214
|
| Tool | Description |
|
|
213
215
|
|------|-------------|
|
|
214
216
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -261,6 +263,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
261
263
|
| `list_websocket_opcodes` | List every WebSocket frame opcode (0x0-0xF) |
|
|
262
264
|
| `build_robots_meta_tag` | Build a `<meta name="robots" content="...">` tag from directives (noindex, nofollow, max-snippet, etc.), the same vocabulary as X-Robots-Tag, optionally scoped to a crawler via the name attribute |
|
|
263
265
|
| `parse_robots_meta_tag` | Parse an existing robots meta tag (or bare content value) into the same shape |
|
|
266
|
+
| `calculate_cookie_size_budget` | Parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's 4096-byte per-cookie guarantee and a configurable budget |
|
|
264
267
|
|
|
265
268
|
### Color (4 tools)
|
|
266
269
|
| Tool | Description |
|
|
@@ -270,7 +273,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
270
273
|
| `color_palette` | Generate a color palette from a base color |
|
|
271
274
|
| `color_shades` | Generate shades or tints from a base color |
|
|
272
275
|
|
|
273
|
-
### Code (
|
|
276
|
+
### Code (21 tools)
|
|
274
277
|
| Tool | Description |
|
|
275
278
|
|------|-------------|
|
|
276
279
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -293,6 +296,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
293
296
|
| `extract_svg_sprite_symbols` | Parse an SVG sprite sheet's `<symbol>` definitions and list each one's id, viewBox, width, and height, alongside a ready-to-paste `<use href="#id"></use>` usage snippet |
|
|
294
297
|
| `parse_commit_trailers` | Parse a commit message's trailer block (Co-authored-by, Signed-off-by, Fixes, Reviewed-by, etc.) per git's RFC 822-style trailer convention, returning each trailer's key/value plus the body with the trailer block removed |
|
|
295
298
|
| `extract_markdown_footnotes` | Extract every Markdown-Extra-style footnote reference and definition from a document, flagging references with no matching definition and definitions that are never referenced |
|
|
299
|
+
| `validate_aria` | Parse an HTML snippet and check every role and aria-* attribute against a curated table of WAI-ARIA 1.2 roles: unknown role names, an aria-* attribute not permitted on the given role, and a widget role missing its required aria-* state |
|
|
296
300
|
|
|
297
301
|
### Data (16 tools)
|
|
298
302
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -11505,5 +11505,345 @@ server.tool(
|
|
|
11505
11505
|
return text(JSON.stringify(result, null, 2));
|
|
11506
11506
|
}
|
|
11507
11507
|
);
|
|
11508
|
+
function _roundMoneyMortgagePointsMcp(n) {
|
|
11509
|
+
return Math.round(n * 100) / 100;
|
|
11510
|
+
}
|
|
11511
|
+
function _calculateLoanForPointsMcp(principal, annualRatePercent, termMonths) {
|
|
11512
|
+
if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
|
|
11513
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
11514
|
+
return { error: "Annual interest rate must be zero or a positive number" };
|
|
11515
|
+
}
|
|
11516
|
+
if (!Number.isInteger(termMonths) || termMonths <= 0) {
|
|
11517
|
+
return { error: "Term must be a positive whole number of months" };
|
|
11518
|
+
}
|
|
11519
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
11520
|
+
let monthlyPayment;
|
|
11521
|
+
if (monthlyRate === 0) {
|
|
11522
|
+
monthlyPayment = principal / termMonths;
|
|
11523
|
+
} else {
|
|
11524
|
+
const factor = Math.pow(1 + monthlyRate, termMonths);
|
|
11525
|
+
monthlyPayment = principal * monthlyRate * factor / (factor - 1);
|
|
11526
|
+
}
|
|
11527
|
+
let balance = _roundMoneyMortgagePointsMcp(principal);
|
|
11528
|
+
let totalPayment = 0;
|
|
11529
|
+
for (let month = 1; month <= termMonths; month++) {
|
|
11530
|
+
const interest = monthlyRate === 0 ? 0 : _roundMoneyMortgagePointsMcp(balance * monthlyRate);
|
|
11531
|
+
let principalPortion = _roundMoneyMortgagePointsMcp(monthlyPayment) - interest;
|
|
11532
|
+
if (month === termMonths || principalPortion > balance) principalPortion = balance;
|
|
11533
|
+
const payment = _roundMoneyMortgagePointsMcp(principalPortion + interest);
|
|
11534
|
+
balance = _roundMoneyMortgagePointsMcp(balance - principalPortion);
|
|
11535
|
+
totalPayment += payment;
|
|
11536
|
+
}
|
|
11537
|
+
return { monthlyPayment: _roundMoneyMortgagePointsMcp(monthlyPayment), totalInterest: _roundMoneyMortgagePointsMcp(totalPayment - principal) };
|
|
11538
|
+
}
|
|
11539
|
+
function _calculateMortgagePointsMcp(loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent) {
|
|
11540
|
+
if (!Number.isFinite(pointsCostPercent) || pointsCostPercent < 0) {
|
|
11541
|
+
return { error: "Points cost must be zero or a positive percent of the loan amount" };
|
|
11542
|
+
}
|
|
11543
|
+
if (!Number.isFinite(rateReductionPercent) || rateReductionPercent < 0) {
|
|
11544
|
+
return { error: "Rate reduction must be zero or a positive number" };
|
|
11545
|
+
}
|
|
11546
|
+
if (rateReductionPercent > baseRatePercent) {
|
|
11547
|
+
return { error: "Rate reduction cannot exceed the base interest rate" };
|
|
11548
|
+
}
|
|
11549
|
+
const withoutPoints = _calculateLoanForPointsMcp(loanAmount, baseRatePercent, termMonths);
|
|
11550
|
+
if ("error" in withoutPoints) return withoutPoints;
|
|
11551
|
+
const withPoints = _calculateLoanForPointsMcp(loanAmount, baseRatePercent - rateReductionPercent, termMonths);
|
|
11552
|
+
if ("error" in withPoints) return withPoints;
|
|
11553
|
+
const pointsCost = _roundMoneyMortgagePointsMcp(loanAmount * (pointsCostPercent / 100));
|
|
11554
|
+
const monthlySavings = _roundMoneyMortgagePointsMcp(withoutPoints.monthlyPayment - withPoints.monthlyPayment);
|
|
11555
|
+
const breakEvenMonths = monthlySavings > 0 ? Math.ceil(pointsCost / monthlySavings) : null;
|
|
11556
|
+
return {
|
|
11557
|
+
pointsCost,
|
|
11558
|
+
monthlyPaymentWithoutPoints: withoutPoints.monthlyPayment,
|
|
11559
|
+
monthlyPaymentWithPoints: withPoints.monthlyPayment,
|
|
11560
|
+
monthlySavings,
|
|
11561
|
+
breakEvenMonths,
|
|
11562
|
+
totalInterestWithoutPoints: withoutPoints.totalInterest,
|
|
11563
|
+
totalInterestWithPoints: withPoints.totalInterest,
|
|
11564
|
+
totalInterestSavings: _roundMoneyMortgagePointsMcp(withoutPoints.totalInterest - withPoints.totalInterest)
|
|
11565
|
+
};
|
|
11566
|
+
}
|
|
11567
|
+
server.tool(
|
|
11568
|
+
"calculate_mortgage_points",
|
|
11569
|
+
"Compare a loan with no discount points against the same loan at a reduced rate bought with points costing a percentage of the loan amount, and compute the monthly savings and the break-even month where cumulative savings recoup the points cost.",
|
|
11570
|
+
{
|
|
11571
|
+
loanAmount: z.number().describe("The loan principal"),
|
|
11572
|
+
baseRatePercent: z.number().describe("The loan's annual interest rate with no points, as a percent, e.g. 6.5"),
|
|
11573
|
+
termMonths: z.number().int().describe("The loan term in months"),
|
|
11574
|
+
pointsCostPercent: z.number().describe("Cost of the discount points, as a percent of the loan amount, e.g. 1 for one point"),
|
|
11575
|
+
rateReductionPercent: z.number().describe("How many percentage points the rate drops when the points are bought, e.g. 0.25")
|
|
11576
|
+
},
|
|
11577
|
+
async ({ loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent }) => {
|
|
11578
|
+
const result = _calculateMortgagePointsMcp(loanAmount, baseRatePercent, termMonths, pointsCostPercent, rateReductionPercent);
|
|
11579
|
+
if ("error" in result) return errText(result);
|
|
11580
|
+
return text(JSON.stringify(result, null, 2));
|
|
11581
|
+
}
|
|
11582
|
+
);
|
|
11583
|
+
var _PER_COOKIE_BYTE_LIMIT_MCP = 4096;
|
|
11584
|
+
var _DEFAULT_COOKIE_BUDGET_BYTES_MCP = 4096;
|
|
11585
|
+
function _calculateCookieSizeBudgetMcp(cookieHeader, budgetBytes = _DEFAULT_COOKIE_BUDGET_BYTES_MCP) {
|
|
11586
|
+
if (typeof cookieHeader !== "string" || !cookieHeader.trim()) {
|
|
11587
|
+
return { error: "A Cookie header value is required" };
|
|
11588
|
+
}
|
|
11589
|
+
if (!Number.isFinite(budgetBytes) || budgetBytes <= 0) {
|
|
11590
|
+
return { error: "Budget must be a positive number of bytes" };
|
|
11591
|
+
}
|
|
11592
|
+
const parts = cookieHeader.split(";").map((p) => p.trim()).filter(Boolean);
|
|
11593
|
+
if (parts.length === 0) return { error: "A Cookie header value is required" };
|
|
11594
|
+
const cookies = [];
|
|
11595
|
+
for (const part of parts) {
|
|
11596
|
+
const eq = part.indexOf("=");
|
|
11597
|
+
if (eq === -1) return { error: `Malformed cookie pair "${part}" is missing '='` };
|
|
11598
|
+
const name = part.slice(0, eq).trim();
|
|
11599
|
+
const value = part.slice(eq + 1).trim();
|
|
11600
|
+
if (!name) return { error: "Cookie name cannot be empty" };
|
|
11601
|
+
const bytes = Buffer.byteLength(`${name}=${value}`, "utf8");
|
|
11602
|
+
cookies.push({ name, value, bytes, exceedsPerCookieLimit: bytes > _PER_COOKIE_BYTE_LIMIT_MCP });
|
|
11603
|
+
}
|
|
11604
|
+
const totalBytes = cookies.reduce((sum, c) => sum + c.bytes, 0) + Math.max(cookies.length - 1, 0) * 2;
|
|
11605
|
+
return {
|
|
11606
|
+
cookies,
|
|
11607
|
+
cookieCount: cookies.length,
|
|
11608
|
+
totalBytes,
|
|
11609
|
+
budgetBytes,
|
|
11610
|
+
exceedsBudget: totalBytes > budgetBytes
|
|
11611
|
+
};
|
|
11612
|
+
}
|
|
11613
|
+
server.tool(
|
|
11614
|
+
"calculate_cookie_size_budget",
|
|
11615
|
+
"Parse a Cookie request header's name=value pairs and compute each cookie's serialized byte size and the total header size, flagging any cookie over RFC 6265's 4096-byte per-cookie guarantee and the total against a configurable budget (4096 bytes by default).",
|
|
11616
|
+
{
|
|
11617
|
+
cookieHeader: z.string().describe('The Cookie header value to parse, e.g. "session=abc123; theme=dark"'),
|
|
11618
|
+
budgetBytes: z.number().optional().describe("Total byte budget to check against; defaults to 4096")
|
|
11619
|
+
},
|
|
11620
|
+
async ({ cookieHeader, budgetBytes }) => {
|
|
11621
|
+
const result = _calculateCookieSizeBudgetMcp(cookieHeader, budgetBytes ?? _DEFAULT_COOKIE_BUDGET_BYTES_MCP);
|
|
11622
|
+
if ("error" in result) return errText(result);
|
|
11623
|
+
return text(JSON.stringify(result, null, 2));
|
|
11624
|
+
}
|
|
11625
|
+
);
|
|
11626
|
+
function _roundMoneyPrepaymentPenaltyMcp(n) {
|
|
11627
|
+
return Math.round(n * 100) / 100;
|
|
11628
|
+
}
|
|
11629
|
+
function _calculateLoanPrepaymentPenaltyMcp(payoffBalance, penaltyType, params) {
|
|
11630
|
+
if (!Number.isFinite(payoffBalance) || payoffBalance <= 0) {
|
|
11631
|
+
return { error: "Payoff balance must be a positive number" };
|
|
11632
|
+
}
|
|
11633
|
+
if (penaltyType !== "percent-of-balance" && penaltyType !== "months-of-interest") {
|
|
11634
|
+
return { error: 'penaltyType must be "percent-of-balance" or "months-of-interest"' };
|
|
11635
|
+
}
|
|
11636
|
+
let penaltyAmount;
|
|
11637
|
+
if (penaltyType === "percent-of-balance") {
|
|
11638
|
+
const penaltyPercent = params.penaltyPercent;
|
|
11639
|
+
if (!Number.isFinite(penaltyPercent) || penaltyPercent < 0) {
|
|
11640
|
+
return { error: "penaltyPercent must be zero or a positive number" };
|
|
11641
|
+
}
|
|
11642
|
+
penaltyAmount = _roundMoneyPrepaymentPenaltyMcp(payoffBalance * (penaltyPercent / 100));
|
|
11643
|
+
} else {
|
|
11644
|
+
const monthsOfInterest = params.monthsOfInterest;
|
|
11645
|
+
const annualRatePercent = params.annualRatePercent;
|
|
11646
|
+
if (!Number.isFinite(monthsOfInterest) || monthsOfInterest < 0) {
|
|
11647
|
+
return { error: "monthsOfInterest must be zero or a positive number" };
|
|
11648
|
+
}
|
|
11649
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
11650
|
+
return { error: "annualRatePercent must be zero or a positive number" };
|
|
11651
|
+
}
|
|
11652
|
+
const monthlyInterest = payoffBalance * (annualRatePercent / 100 / 12);
|
|
11653
|
+
penaltyAmount = _roundMoneyPrepaymentPenaltyMcp(monthlyInterest * monthsOfInterest);
|
|
11654
|
+
}
|
|
11655
|
+
return {
|
|
11656
|
+
payoffBalance: _roundMoneyPrepaymentPenaltyMcp(payoffBalance),
|
|
11657
|
+
penaltyType,
|
|
11658
|
+
penaltyAmount,
|
|
11659
|
+
effectivePenaltyPercent: _roundMoneyPrepaymentPenaltyMcp(penaltyAmount / payoffBalance * 100),
|
|
11660
|
+
totalPayoffCost: _roundMoneyPrepaymentPenaltyMcp(payoffBalance + penaltyAmount)
|
|
11661
|
+
};
|
|
11662
|
+
}
|
|
11663
|
+
server.tool(
|
|
11664
|
+
"calculate_loan_prepayment_penalty",
|
|
11665
|
+
"Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate.",
|
|
11666
|
+
{
|
|
11667
|
+
payoffBalance: z.number().describe("The loan's remaining payoff balance"),
|
|
11668
|
+
penaltyType: z.enum(["percent-of-balance", "months-of-interest"]).describe("The prepayment penalty structure"),
|
|
11669
|
+
penaltyPercent: z.number().optional().describe('Penalty as a percent of the payoff balance (required when penaltyType is "percent-of-balance")'),
|
|
11670
|
+
monthsOfInterest: z.number().optional().describe('Number of months of interest charged as the penalty (required when penaltyType is "months-of-interest")'),
|
|
11671
|
+
annualRatePercent: z.number().optional().describe(`The loan's annual interest rate as a percent (required when penaltyType is "months-of-interest")`)
|
|
11672
|
+
},
|
|
11673
|
+
async ({ payoffBalance, penaltyType, penaltyPercent, monthsOfInterest, annualRatePercent }) => {
|
|
11674
|
+
const result = _calculateLoanPrepaymentPenaltyMcp(payoffBalance, penaltyType, { penaltyPercent, monthsOfInterest, annualRatePercent });
|
|
11675
|
+
if ("error" in result) return errText(result);
|
|
11676
|
+
return text(JSON.stringify(result, null, 2));
|
|
11677
|
+
}
|
|
11678
|
+
);
|
|
11679
|
+
var _ARIA_GLOBAL_ATTRS_MCP = /* @__PURE__ */ new Set([
|
|
11680
|
+
"aria-atomic",
|
|
11681
|
+
"aria-busy",
|
|
11682
|
+
"aria-controls",
|
|
11683
|
+
"aria-current",
|
|
11684
|
+
"aria-describedby",
|
|
11685
|
+
"aria-details",
|
|
11686
|
+
"aria-disabled",
|
|
11687
|
+
"aria-dropeffect",
|
|
11688
|
+
"aria-errormessage",
|
|
11689
|
+
"aria-flowto",
|
|
11690
|
+
"aria-grabbed",
|
|
11691
|
+
"aria-haspopup",
|
|
11692
|
+
"aria-hidden",
|
|
11693
|
+
"aria-invalid",
|
|
11694
|
+
"aria-keyshortcuts",
|
|
11695
|
+
"aria-label",
|
|
11696
|
+
"aria-labelledby",
|
|
11697
|
+
"aria-live",
|
|
11698
|
+
"aria-owns",
|
|
11699
|
+
"aria-relevant",
|
|
11700
|
+
"aria-roledescription"
|
|
11701
|
+
]);
|
|
11702
|
+
var _ARIA_ROLE_TABLE_MCP = {
|
|
11703
|
+
alert: { supported: ["aria-live", "aria-atomic"] },
|
|
11704
|
+
alertdialog: { supported: ["aria-modal"] },
|
|
11705
|
+
article: {},
|
|
11706
|
+
banner: {},
|
|
11707
|
+
button: { supported: ["aria-pressed", "aria-expanded", "aria-disabled"] },
|
|
11708
|
+
checkbox: { required: ["aria-checked"], supported: ["aria-readonly"] },
|
|
11709
|
+
columnheader: { supported: ["aria-sort", "aria-selected", "aria-readonly"] },
|
|
11710
|
+
combobox: { required: ["aria-expanded"], supported: ["aria-controls", "aria-autocomplete", "aria-activedescendant"] },
|
|
11711
|
+
complementary: {},
|
|
11712
|
+
contentinfo: {},
|
|
11713
|
+
dialog: { supported: ["aria-modal"] },
|
|
11714
|
+
form: {},
|
|
11715
|
+
grid: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-level", "aria-readonly"] },
|
|
11716
|
+
gridcell: { supported: ["aria-selected", "aria-readonly", "aria-expanded"] },
|
|
11717
|
+
group: { supported: ["aria-activedescendant", "aria-disabled"] },
|
|
11718
|
+
heading: { required: ["aria-level"] },
|
|
11719
|
+
img: {},
|
|
11720
|
+
link: { supported: ["aria-disabled", "aria-expanded"] },
|
|
11721
|
+
list: {},
|
|
11722
|
+
listbox: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation"] },
|
|
11723
|
+
listitem: { supported: ["aria-level", "aria-posinset", "aria-setsize"] },
|
|
11724
|
+
log: { supported: ["aria-live", "aria-atomic"] },
|
|
11725
|
+
main: {},
|
|
11726
|
+
marquee: {},
|
|
11727
|
+
menu: { supported: ["aria-orientation", "aria-activedescendant"] },
|
|
11728
|
+
menubar: { supported: ["aria-orientation", "aria-activedescendant"] },
|
|
11729
|
+
menuitem: { supported: ["aria-disabled"] },
|
|
11730
|
+
menuitemcheckbox: { required: ["aria-checked"] },
|
|
11731
|
+
menuitemradio: { required: ["aria-checked"] },
|
|
11732
|
+
meter: { required: ["aria-valuenow"], supported: ["aria-valuemin", "aria-valuemax", "aria-valuetext"] },
|
|
11733
|
+
navigation: {},
|
|
11734
|
+
none: {},
|
|
11735
|
+
option: { supported: ["aria-selected", "aria-checked", "aria-posinset", "aria-setsize"] },
|
|
11736
|
+
presentation: {},
|
|
11737
|
+
progressbar: { supported: ["aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-valuetext"] },
|
|
11738
|
+
radio: { required: ["aria-checked"] },
|
|
11739
|
+
radiogroup: { supported: ["aria-activedescendant"] },
|
|
11740
|
+
region: {},
|
|
11741
|
+
row: { supported: ["aria-selected", "aria-level", "aria-expanded"] },
|
|
11742
|
+
rowheader: { supported: ["aria-sort", "aria-selected", "aria-readonly"] },
|
|
11743
|
+
scrollbar: { required: ["aria-valuenow", "aria-controls"], supported: ["aria-valuemin", "aria-valuemax", "aria-orientation"] },
|
|
11744
|
+
search: {},
|
|
11745
|
+
searchbox: { supported: ["aria-multiline", "aria-placeholder", "aria-readonly", "aria-required", "aria-activedescendant", "aria-autocomplete"] },
|
|
11746
|
+
separator: { supported: ["aria-orientation", "aria-valuenow", "aria-valuemin", "aria-valuemax"] },
|
|
11747
|
+
slider: { required: ["aria-valuenow"], supported: ["aria-valuemin", "aria-valuemax", "aria-valuetext", "aria-orientation"] },
|
|
11748
|
+
spinbutton: { supported: ["aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-valuetext", "aria-required"] },
|
|
11749
|
+
status: { supported: ["aria-live", "aria-atomic"] },
|
|
11750
|
+
switch: { required: ["aria-checked"] },
|
|
11751
|
+
tab: { supported: ["aria-selected", "aria-controls", "aria-disabled"] },
|
|
11752
|
+
table: { supported: ["aria-colcount", "aria-rowcount"] },
|
|
11753
|
+
tablist: { supported: ["aria-orientation", "aria-activedescendant", "aria-level"] },
|
|
11754
|
+
tabpanel: {},
|
|
11755
|
+
textbox: { supported: ["aria-multiline", "aria-placeholder", "aria-readonly", "aria-required", "aria-activedescendant", "aria-autocomplete"] },
|
|
11756
|
+
timer: {},
|
|
11757
|
+
toolbar: { supported: ["aria-orientation"] },
|
|
11758
|
+
tooltip: {},
|
|
11759
|
+
tree: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation"] },
|
|
11760
|
+
treegrid: { supported: ["aria-multiselectable", "aria-activedescendant", "aria-orientation", "aria-level", "aria-readonly"] },
|
|
11761
|
+
treeitem: { supported: ["aria-expanded", "aria-selected", "aria-checked", "aria-level"] }
|
|
11762
|
+
};
|
|
11763
|
+
var _ARIA_ALL_KNOWN_ATTRS_MCP = new Set(_ARIA_GLOBAL_ATTRS_MCP);
|
|
11764
|
+
for (const entry of Object.values(_ARIA_ROLE_TABLE_MCP)) {
|
|
11765
|
+
for (const a of entry.required ?? []) _ARIA_ALL_KNOWN_ATTRS_MCP.add(a);
|
|
11766
|
+
for (const a of entry.supported ?? []) _ARIA_ALL_KNOWN_ATTRS_MCP.add(a);
|
|
11767
|
+
}
|
|
11768
|
+
var _ARIA_TAG_REGEX_MCP = /<([a-zA-Z][a-zA-Z0-9]*)\b((?:[^>"'/]|"[^"]*"|'[^']*')*)\/?>/g;
|
|
11769
|
+
var _ARIA_ATTR_REGEX_MCP = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
11770
|
+
function _parseAriaElementsMcp(html) {
|
|
11771
|
+
const elements = [];
|
|
11772
|
+
let match;
|
|
11773
|
+
_ARIA_TAG_REGEX_MCP.lastIndex = 0;
|
|
11774
|
+
while ((match = _ARIA_TAG_REGEX_MCP.exec(html)) !== null) {
|
|
11775
|
+
const tag = match[1].toLowerCase();
|
|
11776
|
+
if (tag === "script" || tag === "style") continue;
|
|
11777
|
+
const attrsStr = match[2] ?? "";
|
|
11778
|
+
const attrs = {};
|
|
11779
|
+
let attrMatch;
|
|
11780
|
+
_ARIA_ATTR_REGEX_MCP.lastIndex = 0;
|
|
11781
|
+
while ((attrMatch = _ARIA_ATTR_REGEX_MCP.exec(attrsStr)) !== null) {
|
|
11782
|
+
attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? "";
|
|
11783
|
+
}
|
|
11784
|
+
elements.push({ tag, attrs, raw: match[0].length > 80 ? match[0].slice(0, 77) + "..." : match[0] });
|
|
11785
|
+
}
|
|
11786
|
+
return elements;
|
|
11787
|
+
}
|
|
11788
|
+
function _validateAriaMcp(html) {
|
|
11789
|
+
if (!html || !html.trim()) return { error: "Input is empty" };
|
|
11790
|
+
const elements = _parseAriaElementsMcp(html);
|
|
11791
|
+
if (elements.length === 0) return { error: "No HTML elements found" };
|
|
11792
|
+
const issues = [];
|
|
11793
|
+
let elementsWithRoleOrAria = 0;
|
|
11794
|
+
for (const el of elements) {
|
|
11795
|
+
const roleAttr = el.attrs["role"];
|
|
11796
|
+
const ariaAttrNames = Object.keys(el.attrs).filter((a) => a.startsWith("aria-"));
|
|
11797
|
+
if (roleAttr === void 0 && ariaAttrNames.length === 0) continue;
|
|
11798
|
+
elementsWithRoleOrAria++;
|
|
11799
|
+
const roles = roleAttr !== void 0 ? roleAttr.trim().split(/\s+/).filter(Boolean) : [];
|
|
11800
|
+
const knownRoles = roles.filter((r) => Object.prototype.hasOwnProperty.call(_ARIA_ROLE_TABLE_MCP, r));
|
|
11801
|
+
const unknownRoles = roles.filter((r) => !Object.prototype.hasOwnProperty.call(_ARIA_ROLE_TABLE_MCP, r));
|
|
11802
|
+
for (const r of unknownRoles) {
|
|
11803
|
+
issues.push({ element: el.raw, role: r, severity: "error", message: `Unknown ARIA role "${r}"` });
|
|
11804
|
+
}
|
|
11805
|
+
const primaryRole = knownRoles[0];
|
|
11806
|
+
const roleDef = primaryRole ? _ARIA_ROLE_TABLE_MCP[primaryRole] : void 0;
|
|
11807
|
+
if (roleDef?.required) {
|
|
11808
|
+
for (const req of roleDef.required) {
|
|
11809
|
+
if (el.attrs[req] === void 0) {
|
|
11810
|
+
issues.push({ element: el.raw, role: primaryRole, severity: "error", message: `role="${primaryRole}" is missing required attribute ${req}` });
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
for (const attrName of ariaAttrNames) {
|
|
11815
|
+
if (_ARIA_GLOBAL_ATTRS_MCP.has(attrName)) continue;
|
|
11816
|
+
if (roleDef) {
|
|
11817
|
+
const allowed = /* @__PURE__ */ new Set([...roleDef.required ?? [], ...roleDef.supported ?? []]);
|
|
11818
|
+
if (!allowed.has(attrName)) {
|
|
11819
|
+
issues.push({
|
|
11820
|
+
element: el.raw,
|
|
11821
|
+
role: primaryRole ?? null,
|
|
11822
|
+
severity: "error",
|
|
11823
|
+
message: primaryRole ? `${attrName} is not permitted on role="${primaryRole}"` : `${attrName} is not a global aria-* attribute`
|
|
11824
|
+
});
|
|
11825
|
+
}
|
|
11826
|
+
} else if (!_ARIA_ALL_KNOWN_ATTRS_MCP.has(attrName)) {
|
|
11827
|
+
issues.push({ element: el.raw, role: null, severity: "warning", message: `${attrName} is not a recognized aria-* attribute (possible typo)` });
|
|
11828
|
+
}
|
|
11829
|
+
}
|
|
11830
|
+
}
|
|
11831
|
+
return {
|
|
11832
|
+
elementCount: elements.length,
|
|
11833
|
+
elementsWithRoleOrAria,
|
|
11834
|
+
issues,
|
|
11835
|
+
valid: issues.every((i) => i.severity !== "error")
|
|
11836
|
+
};
|
|
11837
|
+
}
|
|
11838
|
+
server.tool(
|
|
11839
|
+
"validate_aria",
|
|
11840
|
+
'Parse an HTML snippet and check every role and aria-* attribute against a curated table of WAI-ARIA 1.2 roles: unknown role names, an aria-* attribute not permitted on the given role, and a widget role missing its required aria-* state (e.g. role="checkbox" without aria-checked). Covers a curated subset of common roles, not the full ARIA spec.',
|
|
11841
|
+
{ html: z.string().describe("The HTML snippet to check") },
|
|
11842
|
+
async ({ html }) => {
|
|
11843
|
+
const result = _validateAriaMcp(html);
|
|
11844
|
+
if ("error" in result) return errText(result);
|
|
11845
|
+
return text(JSON.stringify(result, null, 2));
|
|
11846
|
+
}
|
|
11847
|
+
);
|
|
11508
11848
|
var transport = new StdioServerTransport();
|
|
11509
11849
|
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.74.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",
|