@utilix-tech/mcp 0.54.0 → 0.56.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.56.0
6
+ - Add `calculate_debt_to_income_ratio` tool: compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance.
7
+ - Add `infer_csv_column_types` tool: parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string), flagging nullable columns and empty-cell counts.
8
+
9
+ ## 0.55.0
10
+ - Add `validate_package_exports` tool: parse a package.json's exports field and flag common dual-package (ESM/CJS) misconfigurations, a types condition not listed first, a missing default fallback, mismatched require/import extensions, mixed subpath/condition keys, and pattern mismatches.
11
+ - Add `build_x_content_type_options_header` tool: build the fixed X-Content-Type-Options: nosniff header, validate a pasted value, and assess MIME-sniffing risk for a given Content-Type.
12
+
5
13
  ## 0.54.0
6
14
  - Add `calculate_credit_utilization` tool: compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance.
7
15
  - Add `analyze_caption_speed` tool: parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 196 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 200 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
 
@@ -160,7 +160,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
160
160
  | `convert_timezone` | Convert a date/time between timezones |
161
161
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
162
162
 
163
- ### Units (33 tools)
163
+ ### Units (34 tools)
164
164
  | Tool | Description |
165
165
  |------|-------------|
166
166
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -196,8 +196,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
196
196
  | `calculate_moving_averages` | Compute simple moving average (SMA) and exponential moving average (EMA) series from a list of numbers and a window size |
197
197
  | `calculate_gpa` | Compute a weighted GPA from a list of courses, each with a letter grade and credit hours, using the standard 4.0-capped scale |
198
198
  | `calculate_credit_utilization` | Compute per-account and overall credit utilization (balance / limit) from one or more credit accounts, plus a qualitative tier matching common credit-scoring guidance |
199
+ | `calculate_debt_to_income_ratio` | Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debts, plus a qualitative tier matching common mortgage-lending guidance |
199
200
 
200
- ### API / Network (28 tools)
201
+ ### API / Network (29 tools)
201
202
  | Tool | Description |
202
203
  |------|-------------|
203
204
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -228,6 +229,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
228
229
  | `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
229
230
  | `build_content_disposition` | Build a Content-Disposition response header for a file download or inline display, with an RFC 5987 filename* parameter for non-ASCII filenames |
230
231
  | `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
232
+ | `build_x_content_type_options_header` | Build the fixed X-Content-Type-Options: nosniff header, validate a pasted value, or assess MIME-sniffing risk for a given Content-Type |
231
233
 
232
234
  ### Color (4 tools)
233
235
  | Tool | Description |
@@ -237,7 +239,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
237
239
  | `color_palette` | Generate a color palette from a base color |
238
240
  | `color_shades` | Generate shades or tints from a base color |
239
241
 
240
- ### Code (14 tools)
242
+ ### Code (15 tools)
241
243
  | Tool | Description |
242
244
  |------|-------------|
243
245
  | `format_sql` | Format a SQL query with proper indentation |
@@ -254,8 +256,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
254
256
  | `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
255
257
  | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
256
258
  | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
259
+ | `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
257
260
 
258
- ### Data (13 tools)
261
+ ### Data (14 tools)
259
262
  | Tool | Description |
260
263
  |------|-------------|
261
264
  | `validate_yaml` | Validate YAML syntax |
@@ -271,6 +274,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
271
274
  | `find_csv_duplicate_rows` | Group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row |
272
275
  | `calculate_markdown_task_list_progress` | Parse a Markdown document's checkbox task list and report completion percentage overall and per section, grouped by the nearest preceding heading |
273
276
  | `analyze_caption_speed` | Parse an SRT or WebVTT file and report the words-per-minute reading speed of every cue, flagging ones that exceed common broadcast accessibility guidelines |
277
+ | `infer_csv_column_types` | Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string), flagging nullable columns and empty-cell counts |
274
278
 
275
279
  ### CSS (2 tools)
276
280
  | Tool | Description |
package/dist/index.js CHANGED
@@ -7271,6 +7271,71 @@ server.tool(
7271
7271
  return text(JSON.stringify(result, null, 2));
7272
7272
  }
7273
7273
  );
