@utilix-tech/mcp 0.97.0 → 0.98.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 +3 -1
- package/dist/index.js +289 -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.98.0
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
5
9
|
## 0.97.0
|
|
6
10
|
- 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
11
|
- 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
|
|
3
|
+
MCP server exposing 289 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 |
|
|
@@ -194,6 +195,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
194
195
|
| `calculate_break_even` | Compute the break-even unit count and revenue from fixed costs, variable cost per unit, and price per unit |
|
|
195
196
|
| `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
197
|
| `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 |
|
|
198
|
+
| `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
199
|
| `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
200
|
| `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
201
|
| `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 |
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.0",
|
|
4
4
|
"description": "MCP server exposing 287 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",
|