@utilix-tech/mcp 0.54.0 → 0.55.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 +5 -3
- package/dist/index.js +189 -0
- package/package.json +2 -2
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.55.0
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
5
9
|
## 0.54.0
|
|
6
10
|
- 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
11
|
- 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
|
|
3
|
+
MCP server exposing 198 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
|
|
|
@@ -197,7 +197,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
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
199
|
|
|
200
|
-
### API / Network (
|
|
200
|
+
### API / Network (29 tools)
|
|
201
201
|
| Tool | Description |
|
|
202
202
|
|------|-------------|
|
|
203
203
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -228,6 +228,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
228
228
|
| `parse_client_hint_headers` | Decode raw Client Hints request-header text into structured values: brand/version pairs, booleans, strings, and numbers |
|
|
229
229
|
| `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
230
|
| `parse_content_disposition` | Parse a Content-Disposition header value into its disposition type, decoded filename, and raw parameters |
|
|
231
|
+
| `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
232
|
|
|
232
233
|
### Color (4 tools)
|
|
233
234
|
| Tool | Description |
|
|
@@ -237,7 +238,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
237
238
|
| `color_palette` | Generate a color palette from a base color |
|
|
238
239
|
| `color_shades` | Generate shades or tints from a base color |
|
|
239
240
|
|
|
240
|
-
### Code (
|
|
241
|
+
### Code (15 tools)
|
|
241
242
|
| Tool | Description |
|
|
242
243
|
|------|-------------|
|
|
243
244
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -254,6 +255,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
254
255
|
| `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
|
|
255
256
|
| `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
|
|
256
257
|
| `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
|
|
258
|
+
| `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
259
|
|
|
258
260
|
### Data (13 tools)
|
|
259
261
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -9020,5 +9020,194 @@ server.tool(
|
|
|
9020
9020
|
return text(JSON.stringify(result, null, 2));
|
|
9021
9021
|
}
|
|
9022
9022
|
);
|
|
9023
|
+
function _collectExportsStringLeavesMcp(value, out = []) {
|
|
9024
|
+
if (typeof value === "string") out.push(value);
|
|
9025
|
+
else if (Array.isArray(value)) value.forEach((v) => _collectExportsStringLeavesMcp(v, out));
|
|
9026
|
+
else if (value && typeof value === "object") Object.values(value).forEach((v) => _collectExportsStringLeavesMcp(v, out));
|
|
9027
|
+
return out;
|
|
9028
|
+
}
|
|
9029
|
+
function _checkExportsPatternMatchMcp(key, value, path, issues) {
|
|
9030
|
+
const keyHasStar = key.includes("*");
|
|
9031
|
+
const leaves = _collectExportsStringLeavesMcp(value);
|
|
9032
|
+
if (keyHasStar) {
|
|
9033
|
+
const bad = leaves.filter((l) => !l.includes("*"));
|
|
9034
|
+
if (bad.length > 0) {
|
|
9035
|
+
issues.push({ severity: "error", path, message: `pattern key "${key}" contains "*" but target ${bad.map((l) => `"${l}"`).join(", ")} has no "*" to receive the matched text` });
|
|
9036
|
+
}
|
|
9037
|
+
} else {
|
|
9038
|
+
const bad = leaves.filter((l) => l.includes("*"));
|
|
9039
|
+
if (bad.length > 0) {
|
|
9040
|
+
issues.push({ severity: "error", path, message: `target ${bad.map((l) => `"${l}"`).join(", ")} contains "*" but key "${key}" has no "*" pattern to match against` });
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
}
|
|
9044
|
+
function _validateExportsValueMcp(value, path, issues) {
|
|
9045
|
+
if (value === null) return;
|
|
9046
|
+
if (typeof value === "string") {
|
|
9047
|
+
if (!value.startsWith("./")) {
|
|
9048
|
+
issues.push({ severity: "error", path, message: `target "${value}" must start with "./"` });
|
|
9049
|
+
}
|
|
9050
|
+
return;
|
|
9051
|
+
}
|
|
9052
|
+
if (Array.isArray(value)) {
|
|
9053
|
+
if (value.length === 0) {
|
|
9054
|
+
issues.push({ severity: "warning", path, message: "empty fallback array will never resolve" });
|
|
9055
|
+
}
|
|
9056
|
+
value.forEach((v, i) => _validateExportsValueMcp(v, `${path}[${i}]`, issues));
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
if (typeof value === "object") {
|
|
9060
|
+
const obj = value;
|
|
9061
|
+
const keys = Object.keys(obj);
|
|
9062
|
+
if (keys.length === 0) {
|
|
9063
|
+
issues.push({ severity: "warning", path, message: "empty object will never resolve" });
|
|
9064
|
+
return;
|
|
9065
|
+
}
|
|
9066
|
+
const hasDotKeys = keys.some((k) => k.startsWith("."));
|
|
9067
|
+
const hasNonDotKeys = keys.some((k) => !k.startsWith("."));
|
|
9068
|
+
if (hasDotKeys && hasNonDotKeys) {
|
|
9069
|
+
issues.push({ severity: "error", path, message: 'cannot mix subpath keys (starting with ".") with condition keys in the same object' });
|
|
9070
|
+
return;
|
|
9071
|
+
}
|
|
9072
|
+
if (hasDotKeys) {
|
|
9073
|
+
for (const key of keys) {
|
|
9074
|
+
const subpath = `${path}["${key}"]`;
|
|
9075
|
+
if (key !== "." && !key.startsWith("./")) {
|
|
9076
|
+
issues.push({ severity: "error", path: subpath, message: `subpath key "${key}" must be "." or start with "./"` });
|
|
9077
|
+
}
|
|
9078
|
+
_checkExportsPatternMatchMcp(key, obj[key], subpath, issues);
|
|
9079
|
+
_validateExportsValueMcp(obj[key], subpath, issues);
|
|
9080
|
+
}
|
|
9081
|
+
return;
|
|
9082
|
+
}
|
|
9083
|
+
if (keys.includes("types") && keys[0] !== "types") {
|
|
9084
|
+
issues.push({ severity: "warning", path, message: '"types" condition should be listed first so TypeScript resolves it before any other condition matches' });
|
|
9085
|
+
}
|
|
9086
|
+
if (keys.includes("default") && keys[keys.length - 1] !== "default") {
|
|
9087
|
+
issues.push({ severity: "warning", path, message: '"default" condition should be listed last; conditions are matched in order and anything after "default" is unreachable' });
|
|
9088
|
+
}
|
|
9089
|
+
if ((keys.includes("import") || keys.includes("require")) && !keys.includes("default")) {
|
|
9090
|
+
issues.push({ severity: "warning", path, message: 'no "default" fallback condition; resolution fails for any importer using a condition not listed here' });
|
|
9091
|
+
}
|
|
9092
|
+
const requireVal = obj.require;
|
|
9093
|
+
if (typeof requireVal === "string" && requireVal.endsWith(".mjs")) {
|
|
9094
|
+
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` });
|
|
9095
|
+
}
|
|
9096
|
+
const importVal = obj.import;
|
|
9097
|
+
if (typeof importVal === "string" && importVal.endsWith(".cjs")) {
|
|
9098
|
+
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` });
|
|
9099
|
+
}
|
|
9100
|
+
for (const key of keys) {
|
|
9101
|
+
_validateExportsValueMcp(obj[key], `${path}.${key}`, issues);
|
|
9102
|
+
}
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
issues.push({ severity: "error", path, message: `unexpected value type at "${path}"` });
|
|
9106
|
+
}
|
|
9107
|
+
function _validatePackageExportsMcp(packageJsonText) {
|
|
9108
|
+
if (!packageJsonText || !packageJsonText.trim()) return { error: "Input is empty" };
|
|
9109
|
+
let parsed;
|
|
9110
|
+
try {
|
|
9111
|
+
parsed = JSON.parse(packageJsonText);
|
|
9112
|
+
} catch (e) {
|
|
9113
|
+
return { error: `Invalid JSON: ${e.message}` };
|
|
9114
|
+
}
|
|
9115
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
9116
|
+
return { error: "Root of package.json must be an object" };
|
|
9117
|
+
}
|
|
9118
|
+
const obj = parsed;
|
|
9119
|
+
if (!("exports" in obj)) return { error: 'package.json has no "exports" field' };
|
|
9120
|
+
const exportsValue = obj.exports;
|
|
9121
|
+
const issues = [];
|
|
9122
|
+
let exportsShape;
|
|
9123
|
+
if (exportsValue === null) {
|
|
9124
|
+
exportsShape = "null";
|
|
9125
|
+
} else if (typeof exportsValue === "string") {
|
|
9126
|
+
exportsShape = "string";
|
|
9127
|
+
} else if (Array.isArray(exportsValue)) {
|
|
9128
|
+
exportsShape = "array";
|
|
9129
|
+
} else if (typeof exportsValue === "object") {
|
|
9130
|
+
const keys = Object.keys(exportsValue);
|
|
9131
|
+
exportsShape = keys.some((k) => k.startsWith(".")) ? "subpaths" : "conditions";
|
|
9132
|
+
} else {
|
|
9133
|
+
return { error: '"exports" must be a string, array, object, or null' };
|
|
9134
|
+
}
|
|
9135
|
+
_validateExportsValueMcp(exportsValue, "exports", issues);
|
|
9136
|
+
const subpaths = exportsShape === "subpaths" ? Object.keys(exportsValue) : ["."];
|
|
9137
|
+
const valid = !issues.some((i) => i.severity === "error");
|
|
9138
|
+
return { parsed: true, exportsShape, subpaths, issues, valid };
|
|
9139
|
+
}
|
|
9140
|
+
server.tool(
|
|
9141
|
+
"validate_package_exports",
|
|
9142
|
+
'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.',
|
|
9143
|
+
{
|
|
9144
|
+
text: z.string().describe('The full package.json file contents (only the "exports" field is read)')
|
|
9145
|
+
},
|
|
9146
|
+
async ({ text: packageJsonText }) => {
|
|
9147
|
+
const result = _validatePackageExportsMcp(packageJsonText);
|
|
9148
|
+
if ("error" in result) return errText(result);
|
|
9149
|
+
return text(JSON.stringify(result, null, 2));
|
|
9150
|
+
}
|
|
9151
|
+
);
|
|
9152
|
+
var _X_CONTENT_TYPE_OPTIONS_HEADER_MCP = "X-Content-Type-Options: nosniff";
|
|
9153
|
+
var _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP = {
|
|
9154
|
+
"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." },
|
|
9155
|
+
"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." },
|
|
9156
|
+
"": { risk: "high", reason: "A missing Content-Type header gives the browser nothing to trust, so it falls back to sniffing the response body entirely." }
|
|
9157
|
+
};
|
|
9158
|
+
var _X_CONTENT_TYPE_PREFIX_RISK_TABLE_MCP = [
|
|
9159
|
+
{ 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." },
|
|
9160
|
+
{ 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.' },
|
|
9161
|
+
{ prefix: "audio/", risk: "low", reason: "Media types are rarely reinterpreted as HTML by modern sniffing algorithms." },
|
|
9162
|
+
{ prefix: "video/", risk: "low", reason: "Media types are rarely reinterpreted as HTML by modern sniffing algorithms." },
|
|
9163
|
+
{ prefix: "font/", risk: "low", reason: "Font types are rarely reinterpreted as HTML by modern sniffing algorithms." },
|
|
9164
|
+
{ 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." },
|
|
9165
|
+
{ 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." },
|
|
9166
|
+
{ 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." },
|
|
9167
|
+
{ prefix: "application/xml", risk: "medium", reason: "XML can be reinterpreted in ways that overlap with HTML/XHTML parsing in some legacy sniffing paths." }
|
|
9168
|
+
];
|
|
9169
|
+
function _assessContentTypeRiskMcp(contentType) {
|
|
9170
|
+
const normalized = contentType.trim().toLowerCase().split(";")[0].trim();
|
|
9171
|
+
if (normalized in _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP) {
|
|
9172
|
+
const entry = _X_CONTENT_TYPE_EXACT_RISK_TABLE_MCP[normalized];
|
|
9173
|
+
return { contentType: contentType.trim() || "(empty)", risk: entry.risk, reason: entry.reason };
|
|
9174
|
+
}
|
|
9175
|
+
const prefixMatch = _X_CONTENT_TYPE_PREFIX_RISK_TABLE_MCP.find((e) => normalized.startsWith(e.prefix));
|
|
9176
|
+
if (prefixMatch) {
|
|
9177
|
+
return { contentType: contentType.trim(), risk: prefixMatch.risk, reason: prefixMatch.reason };
|
|
9178
|
+
}
|
|
9179
|
+
return {
|
|
9180
|
+
contentType: contentType.trim(),
|
|
9181
|
+
risk: "medium",
|
|
9182
|
+
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."
|
|
9183
|
+
};
|
|
9184
|
+
}
|
|
9185
|
+
server.tool(
|
|
9186
|
+
"build_x_content_type_options_header",
|
|
9187
|
+
'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.',
|
|
9188
|
+
{
|
|
9189
|
+
headerValue: z.string().optional().describe('A pasted header value or full header line to validate, e.g. "nosniff"'),
|
|
9190
|
+
contentType: z.string().optional().describe('A Content-Type value to assess MIME-sniffing risk for, e.g. "text/plain"')
|
|
9191
|
+
},
|
|
9192
|
+
async ({ headerValue, contentType }) => {
|
|
9193
|
+
if (typeof headerValue === "string") {
|
|
9194
|
+
const withoutLabel = headerValue.trim().replace(/^x-content-type-options\s*:\s*/i, "");
|
|
9195
|
+
const value = withoutLabel.trim();
|
|
9196
|
+
if (value === "") return errText({ error: 'No value found after "X-Content-Type-Options:"' });
|
|
9197
|
+
if (value.toLowerCase() === "nosniff") {
|
|
9198
|
+
return text(JSON.stringify({ raw: value, valid: true, message: "Valid. The browser will not MIME-sniff this response." }, null, 2));
|
|
9199
|
+
}
|
|
9200
|
+
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));
|
|
9201
|
+
}
|
|
9202
|
+
if (typeof contentType === "string") {
|
|
9203
|
+
return text(JSON.stringify(_assessContentTypeRiskMcp(contentType), null, 2));
|
|
9204
|
+
}
|
|
9205
|
+
return text(JSON.stringify({
|
|
9206
|
+
header: _X_CONTENT_TYPE_OPTIONS_HEADER_MCP,
|
|
9207
|
+
value: "nosniff",
|
|
9208
|
+
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.`
|
|
9209
|
+
}, null, 2));
|
|
9210
|
+
}
|
|
9211
|
+
);
|
|
9023
9212
|
var transport = new StdioServerTransport();
|
|
9024
9213
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server exposing
|
|
3
|
+
"version": "0.55.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",
|