7274
+ var _CSV_TYPE_ISO_DATE_RE_MCP = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
7275
+ function _classifyCsvValueMcp(raw) {
7276
+ const v = raw.trim();
7277
+ if (v === "") return "empty";
7278
+ if (/^(true|false)$/i.test(v)) return "boolean";
7279
+ if (/^-?\d+$/.test(v)) return "integer";
7280
+ if (/^-?\d+\.\d+$/.test(v)) return "float";
7281
+ if (_CSV_TYPE_ISO_DATE_RE_MCP.test(v)) return "date";
7282
+ return "string";
7283
+ }
7284
+ function _inferCsvColumnTypesMcp(input, delimiter, hasHeader) {
7285
+ if (!input || input.trim() === "") return { error: "Input is empty" };
7286
+ const delim = delimiter || ",";
7287
+ const header = hasHeader !== false;
7288
+ const lines = input.replace(/\r\n/g, "\n").split("\n").filter((l) => l.trim() !== "");
7289
+ if (lines.length === 0) return { error: "No data found" };
7290
+ const allRows = lines.map((l) => _parseCsvRowForSqlMcp(l, delim));
7291
+ const headers = header ? allRows[0].map((h) => h.trim()) : allRows[0].map((_, idx) => `Column ${idx + 1}`);
7292
+ const rows = header ? allRows.slice(1) : allRows;
7293
+ const columns = headers.map((name, colIdx) => {
7294
+ const seen = /* @__PURE__ */ new Set();
7295
+ let sawString = false;
7296
+ let nullCount = 0;
7297
+ const sampleValues = [];
7298
+ for (const row of rows) {
7299
+ const raw = row[colIdx] ?? "";
7300
+ const kind = _classifyCsvValueMcp(raw);
7301
+ if (kind === "empty") {
7302
+ nullCount++;
7303
+ continue;
7304
+ }
7305
+ if (sampleValues.length < 3) sampleValues.push(raw);
7306
+ if (kind === "string") sawString = true;
7307
+ else seen.add(kind);
7308
+ }
7309
+ let inferredType;
7310
+ if (sawString || seen.size === 0) inferredType = "string";
7311
+ else if (seen.size === 1) inferredType = [...seen][0];
7312
+ else if (seen.has("integer") && seen.has("float") && seen.size === 2) inferredType = "float";
7313
+ else inferredType = "string";
7314
+ return {
7315
+ name,
7316
+ inferredType,
7317
+ nullable: nullCount > 0,
7318
+ nullCount,
7319
+ totalCount: rows.length,
7320
+ sampleValues
7321
+ };
7322
+ });
7323
+ return { columns, rowCount: rows.length };
7324
+ }
7325
+ server.tool(
7326
+ "infer_csv_column_types",
7327
+ "Parse CSV/TSV data and infer each column's data type (integer, float, boolean, ISO 8601 date, or string) by classifying every non-empty value and finding the tightest type that fits them all: all-integer values infer as integer, a mix of integers and decimals infers as float, and anything that doesn't fit a narrower type falls back to string. Empty cells mark a column nullable but don't affect its inferred type.",
7328
+ {
7329
+ csv: z.string().min(1).describe("CSV/TSV data to infer column types from"),
7330
+ delimiter: z.string().optional().describe('Field delimiter (default ",")'),
7331
+ hasHeader: z.boolean().default(true).describe("Whether the first row is a header")
7332
+ },
7333
+ async ({ csv, delimiter, hasHeader }) => {
7334
+ const result = _inferCsvColumnTypesMcp(csv, delimiter, hasHeader);
7335
+ if ("error" in result) return errText(result);
7336
+ return text(JSON.stringify(result, null, 2));
7337
+ }
7338
+ );
7274
7339
  var _VARY_HEADER_NOTES_MCP = {
7275
7340
  "accept-encoding": "Caches must store a separate response per compression scheme (gzip, br, etc.) the client accepts.",
7276
7341
  "accept-language": "Caches must store a separate response per requested language, which can fragment a shared cache badly if Accept-Language values are highly varied.",
@@ -8922,6 +8987,59 @@ server.tool(
8922
8987
  return text(JSON.stringify(result, null, 2));
8923
8988
  }
8924
8989
  );
