@utilix-tech/mcp 0.21.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 +8 -0
- package/README.md +8 -4
- package/dist/index.js +233 -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.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
|
+
|
|
9
|
+
## 0.22.0
|
|
10
|
+
- Add `calculate_percentage_change` tool: percentage change between two numbers, with absolute change, direction, and multiplier.
|
|
11
|
+
- Add `resolve_json_pointer` tool: resolve an RFC 6901 JSON Pointer against a document, with ~0/~1 escaping and not-found reasons.
|
|
12
|
+
|
|
5
13
|
## 0.21.0
|
|
6
14
|
- Add `build_cache_control` tool: build and explain a `Cache-Control` response header from directives.
|
|
7
15
|
- Add `detect_filler_words` tool: scan text for filler words, hedge phrases, and clichés.
|
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
|
|
|
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
75
75
|
|
|
76
76
|
## Tools
|
|
77
77
|
|
|
78
|
-
### JSON (
|
|
78
|
+
### JSON (14 tools)
|
|
79
79
|
| Tool | Description |
|
|
80
80
|
|------|-------------|
|
|
81
81
|
| `format_json` | Pretty-print and validate a JSON string |
|
|
@@ -84,6 +84,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
84
84
|
| `json_to_csv` | Convert a JSON array of objects to CSV |
|
|
85
85
|
| `csv_to_json` | Convert CSV text to a JSON array |
|
|
86
86
|
| `jsonpath` | Evaluate a JSONPath expression |
|
|
87
|
+
| `resolve_json_pointer` | Resolve an RFC 6901 JSON Pointer against a JSON document |
|
|
87
88
|
| `validate_json` | Validate JSON against a JSON Schema |
|
|
88
89
|
| `yaml_to_json` | Convert YAML to JSON |
|
|
89
90
|
| `json_to_yaml` | Convert JSON to YAML |
|
|
@@ -141,7 +142,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
141
142
|
| `score_readability` | Score text with Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index |
|
|
142
143
|
| `detect_filler_words` | Scan text for filler words, hedge phrases, and clichés with per-phrase counts and a density score |
|
|
143
144
|
|
|
144
|
-
### Time (
|
|
145
|
+
### Time (6 tools)
|
|
145
146
|
| Tool | Description |
|
|
146
147
|
|------|-------------|
|
|
147
148
|
| `unix_timestamp` | Convert a Unix timestamp to human-readable date |
|
|
@@ -149,8 +150,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
149
150
|
| `cron_next_runs` | Show the next N scheduled run times for a cron expression |
|
|
150
151
|
| `date_diff` | Calculate the difference between two dates |
|
|
151
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 |
|
|
152
154
|
|
|
153
|
-
### Units (
|
|
155
|
+
### Units (14 tools)
|
|
154
156
|
| Tool | Description |
|
|
155
157
|
|------|-------------|
|
|
156
158
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -165,6 +167,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
165
167
|
| `calculate_tip_split` | Compute a tip on a bill amount and split the bill, tip, and total evenly across N people |
|
|
166
168
|
| `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
|
|
167
169
|
| `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
|
|
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 |
|
|
168
172
|
|
|
169
173
|
### API / Network (10 tools)
|
|
170
174
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -3825,5 +3825,238 @@ server.tool(
|
|
|
3825
3825
|
return text(JSON.stringify(_detectFillerWordsMcp(input), null, 2));
|
|
3826
3826
|
}
|
|
3827
3827
|
);
|
|
3828
|
+
function _calculatePercentageChangeMcp(oldValue, newValue) {
|
|
3829
|
+
if (!Number.isFinite(oldValue)) return { error: "Original value must be a finite number" };
|
|
3830
|
+
if (!Number.isFinite(newValue)) return { error: "New value must be a finite number" };
|
|
3831
|
+
if (oldValue === 0 && newValue !== 0) return { error: "Percentage change from zero is undefined (division by zero); use a nonzero original value" };
|
|
3832
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
3833
|
+
const absoluteChange = newValue - oldValue;
|
|
3834
|
+
const percentChange = oldValue === 0 ? 0 : absoluteChange / Math.abs(oldValue) * 100;
|
|
3835
|
+
const multiplier = oldValue === 0 ? 1 : newValue / oldValue;
|
|
3836
|
+
let direction;
|
|
3837
|
+
if (absoluteChange > 0) direction = "increase";
|
|
3838
|
+
else if (absoluteChange < 0) direction = "decrease";
|
|
3839
|
+
else direction = "no change";
|
|
3840
|
+
return {
|
|
3841
|
+
oldValue,
|
|
3842
|
+
newValue,
|
|
3843
|
+
absoluteChange: round4(absoluteChange),
|
|
3844
|
+
percentChange: round4(percentChange),
|
|
3845
|
+
direction,
|
|
3846
|
+
multiplier: round4(multiplier)
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
server.tool(
|
|
3850
|
+
"calculate_percentage_change",
|
|
3851
|
+
"Compute the percentage change from an original value to a new value as ((new - old) / |old|) * 100, plus the absolute difference, the direction (increase/decrease/no change), and the multiplier (new / old). The original value cannot be zero unless the new value is also zero.",
|
|
3852
|
+
{
|
|
3853
|
+
oldValue: z.number().describe("The original / starting value"),
|
|
3854
|
+
newValue: z.number().describe("The new / ending value")
|
|
3855
|
+
},
|
|
3856
|
+
async ({ oldValue, newValue }) => {
|
|
3857
|
+
const result = _calculatePercentageChangeMcp(oldValue, newValue);
|
|
3858
|
+
if ("error" in result) return errText(result);
|
|
3859
|
+
return text(JSON.stringify(result, null, 2));
|
|
3860
|
+
}
|
|
3861
|
+
);
|
|
3862
|
+
function _unescapePointerTokenMcp(token) {
|
|
3863
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
3864
|
+
}
|
|
3865
|
+
function _resolveJsonPointerMcp(json, pointer) {
|
|
3866
|
+
let parsed;
|
|
3867
|
+
try {
|
|
3868
|
+
parsed = JSON.parse(json);
|
|
3869
|
+
} catch {
|
|
3870
|
+
return { pointer, found: false, tokens: [], error: "Invalid JSON input" };
|
|
3871
|
+
}
|
|
3872
|
+
let ptr = pointer;
|
|
3873
|
+
if (ptr.startsWith("#")) {
|
|
3874
|
+
ptr = ptr.slice(1);
|
|
3875
|
+
try {
|
|
3876
|
+
ptr = decodeURIComponent(ptr);
|
|
3877
|
+
} catch {
|
|
3878
|
+
return { pointer, found: false, tokens: [], error: "Invalid percent-encoding in URI-fragment pointer" };
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
if (ptr === "") return { pointer, found: true, value: parsed, tokens: [] };
|
|
3882
|
+
if (!ptr.startsWith("/")) return { pointer, found: false, tokens: [], error: "Pointer must be empty or start with '/' (RFC 6901)" };
|
|
3883
|
+
const tokens = ptr.split("/").slice(1).map(_unescapePointerTokenMcp);
|
|
3884
|
+
let current = parsed;
|
|
3885
|
+
for (const token of tokens) {
|
|
3886
|
+
if (Array.isArray(current)) {
|
|
3887
|
+
if (token === "-") return { pointer, found: false, tokens, failedAt: token, reason: '"-" refers to the nonexistent element after the end of the array' };
|
|
3888
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) return { pointer, found: false, tokens, failedAt: token, reason: `"${token}" is not a valid array index` };
|
|
3889
|
+
const idx = Number(token);
|
|
3890
|
+
if (idx >= current.length) return { pointer, found: false, tokens, failedAt: token, reason: `array index ${idx} is out of bounds (length ${current.length})` };
|
|
3891
|
+
current = current[idx];
|
|
3892
|
+
} else if (current !== null && typeof current === "object") {
|
|
3893
|
+
const obj = current;
|
|
3894
|
+
if (!Object.prototype.hasOwnProperty.call(obj, token)) return { pointer, found: false, tokens, failedAt: token, reason: `key "${token}" not found` };
|
|
3895
|
+
current = obj[token];
|
|
3896
|
+
} else {
|
|
3897
|
+
return { pointer, found: false, tokens, failedAt: token, reason: `cannot descend into ${current === null ? "null" : typeof current} at "${token}"` };
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
return { pointer, found: true, value: current, tokens };
|
|
3901
|
+
}
|
|
3902
|
+
server.tool(
|
|
3903
|
+
"resolve_json_pointer",
|
|
3904
|
+
'Resolve an RFC 6901 JSON Pointer against a JSON document, in both the plain form ("" for the whole document, or "/foo/0") and the URI-fragment form ("#/foo/0"). Handles ~1 -> / and ~0 -> ~ escaping, and distinguishes an invalid document / malformed pointer from a syntactically valid pointer whose target is simply absent.',
|
|
3905
|
+
{
|
|
3906
|
+
json: z.string().describe("The JSON document to resolve against"),
|
|
3907
|
+
pointer: z.string().describe("The RFC 6901 JSON Pointer, e.g. /users/0/name")
|
|
3908
|
+
},
|
|
3909
|
+
async ({ json, pointer }) => {
|
|
3910
|
+
const result = _resolveJsonPointerMcp(json, pointer);
|
|
3911
|
+
if ("error" in result) return errText(result);
|
|
3912
|
+
return text(JSON.stringify(result, null, 2));
|
|
3913
|
+
}
|
|
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
|
+
);
|
|
3828
4061
|
var transport = new StdioServerTransport();
|
|
3829
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",
|