@utilix-tech/mcp 0.43.0 → 0.45.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.45.0
6
+ - Add `build_permissions_policy`, `parse_permissions_policy` tools: build a `Permissions-Policy` response header from browser features and their per-feature allowlist with a plain-English note per feature, or parse an existing one into the same shape.
7
+ - Add `calculate_payback_period` tool: compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
8
+
9
+ ## 0.44.0
10
+ - Add `validate_manifest` tool: validate a web app manifest.json against the W3C Web App Manifest spec's installability requirements, including a 512x512 icon and a maskable icon check.
11
+ - Add `lint_tool_schema` tool: lint an LLM tool/function-calling definition (OpenAI, Anthropic, or MCP format) for missing descriptions, invalid names, empty enums, and required/properties mismatches.
12
+
5
13
  ## 0.43.0
6
14
  - Add `build_vary_header`, `parse_vary_header` tools: build a `Vary` response header from request-header names with a plain-English caching note per header, or parse an existing Vary header into the same shape.
7
15
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 173 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 178 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
 
@@ -159,7 +159,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
159
159
  | `convert_timezone` | Convert a date/time between timezones |
160
160
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
161
161
 
162
- ### Units (26 tools)
162
+ ### Units (27 tools)
163
163
  | Tool | Description |
164
164
  |------|-------------|
165
165
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -188,8 +188,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
188
188
  | `compare_unit_prices` | Compare two or more package options by price and quantity to find the true cost per unit, flag the cheapest, and report how much more expensive each other option is |
189
189
  | `calculate_cash_breakdown` | Break a cash amount down into the minimum count of bills/coins from a set of denominations, largest first |
190
190
  | `calculate_change` | Compute change owed for a payment/tender pair and break it down into the minimum count of bills/coins |
191
+ | `calculate_payback_period` | Compute the simple and discounted payback period from an initial investment and a series of periodic cash flows |
191
192
 
192
- ### API / Network (20 tools)
193
+ ### API / Network (23 tools)
193
194
  | Tool | Description |
194
195
  |------|-------------|
195
196
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -212,6 +213,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
212
213
  | `parse_content_range_header` | Parse a Content-Range response header into its unit, byte bounds, and total resource size |
213
214
  | `build_vary_header` | Build a Vary response header from request-header names, with a plain-English caching note per header |
214
215
  | `parse_vary_header` | Parse an existing Vary header value into its individual header names, each with a caching note |
216
+ | `validate_manifest` | Validate a web app manifest.json against the W3C spec's installability requirements: name/short_name, start_url, display mode, and icon coverage |
217
+ | `build_permissions_policy` | Build a Permissions-Policy response header from browser features and their per-feature allowlist, with a plain-English note per feature |
218
+ | `parse_permissions_policy` | Parse an existing Permissions-Policy header value into its individual feature directives, each with a plain-English note |
215
219
 
216
220
  ### Color (4 tools)
217
221
  | Tool | Description |
@@ -264,7 +268,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
264
268
  | `unicode_inspect` | Inspect Unicode code points of a string |
265
269
  | `optimize_svg` | Optimize and minify an SVG string |
266
270
 
267
- ### AI / Agent (27 tools)
271
+ ### AI / Agent (28 tools)
268
272
  | Tool | Description |
269
273
  |------|-------------|
270
274
  | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
@@ -294,6 +298,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
294
298
  | `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
295
299
  | `vector_similarity` | Cosine similarity, dot product, and Euclidean / Manhattan distance between two numeric vectors |
296
300
  | `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
301
+ | `lint_tool_schema` | Lint an OpenAI, Anthropic, or MCP tool/function-calling definition for missing descriptions, invalid names, empty enums, and required/properties mismatches |
297
302
 
298
303
  ### Image (3 tools)
299
304
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7198,5 +7198,437 @@ server.tool(
7198
7198
  return text(JSON.stringify(result, null, 2));
7199
7199
  }
7200
7200
  );