8990
+ function _classifyDtiMcp(percent) {
8991
+ if (percent < 36) return "healthy";
8992
+ if (percent < 43) return "moderate";
8993
+ if (percent < 50) return "high";
8994
+ return "severe";
8995
+ }
8996
+ function _calculateDebtToIncomeRatioMcp(debts, grossMonthlyIncome) {
8997
+ if (!Array.isArray(debts) || debts.length === 0) {
8998
+ return { error: "debts must be a non-empty array" };
8999
+ }
9000
+ if (typeof grossMonthlyIncome !== "number" || !Number.isFinite(grossMonthlyIncome) || grossMonthlyIncome <= 0) {
9001
+ return { error: "grossMonthlyIncome must be a finite number greater than zero" };
9002
+ }
9003
+ const round2 = (n) => Math.round(n * 100) / 100;
9004
+ let totalMonthlyDebt = 0;
9005
+ const results = [];
9006
+ for (let i = 0; i < debts.length; i++) {
9007
+ const debt = debts[i];
9008
+ if (typeof debt.amount !== "number" || !Number.isFinite(debt.amount) || debt.amount < 0) {
9009
+ return { error: `debt ${i + 1}: amount must be a finite, non-negative number` };
9010
+ }
9011
+ totalMonthlyDebt += debt.amount;
9012
+ results.push({
9013
+ label: debt.label?.trim() || `Debt ${i + 1}`,
9014
+ amount: debt.amount,
9015
+ percentOfIncome: round2(debt.amount / grossMonthlyIncome * 100)
9016
+ });
9017
+ }
9018
+ const dtiPercent = round2(totalMonthlyDebt / grossMonthlyIncome * 100);
9019
+ return {
9020
+ debts: results,
9021
+ totalMonthlyDebt: round2(totalMonthlyDebt),
9022
+ grossMonthlyIncome,
9023
+ dtiPercent,
9024
+ tier: _classifyDtiMcp(dtiPercent)
9025
+ };
9026
+ }
9027
+ server.tool(
9028
+ "calculate_debt_to_income_ratio",
9029
+ "Compute the debt-to-income ratio (total monthly debt payments / gross monthly income) from one or more monthly debt items, plus a qualitative tier matching common mortgage-lending guidance: under 36% is healthy, under 43% is moderate (43% is the back-end ratio ceiling many mortgage lenders use), under 50% is high, and 50%+ is severe. This is a back-end DTI (all monthly debt payments over gross income), not a front-end housing-only ratio.",
9030
+ {
9031
+ debts: z.array(z.object({
9032
+ label: z.string().optional().describe("Optional debt label"),
9033
+ amount: z.number().min(0).describe("Monthly payment amount for this debt")
9034
+ })).min(1).describe("A non-empty list of monthly debt payments"),
9035
+ grossMonthlyIncome: z.number().positive().describe("Gross (pre-tax) monthly income")
9036
+ },
9037
+ async ({ debts, grossMonthlyIncome }) => {
9038
+ const result = _calculateDebtToIncomeRatioMcp(debts, grossMonthlyIncome);
9039
+ if ("error" in result) return errText(result);
9040
+ return text(JSON.stringify(result, null, 2));
9041
+ }
9042
+ );
8925
9043
  var _CAPTION_TIMESTAMP_RE_MCP = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
8926
9044
  var _CAPTION_SKIP_BLOCK_KEYWORDS_MCP = ["NOTE", "STYLE", "REGION"];
8927
9045
  function _parseCaptionTimestampMcp(raw) {
@@ -9020,5 +9138,194 @@ server.tool(
9020
9138
  return text(JSON.stringify(result, null, 2));
9021
9139
  }
9022
9140
  );
