@utilix-tech/mcp 0.97.0 → 0.99.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.99.0
6
+ - Add `calculate_dividend_yield` tool: compute a stock's dividend yield from annual dividend per share and share price, plus payout ratio and retention ratio when trailing EPS is given.
7
+ - Add `check_known_hosts_fingerprints` tool: compute SHA256/MD5 fingerprints for ssh_known_hosts entries the way `ssh-keygen -lf` does, decoding key type and RSA bit length to flag weak or deprecated keys.
8
+
9
+ ## 0.98.0
10
+ - Add `convert_timecode` tool: convert between a frame count, SMPTE timecode (HH:MM:SS:FF), and seconds at a given frame rate, handling NTSC drop-frame timecode's skipped frame numbers correctly.
11
+ - Add `validate_geojson` tool: validate a GeoJSON document against RFC 7946's structural rules, geometry type/coordinates shape, polygon ring closure and winding, and a FeatureCollection's required fields.
12
+
5
13
  ## 0.97.0
6
14
  - Add `calculate_position_size` tool: compute a trade's position size from account size, risk percent per trade, entry price, and stop-loss price, plus the risk/reward ratio against an optional target price.
7
15
  - Add `detect_weasel_words` tool: scan text for weasel words, vague hedging phrases that imply evidence or consensus without naming a checkable source, like "studies show" or "many experts believe".
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 287 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 291 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
 
@@ -89,6 +89,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
89
89
  | `diff_json_patch` | Compare two JSON documents and generate an RFC 6902 JSON Patch operation list |
90
90
  | `apply_json_patch` | Apply a sequence of RFC 6902 JSON Patch operations (add, remove, replace, move, copy, test) to a target document |
91
91
  | `validate_json` | Validate JSON against a JSON Schema |
92
+ | `validate_geojson` | Validate a GeoJSON document against RFC 7946's structural rules: geometry type/coordinates shape, polygon ring closure and winding, and a FeatureCollection's required fields |
92
93
  | `yaml_to_json` | Convert YAML to JSON |
93
94
  | `json_to_yaml` | Convert JSON to YAML |
94
95
  | `json_to_typescript` | Generate TypeScript interfaces from JSON |
@@ -113,7 +114,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
113
114
  | `base62_encode` | Encode text as Base62 (alphanumeric, URL-safe) |
114
115
  | `base62_decode` | Decode a Base62 string back to text |
115
116
 
116
- ### Hashing (6 tools)
117
+ ### Hashing (7 tools)
117
118
  | Tool | Description |
118
119
  |------|-------------|
119
120
  | `hash` | Hash a string with MD5, SHA-1, SHA-256, SHA-384, or SHA-512 |
@@ -122,6 +123,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
122
123
  | `bcrypt_verify` | Verify a password against a bcrypt hash |
123
124
  | `generate_sri_hash` | Compute a Subresource Integrity hash (SHA-256/384/512) for a script or stylesheet's content, formatted for an HTML `integrity` attribute |
124
125
  | `generate_etag` | Compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator prefix |
126
+ | `check_known_hosts_fingerprints` | Parse an ssh_known_hosts file and compute each key's SHA256/MD5 fingerprint the way `ssh-keygen -lf` does, decoding key type and RSA bit length to flag weak or deprecated keys |
125
127
 
126
128
  ### Generators (7 tools)
127
129
  | Tool | Description |
@@ -170,7 +172,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
170
172
  | `convert_timezone` | Convert a date/time between timezones |
171
173
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
172
174
 
173
- ### Units (53 tools)
175
+ ### Units (54 tools)
174
176
  | Tool | Description |
175
177
  |------|-------------|
176
178
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -194,6 +196,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
194
196
  | `calculate_break_even` | Compute the break-even unit count and revenue from fixed costs, variable cost per unit, and price per unit |
195
197
  | `calculate_video_bitrate` | Calculate a video's file size from a bitrate and duration, or the bitrate needed to hit a target file size |
