@utilix-tech/mcp 0.45.0 → 0.47.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 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.47.0
6
+ - Add `calculate_depreciation_schedule` tool: build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits.
7
+ - Add `analyze_keyword_density` tool: count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes.
8
+
9
+ ## 0.46.0
10
+ - Add `calculate_cd_maturity` tool: compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net payout if withdrawn early.
11
+ - Add `build_client_hints`, `parse_client_hint_headers` tools: build an `Accept-CH`/`Critical-CH` response header from a list of client hint names with a plain-English note per hint, or decode raw Client Hints request-header text into structured values.
12
+
5
13
  ## 0.45.0
6
14
  - Add `build_permissions_policy`, `parse_permissions_policy` tools: build a `Permissions-Policy` response header from browser features and their per-feature allowlist with a plain-English note per feature, or parse an existing one into the same shape.
7
15
  - Add `calculate_payback_period` tool: compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 178 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 181 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
 
@@ -130,7 +130,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
130
130
  | `generate_qr` | Generate a QR code as an SVG string |
131
131
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
132
132
 
133
- ### Text (15 tools)
133
+ ### Text (16 tools)
134
134
  | Tool | Description |
135
135
  |------|-------------|
136
136
  | `convert_case` | Convert text case: camelCase, snake_case, PascalCase, etc. |
@@ -148,6 +148,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
148
148
  | `calculate_string_distance` | Compute the Levenshtein (or Damerau) edit distance and normalized similarity between two strings |
149
149
  | `analyze_sentence_lengths` | Split text into sentences and report a histogram of sentence lengths, plus mean, median, standard deviation, and a consistency label |
150
150
  | `lookup_emoji` | Search a curated table of ~280 common emoji by name, keyword, or GitHub-style shortcode, or look one up exactly by shortcode, character, or category |
151
+ | `analyze_keyword_density` | Count keyword/phrase frequency and density (occurrences per 100 words) across a block of text, with single-word or 2/3-word phrase modes |
151
152
 
152
153
  ### Time (6 tools)
153
154
  | Tool | Description |
@@ -159,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
159
160
  | `convert_timezone` | Convert a date/time between timezones |
160
161
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
161
162
 
162
- ### Units (27 tools)
163
+ ### Units (29 tools)
163
164
  | Tool | Description |
164
165
  |------|-------------|
165
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -189,8 +190,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
189
190
  | `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
190
191
  | `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
191
192
  | `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
193
+ | `calculate_cd_maturity` | Compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net early-withdrawal payout |
194
+ | `calculate_depreciation_schedule` | Build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits |
192
195
 
193
- ### API / Network (23 tools)
196
+ ### API / Network (25 tools)
194
197
  | Tool | Description |
195
198
  |------|-------------|
196
199
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -216,6 +219,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
216
219
  | `validate_manifest` | Validate a web app manifest.json against the W3C spec's installability requirements: name/short_name, start_url, display mode, and icon coverage |
217
220
  | `build_permissions_policy` | Build a Permissions-Policy response header from browser features and their per-feature allowlist, with a plain-English note per feature |
218
221
  | `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