9141
+ function _collectExportsStringLeavesMcp(value, out = []) {
9142
+ if (typeof value === "string") out.push(value);
9143
+ else if (Array.isArray(value)) value.forEach((v) => _collectExportsStringLeavesMcp(v, out));
9144
+ else if (value && typeof value === "object") Object.values(value).forEach((v) => _collectExportsStringLeavesMcp(v, out));
9145
+ return out;
9146
+ }
9147
+ function _checkExportsPatternMatchMcp(key, value, path, issues) {
9148
+ const keyHasStar = key.includes("*");
9149
+ const leaves = _collectExportsStringLeavesMcp(value);
9150
+ if (keyHasStar) {
9151
+ const bad = leaves.filter((l) => !l.includes("*"));
9152
+ if (bad.length > 0) {
9153
+ issues.push({ severity: "error", path, message: `pattern key "${key}" contains "*" but target ${bad.map((l) => `"${l}"`).join(", ")} has no "*" to receive the matched text` });
9154
+ }
9155
+ } else {
9156
+ const bad = leaves.filter((l) => l.includes("*"));
9157
+ if (bad.length > 0) {
9158
+ issues.push({ severity: "error", path, message: `target ${bad.map((l) => `"${l}"`).join(", ")} contains "*" but key "${key}" has no "*" pattern to match against` });
9159
+ }
9160
+ }
9161
+ }
9162
+ function _validateExportsValueMcp(value, path, issues) {
9163
+ if (value === null) return;
9164
+ if (typeof value === "string") {
9165
+ if (!value.startsWith("./")) {
9166
+ issues.push({ severity: "error", path, message: `target "${value}" must start with "./"` });
9167
+ }
9168
+ return;
9169
+ }
9170
+ if (Array.isArray(value)) {
9171
+ if (value.length === 0) {
9172
+ issues.push({ severity: "warning", path, message: "empty fallback array will never resolve" });
9173
+ }
9174
+ value.forEach((v, i) => _validateExportsValueMcp(v, `${path}[${i}]`, issues));
9175
+ return;
9176
+ }
9177
+ if (typeof value === "object") {
9178
+ const obj = value;
9179
+ const keys = Object.keys(obj);
9180
+ if (keys.length === 0) {
9181
+ issues.push({ severity: "warning", path, message: "empty object will never resolve" });
9182
+ return;
9183
+ }
9184
+ const hasDotKeys = keys.some((k) => k.startsWith("."));
9185
+ const hasNonDotKeys = keys.some((k) => !k.startsWith("."));
9186
+ if (hasDotKeys && hasNonDotKeys) {
9187
+ issues.push({ severity: "error", path, message: 'cannot mix subpath keys (starting with ".") with condition keys in the same object' });
9188
+ return;
9189
+ }
9190
+ if (hasDotKeys) {
9191
+ for (const key of keys) {
9192
+ const subpath = `${path}["${key}"]`;
9193
+ if (key !== "." && !key.startsWith("./")) {
9194
+ issues.push({ severity: "error", path: subpath, message: `subpath key "${key}" must be "." or start with "./"` });
9195
+ }
9196
+ _checkExportsPatternMatchMcp(key, obj[key], subpath, issues);
9197
+ _validateExportsValueMcp(obj[key], subpath, issues);
9198
+ }
9199
+ return;
9200
+ }
9201
+ if (keys.includes("types") && keys[0] !== "types") {
9202
+ issues.push({ severity: "warning", path, message: '"types" condition should be listed first so TypeScript resolves it before any other condition matches' });
9203
+ }
9204
+ if (keys.includes("default") && keys[keys.length - 1] !== "default") {
9205
+ issues.push({ severity: "warning", path, message: '"default" condition should be listed last; conditions are matched in order and anything after "default" is unreachable' });
9206
+ }
9207
+ if ((keys.includes("import") || keys.includes("require")) && !keys.includes("default")) {
9208
+ issues.push({ severity: "warning", path, message: 'no "default" fallback condition; resolution fails for any importer using a condition not listed here' });
9209
+ }
9210
+ const requireVal = obj.require;
9211
+ if (typeof requireVal === "string" && requireVal.endsWith(".mjs")) {
9212
+ issues.push({ severity: "error", path: `${path}.require`, message: `"require" condition points to a .mjs file ("${requireVal}"), but .mjs is always treated as ESM and cannot be required` });
9213
+ }
9214
+ const importVal = obj.import;
9215
+ if (typeof importVal === "string" && importVal.endsWith(".cjs")) {
9216
+ issues.push({ severity: "error", path: `${path}.import`, message: `"import" condition points to a .cjs file ("${importVal}"), but .cjs is always treated as CommonJS and cannot be imported as ESM` });
9217
+ }
9218
+ for (const key of keys) {
9219
+ _validateExportsValueMcp(obj[key], `${path}.${key}`, issues);
9220
+ }
9221
+ return;
9222
+ }
9223
+ issues.push({ severity: "error", path, message: `unexpected value type at "${path}"` });
9224
+ }
9225
+ function _validatePackageExportsMcp(packageJsonText) {
9226
+ if (!packageJsonText || !packageJsonText.trim()) return { error: "Input is empty" };
9227
+ let parsed;
9228
+ try {
9229
+ parsed = JSON.parse(packageJsonText);
9230
+ } catch (e) {
9231
+ return { error: `Invalid JSON: ${e.message}` };
9232
+ }
9233
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
9234
+ return { error: "Root of package.json must be an object" };
9235
+ }
9236
+ const obj = parsed;
9237
+ if (!("exports" in obj)) return { error: 'package.json has no "exports" field' };
9238
+ const exportsValue = obj.exports;
9239
+ const issues = [];
9240
+ let exportsShape;
9241
+ if (exportsValue === null) {
9242
+ exportsShape = "null";
9243
+ } else if (typeof exportsValue === "string") {
9244
+ exportsShape = "string";
9245
+ } else if (Array.isArray(exportsValue)) {
9246
+ exportsShape = "array";
9247
+ } else if (typeof exportsValue === "object") {
9248
+ const keys = Object.keys(exportsValue);
9249
+ exportsShape = keys.some((k) => k.startsWith(".")) ? "subpaths" : "conditions";
9250
+ } else {
9251
+ return { error: '"exports" must be a string, array, object, or null' };
9252
+ }
9253
+ _validateExportsValueMcp(exportsValue, "exports", issues);
9254
+ const subpaths = exportsShape === "subpaths" ? Object.keys(exportsValue) : ["."];
9255
+ const valid = !issues.some((i) => i.severity === "error");
9256
+ return { parsed: true, exportsShape, subpaths, issues, valid };
9257
+ }
9258
+ server.tool(
9259
+ "validate_package_exports",
9260
+ 'Validate a package.json "exports" field: flag common dual-package (ESM/CJS) misconfigurations \u2014 a "types" condition not listed first, a missing "default" fallback, a "require"/"import" pointing at the wrong file extension, mixing subpath and condition keys in the same object, and a subpath pattern with no matching "*" in its target.',
9261
+ {
9262
+ text: z.string().describe('The full package.json file contents (only the "exports" field is read)')
9263
+ },
9264
+ async ({ text: packageJsonText }) => {
9265
+ const result = _validatePackageExportsMcp(packageJsonText);
9266
+ if ("error" in result) return errText(result);
9267
+ return text(JSON.stringify(result, null, 2));
9268
+ }
9269
+ );
9270
+ var _X_CONTENT_TYPE_OPTIONS_HEADER_MCP = "X-Content-Type-Options: nosniff";
9271
+ var _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP = {
9272
+ "text/plain": { risk: "high", reason: "Browsers historically sniff text/plain responses and may render them as HTML if the body looks like markup, letting user-controlled text (comments, error pages, logs) execute as script." },
9273
+ "application/octet-stream": { risk: "high", reason: "A generic fallback type with no format hint, so browsers are the most likely to sniff its real content and may render it as HTML." },
9274
+ "": { risk: "high", reason: "A missing Content-Type header gives the browser nothing to trust, so it falls back to sniffing the response body entirely." }
9275
+ };
9276
+ var _X_CONTENT_TYPE_PREFIX_RISK_TABLE_MCP = [
9277
+ { prefix: "text/html", risk: "medium", reason: "Already interpreted as HTML, so sniffing mostly matters if the type is wrong on purpose; nosniff still protects against a mismatched charset or embedded polyglot content." },
9278
+ { prefix: "image/", risk: "low", reason: 'Browsers rarely upgrade an image response to HTML, but a crafted "GIFAR"-style polyglot file can still be sniffed as HTML in older engines.' },
9279
+ { prefix: "audio/", risk: "low", reason: "Media types are rarely reinterpreted as HTML by modern sniffing algorithms." },
9280
+ { prefix: "video/", risk: "low", reason: "Media types are rarely reinterpreted as HTML by modern sniffing algorithms." },
9281
+ { prefix: "font/", risk: "low", reason: "Font types are rarely reinterpreted as HTML by modern sniffing algorithms." },
9282
+ { prefix: "application/pdf", risk: "low", reason: "PDF has a strong magic-number signature, so sniffing rarely reclassifies it, though nosniff is still worth setting for user-uploaded PDFs." },
9283
+ { prefix: "application/json", risk: "medium", reason: "Structurally distinct from HTML, but user-controlled JSONP-style responses served with a wrong or missing type have been used in real sniffing attacks." },
9284
+ { prefix: "application/javascript", risk: "medium", reason: "Already executable; nosniff mainly protects against it being served where a script tag was not expected, such as inside an <img> or <object> tag." },
9285
+ { prefix: "application/xml", risk: "medium", reason: "XML can be reinterpreted in ways that overlap with HTML/XHTML parsing in some legacy sniffing paths." }
9286
+ ];
9287
+ function _assessContentTypeRiskMcp(contentType) {
9288
+ const normalized = contentType.trim().toLowerCase().split(";")[0].trim();
9289
+ if (normalized in _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP) {
9290
+ const entry = _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP[normalized];
9291
+ return { contentType: contentType.trim() || "(empty)", risk: entry.risk, reason: entry.reason };
9292
+ }
9293
+ const prefixMatch = _X_CONTENT_TYPE_PREFIX_RISK_TABLE_MCP.find((e) => normalized.startsWith(e.prefix));
9294
+ if (prefixMatch) {
9295
+ return { contentType: contentType.trim(), risk: prefixMatch.risk, reason: prefixMatch.reason };
9296
+ }
9297
+ return {
9298
+ contentType: contentType.trim(),
9299
+ risk: "medium",
9300
+ reason: "Not a well-known type in this tool's lookup table. Treat any response type as sniffable unless you've confirmed the serving browser trusts it, and set nosniff regardless of type as a baseline defense."
9301
+ };
9302
+ }
9303
+ server.tool(
9304
+ "build_x_content_type_options_header",
9305
+ 'Build the X-Content-Type-Options header (only one valid value, "nosniff") with a plain-English explanation of the MIME-sniffing attack it prevents, optionally validating a pasted header value or assessing MIME-sniffing risk for a given Content-Type.',
9306
+ {
9307
+ headerValue: z.string().optional().describe('A pasted header value or full header line to validate, e.g. "nosniff"'),
9308
+ contentType: z.string().optional().describe('A Content-Type value to assess MIME-sniffing risk for, e.g. "text/plain"')
9309
+ },
9310
+ async ({ headerValue, contentType }) => {
9311
+ if (typeof headerValue === "string") {
9312
+ const withoutLabel = headerValue.trim().replace(/^x-content-type-options\s*:\s*/i, "");
9313
+ const value = withoutLabel.trim();
9314
+ if (value === "") return errText({ error: 'No value found after "X-Content-Type-Options:"' });
9315
+ if (value.toLowerCase() === "nosniff") {
9316
+ return text(JSON.stringify({ raw: value, valid: true, message: "Valid. The browser will not MIME-sniff this response." }, null, 2));
9317
+ }
9318
+ return text(JSON.stringify({ raw: value, valid: false, message: `"${value}" is not a recognized value. The only value browsers act on is "nosniff" (case-insensitive); anything else is silently ignored, same as omitting the header.` }, null, 2));
9319
+ }
9320
+ if (typeof contentType === "string") {
9321
+ return text(JSON.stringify(_assessContentTypeRiskMcp(contentType), null, 2));
9322
+ }
9323
+ return text(JSON.stringify({
9324
+ header: _X_CONTENT_TYPE_OPTIONS_HEADER_MCP,
9325
+ value: "nosniff",
9326
+ explanation: `Tells the browser to strictly honor the declared Content-Type header instead of "sniffing" a response's bytes to guess its real type. Without it, a browser that decides an oddly-typed response looks like HTML or JavaScript may render or execute it, letting a file that was only ever meant to be downloaded or displayed as plain text be used to run a cross-site scripting attack.`
9327
+ }, null, 2));
9328
+ }
9329
+ );
9023
9330
  var transport = new StdioServerTransport();
9024
9331
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.54.0",
4
- "description": "MCP server exposing 188 Utilix developer tools to Claude, Cursor, VS Code Copilot, and any MCP-compatible AI assistant.",
3
+ "version": "0.56.0",
4
+ "description": "MCP server exposing 198 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",