196
198
  | `calculate_rule_of_72` | Estimate years to double an investment from a rate, or the rate needed to double in a given number of years, via the Rule of 72 shortcut and the exact logarithmic result |
199
+ | `convert_timecode` | Convert between a frame count, SMPTE timecode, and seconds at a given frame rate, handling NTSC drop-frame timecode's skipped frame numbers correctly |
197
200
  | `calculate_emi` | Compute the Equated Monthly Installment for a loan from principal, annual rate, and tenure in years or months, with a year-by-year principal/interest breakdown |
198
201
  | `calculate_savings_goal` | Solve for months to reach a savings goal at a fixed monthly contribution, or the monthly contribution required to hit a goal by a target month count |
199
202
  | `calculate_annuity_payout` | Solve the standard annuity formula in either direction: the periodic payout a lump sum funds, or the lump sum required to fund a target payout, for an ordinary or annuity-due structure |
@@ -226,6 +229,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
226
229
  | `intersect_semver_ranges` | Determine whether two semver ranges overlap and, if so, the intersecting range, supporting the same caret/tilde/comparison-operator/exact-version syntax as the semver tool |
227
230
  | `validate_routing_number` | Validate a 9-digit US bank routing number (ABA/RTN) via its weighted mod-10 checksum, and identify its routing number type and Federal Reserve district |
228
231
  | `calculate_position_size` | Compute a trade's position size from account size, risk percent per trade, entry price, and stop-loss price, plus the risk/reward ratio against an optional target price |
232
+ | `calculate_dividend_yield` | Compute a stock's dividend yield from annual dividend per share and share price, plus payout ratio and retention ratio when trailing EPS is given |
229
233
 
230
234
  ### API / Network (55 tools)
231
235
  | Tool | Description |
package/dist/index.js CHANGED
@@ -6003,6 +6003,295 @@ server.tool(
6003
6003
  return text(JSON.stringify(result, null, 2));
6004
6004
  }
6005
6005
  );
