@utilix-tech/mcp 0.76.0 → 0.78.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 +7 -0
- package/README.md +6 -3
- package/dist/index.js +262 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.78.0
|
|
6
|
+
- Add `ndjson_to_csv` tool: convert newline-delimited JSON (one flat object per line) into CSV, inferring the header row from the union of keys across all lines.
|
|
7
|
+
|
|
8
|
+
## 0.77.0
|
|
9
|
+
- Add `read_pdf_outline` tool: walk a PDF's /Outlines bookmark tree (the Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order.
|
|
10
|
+
- Add `score_security_headers` tool: score a pasted block of raw HTTP response headers against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*), with an overall 0-100 score and A-F letter grade.
|
|
11
|
+
|
|
5
12
|
## 0.76.0
|
|
6
13
|
- Add `detect_wordy_phrases` tool: scan text against a curated table of wordy/redundant phrases ("due to the fact that", "in order to") and flag each occurrence with a suggested concise replacement, an occurrence count, and character offsets.
|
|
7
14
|
- Add `check_oxford_comma_consistency` tool: scan a document for comma-before-and/or usage in three-or-more-item lists and flag documents that mix Oxford comma styles.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 250 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
|
|
|
@@ -213,7 +213,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
213
213
|
| `calculate_loan_prepayment_penalty` | Compute the dollar penalty cost of paying a loan off early, given its remaining payoff balance and a prepayment penalty structure: either a flat percent of the payoff balance, or a fixed number of months of interest at the loan's annual rate |
|
|
214
214
|
| `estimate_data_transfer_time` | Estimate the theoretical time to transfer a file of a given size over a connection of a given speed, or (given a target time instead) the bandwidth required to hit it |
|
|
215
215
|
|
|
216
|
-
### API / Network (
|
|
216
|
+
### API / Network (51 tools)
|
|
217
217
|
| Tool | Description |
|
|
218
218
|
|------|-------------|
|
|
219
219
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -268,6 +268,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
268
268
|
| `parse_robots_meta_tag` | Parse an existing robots meta tag (or bare content value) into the same shape |
|
|
269
269
|
| `calculate_cookie_size_budget` | Parse a Cookie header's name=value pairs and compute each cookie's and the total header's byte size against RFC 6265's 4096-byte per-cookie guarantee and a configurable budget |
|
|
270
270
|
| `verify_webhook_signature` | Recompute an HMAC over a raw webhook payload and compare it to a provided signature, accepting a `sha256=` prefix style and matching against hex or base64 encoding |
|
|
271
|
+
| `score_security_headers` | Score a pasted block of raw HTTP response headers against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*), with an overall 0-100 score and A-F letter grade |
|
|
271
272
|
|
|
272
273
|
### Color (4 tools)
|
|
273
274
|
| Tool | Description |
|
|
@@ -312,6 +313,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
312
313
|
| `parse_csv` | Parse a CSV string and return the data as JSON |
|
|
313
314
|
| `format_ndjson` | Validate and pretty-print newline-delimited JSON (NDJSON) |
|
|
314
315
|
| `dedupe_ndjson` | Remove duplicate records from NDJSON, by exact match or by a key field |
|
|
316
|
+
| `ndjson_to_csv` | Convert NDJSON into CSV, inferring the header row from the union of keys |
|
|
315
317
|
| `diff_env_example` | Diff a .env file against its .env.example template: flags keys missing from either side, plus empty placeholder values |
|
|
316
318
|
| `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
|
|
317
319
|
| `generate_markdown_toc` | Scan a Markdown document's ATX headings and build a nested table of contents with GitHub-style anchor slugs |
|
|
@@ -375,11 +377,12 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
375
377
|
| `read_ico_info` | Read how many images a .ico file embeds and each one's width, height, color depth, and size from its ICONDIR header |
|
|
376
378
|
| `inspect_gif_frames` | Walk a GIF87a/89a file's block structure to report frame count, total animation duration, and loop count, without decoding any LZW-compressed pixel data |
|
|
377
379
|
|
|
378
|
-
### PDF (
|
|
380
|
+
### PDF (3 tools)
|
|
379
381
|
| Tool | Description |
|
|
380
382
|
|------|-------------|
|
|
381
383
|
| `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
|
|
382
384
|
| `read_pdf_page_dimensions` | List each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree: no PDF rendering library required |
|
|
385
|
+
| `read_pdf_outline` | Walk a PDF's /Outlines bookmark tree (Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order: no PDF rendering library required |
|
|
383
386
|
|
|
384
387
|
### Audio (4 tools)
|
|
385
388
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -2702,6 +2702,61 @@ server.tool(
|
|
|
2702
2702
|
return text(JSON.stringify(result, null, 2));
|
|
2703
2703
|
}
|
|
2704
2704
|
);
|
|
2705
|
+
function _toCsvStringMcp(headers, rows) {
|
|
2706
|
+
const escapeField = (field) => {
|
|
2707
|
+
if (field.includes(",") || field.includes('"') || field.includes("\n") || field.includes("\r")) {
|
|
2708
|
+
return `"${field.replace(/"/g, '""')}"`;
|
|
2709
|
+
}
|
|
2710
|
+
return field;
|
|
2711
|
+
};
|
|
2712
|
+
const lines = [headers.map(escapeField).join(",")];
|
|
2713
|
+
for (const row of rows) lines.push(row.map(escapeField).join(","));
|
|
2714
|
+
return lines.join("\n");
|
|
2715
|
+
}
|
|
2716
|
+
function _stringifyCsvValueMcp(value) {
|
|
2717
|
+
if (value === void 0 || value === null) return "";
|
|
2718
|
+
if (typeof value === "string") return value;
|
|
2719
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
2720
|
+
return String(value);
|
|
2721
|
+
}
|
|
2722
|
+
function _ndjsonToCsvMcp(input) {
|
|
2723
|
+
const validated = _validateNdjson(input);
|
|
2724
|
+
if (!validated.valid) {
|
|
2725
|
+
const firstError = validated.lines.find((l) => !l.valid);
|
|
2726
|
+
return { error: firstError ? `Line ${firstError.line}: ${firstError.error}` : "Invalid NDJSON" };
|
|
2727
|
+
}
|
|
2728
|
+
if (validated.lines.length === 0) return { error: "No records found" };
|
|
2729
|
+
const records = validated.lines.map((l) => JSON.parse(l.raw));
|
|
2730
|
+
for (const record of records) {
|
|
2731
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) {
|
|
2732
|
+
return { error: "Every line must be a flat JSON object to convert to CSV" };
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const headers = [];
|
|
2736
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2737
|
+
for (const record of records) {
|
|
2738
|
+
for (const key of Object.keys(record)) {
|
|
2739
|
+
if (!seen.has(key)) {
|
|
2740
|
+
seen.add(key);
|
|
2741
|
+
headers.push(key);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
const rows = records.map((record) => headers.map((h) => _stringifyCsvValueMcp(record[h])));
|
|
2746
|
+
return { output: _toCsvStringMcp(headers, rows) };
|
|
2747
|
+
}
|
|
2748
|
+
server.tool(
|
|
2749
|
+
"ndjson_to_csv",
|
|
2750
|
+
"Convert NDJSON (newline-delimited JSON, one flat object per line) into CSV, inferring the header row from the union of keys across every line.",
|
|
2751
|
+
{
|
|
2752
|
+
input: z.string().describe("NDJSON content: one flat JSON object per line")
|
|
2753
|
+
},
|
|
2754
|
+
async ({ input }) => {
|
|
2755
|
+
const result = _ndjsonToCsvMcp(input);
|
|
2756
|
+
if ("error" in result) return errText(result);
|
|
2757
|
+
return text(result.output);
|
|
2758
|
+
}
|
|
2759
|
+
);
|
|
2705
2760
|
function _decodePdfString(raw, isHex) {
|
|
2706
2761
|
const bytes = [];
|
|
2707
2762
|
if (isHex) {
|
|
@@ -2972,6 +3027,84 @@ server.tool("read_pdf_page_dimensions", "List each page's MediaBox width/height
|
|
|
2972
3027
|
if ("error" in result) return errText(result);
|
|
2973
3028
|
return text(JSON.stringify(result, null, 2));
|
|
2974
3029
|
});
|
|
3030
|
+
function _parsePdfOutlineDest(dict) {
|
|
3031
|
+
const directRef = dict.match(/\/Dest\s*\[\s*(\d+)\s+(\d+)\s+R/);
|
|
3032
|
+
if (directRef) return { destType: "page", target: `${directRef[1]} ${directRef[2]} R` };
|
|
3033
|
+
const directName = dict.match(/\/Dest\s*\/([^\s/()<>[\]]+)/);
|
|
3034
|
+
if (directName) return { destType: "named", target: directName[1] };
|
|
3035
|
+
const directString = dict.match(/\/Dest\s*\(((?:\\.|[^\\()])*)\)/);
|
|
3036
|
+
if (directString) return { destType: "named", target: _decodePdfString(directString[1], false) };
|
|
3037
|
+
const actionUri = dict.match(/\/S\s*\/URI[\s\S]*?\/URI\s*\(((?:\\.|[^\\()])*)\)/);
|
|
3038
|
+
if (actionUri) return { destType: "uri", target: _decodePdfString(actionUri[1], false) };
|
|
3039
|
+
const actionGotoRef = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\[\s*(\d+)\s+(\d+)\s+R/);
|
|
3040
|
+
if (actionGotoRef) return { destType: "page", target: `${actionGotoRef[1]} ${actionGotoRef[2]} R` };
|
|
3041
|
+
const actionGotoName = dict.match(/\/S\s*\/GoTo[\s\S]*?\/D\s*\/([^\s/()<>[\]]+)/);
|
|
3042
|
+
if (actionGotoName) return { destType: "named", target: actionGotoName[1] };
|
|
3043
|
+
return { destType: "none", target: null };
|
|
3044
|
+
}
|
|
3045
|
+
function _walkPdfOutlineChain(objMap, startRef, level, visited, out) {
|
|
3046
|
+
let ref = startRef;
|
|
3047
|
+
while (ref) {
|
|
3048
|
+
if (visited.has(ref)) break;
|
|
3049
|
+
visited.add(ref);
|
|
3050
|
+
const body = objMap.get(ref);
|
|
3051
|
+
if (!body) break;
|
|
3052
|
+
const titleRaw = _extractPdfField(body, "Title");
|
|
3053
|
+
const { destType, target } = _parsePdfOutlineDest(body);
|
|
3054
|
+
out.push({ title: titleRaw ?? "(untitled)", level, destType, target });
|
|
3055
|
+
const firstMatch = body.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
|
|
3056
|
+
if (firstMatch) _walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, level + 1, visited, out);
|
|
3057
|
+
const nextMatch = body.match(/\/Next\s+(\d+)\s+(\d+)\s+R/);
|
|
3058
|
+
ref = nextMatch ? `${nextMatch[1]} ${nextMatch[2]}` : null;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
function _readPdfOutline(bytes) {
|
|
3062
|
+
if (bytes.length < 8) return { error: "File too small to be a PDF" };
|
|
3063
|
+
const latin1 = (b) => {
|
|
3064
|
+
let out2 = "";
|
|
3065
|
+
const CHUNK = 32768;
|
|
3066
|
+
for (let i = 0; i < b.length; i += CHUNK) out2 += String.fromCharCode(...b.subarray(i, i + CHUNK));
|
|
3067
|
+
return out2;
|
|
3068
|
+
};
|
|
3069
|
+
const header = latin1(bytes.subarray(0, 16));
|
|
3070
|
+
const versionMatch = header.match(/%PDF-(\d\.\d)/);
|
|
3071
|
+
if (!versionMatch) return { error: "Not a valid PDF file (missing %PDF header)" };
|
|
3072
|
+
const version = versionMatch[1];
|
|
3073
|
+
const text2 = latin1(bytes);
|
|
3074
|
+
const objBlocks = [...text2.matchAll(/(\d+)\s+(\d+)\s+obj([\s\S]*?)endobj/g)];
|
|
3075
|
+
const objMap = /* @__PURE__ */ new Map();
|
|
3076
|
+
for (const b of objBlocks) objMap.set(`${b[1]} ${b[2]}`, b[3]);
|
|
3077
|
+
let outlinesBody = null;
|
|
3078
|
+
const trailerMatches = [...text2.matchAll(/trailer\s*<<([\s\S]*?)>>/g)];
|
|
3079
|
+
if (trailerMatches.length > 0) {
|
|
3080
|
+
const trailer = trailerMatches[trailerMatches.length - 1][1];
|
|
3081
|
+
const rootRef = trailer.match(/\/Root\s+(\d+)\s+(\d+)\s+R/);
|
|
3082
|
+
if (rootRef) {
|
|
3083
|
+
const catalogBody = objMap.get(`${rootRef[1]} ${rootRef[2]}`);
|
|
3084
|
+
const outlinesRef = catalogBody?.match(/\/Outlines\s+(\d+)\s+(\d+)\s+R/);
|
|
3085
|
+
if (outlinesRef) outlinesBody = objMap.get(`${outlinesRef[1]} ${outlinesRef[2]}`) ?? null;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (!outlinesBody) {
|
|
3089
|
+
for (const b of objBlocks) {
|
|
3090
|
+
if (/\/Type\s*\/Outlines\b/.test(b[3])) {
|
|
3091
|
+
outlinesBody = b[3];
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
if (!outlinesBody) return { error: "No /Outlines bookmark tree found in this PDF" };
|
|
3097
|
+
const out = [];
|
|
3098
|
+
const firstMatch = outlinesBody.match(/\/First\s+(\d+)\s+(\d+)\s+R/);
|
|
3099
|
+
if (firstMatch) _walkPdfOutlineChain(objMap, `${firstMatch[1]} ${firstMatch[2]}`, 0, /* @__PURE__ */ new Set(), out);
|
|
3100
|
+
return { version, bookmarkCount: out.length, bookmarks: out };
|
|
3101
|
+
}
|
|
3102
|
+
server.tool("read_pdf_outline", "Walk a PDF's /Outlines bookmark tree (Root -> Outlines -> First/Next/First chain) and list every bookmark's title, nesting level, and link target, in document order: no PDF rendering library required. Does not resolve a page-ref target to a page number.", { pdf_base64: z.string().describe("Base64-encoded PDF bytes, optionally prefixed with a data: URL scheme") }, async ({ pdf_base64 }) => {
|
|
3103
|
+
const bytes = new Uint8Array(Buffer.from(pdf_base64.replace(/^data:.*;base64,/, ""), "base64"));
|
|
3104
|
+
const result = _readPdfOutline(bytes);
|
|
3105
|
+
if ("error" in result) return errText(result);
|
|
3106
|
+
return text(JSON.stringify(result, null, 2));
|
|
3107
|
+
});
|
|
2975
3108
|
var _EXIF_ORIENTATION_LABELS = {
|
|
2976
3109
|
1: "Normal",
|
|
2977
3110
|
2: "Mirrored horizontal",
|
|
@@ -11731,6 +11864,135 @@ server.tool(
|
|
|
11731
11864
|
return text(JSON.stringify(result, null, 2));
|
|
11732
11865
|
}
|
|
11733
11866
|
);
|
|
11867
|
+
function _parseRawHeaderBlockMcp(raw) {
|
|
11868
|
+
const map = /* @__PURE__ */ new Map();
|
|
11869
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
11870
|
+
const trimmed = line.trim();
|
|
11871
|
+
if (!trimmed) continue;
|
|
11872
|
+
const idx = trimmed.indexOf(":");
|
|
11873
|
+
if (idx === -1) continue;
|
|
11874
|
+
const name = trimmed.slice(0, idx).trim().toLowerCase();
|
|
11875
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
11876
|
+
if (!map.has(name)) map.set(name, value);
|
|
11877
|
+
}
|
|
11878
|
+
return map;
|
|
11879
|
+
}
|
|
11880
|
+
var _SECURITY_HEADER_CHECKS_MCP = [
|
|
11881
|
+
{
|
|
11882
|
+
header: "Content-Security-Policy",
|
|
11883
|
+
evaluate: (v) => {
|
|
11884
|
+
if (!v) return { status: "missing", note: "Without a CSP, an injected script runs with full page privileges." };
|
|
11885
|
+
if (/unsafe-inline|unsafe-eval/i.test(v)) return { status: "warning", note: "Contains unsafe-inline or unsafe-eval, which weakens XSS protection." };
|
|
11886
|
+
return { status: "pass", note: "Present. Use the CSP Header Builder tool to validate its directives in depth." };
|
|
11887
|
+
}
|
|
11888
|
+
},
|
|
11889
|
+
{
|
|
11890
|
+
header: "Strict-Transport-Security",
|
|
11891
|
+
evaluate: (v) => {
|
|
11892
|
+
if (!v) return { status: "missing", note: "Without HSTS, a user's first visit (and any later downgrade) can be sent over plain HTTP." };
|
|
11893
|
+
const maxAgeMatch = v.match(/max-age\s*=\s*(\d+)/i);
|
|
11894
|
+
if (!maxAgeMatch) return { status: "warning", note: "Present but missing a max-age directive." };
|
|
11895
|
+
const maxAge = Number(maxAgeMatch[1]);
|
|
11896
|
+
if (maxAge < 31536e3) return { status: "warning", note: "max-age is below the recommended 1 year (31536000 seconds)." };
|
|
11897
|
+
return { status: "pass", note: "Present with a max-age of at least 1 year." };
|
|
11898
|
+
}
|
|
11899
|
+
},
|
|
11900
|
+
{
|
|
11901
|
+
header: "X-Content-Type-Options",
|
|
11902
|
+
evaluate: (v) => {
|
|
11903
|
+
if (!v) return { status: "missing", note: "Without this header, browsers may MIME-sniff a response into an unintended content type." };
|
|
11904
|
+
if (v.trim().toLowerCase() !== "nosniff") return { status: "warning", note: `"nosniff" is the only valid value; this value won't take effect.` };
|
|
11905
|
+
return { status: "pass", note: "Set to the only valid value, nosniff." };
|
|
11906
|
+
}
|
|
11907
|
+
},
|
|
11908
|
+
{
|
|
11909
|
+
header: "X-Frame-Options",
|
|
11910
|
+
evaluate: (v) => {
|
|
11911
|
+
if (!v) return { status: "missing", note: "Without this (or a CSP frame-ancestors directive), the page can be framed by another site for clickjacking." };
|
|
11912
|
+
const upper = v.trim().toUpperCase();
|
|
11913
|
+
if (upper === "DENY" || upper === "SAMEORIGIN") return { status: "pass", note: `Framing restricted (${upper}).` };
|
|
11914
|
+
if (upper.startsWith("ALLOW-FROM")) return { status: "warning", note: "ALLOW-FROM is deprecated and ignored by most modern browsers." };
|
|
11915
|
+
return { status: "warning", note: "Unrecognized value." };
|
|
11916
|
+
}
|
|
11917
|
+
},
|
|
11918
|
+
{
|
|
11919
|
+
header: "Referrer-Policy",
|
|
11920
|
+
evaluate: (v) => {
|
|
11921
|
+
if (!v) return { status: "missing", note: "Without this header, the browser's default referrer behavior applies, which can leak full URLs to third parties." };
|
|
11922
|
+
if (v.trim().toLowerCase() === "unsafe-url") return { status: "warning", note: "unsafe-url leaks the full URL, including path and query string, to every destination." };
|
|
11923
|
+
return { status: "pass", note: "Present." };
|
|
11924
|
+
}
|
|
11925
|
+
},
|
|
11926
|
+
{
|
|
11927
|
+
header: "Permissions-Policy",
|
|
11928
|
+
evaluate: (v) => {
|
|
11929
|
+
if (!v) return { status: "missing", note: "Without this header, embedded content can use powerful browser features (camera, geolocation, etc.) unless restricted another way." };
|
|
11930
|
+
return { status: "pass", note: "Present." };
|
|
11931
|
+
}
|
|
11932
|
+
},
|
|
11933
|
+
{
|
|
11934
|
+
header: "Cross-Origin-Opener-Policy",
|
|
11935
|
+
evaluate: (v) => {
|
|
11936
|
+
if (!v) return { status: "missing", note: "Without COOP, this page can be put in the same browsing context group as a malicious opener window." };
|
|
11937
|
+
const lower = v.trim().toLowerCase();
|
|
11938
|
+
if (lower === "same-origin") return { status: "pass", note: "Set to same-origin, the strongest isolation value." };
|
|
11939
|
+
return { status: "warning", note: `Set to "${v.trim()}", which is weaker than same-origin.` };
|
|
11940
|
+
}
|
|
11941
|
+
},
|
|
11942
|
+
{
|
|
11943
|
+
header: "Cross-Origin-Embedder-Policy",
|
|
11944
|
+
evaluate: (v) => {
|
|
11945
|
+
if (!v) return { status: "missing", note: "Required alongside COOP for cross-origin isolation (e.g. to use SharedArrayBuffer)." };
|
|
11946
|
+
const lower = v.trim().toLowerCase();
|
|
11947
|
+
if (lower === "require-corp" || lower === "credentialless") return { status: "pass", note: `Set to ${lower}.` };
|
|
11948
|
+
return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
|
|
11949
|
+
}
|
|
11950
|
+
},
|
|
11951
|
+
{
|
|
11952
|
+
header: "Cross-Origin-Resource-Policy",
|
|
11953
|
+
evaluate: (v) => {
|
|
11954
|
+
if (!v) return { status: "missing", note: "Without CORP, this resource can be loaded cross-origin by any site that knows its URL." };
|
|
11955
|
+
const lower = v.trim().toLowerCase();
|
|
11956
|
+
if (lower === "same-origin" || lower === "same-site") return { status: "pass", note: `Set to ${lower}.` };
|
|
11957
|
+
if (lower === "cross-origin") return { status: "warning", note: "Set to cross-origin, which allows any site to load this resource." };
|
|
11958
|
+
return { status: "warning", note: `Unrecognized value "${v.trim()}".` };
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
];
|
|
11962
|
+
function _scoreSecurityHeadersMcp(rawHeaders) {
|
|
11963
|
+
if (typeof rawHeaders !== "string" || !rawHeaders.trim()) {
|
|
11964
|
+
return { error: 'Paste one or more response headers, one per line in "Name: value" form' };
|
|
11965
|
+
}
|
|
11966
|
+
const headerMap = _parseRawHeaderBlockMcp(rawHeaders);
|
|
11967
|
+
if (headerMap.size === 0) {
|
|
11968
|
+
return { error: 'Could not parse any "Name: value" header lines from the input' };
|
|
11969
|
+
}
|
|
11970
|
+
const checks = _SECURITY_HEADER_CHECKS_MCP.map((def) => {
|
|
11971
|
+
const value = headerMap.get(def.header.toLowerCase()) ?? null;
|
|
11972
|
+
const { status, note } = def.evaluate(value);
|
|
11973
|
+
return { header: def.header, status, value, note };
|
|
11974
|
+
});
|
|
11975
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
11976
|
+
const warningCount = checks.filter((c) => c.status === "warning").length;
|
|
11977
|
+
const missingCount = checks.filter((c) => c.status === "missing").length;
|
|
11978
|
+
const totalChecks = checks.length;
|
|
11979
|
+
const points = passCount + warningCount * 0.5;
|
|
11980
|
+
const score = Math.round(points / totalChecks * 100);
|
|
11981
|
+
const grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F";
|
|
11982
|
+
return { checks, score, grade, passCount, warningCount, missingCount, totalChecks };
|
|
11983
|
+
}
|
|
11984
|
+
server.tool(
|
|
11985
|
+
"score_security_headers",
|
|
11986
|
+
'Parse a pasted block of raw HTTP response headers (one "Name: value" pair per line) and score it against commonly recommended security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and the three Cross-Origin-* isolation headers), with an overall 0-100 score and A-F letter grade. Evaluates the header text you give it, not a live request.',
|
|
11987
|
+
{
|
|
11988
|
+
headers: z.string().describe('Raw response headers, one "Name: value" pair per line')
|
|
11989
|
+
},
|
|
11990
|
+
async ({ headers }) => {
|
|
11991
|
+
const result = _scoreSecurityHeadersMcp(headers);
|
|
11992
|
+
if ("error" in result) return errText(result);
|
|
11993
|
+
return text(JSON.stringify(result, null, 2));
|
|
11994
|
+
}
|
|
11995
|
+
);
|
|
11734
11996
|
function _roundMoneyPrepaymentPenaltyMcp(n) {
|
|
11735
11997
|
return Math.round(n * 100) / 100;
|
|
11736
11998
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilix-tech/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
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",
|