@utilix-tech/mcp 0.22.0 → 0.24.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,13 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
+ ## 0.24.0
6
+ - Add `calculate_margin_markup` tool: given exactly two of cost, price, margin percent, and markup percent, solve for the other two plus the gross profit.
7
+
8
+ ## 0.23.0
9
+ - Add `convert_apr_apy` tool: convert between a nominal APR and effective APY for a given compounding frequency, in both directions.
10
+ - Add `convert_iso8601_duration` tool: parse an ISO 8601 duration into components and total seconds, or format seconds into a canonical duration string.
11
+
5
12
  ## 0.22.0
6
13
  - Add `calculate_percentage_change` tool: percentage change between two numbers, with absolute change, direction, and multiplier.
7
14
  - Add `resolve_json_pointer` tool: resolve an RFC 6901 JSON Pointer against a document, with ~0/~1 escaping and not-found reasons.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 132 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 135 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
 
@@ -142,7 +142,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
142
142
  | `score_readability` | Score text with Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index |
143
143
  | `detect_filler_words` | Scan text for filler words, hedge phrases, and clichés with per-phrase counts and a density score |
144
144
 
145
- ### Time (5 tools)
145
+ ### Time (6 tools)
146
146
  | Tool | Description |
147
147
  |------|-------------|
148
148
  | `unix_timestamp` | Convert a Unix timestamp to human-readable date |
@@ -150,8 +150,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
150
150
  | `cron_next_runs` | Show the next N scheduled run times for a cron expression |
151
151
  | `date_diff` | Calculate the difference between two dates |
152
152
  | `convert_timezone` | Convert a date/time between timezones |
153
+ | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
153
154
 
154
- ### Units (13 tools)
155
+ ### Units (15 tools)
155
156
  | Tool | Description |
156
157
  |------|-------------|
157
158
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -167,6 +168,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
167
168
  | `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
168
169
  | `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
169
170
  | `calculate_percentage_change` | Compute the percentage change between two numbers, with absolute change, direction, and multiplier |
171
+ | `convert_apr_apy` | Convert between a nominal APR and effective APY for a given compounding frequency, in both directions |
172
+ | `calculate_margin_markup` | Given any two of cost, price, margin percent, and markup percent, solve for the other two plus gross profit |
170
173
 
171
174
  ### API / Network (10 tools)
172
175
  | Tool | Description |
package/dist/index.js CHANGED
@@ -3912,5 +3912,217 @@ server.tool(
3912
3912
  return text(JSON.stringify(result, null, 2));
3913
3913
  }
3914
3914
  );