222
+ | `build_client_hints` | Build an Accept-CH (and optional Critical-CH) response header from a list of client hint names, with a plain-English note on what each hint reveals |
223
+ | `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
219
224
 
220
225
  ### Color (4 tools)
221
226
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7630,5 +7630,376 @@ server.tool(
7630
7630
  return text(JSON.stringify(result, null, 2));
7631
7631
  }
7632
7632
  );
7633
+ function _calculateCdMaturityMcp(principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths) {
7634
+ const round2 = (n) => Math.round(n * 100) / 100;
7635
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7636
+ if (typeof principal !== "number" || !Number.isFinite(principal) || principal <= 0) {
7637
+ return { error: "Principal must be a finite number greater than zero" };
7638
+ }
7639
+ if (typeof annualRatePercent !== "number" || !Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
7640
+ return { error: "Annual interest rate must be zero or a positive number" };
7641
+ }
7642
+ if (typeof termMonths !== "number" || !Number.isFinite(termMonths) || termMonths <= 0) {
7643
+ return { error: "Term must be a positive number of months" };
7644
+ }
7645
+ if (typeof compoundsPerYear !== "number" || !Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
7646
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
7647
+ }
7648
+ if (earlyWithdrawalPenaltyMonths !== void 0 && (typeof earlyWithdrawalPenaltyMonths !== "number" || !Number.isFinite(earlyWithdrawalPenaltyMonths) || earlyWithdrawalPenaltyMonths < 0)) {
7649
+ return { error: "earlyWithdrawalPenaltyMonths must be zero or a positive number when provided" };
7650
+ }
7651
+ const ratePerPeriod = annualRatePercent / 100 / compoundsPerYear;
7652
+ const totalPeriods = compoundsPerYear * (termMonths / 12);
7653
+ const growth = ratePerPeriod === 0 ? 1 : Math.pow(1 + ratePerPeriod, totalPeriods);
7654
+ const maturityValue = principal * growth;
7655
+ const totalInterest = maturityValue - principal;
7656
+ const apy = ratePerPeriod === 0 ? 0 : (Math.pow(1 + ratePerPeriod, compoundsPerYear) - 1) * 100;
7657
+ const result = {
7658
+ principal: round2(principal),
7659
+ annualRatePercent: round4(annualRatePercent),
7660
+ termMonths,
7661
+ compoundsPerYear,
7662
+ maturityValue: round2(maturityValue),
7663
+ totalInterest: round2(totalInterest),
7664
+ apy: round4(apy)
7665
+ };
7666
+ if (earlyWithdrawalPenaltyMonths !== void 0) {
7667
+ const monthlyRate = annualRatePercent / 100 / 12;
7668
+ const forfeited = Math.min(principal * monthlyRate * earlyWithdrawalPenaltyMonths, Math.max(totalInterest, 0));
7669
+ result.earlyWithdrawalPenaltyMonths = earlyWithdrawalPenaltyMonths;
7670
+ result.earlyWithdrawalForfeitedInterest = round2(forfeited);
7671
+ result.earlyWithdrawalPayout = round2(maturityValue - forfeited);
7672
+ }
7673
+ return result;
7674
+ }
7675
+ server.tool(
7676
+ "calculate_cd_maturity",
7677
+ "Compute the maturity value of a certificate of deposit given a principal, a nominal annual interest rate (as a percent), a term in months, and a compounding frequency (periods per year, e.g. 1 = annually, 12 = monthly). Also reports the effective annual yield (APY) implied by the nominal rate and compounding frequency, and, when an early-withdrawal penalty in months of forfeited interest is given, the net payout if withdrawn before maturity.",
7678
+ {
7679
+ principal: z.number().describe("The deposit principal (must be greater than zero)"),
7680
+ annualRatePercent: z.number().describe("Nominal annual interest rate, as a percent, e.g. 5"),
7681
+ termMonths: z.number().describe("CD term in months"),
7682
+ compoundsPerYear: z.number().int().describe("Compounding periods per year, e.g. 1 = annually, 4 = quarterly, 12 = monthly, 365 = daily"),
7683
+ earlyWithdrawalPenaltyMonths: z.number().optional().describe("Optional early-withdrawal penalty, in months of forfeited interest; when given, also computes the net early-withdrawal payout")
7684
+ },
7685
+ async ({ principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths }) => {
7686
+ const result = _calculateCdMaturityMcp(principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths);
7687
+ if ("error" in result) return errText(result);
7688
+ return text(JSON.stringify(result, null, 2));
7689
+ }
7690
+ );
7691
+ var _CLIENT_HINTS_MCP = [
7692
+ { name: "Sec-CH-UA", description: "The browser's brand(s) and significant version, sent automatically on every request without an Accept-CH opt-in.", lowEntropy: true, valueType: "sf-list-brand" },
7693
+ { name: "Sec-CH-UA-Mobile", description: "Whether the browser is running on a mobile device (?1) or not (?0).", lowEntropy: true, valueType: "boolean" },
7694
+ { name: "Sec-CH-UA-Platform", description: 'The platform/OS the browser is running on, e.g. "Windows" or "Android".', lowEntropy: true, valueType: "string" },
7695
+ { name: "Sec-CH-UA-Full-Version-List", description: "The full version number for each brand in the user agent, not just the significant version.", lowEntropy: false, valueType: "sf-list-brand" },
7696
+ { name: "Sec-CH-UA-Platform-Version", description: 'The exact OS version, e.g. "15.0.0".', lowEntropy: false, valueType: "string" },
7697
+ { name: "Sec-CH-UA-Arch", description: 'The underlying CPU architecture, e.g. "x86" or "arm".', lowEntropy: false, valueType: "string" },
7698
+ { name: "Sec-CH-UA-Bitness", description: 'The CPU architecture bitness, e.g. "64".', lowEntropy: false, valueType: "string" },
7699
+ { name: "Sec-CH-UA-Model", description: "The device model, mostly populated on mobile devices.", lowEntropy: false, valueType: "string" },
7700
+ { name: "Sec-CH-UA-WoW64", description: "Whether a 32-bit browser is running on 64-bit Windows.", lowEntropy: false, valueType: "boolean" },
7701
+ { name: "Sec-CH-UA-Form-Factor", description: `The device's form factor, e.g. "Desktop", "Mobile", "Tablet", or "XR".`, lowEntropy: false, valueType: "string" },
7702
+ { name: "Sec-CH-Prefers-Color-Scheme", description: "The user's preferred color scheme, mirrors the prefers-color-scheme media feature.", lowEntropy: false, valueType: "string" },
7703
+ { name: "Sec-CH-Prefers-Reduced-Motion", description: "Whether the user has requested reduced motion.", lowEntropy: false, valueType: "string" },
7704
+ { name: "Sec-CH-Prefers-Reduced-Transparency", description: "Whether the user has requested reduced transparency.", lowEntropy: false, valueType: "string" },
7705
+ { name: "Sec-CH-Viewport-Width", description: "The layout viewport width in CSS pixels.", lowEntropy: false, valueType: "number" },
7706
+ { name: "Sec-CH-Viewport-Height", description: "The layout viewport height in CSS pixels.", lowEntropy: false, valueType: "number" },
7707
+ { name: "Sec-CH-DPR", description: "The device pixel ratio.", lowEntropy: false, valueType: "number" },
7708
+ { name: "Sec-CH-Width", description: "The intrinsic width of a requested image resource, set per-request via the image's sizes attribute.", lowEntropy: false, valueType: "number" },
7709
+ { name: "Sec-CH-Device-Memory", description: "Approximate device RAM in GiB, rounded to the nearest power of two for privacy.", lowEntropy: false, valueType: "number" },
7710
+ { name: "Save-Data", description: "Legacy hint signaling the user opted into a reduced-data mode; sent automatically like a low-entropy hint.", lowEntropy: true, valueType: "boolean" }
7711
+ ];
7712
+ function _findClientHintMcp(raw) {
7713
+ const normalized = raw.trim().toLowerCase();
7714
+ return _CLIENT_HINTS_MCP.find((d) => d.name.toLowerCase() === normalized);
7715
+ }
7716
+ function _buildClientHintsMcp(hintNames, criticalHintNames = []) {
7717
+ if (!Array.isArray(hintNames) || hintNames.length === 0) return { error: "At least one client hint is required" };
7718
+ const resolved = [];
7719
+ for (const raw of hintNames) {
7720
+ if (typeof raw !== "string" || raw.trim() === "") continue;
7721
+ const def = _findClientHintMcp(raw);
7722
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
7723
+ resolved.push(def);
7724
+ }
7725
+ if (resolved.length === 0) return { error: "At least one client hint is required" };
7726
+ const critical = [];
7727
+ for (const raw of criticalHintNames) {
7728
+ if (typeof raw !== "string" || raw.trim() === "") continue;
7729
+ const def = _findClientHintMcp(raw);
7730
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
7731
+ if (!resolved.some((d) => d.name === def.name)) {
7732
+ return { error: `Critical hint "${def.name}" must also be included in hintNames` };
7733
+ }
7734
+ critical.push(def);
7735
+ }
7736
+ const uniqueNames = Array.from(new Set(resolved.map((d) => d.name)));
7737
+ const result = {
7738
+ acceptCH: uniqueNames.join(", "),
7739
+ hints: resolved.map((d) => ({ name: d.name, description: d.description, lowEntropy: d.lowEntropy }))
7740
+ };
7741
+ if (critical.length > 0) {
7742
+ result.criticalCH = Array.from(new Set(critical.map((d) => d.name))).join(", ");
7743
+ }
7744
+ return result;
7745
+ }
7746
+ server.tool(
7747
+ "build_client_hints",
7748
+ "Build an Accept-CH response-header value from a list of client hint names (e.g. Sec-CH-UA-Platform, Sec-CH-DPR) the server wants the browser to send on future requests, attaching a plain-English note on what each hint reveals. Pass a subset of those names as criticalHintNames to also build a Critical-CH header value, for hints the server needs on the very first request of a session.",
7749
+ {
7750
+ hintNames: z.array(z.string()).min(1).describe('Client hint names to request, e.g. "Sec-CH-UA-Platform"'),
7751
+ criticalHintNames: z.array(z.string()).optional().describe("Optional subset of hintNames to also mark critical (builds a Critical-CH header value)")
7752
+ },
7753
+ async ({ hintNames, criticalHintNames }) => {
7754
+ const result = _buildClientHintsMcp(hintNames, criticalHintNames ?? []);
7755
+ if ("error" in result) return errText(result);
7756
+ return text(JSON.stringify(result, null, 2));
7757
+ }
7758
+ );
7759
+ function _decodeClientHintValueMcp(valueType, rawValue) {
7760
+ switch (valueType) {
7761
+ case "boolean":
7762
+ if (rawValue === "?1") return true;
7763
+ if (rawValue === "?0") return false;
7764
+ return rawValue;
7765
+ case "string":
7766
+ return rawValue.replace(/^"|"$/g, "");
7767
+ case "number": {
7768
+ const n = Number(rawValue);
7769
+ return Number.isFinite(n) ? n : rawValue;
7770
+ }
7771
+ case "sf-list-brand":
7772
+ return rawValue.split(",").map((part) => {
7773
+ const m = part.trim().match(/^"([^"]*)";v="([^"]*)"$/);
7774
+ return m ? { brand: m[1], version: m[2] } : { raw: part.trim() };
7775
+ });
7776
+ default:
7777
+ return rawValue;
7778
+ }
7779
+ }
7780
+ function _parseClientHintHeadersMcp(rawHeaders) {
7781
+ if (typeof rawHeaders !== "string" || rawHeaders.trim() === "") return { error: "Provide one or more header lines to parse" };
7782
+ const lines = rawHeaders.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
7783
+ const hints = [];
7784
+ const unrecognized = [];
7785
+ for (const line of lines) {
7786
+ const idx = line.indexOf(":");
7787
+ if (idx === -1) continue;
7788
+ const name = line.slice(0, idx).trim();
7789
+ const rawValue = line.slice(idx + 1).trim();
7790
+ const def = _findClientHintMcp(name);
7791
+ if (!def) {
7792
+ unrecognized.push({ name, rawValue });
7793
+ continue;
7794
+ }
7795
+ hints.push({ name: def.name, rawValue, description: def.description, value: _decodeClientHintValueMcp(def.valueType, rawValue) });
7796
+ }
7797
+ if (hints.length === 0 && unrecognized.length === 0) {
7798
+ return { error: 'No parsable header lines found (expected "Header-Name: value" per line)' };
7799
+ }
7800
+ return { hints, unrecognized };
7801
+ }
7802
+ server.tool(
7803
+ "parse_client_hint_headers",
7804
+ `Parse raw request-header text, one "Header-Name: value" per line, into decoded client hint values: brand/version pairs for Sec-CH-UA-style headers, booleans for ?1/?0 values, unquoted strings, and numbers. Lines whose header name isn't a recognized client hint are returned separately under "unrecognized" rather than dropped.`,
7805
+ {
7806
+ headers: z.string().min(1).describe('Raw request-header text to parse, one "Header-Name: value" per line')
7807
+ },
7808
+ async ({ headers }) => {
7809
+ const result = _parseClientHintHeadersMcp(headers);
7810
+ if ("error" in result) return errText(result);
7811
+ return text(JSON.stringify(result, null, 2));
7812
+ }
7813
+ );
7814
+ var _DEPRECIATION_METHODS_MCP = ["straight-line", "double-declining-balance", "sum-of-years-digits"];
7815
+ function _calculateDepreciationScheduleMcp(cost, salvageValue, usefulLifeYears, method) {
7816
+ if (!Number.isFinite(cost) || cost <= 0) return { error: "Cost must be a positive number" };
7817
+ if (!Number.isFinite(salvageValue) || salvageValue < 0) return { error: "Salvage value must be zero or a positive number" };
7818
+ if (salvageValue >= cost) return { error: "Salvage value must be less than cost" };
7819
+ if (!Number.isInteger(usefulLifeYears) || usefulLifeYears <= 0) {
7820
+ return { error: "Useful life must be a positive whole number of years" };
7821
+ }
7822
+ if (!_DEPRECIATION_METHODS_MCP.includes(method)) {
7823
+ return { error: `Method must be one of: ${_DEPRECIATION_METHODS_MCP.join(", ")}` };
7824
+ }
7825
+ const depreciableBase = _roundMoneyMcp(cost - salvageValue);
7826
+ const schedule = [];
7827
+ let accumulated = 0;
7828
+ if (method === "straight-line") {
7829
+ const annual = _roundMoneyMcp(depreciableBase / usefulLifeYears);
7830
+ for (let year = 1; year <= usefulLifeYears; year++) {
7831
+ const expense = year === usefulLifeYears ? _roundMoneyMcp(depreciableBase - accumulated) : annual;
7832
+ accumulated = _roundMoneyMcp(accumulated + expense);
7833
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: _roundMoneyMcp(cost - accumulated) });
7834
+ }
7835
+ } else if (method === "double-declining-balance") {
7836
+ const rate = 2 / usefulLifeYears;
7837
+ let bookValue = cost;
7838
+ for (let year = 1; year <= usefulLifeYears; year++) {
7839
+ let expense = _roundMoneyMcp(bookValue * rate);
7840
+ if (bookValue - expense < salvageValue) expense = _roundMoneyMcp(bookValue - salvageValue);
7841
+ if (expense < 0) expense = 0;
7842
+ bookValue = _roundMoneyMcp(bookValue - expense);
7843
+ accumulated = _roundMoneyMcp(accumulated + expense);
7844
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue });
7845
+ }
7846
+ } else {
7847
+ const sumOfYears = usefulLifeYears * (usefulLifeYears + 1) / 2;
7848
+ for (let year = 1; year <= usefulLifeYears; year++) {
7849
+ const fraction = (usefulLifeYears - year + 1) / sumOfYears;
7850
+ const expense = year === usefulLifeYears ? _roundMoneyMcp(depreciableBase - accumulated) : _roundMoneyMcp(fraction * depreciableBase);
7851
+ accumulated = _roundMoneyMcp(accumulated + expense);
7852
+ schedule.push({ year, depreciationExpense: expense, accumulatedDepreciation: accumulated, bookValue: _roundMoneyMcp(cost - accumulated) });
7853
+ }
7854
+ }
7855
+ return { cost, salvageValue, usefulLifeYears, method, totalDepreciation: accumulated, schedule };
7856
+ }
7857
+ server.tool(
7858
+ "calculate_depreciation_schedule",
7859
+ "Build a year-by-year depreciation schedule for a fixed asset from its cost, salvage value, and useful life in years, using straight-line, double-declining-balance, or sum-of-years-digits. The double-declining-balance method clamps at salvage value rather than auto-switching to straight-line partway through.",
7860
+ {
7861
+ cost: z.number().describe("The asset's original cost"),
7862
+ salvageValue: z.number().describe("Estimated residual value at the end of its useful life; must be less than cost"),
7863
+ usefulLifeYears: z.number().int().describe("Useful life in whole years"),
7864
+ method: z.enum(["straight-line", "double-declining-balance", "sum-of-years-digits"]).describe("Depreciation method to use")
7865
+ },
7866
+ async ({ cost, salvageValue, usefulLifeYears, method }) => {
7867
+ const result = _calculateDepreciationScheduleMcp(cost, salvageValue, usefulLifeYears, method);
7868
+ if ("error" in result) return errText(result);
7869
+ return text(JSON.stringify(result, null, 2));
7870
+ }
7871
+ );
7872
+ var _KEYWORD_DENSITY_STOPWORDS_MCP = /* @__PURE__ */ new Set([
7873
+ "a",
7874
+ "an",
7875
+ "the",
7876
+ "and",
7877
+ "or",
7878
+ "but",
7879
+ "if",
7880
+ "then",
7881
+ "else",
7882
+ "for",
7883
+ "nor",
7884
+ "so",
7885
+ "yet",
7886
+ "of",
7887
+ "in",
7888
+ "on",
7889
+ "at",
7890
+ "to",
7891
+ "from",
7892
+ "by",
7893
+ "with",
7894
+ "about",
7895
+ "against",
7896
+ "between",
7897
+ "into",
7898
+ "through",
7899
+ "during",
7900
+ "before",
7901
+ "after",
7902
+ "above",
7903
+ "below",
7904
+ "up",
7905
+ "down",
7906
+ "out",
7907
+ "off",
7908
+ "over",
7909
+ "under",
7910
+ "again",
7911
+ "further",
7912
+ "once",
7913
+ "is",
7914
+ "are",
7915
+ "was",
7916
+ "were",
7917
+ "be",
7918
+ "been",
7919
+ "being",
7920
+ "am",
7921
+ "do",
7922
+ "does",
7923
+ "did",
7924
+ "doing",
7925
+ "have",
7926
+ "has",
7927
+ "had",
7928
+ "having",
7929
+ "i",
7930
+ "me",
7931
+ "my",
7932
+ "we",
7933
+ "our",
7934
+ "you",
7935
+ "your",
7936
+ "he",
7937
+ "him",
7938
+ "his",
7939
+ "she",
7940
+ "her",
7941
+ "it",
7942
+ "its",
7943
+ "they",
7944
+ "them",
7945
+ "their",
7946
+ "this",
7947
+ "that",
7948
+ "these",
7949
+ "those",
7950
+ "as",
7951
+ "not",
7952
+ "no",
7953
+ "can",
7954
+ "will",
7955
+ "just",
7956
+ "than",
7957
+ "too",
7958
+ "very",
7959
+ "would",
7960
+ "should",
7961
+ "could",
7962
+ "there",
7963
+ "here",
7964
+ "what",
7965
+ "which",
7966
+ "who",
7967
+ "whom"
7968
+ ]);
7969
+ function _analyzeKeywordDensityMcp(input, topN, minWordLength, ngramSize) {
7970
+ if (!input || !input.trim()) return { error: "Input text is empty" };
7971
+ if (!Number.isInteger(topN) || topN <= 0) return { error: "topN must be a positive whole number" };
7972
+ if (!Number.isInteger(minWordLength) || minWordLength < 1) return { error: "minWordLength must be a positive whole number" };
7973
+ if (!Number.isInteger(ngramSize) || ngramSize < 1 || ngramSize > 3) return { error: "ngramSize must be 1, 2, or 3" };
7974
+ const tokens = input.toLowerCase().match(/[a-z0-9']+/g) ?? [];
7975
+ const totalWords = tokens.length;
7976
+ if (totalWords === 0) return { error: "No words found in input text" };
7977
+ if (totalWords < ngramSize) return { error: "Input text has fewer words than the requested phrase length" };
7978
+ const counts = /* @__PURE__ */ new Map();
7979
+ for (let i = 0; i <= tokens.length - ngramSize; i++) {
7980
+ const phraseWords = tokens.slice(i, i + ngramSize);
7981
+ if (phraseWords.some((w) => w.length < minWordLength)) continue;
7982
+ if (ngramSize === 1 && _KEYWORD_DENSITY_STOPWORDS_MCP.has(phraseWords[0])) continue;
7983
+ const phrase = phraseWords.join(" ");
7984
+ counts.set(phrase, (counts.get(phrase) ?? 0) + 1);
7985
+ }
7986
+ const keywords = Array.from(counts.entries()).map(([keyword, count]) => ({ keyword, count, density: Math.round(count / totalWords * 1e3) / 10 })).sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword)).slice(0, topN);
7987
+ return { totalWords, uniqueKeywords: counts.size, ngramSize, keywords };
7988
+ }
7989
+ server.tool(
7990
+ "analyze_keyword_density",
7991
+ "Count keyword or phrase frequency and density (occurrences per 100 words) across a block of text, the way an SEO keyword-density checker does. With the default ngramSize of 1, common English stopwords and words shorter than minWordLength are excluded; for ngramSize 2 or 3 (phrases), stopwords are kept since useful phrases often contain them.",
7992
+ {
7993
+ text: z.string().describe("The text to analyze"),
7994
+ topN: z.number().int().optional().describe("Number of top keywords/phrases to return (default 10)"),
7995
+ minWordLength: z.number().int().optional().describe("Minimum word length to include (default 3)"),
7996
+ ngramSize: z.number().int().min(1).max(3).optional().describe("Phrase length in words: 1 for single words, 2 or 3 for phrases (default 1)")
7997
+ },
7998
+ async ({ text: input, topN, minWordLength, ngramSize }) => {
7999
+ const result = _analyzeKeywordDensityMcp(input, topN ?? 10, minWordLength ?? 3, ngramSize ?? 1);
8000
+ if ("error" in result) return errText(result);
8001
+ return text(JSON.stringify(result, null, 2));
8002
+ }
8003
+ );
7633
8004
  var transport = new StdioServerTransport();
7634
8005
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "MCP server exposing 175 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",