@utilix-tech/mcp 0.92.0 → 0.94.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 +7 -3
- package/dist/index.js +358 -0
- package/package.json +2 -2
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.94.0
|
|
6
|
+
- Add `build_alt_svc_header`/`parse_alt_svc_header` tools: build an `Alt-Svc` response header (RFC 7838) from one or more alternative services with a plain-English note per protocol-id, flagging outdated HTTP/3 draft protocol-ids, or parse an existing header back into its alternatives, including the special `clear` value.
|
|
7
|
+
|
|
8
|
+
## 0.93.0
|
|
9
|
+
- Add `find_duplicate_lockfile_versions` tool: parse an npm, yarn, or pnpm lockfile and report every package installed at more than one resolved version, with a count of install locations per version.
|
|
10
|
+
- Add `summarize_git_diff_stat` tool: parse `git diff --stat` / `git log --stat` output into structured per-file insertion/deletion counts plus overall totals, flagging a scaled-down bar's split as approximate.
|
|
11
|
+
|
|
5
12
|
## 0.92.0
|
|
6
13
|
- Add `build_clear_site_data_header`/`parse_clear_site_data_header` tools: build a `Clear-Site-Data` response header from directives (cache, cookies, storage, executionContexts, or `*` for all of them), flagging redundant directives alongside the wildcard, or parse an existing header back into its directives.
|
|
7
14
|
- Add `summarize_csv_column_stats` tool: parse CSV/TSV data and compute per-column null/unique counts plus min/max/mean/median/standard deviation for numeric columns, or the most common value for categorical ones.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 281 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
|
|
|
@@ -224,7 +224,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
224
224
|
| `calculate_sales_commission` | Compute a sales commission from either a flat percent rate applied to the whole sale amount, or a tiered (marginal) rate structure where each tier applies only to the slice of sales within it, plus an optional base salary added to the total payout |
|
|
225
225
|
| `intersect_semver_ranges` | Determine whether two semver ranges overlap and, if so, the intersecting range, supporting the same caret/tilde/comparison-operator/exact-version syntax as the semver tool |
|
|
226
226
|
|
|
227
|
-
### API / Network (
|
|
227
|
+
### API / Network (55 tools)
|
|
228
228
|
| Tool | Description |
|
|
229
229
|
|------|-------------|
|
|
230
230
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -283,6 +283,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
283
283
|
| `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 |
|
|
284
284
|
| `build_clear_site_data_header` | Build a Clear-Site-Data response header from directives (cache, cookies, storage, executionContexts, or * for all of them), flagging redundant directives alongside the wildcard |
|
|
285
285
|
| `parse_clear_site_data_header` | Parse an existing Clear-Site-Data header value into its individual directives |
|
|
286
|
+
| `build_alt_svc_header` | Build an Alt-Svc response header (RFC 7838) from one or more alternative services (protocol-id, authority, max-age, persist), with a plain-English note per protocol-id |
|
|
287
|
+
| `parse_alt_svc_header` | Parse an existing Alt-Svc header value into its individual alternatives, including the special "clear" value |
|
|
286
288
|
|
|
287
289
|
### Color (4 tools)
|
|
288
290
|
| Tool | Description |
|
|
@@ -292,7 +294,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
292
294
|
| `color_palette` | Generate a color palette from a base color |
|
|
293
295
|
| `color_shades` | Generate shades or tints from a base color |
|
|
294
296
|
|
|
295
|
-
### Code (
|
|
297
|
+
### Code (29 tools)
|
|
296
298
|
| Tool | Description |
|
|
297
299
|
|------|-------------|
|
|
298
300
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -323,6 +325,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
323
325
|
| `count_sql_placeholders` | Count positional (`?`, `$1`) and named (`:name`, `@name`) parameter placeholders in a SQL query, flagging mixed styles and numbered-sequence gaps or duplicates |
|
|
324
326
|
| `extract_markdown_link_references` | Extract every reference-style Markdown link ([text][ref], [text][], ![alt][ref]) and match it against its [ref]: url definition, flagging unresolved references and unused definitions |
|
|
325
327
|
| `build_character_class` | Assemble a [...] regex character class from named building blocks (lowercase, uppercase, digits, whitespace), custom ranges, and literal custom characters, with optional negation |
|
|
328
|
+
| `find_duplicate_lockfile_versions` | Parse an npm, yarn, or pnpm lockfile and report every package installed at more than one resolved version, with a count of install locations per version |
|
|
329
|
+
| `summarize_git_diff_stat` | Parse `git diff --stat` / `git log --stat` output into structured per-file insertion/deletion counts plus overall totals |
|
|
326
330
|
|
|
327
331
|
### Data (21 tools)
|
|
328
332
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -14732,5 +14732,363 @@ server.tool(
|
|
|
14732
14732
|
return text(JSON.stringify(result, null, 2));
|
|
14733
14733
|
}
|
|
14734
14734
|
);
|
|
14735
|
+
function _detectLockfileFormatMcp(input) {
|
|
14736
|
+
const trimmed = input.trim();
|
|
14737
|
+
if (trimmed.startsWith("{")) return "npm";
|
|
14738
|
+
if (/^lockfileVersion:/m.test(trimmed) || /^packages:\s*$/m.test(trimmed)) return "pnpm";
|
|
14739
|
+
if (/yarn lockfile v\d/i.test(trimmed)) return "yarn";
|
|
14740
|
+
if (/^\s*version\s+"[^"]+"/m.test(trimmed) && /@[^\s",]+["']?\s*:?\s*$/m.test(trimmed)) return "yarn";
|
|
14741
|
+
return null;
|
|
14742
|
+
}
|
|
14743
|
+
function _parseNpmLockfileMcp(input) {
|
|
14744
|
+
const data = JSON.parse(input);
|
|
14745
|
+
const entries = [];
|
|
14746
|
+
const packages = data.packages;
|
|
14747
|
+
if (packages && typeof packages === "object" && !Array.isArray(packages)) {
|
|
14748
|
+
for (const [path, metaRaw] of Object.entries(packages)) {
|
|
14749
|
+
if (path === "" || !metaRaw || typeof metaRaw !== "object") continue;
|
|
14750
|
+
const meta = metaRaw;
|
|
14751
|
+
if (typeof meta.version !== "string") continue;
|
|
14752
|
+
const idx = path.lastIndexOf("node_modules/");
|
|
14753
|
+
const name = idx === -1 ? path : path.slice(idx + "node_modules/".length);
|
|
14754
|
+
entries.push({ name, version: meta.version });
|
|
14755
|
+
}
|
|
14756
|
+
return entries;
|
|
14757
|
+
}
|
|
14758
|
+
const dependencies = data.dependencies;
|
|
14759
|
+
if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
|
|
14760
|
+
const walk = (deps) => {
|
|
14761
|
+
for (const [name, metaRaw] of Object.entries(deps)) {
|
|
14762
|
+
if (!metaRaw || typeof metaRaw !== "object") continue;
|
|
14763
|
+
const meta = metaRaw;
|
|
14764
|
+
if (typeof meta.version === "string") entries.push({ name, version: meta.version });
|
|
14765
|
+
if (meta.dependencies && typeof meta.dependencies === "object") walk(meta.dependencies);
|
|
14766
|
+
}
|
|
14767
|
+
};
|
|
14768
|
+
walk(dependencies);
|
|
14769
|
+
}
|
|
14770
|
+
return entries;
|
|
14771
|
+
}
|
|
14772
|
+
function _splitYarnKeyMcp(key) {
|
|
14773
|
+
const trimmedKey = key.trim().replace(/^"|"$/g, "");
|
|
14774
|
+
const idx = trimmedKey.lastIndexOf("@");
|
|
14775
|
+
if (idx <= 0) return null;
|
|
14776
|
+
return { name: trimmedKey.slice(0, idx), range: trimmedKey.slice(idx + 1) };
|
|
14777
|
+
}
|
|
14778
|
+
function _parseYarnLockfileMcp(input) {
|
|
14779
|
+
const entries = [];
|
|
14780
|
+
const blocks = input.replace(/\r\n/g, "\n").split(/\n\n+/);
|
|
14781
|
+
for (const block of blocks) {
|
|
14782
|
+
const lines = block.split("\n");
|
|
14783
|
+
const headerLines = [];
|
|
14784
|
+
let i = 0;
|
|
14785
|
+
while (i < lines.length && lines[i].trim() !== "" && !lines[i].startsWith(" ") && !lines[i].startsWith("#")) {
|
|
14786
|
+
headerLines.push(lines[i]);
|
|
14787
|
+
i++;
|
|
14788
|
+
}
|
|
14789
|
+
if (headerLines.length === 0) continue;
|
|
14790
|
+
const header = headerLines.join(" ").replace(/:\s*$/, "");
|
|
14791
|
+
const keys = header.split(",").map((k) => k.trim()).filter(Boolean);
|
|
14792
|
+
const versionLine = lines.find((l) => /^\s+version\s+"/.test(l));
|
|
14793
|
+
if (!versionLine) continue;
|
|
14794
|
+
const versionMatch = versionLine.match(/version\s+"([^"]+)"/);
|
|
14795
|
+
if (!versionMatch) continue;
|
|
14796
|
+
const version = versionMatch[1];
|
|
14797
|
+
const names = /* @__PURE__ */ new Set();
|
|
14798
|
+
for (const key of keys) {
|
|
14799
|
+
const split = _splitYarnKeyMcp(key);
|
|
14800
|
+
if (split) names.add(split.name);
|
|
14801
|
+
}
|
|
14802
|
+
for (const name of names) entries.push({ name, version });
|
|
14803
|
+
}
|
|
14804
|
+
return entries;
|
|
14805
|
+
}
|
|
14806
|
+
function _parsePnpmPackageKeyMcp(rawKey) {
|
|
14807
|
+
let key = rawKey.trim();
|
|
14808
|
+
if (key.startsWith("/")) key = key.slice(1);
|
|
14809
|
+
const parenIdx = key.indexOf("(");
|
|
14810
|
+
if (parenIdx !== -1) key = key.slice(0, parenIdx);
|
|
14811
|
+
const idx = key.lastIndexOf("@");
|
|
14812
|
+
if (idx <= 0) return null;
|
|
14813
|
+
const name = key.slice(0, idx);
|
|
14814
|
+
const version = key.slice(idx + 1).trim();
|
|
14815
|
+
if (!version) return null;
|
|
14816
|
+
return { name, version };
|
|
14817
|
+
}
|
|
14818
|
+
function _parsePnpmLockfileMcp(input) {
|
|
14819
|
+
const data = yaml.load(input);
|
|
14820
|
+
const entries = [];
|
|
14821
|
+
const packages = data?.packages;
|
|
14822
|
+
if (!packages || typeof packages !== "object" || Array.isArray(packages)) return entries;
|
|
14823
|
+
for (const rawKey of Object.keys(packages)) {
|
|
14824
|
+
const parsed = _parsePnpmPackageKeyMcp(rawKey);
|
|
14825
|
+
if (parsed) entries.push(parsed);
|
|
14826
|
+
}
|
|
14827
|
+
return entries;
|
|
14828
|
+
}
|
|
14829
|
+
function _findDuplicateLockfileVersionsMcp(input, format) {
|
|
14830
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
14831
|
+
const detected = format ?? _detectLockfileFormatMcp(input);
|
|
14832
|
+
if (!detected) {
|
|
14833
|
+
return { error: "Could not detect lockfile format; expected an npm package-lock.json, a yarn.lock, or a pnpm-lock.yaml" };
|
|
14834
|
+
}
|
|
14835
|
+
let rawEntries;
|
|
14836
|
+
try {
|
|
14837
|
+
if (detected === "npm") rawEntries = _parseNpmLockfileMcp(input);
|
|
14838
|
+
else if (detected === "yarn") rawEntries = _parseYarnLockfileMcp(input);
|
|
14839
|
+
else rawEntries = _parsePnpmLockfileMcp(input);
|
|
14840
|
+
} catch (e) {
|
|
14841
|
+
return { error: `Failed to parse ${detected} lockfile: ${e.message}` };
|
|
14842
|
+
}
|
|
14843
|
+
if (rawEntries.length === 0) {
|
|
14844
|
+
return { error: `No packages found in ${detected} lockfile. If this is a pnpm lockfile older than v6 (slash-separated versions instead of "name@version"), re-generate it with a current pnpm version first.` };
|
|
14845
|
+
}
|
|
14846
|
+
const byName = /* @__PURE__ */ new Map();
|
|
14847
|
+
for (const { name, version } of rawEntries) {
|
|
14848
|
+
let versions = byName.get(name);
|
|
14849
|
+
if (!versions) {
|
|
14850
|
+
versions = /* @__PURE__ */ new Map();
|
|
14851
|
+
byName.set(name, versions);
|
|
14852
|
+
}
|
|
14853
|
+
versions.set(version, (versions.get(version) ?? 0) + 1);
|
|
14854
|
+
}
|
|
14855
|
+
const packages = [];
|
|
14856
|
+
for (const [name, versions] of byName) {
|
|
14857
|
+
const versionList = [...versions.entries()].map(([version, count]) => ({ version, count })).sort((a, b) => b.count - a.count || a.version.localeCompare(b.version));
|
|
14858
|
+
packages.push({ name, versions: versionList, isDuplicate: versionList.length > 1 });
|
|
14859
|
+
}
|
|
14860
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
14861
|
+
const duplicatePackages = packages.filter((p) => p.isDuplicate);
|
|
14862
|
+
return { format: detected, totalPackages: packages.length, duplicatePackages, packages };
|
|
14863
|
+
}
|
|
14864
|
+
server.tool(
|
|
14865
|
+
"find_duplicate_lockfile_versions",
|
|
14866
|
+
"Parse an npm (package-lock.json), yarn (yarn.lock), or pnpm (pnpm-lock.yaml) lockfile and report every package installed at more than one resolved version, with a count of how many install locations resolve to each version. Format is auto-detected from content unless given explicitly.",
|
|
14867
|
+
{
|
|
14868
|
+
lockfile: z.string().min(1).describe("The full lockfile contents"),
|
|
14869
|
+
format: z.enum(["npm", "yarn", "pnpm"]).optional().describe("Force a format instead of auto-detecting it from content")
|
|
14870
|
+
},
|
|
14871
|
+
async ({ lockfile, format }) => {
|
|
14872
|
+
const result = _findDuplicateLockfileVersionsMcp(lockfile, format);
|
|
14873
|
+
if ("error" in result) return errText(result);
|
|
14874
|
+
return text(JSON.stringify(result, null, 2));
|
|
14875
|
+
}
|
|
14876
|
+
);
|
|
14877
|
+
var _GIT_DIFF_STAT_FILE_RE_MCP = /^\s*(.+?)\s*\|\s*(Bin|\d+)\s*(.*)$/;
|
|
14878
|
+
var _GIT_DIFF_STAT_SUMMARY_RE_MCP = /^\s*(\d+)\s+files?\s+changed(?:,\s*(\d+)\s+insertions?\(\+\))?(?:,\s*(\d+)\s+deletions?\(-\))?/;
|
|
14879
|
+
function _summarizeGitDiffStatMcp(input) {
|
|
14880
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
14881
|
+
const lines = input.replace(/\r\n/g, "\n").split("\n");
|
|
14882
|
+
const files = [];
|
|
14883
|
+
let summaryMatch = null;
|
|
14884
|
+
for (const line of lines) {
|
|
14885
|
+
if (!line.trim()) continue;
|
|
14886
|
+
const sm = line.match(_GIT_DIFF_STAT_SUMMARY_RE_MCP);
|
|
14887
|
+
if (sm) {
|
|
14888
|
+
summaryMatch = sm;
|
|
14889
|
+
continue;
|
|
14890
|
+
}
|
|
14891
|
+
const m = line.match(_GIT_DIFF_STAT_FILE_RE_MCP);
|
|
14892
|
+
if (!m) continue;
|
|
14893
|
+
const rawName = m[1];
|
|
14894
|
+
const countRaw = m[2];
|
|
14895
|
+
const bar = m[3];
|
|
14896
|
+
let file = rawName;
|
|
14897
|
+
let oldFile;
|
|
14898
|
+
const renameMatch = rawName.match(/^(.*?)\s*=>\s*(.*)$/);
|
|
14899
|
+
if (renameMatch) {
|
|
14900
|
+
oldFile = renameMatch[1].trim();
|
|
14901
|
+
file = renameMatch[2].trim();
|
|
14902
|
+
}
|
|
14903
|
+
if (countRaw === "Bin") {
|
|
14904
|
+
files.push({ file, oldFile, changes: null, insertions: null, deletions: null, binary: true, approximate: false });
|
|
14905
|
+
continue;
|
|
14906
|
+
}
|
|
14907
|
+
const changes = parseInt(countRaw, 10);
|
|
14908
|
+
const plusCount = (bar.match(/\+/g) ?? []).length;
|
|
14909
|
+
const minusCount = (bar.match(/-/g) ?? []).length;
|
|
14910
|
+
const barTotal = plusCount + minusCount;
|
|
14911
|
+
let insertions;
|
|
14912
|
+
let deletions;
|
|
14913
|
+
let approximate = false;
|
|
14914
|
+
if (barTotal === changes) {
|
|
14915
|
+
insertions = plusCount;
|
|
14916
|
+
deletions = minusCount;
|
|
14917
|
+
} else if (barTotal > 0) {
|
|
14918
|
+
approximate = true;
|
|
14919
|
+
insertions = Math.round(changes * plusCount / barTotal);
|
|
14920
|
+
deletions = changes - insertions;
|
|
14921
|
+
} else if (changes === 0) {
|
|
14922
|
+
insertions = 0;
|
|
14923
|
+
deletions = 0;
|
|
14924
|
+
} else {
|
|
14925
|
+
approximate = true;
|
|
14926
|
+
insertions = null;
|
|
14927
|
+
deletions = null;
|
|
14928
|
+
}
|
|
14929
|
+
files.push({ file, oldFile, changes, insertions, deletions, binary: false, approximate });
|
|
14930
|
+
}
|
|
14931
|
+
if (files.length === 0) {
|
|
14932
|
+
return { error: "No file stat lines found; expected output from `git diff --stat` or `git log --stat`" };
|
|
14933
|
+
}
|
|
14934
|
+
let filesChanged = files.length;
|
|
14935
|
+
let totalInsertions = files.reduce((sum, f) => sum + (f.insertions ?? 0), 0);
|
|
14936
|
+
let totalDeletions = files.reduce((sum, f) => sum + (f.deletions ?? 0), 0);
|
|
14937
|
+
if (summaryMatch) {
|
|
14938
|
+
filesChanged = parseInt(summaryMatch[1], 10);
|
|
14939
|
+
if (summaryMatch[2]) totalInsertions = parseInt(summaryMatch[2], 10);
|
|
14940
|
+
if (summaryMatch[3]) totalDeletions = parseInt(summaryMatch[3], 10);
|
|
14941
|
+
}
|
|
14942
|
+
return { files, filesChanged, totalInsertions, totalDeletions };
|
|
14943
|
+
}
|
|
14944
|
+
server.tool(
|
|
14945
|
+
"summarize_git_diff_stat",
|
|
14946
|
+
"Parse the per-file lines and trailing summary line of `git diff --stat` or `git log --stat` output into structured per-file insertion/deletion counts plus overall totals. Once a diff is wide enough that git scales the bar down, the per-file split is a proportional estimate (flagged via approximate) rather than an exact count, though the overall totals stay exact.",
|
|
14947
|
+
{
|
|
14948
|
+
diffStat: z.string().min(1).describe("Output of `git diff --stat` or `git log --stat`")
|
|
14949
|
+
},
|
|
14950
|
+
async ({ diffStat }) => {
|
|
14951
|
+
const result = _summarizeGitDiffStatMcp(diffStat);
|
|
14952
|
+
if ("error" in result) return errText(result);
|
|
14953
|
+
return text(JSON.stringify(result, null, 2));
|
|
14954
|
+
}
|
|
14955
|
+
);
|
|
14956
|
+
var _ALT_SVC_DEPRECATED_H3_DRAFTS_MCP = /* @__PURE__ */ new Set(["h3-29", "h3-27", "h3-25", "h3-24", "h3-23", "h3-22"]);
|
|
14957
|
+
var _ALT_SVC_PROTOCOL_NOTES_MCP = {
|
|
14958
|
+
"h3": "HTTP/3 over QUIC, the current protocol ID clients should be offered.",
|
|
14959
|
+
"h3-29": 'An outdated HTTP/3 draft version (draft-29); modern clients only match "h3".',
|
|
14960
|
+
"h3-27": 'An outdated HTTP/3 draft version (draft-27); modern clients only match "h3".',
|
|
14961
|
+
"h3-25": 'An outdated HTTP/3 draft version (draft-25); modern clients only match "h3".',
|
|
14962
|
+
"h2": "HTTP/2 over TLS.",
|
|
14963
|
+
"h2c": "HTTP/2 over cleartext TCP; rarely advertised via Alt-Svc since it needs an out-of-band upgrade rather than ALPN negotiation.",
|
|
14964
|
+
"http/1.1": "Plain HTTP/1.1; advertising it via Alt-Svc is unusual since it is already the universal fallback."
|
|
14965
|
+
};
|
|
14966
|
+
function _buildAltSvcHeaderMcp(alternatives) {
|
|
14967
|
+
if (!Array.isArray(alternatives) || alternatives.length === 0) {
|
|
14968
|
+
return { error: "At least one alternative service is required" };
|
|
14969
|
+
}
|
|
14970
|
+
const warnings = [];
|
|
14971
|
+
const entries = [];
|
|
14972
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14973
|
+
for (const alt of alternatives) {
|
|
14974
|
+
const protocolId = typeof alt.protocolId === "string" ? alt.protocolId.trim() : "";
|
|
14975
|
+
if (!protocolId) return { error: 'Each alternative needs a protocol-id (e.g. "h3")' };
|
|
14976
|
+
if (!Number.isInteger(alt.port) || alt.port < 1 || alt.port > 65535) {
|
|
14977
|
+
return { error: `Each alternative needs a valid port (1-65535), got ${JSON.stringify(alt.port)}` };
|
|
14978
|
+
}
|
|
14979
|
+
const host = alt.host?.trim();
|
|
14980
|
+
const authority = `${host ?? ""}:${alt.port}`;
|
|
14981
|
+
const maxAge = alt.maxAge !== void 0 ? alt.maxAge : 86400;
|
|
14982
|
+
if (!Number.isFinite(maxAge) || maxAge < 0) return { error: "maxAge must be a non-negative number of seconds" };
|
|
14983
|
+
const persist = alt.persist === true;
|
|
14984
|
+
const dedupeKey = `${protocolId.toLowerCase()}|${authority.toLowerCase()}`;
|
|
14985
|
+
if (seen.has(dedupeKey)) {
|
|
14986
|
+
warnings.push(`duplicate alternative "${protocolId}=${JSON.stringify(authority)}" was removed`);
|
|
14987
|
+
continue;
|
|
14988
|
+
}
|
|
14989
|
+
seen.add(dedupeKey);
|
|
14990
|
+
if (maxAge === 0) warnings.push(`"${protocolId}" has ma=0, so clients should treat it as already expired`);
|
|
14991
|
+
if (persist && maxAge === 0) warnings.push(`"${protocolId}" sets persist=1 with ma=0, which is contradictory: the entry expires immediately`);
|
|
14992
|
+
if (_ALT_SVC_DEPRECATED_H3_DRAFTS_MCP.has(protocolId.toLowerCase())) {
|
|
14993
|
+
warnings.push(`"${protocolId}" is an outdated HTTP/3 draft protocol-id; modern clients only match "h3"`);
|
|
14994
|
+
}
|
|
14995
|
+
const note = _ALT_SVC_PROTOCOL_NOTES_MCP[protocolId.toLowerCase()] ?? `Advertises "${protocolId}" as an alternative protocol clients may switch to for this authority.`;
|
|
14996
|
+
entries.push({ protocolId, authority, maxAge, persist, note });
|
|
14997
|
+
}
|
|
14998
|
+
if (entries.length === 0) return { error: "At least one alternative service is required" };
|
|
14999
|
+
const headerValue = entries.map((e) => {
|
|
15000
|
+
let s = `${e.protocolId}="${e.authority}"; ma=${e.maxAge}`;
|
|
15001
|
+
if (e.persist) s += "; persist=1";
|
|
15002
|
+
return s;
|
|
15003
|
+
}).join(", ");
|
|
15004
|
+
return { header: `Alt-Svc: ${headerValue}`, headerValue, entries, cleared: false, warnings };
|
|
15005
|
+
}
|
|
15006
|
+
function _altSvcSplitTopLevelMcp(s, sep) {
|
|
15007
|
+
const out = [];
|
|
15008
|
+
let inQuotes = false;
|
|
15009
|
+
let current = "";
|
|
15010
|
+
for (const ch of s) {
|
|
15011
|
+
if (ch === '"') inQuotes = !inQuotes;
|
|
15012
|
+
if (ch === sep && !inQuotes) {
|
|
15013
|
+
out.push(current);
|
|
15014
|
+
current = "";
|
|
15015
|
+
} else {
|
|
15016
|
+
current += ch;
|
|
15017
|
+
}
|
|
15018
|
+
}
|
|
15019
|
+
out.push(current);
|
|
15020
|
+
return out;
|
|
15021
|
+
}
|
|
15022
|
+
function _parseAltSvcHeaderMcp(raw) {
|
|
15023
|
+
if (!raw || raw.trim() === "") return { error: "Input is empty" };
|
|
15024
|
+
const withoutLabel = raw.trim().replace(/^alt-svc:\s*/i, "");
|
|
15025
|
+
if (withoutLabel.toLowerCase() === "clear") {
|
|
15026
|
+
return { header: "Alt-Svc: clear", headerValue: "clear", entries: [], cleared: true, warnings: [] };
|
|
15027
|
+
}
|
|
15028
|
+
const parts = _altSvcSplitTopLevelMcp(withoutLabel, ",");
|
|
15029
|
+
const alternatives = [];
|
|
15030
|
+
for (const part of parts) {
|
|
15031
|
+
const trimmedPart = part.trim();
|
|
15032
|
+
if (!trimmedPart) continue;
|
|
15033
|
+
const eq = trimmedPart.indexOf("=");
|
|
15034
|
+
if (eq === -1) return { error: `Could not parse alternative: "${trimmedPart}"` };
|
|
15035
|
+
const protocolId = trimmedPart.slice(0, eq).trim();
|
|
15036
|
+
const rest = trimmedPart.slice(eq + 1).trim();
|
|
15037
|
+
const segments = _altSvcSplitTopLevelMcp(rest, ";").map((s) => s.trim());
|
|
15038
|
+
const authorityMatch = segments[0]?.match(/^"(.*)"$/);
|
|
15039
|
+
if (!authorityMatch) return { error: `Alternative "${protocolId}" is missing a quoted authority (e.g. ":443")` };
|
|
15040
|
+
const authority = authorityMatch[1];
|
|
15041
|
+
const colonIdx = authority.lastIndexOf(":");
|
|
15042
|
+
if (colonIdx === -1) return { error: `Alternative "${protocolId}" has an authority with no port: "${authority}"` };
|
|
15043
|
+
const host = authority.slice(0, colonIdx) || void 0;
|
|
15044
|
+
const portStr = authority.slice(colonIdx + 1);
|
|
15045
|
+
const port = Number(portStr);
|
|
15046
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { error: `Alternative "${protocolId}" has an invalid port: "${portStr}"` };
|
|
15047
|
+
let maxAge;
|
|
15048
|
+
let persist = false;
|
|
15049
|
+
for (const seg of segments.slice(1)) {
|
|
15050
|
+
const sepIdx = seg.indexOf("=");
|
|
15051
|
+
if (sepIdx === -1) continue;
|
|
15052
|
+
const key = seg.slice(0, sepIdx).trim().toLowerCase();
|
|
15053
|
+
const value = seg.slice(sepIdx + 1).trim();
|
|
15054
|
+
if (key === "ma") maxAge = Number(value);
|
|
15055
|
+
if (key === "persist") persist = value === "1";
|
|
15056
|
+
}
|
|
15057
|
+
if (maxAge !== void 0 && !Number.isFinite(maxAge)) return { error: `Alternative "${protocolId}" has a non-numeric ma value` };
|
|
15058
|
+
alternatives.push({ protocolId, host, port, maxAge, persist });
|
|
15059
|
+
}
|
|
15060
|
+
if (alternatives.length === 0) return { error: "No alternative services found" };
|
|
15061
|
+
return _buildAltSvcHeaderMcp(alternatives);
|
|
15062
|
+
}
|
|
15063
|
+
server.tool(
|
|
15064
|
+
"build_alt_svc_header",
|
|
15065
|
+
'Build an Alt-Svc response-header value (RFC 7838) from one or more alternative services, each a protocol-id (e.g. "h3"), optional host, port, max-age in seconds, and an optional persist flag, attaching a plain-English note for each well-known protocol-id.',
|
|
15066
|
+
{
|
|
15067
|
+
alternatives: z.array(z.object({
|
|
15068
|
+
protocolId: z.string().min(1).describe('ALPN protocol-id, e.g. "h3" or "h2"'),
|
|
15069
|
+
host: z.string().optional().describe("Alternative host; omit to mean the same host"),
|
|
15070
|
+
port: z.number().int().min(1).max(65535),
|
|
15071
|
+
maxAge: z.number().min(0).optional().describe("Seconds the alternative stays valid (default 86400)"),
|
|
15072
|
+
persist: z.boolean().optional().describe("Whether the entry should survive a network configuration change")
|
|
15073
|
+
})).min(1).describe("Alternative services to advertise")
|
|
15074
|
+
},
|
|
15075
|
+
async ({ alternatives }) => {
|
|
15076
|
+
const result = _buildAltSvcHeaderMcp(alternatives);
|
|
15077
|
+
if ("error" in result) return errText(result);
|
|
15078
|
+
return text(JSON.stringify(result, null, 2));
|
|
15079
|
+
}
|
|
15080
|
+
);
|
|
15081
|
+
server.tool(
|
|
15082
|
+
"parse_alt_svc_header",
|
|
15083
|
+
'Parse an existing Alt-Svc header value (with or without a leading "Alt-Svc:" label), including the special "clear" value, into its individual alternative services with plain-English protocol notes.',
|
|
15084
|
+
{
|
|
15085
|
+
header: z.string().min(1).describe("The Alt-Svc header value to parse")
|
|
15086
|
+
},
|
|
15087
|
+
async ({ header }) => {
|
|
15088
|
+
const result = _parseAltSvcHeaderMcp(header);
|
|
15089
|
+
if ("error" in result) return errText(result);
|
|
15090
|
+
return text(JSON.stringify(result, null, 2));
|
|
15091
|
+
}
|
|
15092
|
+
);
|
|
14735
15093
|
var transport = new StdioServerTransport();
|
|
14736
15094
|
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.94.0",
|
|
4
|
+
"description": "MCP server exposing 279 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",
|