3915
+ function _convertAprApyMcp(rate, compoundsPerYear, direction) {
3916
+ if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
3917
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) return { error: "Compounding frequency must be a positive whole number of periods per year" };
3918
+ if (direction !== "apr-to-apy" && direction !== "apy-to-apr") return { error: 'Direction must be "apr-to-apy" or "apy-to-apr"' };
3919
+ const round6 = (n2) => Math.round(n2 * 1e6) / 1e6;
3920
+ const round8 = (n2) => Math.round(n2 * 1e8) / 1e8;
3921
+ const n = compoundsPerYear;
3922
+ let aprDecimal;
3923
+ let apyDecimal;
3924
+ if (direction === "apr-to-apy") {
3925
+ aprDecimal = rate / 100;
3926
+ apyDecimal = Math.pow(1 + aprDecimal / n, n) - 1;
3927
+ } else {
3928
+ apyDecimal = rate / 100;
3929
+ aprDecimal = n * (Math.pow(1 + apyDecimal, 1 / n) - 1);
3930
+ }
3931
+ const apr = round6(aprDecimal * 100);
3932
+ const apy = round6(apyDecimal * 100);
3933
+ return {
3934
+ direction,
3935
+ compoundsPerYear: n,
3936
+ apr,
3937
+ apy,
3938
+ aprDecimal: round8(aprDecimal),
3939
+ apyDecimal: round8(apyDecimal),
3940
+ difference: round6(apy - apr)
3941
+ };
3942
+ }
3943
+ server.tool(
3944
+ "convert_apr_apy",
3945
+ 'Convert between a nominal annual rate (APR) and the effective annual yield (APY) for a given compounding frequency. With direction "apr-to-apy" the input is the nominal APR and the effective APY is computed as (1 + apr/n)^n - 1; with "apy-to-apr" the input is the effective APY and the APR is recovered as n * ((1 + apy)^(1/n) - 1). Both rates are percentages.',
3946
+ {
3947
+ rate: z.number().describe('The known rate as a percent: the nominal APR when direction is "apr-to-apy", or the effective APY when direction is "apy-to-apr"'),
3948
+ compoundsPerYear: z.number().int().describe("Compounding periods per year (12 = monthly, 365 = daily)"),
3949
+ direction: z.enum(["apr-to-apy", "apy-to-apr"]).describe("Which rate the input represents and which to compute")
3950
+ },
3951
+ async ({ rate, compoundsPerYear, direction }) => {
3952
+ const result = _convertAprApyMcp(rate, compoundsPerYear, direction);
3953
+ if ("error" in result) return errText(result);
3954
+ return text(JSON.stringify(result, null, 2));
3955
+ }
3956
+ );
3957
+ var _DURATION_SECONDS_PER_YEAR_MCP = 365 * 86400;
3958
+ var _DURATION_SECONDS_PER_MONTH_MCP = 30 * 86400;
3959
+ var _DURATION_RE_MCP = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
3960
+ function _humanizeDurationPartsMcp(parts) {
3961
+ const pieces = [];
3962
+ for (const [value, label] of parts) {
3963
+ if (value === 0) continue;
3964
+ pieces.push(`${value} ${label}${value === 1 ? "" : "s"}`);
3965
+ }
3966
+ return pieces.length > 0 ? pieces.join(", ") : "0 seconds";
3967
+ }
3968
+ function _parseIso8601DurationMcp(input) {
3969
+ const trimmed = input.trim();
3970
+ if (trimmed.length === 0) return { error: "Duration is empty" };
3971
+ const match = trimmed.match(_DURATION_RE_MCP);
3972
+ if (!match) return { error: 'Not a valid ISO 8601 duration (expected a form like "P3DT4H30M")' };
3973
+ const years = match[1] ? parseInt(match[1], 10) : 0;
3974
+ const months = match[2] ? parseInt(match[2], 10) : 0;
3975
+ const weeks = match[3] ? parseInt(match[3], 10) : 0;
3976
+ const days = match[4] ? parseInt(match[4], 10) : 0;
3977
+ const hours = match[5] ? parseInt(match[5], 10) : 0;
3978
+ const minutes = match[6] ? parseInt(match[6], 10) : 0;
3979
+ const seconds = match[7] ? parseFloat(match[7]) : 0;
3980
+ if (years + months + weeks + days + hours + minutes + seconds === 0) {
3981
+ return { error: 'Duration has no components (a bare "P" or "PT" is not valid)' };
3982
+ }
3983
+ const approximate = years > 0 || months > 0;
3984
+ const totalSeconds = years * _DURATION_SECONDS_PER_YEAR_MCP + months * _DURATION_SECONDS_PER_MONTH_MCP + weeks * 7 * 86400 + days * 86400 + hours * 3600 + minutes * 60 + seconds;
3985
+ return {
3986
+ input: trimmed,
3987
+ years,
3988
+ months,
3989
+ weeks,
3990
+ days,
3991
+ hours,
3992
+ minutes,
3993
+ seconds,
3994
+ totalSeconds: Math.round(totalSeconds * 1e6) / 1e6,
3995
+ approximate,
3996
+ human: _humanizeDurationPartsMcp([
3997
+ [years, "year"],
3998
+ [months, "month"],
3999
+ [weeks, "week"],
4000
+ [days, "day"],
4001
+ [hours, "hour"],
4002
+ [minutes, "minute"],
4003
+ [seconds, "second"]
4004
+ ])
4005
+ };
4006
+ }
4007
+ function _formatIso8601DurationMcp(seconds) {
4008
+ if (!Number.isFinite(seconds) || seconds < 0) return { error: "Seconds must be zero or a positive number" };
4009
+ const totalDays = Math.floor(seconds / 86400);
4010
+ let rem = seconds - totalDays * 86400;
4011
+ const hours = Math.floor(rem / 3600);
4012
+ rem -= hours * 3600;
4013
+ const minutes = Math.floor(rem / 60);
4014
+ let secs = rem - minutes * 60;
4015
+ secs = Math.round(secs * 1e6) / 1e6;
4016
+ let iso = "P";
4017
+ if (totalDays > 0) iso += `${totalDays}D`;
4018
+ const hasTime = hours > 0 || minutes > 0 || secs > 0;
4019
+ if (hasTime) {
4020
+ iso += "T";
4021
+ if (hours > 0) iso += `${hours}H`;
4022
+ if (minutes > 0) iso += `${minutes}M`;
4023
+ if (secs > 0) iso += `${secs}S`;
4024
+ }
4025
+ if (iso === "P") iso = "PT0S";
4026
+ return {
4027
+ seconds,
4028
+ iso,
4029
+ days: totalDays,
4030
+ hours,
4031
+ minutes,
4032
+ human: _humanizeDurationPartsMcp([
4033
+ [totalDays, "day"],
4034
+ [hours, "hour"],
4035
+ [minutes, "minute"],
4036
+ [secs, "second"]
4037
+ ])
4038
+ };
4039
+ }
4040
+ server.tool(
4041
+ "convert_iso8601_duration",
4042
+ 'Parse or format an ISO 8601 duration. With mode "parse", break a duration string like "P3DT4H30M" into its components and a total number of seconds (years and months use nominal 365-day / 30-day conversions and set an "approximate" flag). With mode "format", render a non-negative number of seconds into a canonical duration string using days/hours/minutes/seconds.',
4043
+ {
4044
+ mode: z.enum(["parse", "format"]).describe("parse an ISO string, or format a number of seconds"),
4045
+ input: z.string().optional().describe('The ISO 8601 duration to parse (required when mode is "parse")'),
4046
+ seconds: z.number().optional().describe('The number of seconds to format (required when mode is "format")')
4047
+ },
4048
+ async ({ mode, input, seconds }) => {
4049
+ let result;
4050
+ if (mode === "parse") {
4051
+ if (typeof input !== "string") return errText({ error: 'input is required when mode is "parse"' });
4052
+ result = _parseIso8601DurationMcp(input);
4053
+ } else {
4054
+ if (typeof seconds !== "number") return errText({ error: 'seconds is required when mode is "format"' });
4055
+ result = _formatIso8601DurationMcp(seconds);
4056
+ }
4057
+ if ("error" in result) return errText(result);
4058
+ return text(JSON.stringify(result, null, 2));
4059
+ }
4060
+ );
4061
+ var _MARGIN_FIELDS_MCP = ["cost", "price", "marginPercent", "markupPercent"];
4062
+ function _calculateMarginMarkupMcp(input) {
4063
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
4064
+ const given = _MARGIN_FIELDS_MCP.filter((f) => provided(input[f]));
4065
+ if (given.length !== 2) {
4066
+ return { error: `Provide exactly two of cost, price, marginPercent, markupPercent (got ${given.length})` };
4067
+ }
4068
+ const has = (f) => given.includes(f);
4069
+ const cost0 = input.cost;
4070
+ const price0 = input.price;
4071
+ const margin0 = input.marginPercent;
4072
+ const markup0 = input.markupPercent;
4073
+ if (has("cost") && cost0 <= 0) return { error: "Cost must be greater than zero" };
4074
+ if (has("price") && price0 <= 0) return { error: "Price must be greater than zero" };
4075
+ if (has("marginPercent") && margin0 >= 100) return { error: "Margin percent must be below 100 (a 100% margin implies a zero cost)" };
4076
+ if (has("markupPercent") && markup0 <= -100) return { error: "Markup percent must be greater than -100" };
4077
+ let cost;
4078
+ let price;
4079
+ if (has("cost") && has("price")) {
4080
+ cost = cost0;
4081
+ price = price0;
4082
+ } else if (has("cost") && has("marginPercent")) {
4083
+ cost = cost0;
4084
+ price = cost0 / (1 - margin0 / 100);
4085
+ } else if (has("cost") && has("markupPercent")) {
4086
+ cost = cost0;
4087
+ price = cost0 * (1 + markup0 / 100);
4088
+ } else if (has("price") && has("marginPercent")) {
4089
+ price = price0;
4090
+ cost = price0 * (1 - margin0 / 100);
4091
+ } else if (has("price") && has("markupPercent")) {
4092
+ price = price0;
4093
+ cost = price0 / (1 + markup0 / 100);
4094
+ } else {
4095
+ return { error: "Margin and markup alone cannot be solved: also provide a cost or a price" };
4096
+ }
4097
+ if (!Number.isFinite(cost) || !Number.isFinite(price)) return { error: "Inputs produce a non-finite result" };
4098
+ if (cost <= 0) return { error: "Those inputs imply a cost of zero or less" };
4099
+ if (price <= 0) return { error: "Those inputs imply a price of zero or less" };
4100
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
4101
+ const profit = price - cost;
4102
+ return {
4103
+ cost: round4(cost),
4104
+ price: round4(price),
4105
+ profit: round4(profit),
4106
+ marginPercent: round4(profit / price * 100),
4107
+ markupPercent: round4(profit / cost * 100),
4108
+ given,
4109
+ solvedFor: _MARGIN_FIELDS_MCP.filter((f) => !given.includes(f))
4110
+ };
4111
+ }
4112
+ server.tool(
4113
+ "calculate_margin_markup",
4114
+ "Solve a pricing triangle: given exactly two of cost, price, gross margin percent, and markup percent, derive the other two plus the gross profit. Margin is profit as a share of the selling price (profit / price) while markup is profit as a share of the cost (profit / cost), so a 20% margin equals a 25% markup. Margin plus markup alone cannot be solved, since neither carries an absolute amount.",
4115
+ {
4116
+ cost: z.number().optional().describe("Unit cost (what you pay). Provide exactly two of the four fields."),
4117
+ price: z.number().optional().describe("Selling price (what the customer pays). Provide exactly two of the four fields."),
4118
+ marginPercent: z.number().optional().describe("Gross margin as a percent of the selling price (must be below 100)"),
4119
+ markupPercent: z.number().optional().describe("Markup as a percent of the cost (must be greater than -100)")
4120
+ },
4121
+ async ({ cost, price, marginPercent, markupPercent }) => {
4122
+ const result = _calculateMarginMarkupMcp({ cost, price, marginPercent, markupPercent });
4123
+ if ("error" in result) return errText(result);
4124
+ return text(JSON.stringify(result, null, 2));
4125
+ }
4126
+ );
3915
4127
  var transport = new StdioServerTransport();
3916
4128
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "MCP server exposing 125 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",