@utilix-tech/mcp 0.22.0 → 0.23.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 +4 -0
- package/README.md +5 -3
- package/dist/index.js +146 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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.23.0
|
|
6
|
+
- Add `convert_apr_apy` tool: convert between a nominal APR and effective APY for a given compounding frequency, in both directions.
|
|
7
|
+
- Add `convert_iso8601_duration` tool: parse an ISO 8601 duration into components and total seconds, or format seconds into a canonical duration string.
|
|
8
|
+
|
|
5
9
|
## 0.22.0
|
|
6
10
|
- Add `calculate_percentage_change` tool: percentage change between two numbers, with absolute change, direction, and multiplier.
|
|
7
11
|
- 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
|
|
3
|
+
MCP server exposing 134 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 (
|
|
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 (
|
|
155
|
+
### Units (14 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,7 @@ 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 |
|
|
170
172
|
|
|
171
173
|
### API / Network (10 tools)
|
|
172
174
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -3912,5 +3912,151 @@ 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
|
+
);
|
|
3915
4061
|
var transport = new StdioServerTransport();
|
|
3916
4062
|
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.23.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",
|