6006
+ var _TIMECODE_NOMINAL_FPS_MCP = {
6007
+ "23.976": 24,
6008
+ "24": 24,
6009
+ "25": 25,
6010
+ "29.97": 30,
6011
+ "30": 30,
6012
+ "50": 50,
6013
+ "59.94": 60,
6014
+ "60": 60
6015
+ };
6016
+ function _framesToPlainTimecodeMcp(frameNumber, nominalFps) {
6017
+ const framesPerDay = nominalFps * 86400;
6018
+ const fn = (frameNumber % framesPerDay + framesPerDay) % framesPerDay;
6019
+ const frames = fn % nominalFps;
6020
+ const totalSeconds = Math.floor(fn / nominalFps);
6021
+ return {
6022
+ hours: Math.floor(totalSeconds / 3600) % 24,
6023
+ minutes: Math.floor(totalSeconds / 60) % 60,
6024
+ seconds: totalSeconds % 60,
6025
+ frames
6026
+ };
6027
+ }
6028
+ function _plainTimecodeToFramesMcp(hh, mm, ss, ff, nominalFps) {
6029
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff;
6030
+ }
6031
+ function _framesToDropFrameTimecodeMcp(frameNumber, nominalFps, frameRate) {
6032
+ const dropFrames = nominalFps === 30 ? 2 : 4;
6033
+ const framesPer24Hours = Math.round(frameRate * 86400);
6034
+ const framesPer10Minutes = Math.round(frameRate * 600);
6035
+ const framesPerMinute = nominalFps * 60 - dropFrames;
6036
+ const fn = (frameNumber % framesPer24Hours + framesPer24Hours) % framesPer24Hours;
6037
+ const d = Math.floor(fn / framesPer10Minutes);
6038
+ const m = fn % framesPer10Minutes;
6039
+ let frameNumberOut = fn + 9 * dropFrames * d;
6040
+ if (m > dropFrames) {
6041
+ frameNumberOut += dropFrames * Math.floor((m - dropFrames) / framesPerMinute);
6042
+ }
6043
+ const frames = frameNumberOut % nominalFps;
6044
+ const totalSeconds = Math.floor(frameNumberOut / nominalFps);
6045
+ return {
6046
+ hours: Math.floor(totalSeconds / 3600) % 24,
6047
+ minutes: Math.floor(totalSeconds / 60) % 60,
6048
+ seconds: totalSeconds % 60,
6049
+ frames
6050
+ };
6051
+ }
6052
+ function _dropFrameTimecodeToFramesMcp(hh, mm, ss, ff, nominalFps) {
6053
+ const dropFrames = nominalFps === 30 ? 2 : 4;
6054
+ const totalMinutes = 60 * hh + mm;
6055
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff - dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
6056
+ }
6057
+ function _formatTimecodeMcp(parts, dropFrame) {
6058
+ const p2 = (n) => String(n).padStart(2, "0");
6059
+ const sep = dropFrame ? ";" : ":";
6060
+ return `${p2(parts.hours)}:${p2(parts.minutes)}:${p2(parts.seconds)}${sep}${p2(parts.frames)}`;
6061
+ }
6062
+ function _convertTimecodeMcp(mode, frameRate, dropFrame, frameCount, timecode) {
6063
+ const nominalFps = _TIMECODE_NOMINAL_FPS_MCP[String(frameRate)] ?? null;
6064
+ if (nominalFps === null) return { error: "frameRate must be one of 23.976, 24, 25, 29.97, 30, 50, 59.94, 60" };
6065
+ if (dropFrame && frameRate !== 29.97 && frameRate !== 59.94) {
6066
+ return { error: "dropFrame is only valid for frameRate 29.97 or 59.94" };
6067
+ }
6068
+ if (mode === "frames-to-timecode") {
6069
+ if (typeof frameCount !== "number" || !Number.isFinite(frameCount) || frameCount < 0 || Math.floor(frameCount) !== frameCount) {
6070
+ return { error: 'frameCount must be a non-negative integer for mode "frames-to-timecode"' };
6071
+ }
6072
+ const parts = dropFrame ? _framesToDropFrameTimecodeMcp(frameCount, nominalFps, frameRate) : _framesToPlainTimecodeMcp(frameCount, nominalFps);
6073
+ return {
6074
+ mode,
6075
+ frameRate,
6076
+ dropFrame,
6077
+ frameCount,
6078
+ timecode: _formatTimecodeMcp(parts, dropFrame),
6079
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
6080
+ };
6081
+ }
6082
+ if (mode === "timecode-to-frames") {
6083
+ if (typeof timecode !== "string") return { error: 'timecode must be a string for mode "timecode-to-frames"' };
6084
+ const match = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$/.exec(timecode.trim());
6085
+ if (!match) return { error: "timecode must be in the form HH:MM:SS:FF (or HH:MM:SS;FF)" };
6086
+ const [, hhS, mmS, ssS, ffS] = match;
6087
+ const hh = Number(hhS), mm = Number(mmS), ss = Number(ssS), ff = Number(ffS);
6088
+ if (hh > 23) return { error: "hours must be 00-23" };
6089
+ if (mm > 59) return { error: "minutes must be 00-59" };
6090
+ if (ss > 59) return { error: "seconds must be 00-59" };
6091
+ if (ff >= nominalFps) return { error: `frames must be 00-${nominalFps - 1} at this frame rate` };
6092
+ if (dropFrame && ff <= 1 && ss === 0 && mm % 10 !== 0) {
6093
+ return { error: `frame ${ffS} at ${hhS}:${mmS}:${ssS} does not exist in drop-frame timecode: frames 00 and 01 are dropped at the start of every minute except every 10th minute` };
6094
+ }
6095
+ const frameCountOut = dropFrame ? _dropFrameTimecodeToFramesMcp(hh, mm, ss, ff, nominalFps) : _plainTimecodeToFramesMcp(hh, mm, ss, ff, nominalFps);
6096
+ return {
6097
+ mode,
6098
+ frameRate,
6099
+ dropFrame,
6100
+ frameCount: frameCountOut,
6101
+ timecode: _formatTimecodeMcp({ hours: hh, minutes: mm, seconds: ss, frames: ff }, dropFrame),
6102
+ seconds: Math.round(frameCountOut / frameRate * 1e6) / 1e6
6103
+ };
6104
+ }
6105
+ return { error: 'mode must be "frames-to-timecode" or "timecode-to-frames"' };
6106
+ }
6107
+ server.tool(
6108
+ "convert_timecode",
6109
+ "Convert between a frame count, SMPTE timecode (HH:MM:SS:FF, or HH:MM:SS;FF for drop-frame), and seconds at a given frame rate. Handles NTSC drop-frame timecode (29.97/59.94 fps) correctly via the standard SMPTE 12M algorithm, which skips frame numbers 00 and 01 at the start of every minute except every 10th minute so the nominal 30/60 fps digit count stays in sync with the true 29.97/59.94 fps signal. Pure arithmetic; no video file or container is read.",
6110
+ {
6111
+ mode: z.enum(["frames-to-timecode", "timecode-to-frames"]).describe("frames-to-timecode requires frameCount; timecode-to-frames requires timecode"),
6112
+ frameRate: z.number().describe("One of 23.976, 24, 25, 29.97, 30, 50, 59.94, 60"),
6113
+ dropFrame: z.boolean().default(false).describe("Only valid for frameRate 29.97 or 59.94"),
6114
+ frameCount: z.number().optional().describe('Required for mode "frames-to-timecode"'),
6115
+ timecode: z.string().optional().describe('Required for mode "timecode-to-frames"; "HH:MM:SS:FF" or "HH:MM:SS;FF"')
6116
+ },
6117
+ async ({ mode, frameRate, dropFrame, frameCount, timecode }) => {
6118
+ const result = _convertTimecodeMcp(mode, frameRate, dropFrame, frameCount, timecode);
6119
+ if ("error" in result) return errText(result);
6120
+ return text(JSON.stringify(result, null, 2));
6121
+ }
6122
+ );
6123
+ var _GEOJSON_GEOMETRY_TYPES_MCP = /* @__PURE__ */ new Set([
6124
+ "Point",
6125
+ "MultiPoint",
6126
+ "LineString",
6127
+ "MultiLineString",
6128
+ "Polygon",
6129
+ "MultiPolygon",
6130
+ "GeometryCollection"
6131
+ ]);
6132
+ function _isGeoJsonNumberMcp(v) {
6133
+ return typeof v === "number" && Number.isFinite(v);
6134
+ }
6135
+ function _isGeoJsonPositionMcp(v) {
6136
+ return Array.isArray(v) && (v.length === 2 || v.length === 3) && v.every(_isGeoJsonNumberMcp);
6137
+ }
6138
+ function _geoJsonPositionsEqualMcp(a, b) {
6139
+ return a.length === b.length && a.every((v, i) => v === b[i]);
6140
+ }
6141
+ function _geoJsonRingSignedAreaMcp(ring) {
6142
+ let sum = 0;
6143
+ for (let i = 0; i < ring.length - 1; i++) {
6144
+ const [x1, y1] = ring[i];
6145
+ const [x2, y2] = ring[i + 1];
6146
+ sum += x1 * y2 - x2 * y1;
6147
+ }
6148
+ return sum / 2;
6149
+ }
6150
+ function _validateGeoJsonPolygonCoordsMcp(coords, path, errors, warnings) {
6151
+ if (!Array.isArray(coords) || coords.length === 0) {
6152
+ errors.push({ path, message: "Polygon coordinates must be a non-empty array of linear rings" });
6153
+ return;
6154
+ }
6155
+ coords.forEach((ring, i) => {
6156
+ const ringPath = `${path}[${i}]`;
6157
+ if (!Array.isArray(ring) || ring.length < 4 || !ring.every(_isGeoJsonPositionMcp)) {
6158
+ errors.push({ path: ringPath, message: "each linear ring must have at least 4 positions" });
6159
+ return;
6160
+ }
6161
+ const first = ring[0];
6162
+ const last = ring[ring.length - 1];
6163
+ if (!_geoJsonPositionsEqualMcp(first, last)) {
6164
+ errors.push({ path: ringPath, message: "the first and last positions of a linear ring must be identical (the ring must be closed)" });
6165
+ return;
6166
+ }
6167
+ const area = _geoJsonRingSignedAreaMcp(ring);
6168
+ const isExterior = i === 0;
6169
+ if (isExterior && area < 0) {
6170
+ warnings.push({ path: ringPath, message: "exterior ring winds clockwise; RFC 7946 recommends counterclockwise for exterior rings (right-hand rule)" });
6171
+ } else if (!isExterior && area > 0) {
6172
+ warnings.push({ path: ringPath, message: "interior ring (hole) winds counterclockwise; RFC 7946 recommends clockwise for interior rings (right-hand rule)" });
6173
+ }
6174
+ });
6175
+ }
6176
+ function _validateGeoJsonGeometryMcp(geom, path, errors, warnings) {
6177
+ if (geom === null) return;
6178
+ if (typeof geom !== "object" || Array.isArray(geom)) {
6179
+ errors.push({ path, message: "geometry must be an object or null" });
6180
+ return;
6181
+ }
6182
+ const type = geom.type;
6183
+ if (typeof type !== "string" || !_GEOJSON_GEOMETRY_TYPES_MCP.has(type)) {
6184
+ errors.push({ path: `${path}.type`, message: `geometry type must be one of ${[..._GEOJSON_GEOMETRY_TYPES_MCP].join(", ")}` });
6185
+ return;
6186
+ }
6187
+ if (type === "GeometryCollection") {
6188
+ if (!Array.isArray(geom.geometries)) {
6189
+ errors.push({ path: `${path}.geometries`, message: 'GeometryCollection must have a "geometries" array' });
6190
+ return;
6191
+ }
6192
+ geom.geometries.forEach((g, i) => _validateGeoJsonGeometryMcp(g, `${path}.geometries[${i}]`, errors, warnings));
6193
+ return;
6194
+ }
6195
+ const coords = geom.coordinates;
6196
+ const coordPath = `${path}.coordinates`;
6197
+ switch (type) {
6198
+ case "Point":
6199
+ if (!_isGeoJsonPositionMcp(coords)) errors.push({ path: coordPath, message: "Point coordinates must be an array of 2 or 3 numbers" });
6200
+ break;
6201
+ case "MultiPoint":
6202
+ if (!Array.isArray(coords) || !coords.every(_isGeoJsonPositionMcp)) {
6203
+ errors.push({ path: coordPath, message: "MultiPoint coordinates must be an array of positions" });
6204
+ }
6205
+ break;
6206
+ case "LineString":
6207
+ if (!Array.isArray(coords) || coords.length < 2 || !coords.every(_isGeoJsonPositionMcp)) {
6208
+ errors.push({ path: coordPath, message: "LineString coordinates must be an array of at least 2 positions" });
6209
+ }
6210
+ break;
6211
+ case "MultiLineString":
6212
+ if (!Array.isArray(coords) || !coords.every((line) => Array.isArray(line) && line.length >= 2 && line.every(_isGeoJsonPositionMcp))) {
6213
+ errors.push({ path: coordPath, message: "MultiLineString coordinates must be an array of LineString coordinate arrays" });
6214
+ }
6215
+ break;
6216
+ case "Polygon":
6217
+ _validateGeoJsonPolygonCoordsMcp(coords, coordPath, errors, warnings);
6218
+ break;
6219
+ case "MultiPolygon":
6220
+ if (!Array.isArray(coords)) {
6221
+ errors.push({ path: coordPath, message: "MultiPolygon coordinates must be an array of Polygon coordinate arrays" });
6222
+ } else {
6223
+ coords.forEach((poly, i) => _validateGeoJsonPolygonCoordsMcp(poly, `${coordPath}[${i}]`, errors, warnings));
6224
+ }
6225
+ break;
6226
+ }
6227
+ }
6228
+ function _validateGeoJsonBboxMcp(bbox, path, errors) {
6229
+ if (bbox === void 0) return;
6230
+ if (!Array.isArray(bbox) || bbox.length % 2 !== 0 || bbox.length < 4 || !bbox.every(_isGeoJsonNumberMcp)) {
6231
+ errors.push({ path: `${path}.bbox`, message: "bbox must be an array of 2*n numbers (min/max per axis)" });
6232
+ }
6233
+ }
6234
+ function _validateGeoJsonFeatureMcp(feature, path, errors, warnings) {
6235
+ if (typeof feature !== "object" || feature === null || Array.isArray(feature)) {
6236
+ errors.push({ path, message: "Feature must be an object" });
6237
+ return;
6238
+ }
6239
+ if (feature.type !== "Feature") {
6240
+ errors.push({ path: `${path}.type`, message: '"type" must be "Feature"' });
6241
+ }
6242
+ if (!("properties" in feature) || typeof feature.properties !== "object") {
6243
+ errors.push({ path: `${path}.properties`, message: '"properties" member is required and must be an object or null' });
6244
+ }
6245
+ if (!("geometry" in feature)) {
6246
+ errors.push({ path: `${path}.geometry`, message: '"geometry" member is required (an object or null)' });
6247
+ } else {
6248
+ _validateGeoJsonGeometryMcp(feature.geometry, `${path}.geometry`, errors, warnings);
6249
+ }
6250
+ _validateGeoJsonBboxMcp(feature.bbox, path, errors);
6251
+ }
6252
+ function _validateGeoJsonMcp(input) {
6253
+ let doc;
6254
+ try {
6255
+ doc = JSON.parse(input);
6256
+ } catch (e) {
6257
+ return { valid: false, type: null, errors: [{ path: "$", message: `invalid JSON: ${e.message}` }], warnings: [] };
6258
+ }
6259
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
6260
+ return { valid: false, type: null, errors: [{ path: "$", message: "document must be a JSON object" }], warnings: [] };
6261
+ }
6262
+ const type = doc.type;
6263
+ if (typeof type !== "string") {
6264
+ return { valid: false, type: null, errors: [{ path: "$.type", message: '"type" member is required' }], warnings: [] };
6265
+ }
6266
+ const errors = [];
6267
+ const warnings = [];
6268
+ _validateGeoJsonBboxMcp(doc.bbox, "$", errors);
6269
+ if (type === "FeatureCollection") {
6270
+ if (!Array.isArray(doc.features)) {
6271
+ errors.push({ path: "$.features", message: 'FeatureCollection must have a "features" array' });
6272
+ } else {
6273
+ doc.features.forEach((feature, i) => _validateGeoJsonFeatureMcp(feature, `$.features[${i}]`, errors, warnings));
6274
+ }
6275
+ } else if (type === "Feature") {
6276
+ _validateGeoJsonFeatureMcp(doc, "$", errors, warnings);
6277
+ } else if (_GEOJSON_GEOMETRY_TYPES_MCP.has(type)) {
6278
+ _validateGeoJsonGeometryMcp(doc, "$", errors, warnings);
6279
+ } else {
6280
+ errors.push({ path: "$.type", message: '"type" must be one of Feature, FeatureCollection, or a geometry type' });
6281
+ }
6282
+ return { valid: errors.length === 0, type, errors, warnings };
6283
+ }
6284
+ server.tool(
6285
+ "validate_geojson",
6286
+ `Validate a GeoJSON document's structure against RFC 7946: the correct geometry type and coordinates shape for each of the seven geometry types, linear ring closure for Polygon/MultiPolygon, a Feature's required properties/geometry members, and a FeatureCollection's required features array. Also returns non-failing warnings when a Polygon ring winds the "wrong" way under RFC 7946's right-hand rule recommendation. Pure structural validation; no map is rendered and no tile server is contacted.`,
6287
+ {
6288
+ geojson: z.string().describe("The GeoJSON document, as a JSON string")
6289
+ },
6290
+ async ({ geojson }) => {
6291
+ const result = _validateGeoJsonMcp(geojson);
6292
+ return text(JSON.stringify(result, null, 2));
6293
+ }
6294
+ );
6006
6295
  function _videoReadU32BE(bytes, offset) {
6007
6296
  return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
6008
6297
  }
