@utilix-tech/mcp 0.43.0 → 0.44.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,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.44.0
6
+ - 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.
7
+ - 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.
8
+
5
9
  ## 0.43.0
6
10
  - 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
11
 
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 175 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
 
@@ -189,7 +189,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
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
191
 
192
- ### API / Network (20 tools)
192
+ ### API / Network (21 tools)
193
193
  | Tool | Description |
194
194
  |------|-------------|
195
195
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -212,6 +212,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
212
212
  | `parse_content_range_header` | Parse a Content-Range response header into its unit, byte bounds, and total resource size |
213
213
  | `build_vary_header` | Build a Vary response header from request-header names, with a plain-English caching note per header |
214
214
  | `parse_vary_header` | Parse an existing Vary header value into its individual header names, each with a caching note |
215
+ | `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 |
215
216
 
216
217
  ### Color (4 tools)
217
218
  | Tool | Description |
@@ -264,7 +265,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
264
265
  | `unicode_inspect` | Inspect Unicode code points of a string |
265
266
  | `optimize_svg` | Optimize and minify an SVG string |
266
267
 
267
- ### AI / Agent (27 tools)
268
+ ### AI / Agent (28 tools)
268
269
  | Tool | Description |
269
270
  |------|-------------|
270
271
  | `estimate_tokens` | Estimate token count and cost for an LLM prompt |
@@ -294,6 +295,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
294
295
  | `diff_json_detailed` | Diff two parsed JSON values, returning every path with its op and unchanged paths |
295
296
  | `vector_similarity` | Cosine similarity, dot product, and Euclidean / Manhattan distance between two numeric vectors |
296
297
  | `format_few_shot_examples` | Format input/output example pairs into a numbered, XML-tagged, or delimiter-separated few-shot prompt template |
298
+ | `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
299
 
298
300
  ### Image (3 tools)
299
301
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7198,5 +7198,243 @@ 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
+ );
7201
7439
  var transport = new StdioServerTransport();
7202
7440
  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.44.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",