7201
+ var _VALID_MANIFEST_DISPLAY_MCP = /* @__PURE__ */ new Set(["fullscreen", "standalone", "minimal-ui", "browser"]);
7202
+ function _validateManifestMcp(jsonText) {
7203
+ if (!jsonText || !jsonText.trim()) return { error: "Input is empty" };
7204
+ let manifest;
7205
+ try {
7206
+ manifest = JSON.parse(jsonText);
7207
+ } catch (e) {
7208
+ return { error: `Invalid JSON: ${e.message}` };
7209
+ }
7210
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
7211
+ return { error: "Manifest must be a JSON object" };
7212
+ }
7213
+ const m = manifest;
7214
+ const issues = [];
7215
+ if (!m.name && !m.short_name) {
7216
+ issues.push({ severity: "error", path: "name", message: 'Manifest must have at least one of "name" or "short_name"' });
7217
+ }
7218
+ if (m.name !== void 0 && typeof m.name !== "string") {
7219
+ issues.push({ severity: "error", path: "name", message: '"name" must be a string' });
7220
+ } else if (typeof m.name === "string" && m.name.length > 45) {
7221
+ issues.push({ severity: "warning", path: "name", message: `"name" is ${m.name.length} characters; keep it under ~45 to avoid truncation in install prompts` });
7222
+ }
7223
+ if (m.short_name !== void 0 && typeof m.short_name !== "string") {
7224
+ issues.push({ severity: "error", path: "short_name", message: '"short_name" must be a string' });
7225
+ } else if (typeof m.short_name === "string" && m.short_name.length > 12) {
7226
+ issues.push({ severity: "warning", path: "short_name", message: `"short_name" is ${m.short_name.length} characters; keep it under ~12 to avoid truncation on home screens` });
7227
+ }
7228
+ if (!m.start_url) {
7229
+ issues.push({ severity: "error", path: "start_url", message: 'Manifest is missing "start_url"' });
7230
+ } else if (typeof m.start_url !== "string") {
7231
+ issues.push({ severity: "error", path: "start_url", message: '"start_url" must be a string' });
7232
+ } else if (m.start_url.startsWith("//")) {
7233
+ issues.push({ severity: "warning", path: "start_url", message: '"start_url" looks protocol-relative; use a relative path (e.g. "/") or a full absolute URL instead' });
7234
+ }
7235
+ if (m.display !== void 0) {
7236
+ if (typeof m.display !== "string" || !_VALID_MANIFEST_DISPLAY_MCP.has(m.display)) {
7237
+ issues.push({ severity: "error", path: "display", message: `"display" must be one of: ${[..._VALID_MANIFEST_DISPLAY_MCP].join(", ")}` });
7238
+ }
7239
+ } else {
7240
+ issues.push({ severity: "warning", path: "display", message: 'Missing "display"; it defaults to "browser", which will not open the installed app in its own standalone window' });
7241
+ }
7242
+ const icons = [];
7243
+ let hasMaskableIcon = false;
7244
+ let has512Icon = false;
7245
+ if (!Array.isArray(m.icons) || m.icons.length === 0) {
7246
+ issues.push({ severity: "error", path: "icons", message: 'Manifest must have a non-empty "icons" array' });
7247
+ } else {
7248
+ m.icons.forEach((icon, i) => {
7249
+ const path = `icons[${i}]`;
7250
+ if (!icon || typeof icon !== "object") {
7251
+ issues.push({ severity: "error", path, message: "Icon entry must be an object" });
7252
+ return;
7253
+ }
7254
+ const iconObj = icon;
7255
+ if (!iconObj.src || typeof iconObj.src !== "string") {
7256
+ issues.push({ severity: "error", path, message: 'Icon entry is missing a required "src"' });
7257
+ }
7258
+ if (iconObj.sizes !== void 0) {
7259
+ if (typeof iconObj.sizes !== "string" || iconObj.sizes !== "any" && !/^\d+x\d+(\s+\d+x\d+)*$/i.test(iconObj.sizes)) {
7260
+ issues.push({ severity: "warning", path, message: `"sizes" value "${String(iconObj.sizes)}" doesn't match the expected "WIDTHxHEIGHT" format` });
7261
+ } else if (/(^|\s)512x512(\s|$)/i.test(iconObj.sizes)) {
7262
+ has512Icon = true;
7263
+ }
7264
+ }
7265
+ if (typeof iconObj.purpose === "string" && iconObj.purpose.toLowerCase().includes("maskable")) {
7266
+ hasMaskableIcon = true;
7267
+ }
7268
+ icons.push({
7269
+ src: typeof iconObj.src === "string" ? iconObj.src : "",
7270
+ sizes: typeof iconObj.sizes === "string" ? iconObj.sizes : void 0,
7271
+ type: typeof iconObj.type === "string" ? iconObj.type : void 0,
7272
+ purpose: typeof iconObj.purpose === "string" ? iconObj.purpose : void 0
7273
+ });
7274
+ });
7275
+ if (icons.length > 0) {
7276
+ if (!has512Icon) {
7277
+ issues.push({ severity: "warning", path: "icons", message: "No icon sized 512x512 found; this size is used for splash screens and app store listings" });
7278
+ }
7279
+ if (!hasMaskableIcon) {
7280
+ issues.push({ severity: "warning", path: "icons", message: 'No icon with purpose "maskable" (or "any maskable") found; without one, Android may crop your icon into its adaptive icon mask' });
7281
+ }
7282
+ }
7283
+ }
7284
+ if (m.theme_color !== void 0 && typeof m.theme_color !== "string") {
7285
+ issues.push({ severity: "error", path: "theme_color", message: '"theme_color" must be a CSS color string' });
7286
+ }
7287
+ if (m.background_color !== void 0 && typeof m.background_color !== "string") {
7288
+ issues.push({ severity: "error", path: "background_color", message: '"background_color" must be a CSS color string' });
7289
+ } else if (m.background_color === void 0) {
7290
+ issues.push({ severity: "warning", path: "background_color", message: 'Missing "background_color"; without it the splash screen background defaults to white' });
7291
+ }
7292
+ const valid = !issues.some((i) => i.severity === "error");
7293
+ return {
7294
+ issues,
7295
+ valid,
7296
+ summary: {
7297
+ name: typeof m.name === "string" ? m.name : void 0,
7298
+ shortName: typeof m.short_name === "string" ? m.short_name : void 0,
7299
+ startUrl: typeof m.start_url === "string" ? m.start_url : void 0,
7300
+ display: typeof m.display === "string" ? m.display : void 0,
7301
+ icons,
7302
+ hasMaskableIcon,
7303
+ has512Icon
7304
+ }
7305
+ };
7306
+ }
7307
+ server.tool(
7308
+ "validate_manifest",
7309
+ "Validate a web app manifest.json against the W3C Web App Manifest spec's installability requirements: name/short_name, start_url, a recognized display mode, and an icons array covering a 512x512 icon and a maskable icon, plus recommended theme_color/background_color fields.",
7310
+ { manifest: z.string().describe("The manifest.json contents") },
7311
+ async ({ manifest }) => {
7312
+ const result = _validateManifestMcp(manifest);
7313
+ if ("error" in result) return errText(result);
7314
+ return text(JSON.stringify(result, null, 2));
7315
+ }
7316
+ );
7317
+ var _TOOL_NAME_RE_MCP = /^[a-zA-Z0-9_-]{1,64}$/;
7318
+ function _normalizeToolDefsMcp(data) {
7319
+ let rawList;
7320
+ if (Array.isArray(data)) {
7321
+ rawList = data;
7322
+ } else if (data && typeof data === "object" && Array.isArray(data.tools)) {
7323
+ rawList = data.tools;
7324
+ } else {
7325
+ rawList = [data];
7326
+ }
7327
+ const out = [];
7328
+ for (const item of rawList) {
7329
+ if (!item || typeof item !== "object") {
7330
+ return { error: "Each tool definition must be a JSON object" };
7331
+ }
7332
+ const obj = item;
7333
+ if (obj.type === "function" && obj.function && typeof obj.function === "object") {
7334
+ const fn = obj.function;
7335
+ out.push({ name: String(fn.name ?? ""), description: typeof fn.description === "string" ? fn.description : void 0, schema: fn.parameters });
7336
+ } else if ("input_schema" in obj) {
7337
+ out.push({ name: String(obj.name ?? ""), description: typeof obj.description === "string" ? obj.description : void 0, schema: obj.input_schema });
7338
+ } else if ("inputSchema" in obj) {
7339
+ out.push({ name: String(obj.name ?? ""), description: typeof obj.description === "string" ? obj.description : void 0, schema: obj.inputSchema });
7340
+ } else if ("parameters" in obj || "name" in obj) {
7341
+ out.push({ name: String(obj.name ?? ""), description: typeof obj.description === "string" ? obj.description : void 0, schema: obj.parameters });
7342
+ } else {
7343
+ return { error: 'Unrecognized tool-definition format: expected an OpenAI ({type:"function",function:{...}}), Anthropic ({name,input_schema}), or MCP ({name,inputSchema}) shape' };
7344
+ }
7345
+ }
7346
+ return out;
7347
+ }
7348
+ function _lintToolSchemaMcp(jsonText) {
7349
+ if (!jsonText || !jsonText.trim()) return { error: "Input is empty" };
7350
+ let data;
7351
+ try {
7352
+ data = JSON.parse(jsonText);
7353
+ } catch (e) {
7354
+ return { error: `Invalid JSON: ${e.message}` };
7355
+ }
7356
+ const normalized = _normalizeToolDefsMcp(data);
7357
+ if ("error" in normalized) return normalized;
7358
+ if (normalized.length === 0) return { error: "No tool definitions found" };
7359
+ const issues = [];
7360
+ const seenNames = /* @__PURE__ */ new Map();
7361
+ for (const tool of normalized) {
7362
+ const label = tool.name || "(unnamed)";
7363
+ seenNames.set(label, (seenNames.get(label) ?? 0) + 1);
7364
+ if (!tool.name) {
7365
+ issues.push({ severity: "error", tool: label, path: "name", message: 'Tool is missing a "name"' });
7366
+ } else if (!_TOOL_NAME_RE_MCP.test(tool.name)) {
7367
+ issues.push({ severity: "error", tool: label, path: "name", message: `Tool name "${tool.name}" must match ^[a-zA-Z0-9_-]{1,64}$ (letters, digits, underscores, hyphens, max 64 chars) to be accepted by OpenAI/Anthropic function calling` });
7368
+ }
7369
+ if (!tool.description || !tool.description.trim()) {
7370
+ issues.push({ severity: "error", tool: label, path: "description", message: 'Tool is missing a "description" \u2014 the model relies entirely on this text to decide when to call it' });
7371
+ } else {
7372
+ if (tool.description.trim().length < 10) {
7373
+ issues.push({ severity: "warning", tool: label, path: "description", message: "Description is very short; a vague description measurably hurts the model's ability to pick the right tool" });
7374
+ }
7375
+ if (tool.description.length > 1024) {
7376
+ issues.push({ severity: "warning", tool: label, path: "description", message: `Description is ${tool.description.length} characters; long descriptions cost tokens on every request that includes this tool` });
7377
+ }
7378
+ }
7379
+ if (tool.schema === void 0 || tool.schema === null) {
7380
+ issues.push({ severity: "warning", tool: label, path: "parameters", message: "Tool has no parameter schema; only correct if the tool genuinely takes no arguments" });
7381
+ continue;
7382
+ }
7383
+ if (typeof tool.schema !== "object" || Array.isArray(tool.schema)) {
7384
+ issues.push({ severity: "error", tool: label, path: "parameters", message: "Parameter schema must be a JSON Schema object" });
7385
+ continue;
7386
+ }
7387
+ const schema = tool.schema;
7388
+ if (schema.type !== void 0 && schema.type !== "object") {
7389
+ issues.push({ severity: "error", tool: label, path: "parameters.type", message: `Top-level parameter schema type is "${String(schema.type)}"; OpenAI and Anthropic both require "object" at the top level` });
7390
+ }
7391
+ const properties = schema.properties;
7392
+ if (properties !== void 0 && (typeof properties !== "object" || properties === null || Array.isArray(properties))) {
7393
+ issues.push({ severity: "error", tool: label, path: "parameters.properties", message: '"properties" must be an object' });
7394
+ } else if (properties && typeof properties === "object") {
7395
+ const propsObj = properties;
7396
+ const propNames = Object.keys(propsObj);
7397
+ for (const propName of propNames) {
7398
+ const prop = propsObj[propName];
7399
+ const propPath = `parameters.properties.${propName}`;
7400
+ if (!prop || typeof prop !== "object") {
7401
+ issues.push({ severity: "error", tool: label, path: propPath, message: "Property schema must be an object" });
7402
+ continue;
7403
+ }
7404
+ const propObj = prop;
7405
+ if (!propObj.description) {
7406
+ issues.push({ severity: "warning", tool: label, path: propPath, message: `Property "${propName}" has no description; missing per-parameter descriptions measurably hurt model accuracy on which value to pass` });
7407
+ }
7408
+ if (Array.isArray(propObj.enum) && propObj.enum.length === 0) {
7409
+ issues.push({ severity: "error", tool: label, path: propPath, message: `Property "${propName}" has an empty "enum" array` });
7410
+ }
7411
+ }
7412
+ if (Array.isArray(schema.required)) {
7413
+ for (const req of schema.required) {
7414
+ if (typeof req === "string" && !propNames.includes(req)) {
7415
+ issues.push({ severity: "error", tool: label, path: "parameters.required", message: `"required" lists "${req}", which isn't defined in "properties"` });
7416
+ }
7417
+ }
7418
+ }
7419
+ }
7420
+ }
7421
+ for (const [name, count] of seenNames) {
7422
+ if (count > 1 && name !== "(unnamed)") {
7423
+ issues.push({ severity: "error", tool: name, path: "name", message: `Tool name "${name}" is used ${count} times; tool names must be unique within a single request` });
7424
+ }
7425
+ }
7426
+ const valid = !issues.some((i) => i.severity === "error");
7427
+ return { toolCount: normalized.length, issues, valid };
7428
+ }
7429
+ server.tool(
7430
+ "lint_tool_schema",
7431
+ 'Lint an LLM tool/function-calling definition (OpenAI, Anthropic, or MCP format \u2014 a single definition, an array of them, or a {tools:[...]} wrapper) for mistakes that measurably hurt model accuracy: a missing or vague description, an invalid tool name, a non-object top-level parameter schema, properties missing descriptions, an empty enum, a "required" entry with no matching property, or duplicate tool names.',
7432
+ { schema: z.string().describe("JSON text: a single tool definition, an array of them, or a {tools:[...]} wrapper, in OpenAI/Anthropic/MCP format") },
7433
+ async ({ schema }) => {
7434
+ const result = _lintToolSchemaMcp(schema);
7435
+ if ("error" in result) return errText(result);
7436
+ return text(JSON.stringify(result, null, 2));
7437
+ }
7438
+ );
7439
+ var _PERMISSIONS_POLICY_NOTES_MCP = {
7440
+ camera: "Controls access to video capture devices (getUserMedia video).",
7441
+ microphone: "Controls access to audio capture devices (getUserMedia audio).",
7442
+ geolocation: "Controls access to the Geolocation API.",
7443
+ autoplay: "Controls whether media elements can autoplay without a user gesture.",
7444
+ fullscreen: "Controls access to the Fullscreen API (requestFullscreen).",
7445
+ payment: "Controls access to the Payment Request API.",
7446
+ usb: "Controls access to the WebUSB API.",
7447
+ accelerometer: "Controls access to the Accelerometer sensor API.",
7448
+ gyroscope: "Controls access to the Gyroscope sensor API.",
7449
+ magnetometer: "Controls access to the Magnetometer sensor API.",
7450
+ midi: "Controls access to the Web MIDI API.",
7451
+ "picture-in-picture": "Controls whether video elements can enter picture-in-picture mode.",
7452
+ "sync-xhr": "Controls whether synchronous XMLHttpRequest is allowed.",
7453
+ "display-capture": "Controls access to screen/tab capture (getDisplayMedia).",
7454
+ "encrypted-media": "Controls access to the Encrypted Media Extensions API (DRM playback).",
7455
+ "clipboard-read": "Controls read access to the async Clipboard API.",
7456
+ "clipboard-write": "Controls write access to the async Clipboard API.",
7457
+ "screen-wake-lock": "Controls access to the Screen Wake Lock API.",
7458
+ "web-share": "Controls access to the Web Share API.",
7459
+ "xr-spatial-tracking": "Controls access to WebXR device sensors and spatial tracking."
7460
+ };
7461
+ function _formatPermissionsPolicyAllowlistMcp(allowlist) {
7462
+ if (allowlist.includes("*")) return "*";
7463
+ if (allowlist.length === 0) return "()";
7464
+ return "(" + allowlist.map((o) => o === "self" ? "self" : `"${o}"`).join(" ") + ")";
7465
+ }
7466
+ function _buildPermissionsPolicyMcp(features) {
7467
+ if (!Array.isArray(features) || features.length === 0) return { error: "At least one feature is required" };
7468
+ const warnings = [];
7469
+ const seen = /* @__PURE__ */ new Map();
7470
+ for (const f of features) {
7471
+ if (!f || typeof f.feature !== "string" || f.feature.trim() === "") continue;
7472
+ const name = f.feature.trim().toLowerCase();
7473
+ const allowlist = Array.isArray(f.allowlist) ? f.allowlist.map((o) => typeof o === "string" ? o.trim() : "").filter((o) => o.length > 0) : [];
7474
+ if (seen.has(name)) warnings.push(`duplicate feature "${name}" was merged, the last occurrence wins`);
7475
+ seen.set(name, { feature: name, allowlist });
7476
+ }
7477
+ if (seen.size === 0) return { error: "At least one feature is required" };
7478
+ const directives = [];
7479
+ for (const { feature, allowlist } of seen.values()) {
7480
+ if (!_PERMISSIONS_POLICY_NOTES_MCP[feature]) {
7481
+ warnings.push(`"${feature}" is not a widely recognized feature name, it will still be included as written`);
7482
+ }
7483
+ if (allowlist.includes("*") && allowlist.length > 1) {
7484
+ warnings.push(`"${feature}" combines "*" with other origins; "*" already allows every origin so the others are redundant`);
7485
+ }
7486
+ const invalidOrigin = allowlist.find((o) => o !== "self" && o !== "*" && !/^https?:\/\//i.test(o));
7487
+ if (invalidOrigin) {
7488
+ warnings.push(`"${invalidOrigin}" on "${feature}" doesn't look like an origin (expected "self", "*", or an http(s):// URL); it will still be quoted as written`);
7489
+ }
7490
+ directives.push({
7491
+ feature,
7492
+ value: _formatPermissionsPolicyAllowlistMcp(allowlist),
7493
+ note: _PERMISSIONS_POLICY_NOTES_MCP[feature] ?? `Controls access to the "${feature}" feature.`
7494
+ });
7495
+ }
7496
+ const headerValue = directives.map((d) => `${d.feature}=${d.value}`).join(", ");
7497
+ return { header: `Permissions-Policy: ${headerValue}`, headerValue, directives, warnings };
7498
+ }
7499
+ function _splitPermissionsPolicyTopLevelMcp(input) {
7500
+ const segments = [];
7501
+ let depth = 0;
7502
+ let current = "";
7503
+ for (const ch of input) {
7504
+ if (ch === "(") depth++;
7505
+ if (ch === ")") depth--;
7506
+ if (ch === "," && depth === 0) {
7507
+ if (current.trim()) segments.push(current.trim());
7508
+ current = "";
7509
+ } else {
7510
+ current += ch;
7511
+ }
7512
+ }
7513
+ if (current.trim()) segments.push(current.trim());
7514
+ return segments;
7515
+ }
7516
+ function _parsePermissionsPolicyAllowlistMcp(rawValue) {
7517
+ if (rawValue === "*") return ["*"];
7518
+ if (rawValue === "()" || rawValue === "") return [];
7519
+ const inner = rawValue.replace(/^\(/, "").replace(/\)$/, "");
7520
+ const tokens = inner.match(/"[^"]*"|\S+/g) ?? [];
7521
+ return tokens.map((t) => t.replace(/^"|"$/g, ""));
7522
+ }
7523
+ function _parsePermissionsPolicyMcp(raw) {
7524
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
7525
+ const withoutLabel = raw.trim().replace(/^permissions-policy:\s*/i, "");
7526
+ const segments = _splitPermissionsPolicyTopLevelMcp(withoutLabel);
7527
+ if (segments.length === 0) return { error: "No directives found" };
7528
+ const features = [];
7529
+ for (const segment of segments) {
7530
+ const eq = segment.indexOf("=");
7531
+ if (eq === -1) continue;
7532
+ const feature = segment.slice(0, eq).trim();
7533
+ const rawValue = segment.slice(eq + 1).trim();
7534
+ features.push({ feature, allowlist: _parsePermissionsPolicyAllowlistMcp(rawValue) });
7535
+ }
7536
+ if (features.length === 0) return { error: "No directives found" };
7537
+ return _buildPermissionsPolicyMcp(features);
7538
+ }
7539
+ server.tool(
7540
+ "build_permissions_policy",
7541
+ 'Build a Permissions-Policy response-header value from a list of browser features (e.g. camera, microphone, geolocation) and their per-feature allowlist ("self", specific origins, "*" for all origins, or an empty allowlist to disable the feature entirely), attaching a plain-English note on what each feature restricts.',
7542
+ {
7543
+ features: z.array(z.object({
7544
+ feature: z.string().describe('The Permissions-Policy feature name, e.g. "camera"'),
7545
+ allowlist: z.array(z.string()).describe('Origins allowed to use the feature: "self", "*" for all origins, specific https:// origins, or an empty array to disable it entirely')
7546
+ })).min(1).describe("Features and their allowlists to include in the header")
7547
+ },
7548
+ async ({ features }) => {
7549
+ const result = _buildPermissionsPolicyMcp(features);
7550
+ if ("error" in result) return errText(result);
7551
+ return text(JSON.stringify(result, null, 2));
7552
+ }
7553
+ );
7554
+ server.tool(
7555
+ "parse_permissions_policy",
7556
+ 'Parse an existing Permissions-Policy header value (with or without a leading "Permissions-Policy:" label) into its individual feature directives, each with a plain-English note on what it restricts.',
7557
+ {
7558
+ header: z.string().min(1).describe("The Permissions-Policy header value to parse")
7559
+ },
7560
+ async ({ header }) => {
7561
+ const result = _parsePermissionsPolicyMcp(header);
7562
+ if ("error" in result) return errText(result);
7563
+ return text(JSON.stringify(result, null, 2));
7564
+ }
7565
+ );
7566
+ function _computePaybackMcp(initialInvestment, flows) {
7567
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7568
+ let cumulative = 0;
7569
+ const cumulativeCashFlows = [];
7570
+ let period = null;
7571
+ for (let i = 0; i < flows.length; i++) {
7572
+ const prevCumulative = cumulative;
7573
+ cumulative += flows[i];
7574
+ cumulativeCashFlows.push(round4(cumulative));
7575
+ if (period === null && cumulative >= initialInvestment) {
7576
+ const remaining = initialInvestment - prevCumulative;
7577
+ const fraction = flows[i] !== 0 ? remaining / flows[i] : 0;
7578
+ period = i + fraction;
7579
+ }
7580
+ }
7581
+ return { cumulative: cumulativeCashFlows, period };
7582
+ }
7583
+ function _calculatePaybackPeriodMcp(initialInvestment, cashFlows, discountRatePercent) {
7584
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
7585
+ if (typeof initialInvestment !== "number" || !Number.isFinite(initialInvestment) || initialInvestment <= 0) {
7586
+ return { error: "initialInvestment must be a finite number greater than zero" };
7587
+ }
7588
+ if (!Array.isArray(cashFlows) || cashFlows.length === 0) {
7589
+ return { error: "cashFlows must be a non-empty array of numbers" };
7590
+ }
7591
+ if (cashFlows.some((f) => typeof f !== "number" || !Number.isFinite(f))) {
7592
+ return { error: "Every cash flow must be a finite number" };
7593
+ }
7594
+ const { cumulative, period } = _computePaybackMcp(initialInvestment, cashFlows);
7595
+ const result = {
7596
+ initialInvestment: round4(initialInvestment),
7597
+ cashFlows: cashFlows.map(round4),
7598
+ cumulativeCashFlows: cumulative,
7599
+ paybackPeriod: period !== null ? round4(period) : null,
7600
+ paybackPeriodRounded: period !== null ? Math.ceil(period) : null,
7601
+ recovered: period !== null
7602
+ };
7603
+ if (discountRatePercent !== void 0) {
7604
+ if (typeof discountRatePercent !== "number" || !Number.isFinite(discountRatePercent) || discountRatePercent < 0) {
7605
+ return { error: "discountRatePercent must be a finite, non-negative number when provided" };
7606
+ }
7607
+ const r = discountRatePercent / 100;
7608
+ const discountedCashFlows = cashFlows.map((f, i) => f / Math.pow(1 + r, i + 1));
7609
+ const { cumulative: discountedCumulative, period: discountedPeriod } = _computePaybackMcp(initialInvestment, discountedCashFlows);
7610
+ result.discountRatePercent = round4(discountRatePercent);
7611
+ result.discountedCashFlows = discountedCashFlows.map(round4);
7612
+ result.discountedCumulativeCashFlows = discountedCumulative;
7613
+ result.discountedPaybackPeriod = discountedPeriod !== null ? round4(discountedPeriod) : null;
7614
+ result.discountedPaybackPeriodRounded = discountedPeriod !== null ? Math.ceil(discountedPeriod) : null;
7615
+ result.discountedRecovered = discountedPeriod !== null;
7616
+ }
7617
+ return result;
7618
+ }
7619
+ server.tool(
7620
+ "calculate_payback_period",
7621
+ "From an initial investment and a series of periodic cash flows, compute the simple payback period (the point where cumulative cash flow turns positive), plus the discounted payback period when a discount rate is supplied. Pure arithmetic over a running cumulative sum, no live rate feed.",
7622
+ {
7623
+ initialInvestment: z.number().describe("The initial investment amount (must be greater than zero)"),
7624
+ cashFlows: z.array(z.number()).min(1).describe("Net cash flow for each period, in order"),
7625
+ discountRatePercent: z.number().optional().describe("Optional discount rate per period, as a percent; when provided, also computes the discounted payback period")
7626
+ },
7627
+ async ({ initialInvestment, cashFlows, discountRatePercent }) => {
7628
+ const result = _calculatePaybackPeriodMcp(initialInvestment, cashFlows, discountRatePercent);
7629
+ if ("error" in result) return errText(result);
7630
+ return text(JSON.stringify(result, null, 2));
7631
+ }
7632
+ );
7201
7633
  var transport = new StdioServerTransport();
7202
7634
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.43.0",
4
- "description": "MCP server exposing 168 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.45.0",
4
+ "description": "MCP server exposing 175 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",