@@ -15455,6 +15744,130 @@ server.tool(
15455
15744
  return text(JSON.stringify(result, null, 2));
15456
15745
  }
15457
15746
  );
15747
+ function _calculateDividendYieldMcp(input) {
15748
+ const { annualDividendPerShare, sharePrice, epsPerShare } = input;
15749
+ if (!(annualDividendPerShare >= 0)) return { error: "annualDividendPerShare must be greater than or equal to zero" };
15750
+ if (!(sharePrice > 0)) return { error: "sharePrice must be greater than zero" };
15751
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
15752
+ const result = {
15753
+ annualDividendPerShare: round4(annualDividendPerShare),
15754
+ sharePrice: round4(sharePrice),
15755
+ dividendYieldPercent: round4(annualDividendPerShare / sharePrice * 100)
15756
+ };
15757
+ if (epsPerShare !== void 0) {
15758
+ if (!(epsPerShare > 0)) return { error: "epsPerShare must be greater than zero when provided" };
15759
+ const payoutRatioPercent = annualDividendPerShare / epsPerShare * 100;
15760
+ result.epsPerShare = round4(epsPerShare);
15761
+ result.payoutRatioPercent = round4(payoutRatioPercent);
15762
+ result.retentionRatioPercent = round4(100 - payoutRatioPercent);
15763
+ }
15764
+ return result;
15765
+ }
15766
+ server.tool(
15767
+ "calculate_dividend_yield",
15768
+ "Compute a stock's dividend yield (annual dividend per share / share price) and, given trailing EPS, its payout ratio (dividend / EPS) and retention ratio (100 - payout ratio). A payout ratio over 100% is valid and reported as-is. Pure arithmetic, no live quote feed.",
15769
+ {
15770
+ annualDividendPerShare: z.number().describe("Total dividend paid per share over the trailing year"),
15771
+ sharePrice: z.number().describe("Current share price"),
15772
+ epsPerShare: z.number().optional().describe("Optional trailing earnings per share, used to compute payout and retention ratio")
15773
+ },
15774
+ async ({ annualDividendPerShare, sharePrice, epsPerShare }) => {
15775
+ const result = _calculateDividendYieldMcp({ annualDividendPerShare, sharePrice, epsPerShare });
15776
+ if ("error" in result) return errText(result);
15777
+ return text(JSON.stringify(result, null, 2));
15778
+ }
15779
+ );
15780
+ function _readSshStringFieldMcp(buf, offset) {
15781
+ if (offset + 4 > buf.length) return null;
15782
+ const len = buf.readUInt32BE(offset);
15783
+ const start = offset + 4;
15784
+ const end = start + len;
15785
+ if (end > buf.length) return null;
15786
+ return { value: buf.subarray(start, end), next: end };
15787
+ }
15788
+ function _mpintBitLengthMcp(buf) {
15789
+ let i = 0;
15790
+ while (i < buf.length - 1 && buf[i] === 0) i++;
15791
+ const trimmed = buf.subarray(i);
15792
+ if (trimmed.length === 0) return 0;
15793
+ let bits = (trimmed.length - 1) * 8;
15794
+ let firstByte = trimmed[0];
15795
+ while (firstByte > 0) {
15796
+ bits++;
15797
+ firstByte >>= 1;
15798
+ }
15799
+ return bits;
15800
+ }
15801
+ function _analyzeKnownHostsKeyMcp(keyType, blob) {
15802
+ if (keyType === "ssh-dss") {
15803
+ return { weak: true, weakReason: "DSA (ssh-dss) keys are deprecated and considered weak" };
15804
+ }
15805
+ if (keyType === "ssh-rsa") {
15806
+ const typeField = _readSshStringFieldMcp(blob, 0);
15807
+ if (!typeField) return { weak: false };
15808
+ const eField = _readSshStringFieldMcp(blob, typeField.next);
15809
+ if (!eField) return { weak: false };
15810
+ const nField = _readSshStringFieldMcp(blob, eField.next);
15811
+ if (!nField) return { weak: false };
15812
+ const bits = _mpintBitLengthMcp(nField.value);
15813
+ return bits > 0 && bits < 2048 ? { bits, weak: true, weakReason: `RSA modulus is only ${bits} bits; 2048+ is recommended` } : { bits, weak: false };
15814
+ }
15815
+ if (keyType === "ssh-ed25519") {
15816
+ return { bits: 256, weak: false };
15817
+ }
15818
+ if (keyType.startsWith("ecdsa-sha2-")) {
15819
+ const curve = keyType.split("-").pop() ?? "";
15820
+ const bits = curve === "nistp256" ? 256 : curve === "nistp384" ? 384 : curve === "nistp521" ? 521 : void 0;
15821
+ return { bits, weak: false };
15822
+ }
15823
+ return { weak: false };
15824
+ }
15825
+ function _checkKnownHostsFingerprintsMcp(input) {
15826
+ if (typeof input !== "string" || input.trim().length === 0) {
15827
+ return { error: "input is required" };
15828
+ }
15829
+ const lines = input.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
15830
+ if (lines.length === 0) {
15831
+ return { error: "No known_hosts entries found" };
15832
+ }
15833
+ const entries = [];
15834
+ for (const line of lines) {
15835
+ const parts = line.split(/\s+/);
15836
+ let idx = 0;
15837
+ if (parts[0] === "@cert-authority" || parts[0] === "@revoked") idx = 1;
15838
+ if (parts.length < idx + 3) return { error: `Malformed line: "${line}"` };
15839
+ const host = parts[idx];
15840
+ const keyType = parts[idx + 1];
15841
+ const base64Key = parts[idx + 2];
15842
+ const blob = Buffer.from(base64Key, "base64");
15843
+ if (blob.length === 0) return { error: `Invalid base64 key for host "${host}"` };
15844
+ const sha256 = createHash("sha256").update(blob).digest("base64").replace(/=+$/, "");
15845
+ const md5 = createHash("md5").update(blob).digest("hex").replace(/(.{2})(?=.)/g, "$1:");
15846
+ const analysis = _analyzeKnownHostsKeyMcp(keyType, blob);
15847
+ entries.push({
15848
+ host,
15849
+ keyType,
15850
+ fingerprintSha256: `SHA256:${sha256}`,
15851
+ fingerprintMd5: `MD5:${md5}`,
15852
+ bits: analysis.bits,
15853
+ weak: analysis.weak,
15854
+ weakReason: analysis.weakReason
15855
+ });
15856
+ }
15857
+ return { entries };
15858
+ }
15859
+ server.tool(
15860
+ "check_known_hosts_fingerprints",
15861
+ "Parse an ssh_known_hosts file (or a single `host keytype base64key` line) and compute each entry's SHA256 and MD5 fingerprint the same way `ssh-keygen -lf` does, decoding the key type and, for RSA, the modulus bit length, flagging a weak RSA key (<2048 bits) or a deprecated ssh-dss (DSA) key.",
15862
+ {
15863
+ input: z.string().describe("ssh_known_hosts file contents, or a single entry line")
15864
+ },
15865
+ async ({ input }) => {
15866
+ const result = _checkKnownHostsFingerprintsMcp(input);
15867
+ if ("error" in result) return errText(result);
15868
+ return text(JSON.stringify(result, null, 2));
15869
+ }
15870
+ );
15458
15871
  var _WEASEL_PHRASES_MCP = [
15459
15872
  "many experts believe",
15460
15873
  "some experts say",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.97.0",
4
- "description": "MCP server exposing 287 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.99.0",
4
+ "description": "MCP server exposing 291 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",
7
7
  "homepage": "https://utilix.tech/mcp",