@utilix-tech/mcp 0.48.0 → 0.49.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 +135 -0
- package/package.json +4 -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.49.0
|
|
6
|
+
- Add `resolve_yaml_anchors` tool: expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.
|
|
7
|
+
- Add `split_sitemap` tool: split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.
|
|
8
|
+
|
|
5
9
|
## 0.48.0
|
|
6
10
|
- Add `build_content_disposition`, `parse_content_disposition` tools: build a `Content-Disposition` response header from a disposition type and filename, with an RFC 5987 `filename*` extended parameter for non-ASCII filenames, or parse an existing header into its type, filename, and raw parameters.
|
|
7
11
|
- Add `inspect_gif_frames` tool: 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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 188 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
|
|
|
@@ -193,7 +193,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
193
193
|
| `calculate_cd_maturity` | Compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net early-withdrawal payout |
|
|
194
194
|
| `calculate_depreciation_schedule` | Build a year-by-year fixed-asset depreciation schedule from cost, salvage value, and useful life, using straight-line, double-declining-balance, or sum-of-years-digits |
|
|
195
195
|
|
|
196
|
-
### API / Network (
|
|
196
|
+
### API / Network (28 tools)
|
|
197
197
|
| Tool | Description |
|
|
198
198
|
|------|-------------|
|
|
199
199
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -204,6 +204,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
204
204
|
| `cors_headers` | Generate CORS response headers for a configuration |
|
|
205
205
|
| `parse_har` | Parse a DevTools .har network export into requests and summary stats |
|
|
206
206
|
| `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
|
|
207
|
+
| `split_sitemap` | Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file |
|
|
207
208
|
| `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
|
|
208
209
|
| `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
|
|
209
210
|
| `build_cache_control` | Build and explain an HTTP Cache-Control response header from directives (max-age, no-store, immutable, etc.) |
|
|
@@ -248,10 +249,11 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
248
249
|
| `lint_conventional_commit` | Lint a commit message against the Conventional Commits v1.0.0 spec: header format, blank-line separation, footer tokens, and BREAKING CHANGE casing |
|
|
249
250
|
| `test_gitignore_patterns` | Test a list of paths against a .gitignore file's patterns, following git's last-match-wins and negation rules |
|
|
250
251
|
|
|
251
|
-
### Data (
|
|
252
|
+
### Data (11 tools)
|
|
252
253
|
| Tool | Description |
|
|
253
254
|
|------|-------------|
|
|
254
255
|
| `validate_yaml` | Validate YAML syntax |
|
|
256
|
+
| `resolve_yaml_anchors` | Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused |
|
|
255
257
|
| `toml_to_json` | Convert TOML to JSON |
|
|
256
258
|
| `xml_to_json` | Convert XML to JSON |
|
|
257
259
|
| `parse_csv` | Parse a CSV string and return the data as JSON |
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
|
+
import * as yaml from "js-yaml";
|
|
8
9
|
import {
|
|
9
10
|
parseJson,
|
|
10
11
|
minifyJson,
|
|
@@ -730,6 +731,78 @@ Documents: ${result.documentCount}
|
|
|
730
731
|
Keys: ${result.keyCount}`);
|
|
731
732
|
}
|
|
732
733
|
);
|
|
734
|
+
var _YAML_ANCHOR_RE = /(?:^|\s)&([A-Za-z0-9_.-]+)/g;
|
|
735
|
+
var _YAML_ALIAS_RE = /(?:^|\s)\*([A-Za-z0-9_.-]+)/g;
|
|
736
|
+
function _stripYamlLineComment(line) {
|
|
737
|
+
let inSingle = false;
|
|
738
|
+
let inDouble = false;
|
|
739
|
+
for (let i = 0; i < line.length; i++) {
|
|
740
|
+
const ch = line[i];
|
|
741
|
+
if (ch === "'" && !inDouble) {
|
|
742
|
+
inSingle = !inSingle;
|
|
743
|
+
} else if (ch === '"' && !inSingle) {
|
|
744
|
+
inDouble = !inDouble;
|
|
745
|
+
} else if (ch === "#" && !inSingle && !inDouble && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
746
|
+
return line.slice(0, i);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return line;
|
|
750
|
+
}
|
|
751
|
+
function _resolveYamlAnchors(yamlText) {
|
|
752
|
+
if (!yamlText || !yamlText.trim()) return { error: "Input is empty" };
|
|
753
|
+
const anchorDefs = [];
|
|
754
|
+
const referencedNames = /* @__PURE__ */ new Set();
|
|
755
|
+
let aliasCount = 0;
|
|
756
|
+
const lines = yamlText.split("\n");
|
|
757
|
+
for (let i = 0; i < lines.length; i++) {
|
|
758
|
+
const line = _stripYamlLineComment(lines[i]);
|
|
759
|
+
const lineNum = i + 1;
|
|
760
|
+
_YAML_ANCHOR_RE.lastIndex = 0;
|
|
761
|
+
let m;
|
|
762
|
+
while ((m = _YAML_ANCHOR_RE.exec(line)) !== null) {
|
|
763
|
+
anchorDefs.push({ name: m[1], line: lineNum });
|
|
764
|
+
}
|
|
765
|
+
_YAML_ALIAS_RE.lastIndex = 0;
|
|
766
|
+
while ((m = _YAML_ALIAS_RE.exec(line)) !== null) {
|
|
767
|
+
referencedNames.add(m[1]);
|
|
768
|
+
aliasCount++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
let parsed;
|
|
772
|
+
try {
|
|
773
|
+
parsed = yaml.load(yamlText);
|
|
774
|
+
} catch (e) {
|
|
775
|
+
const err = e;
|
|
776
|
+
return { error: err.reason ?? err.message };
|
|
777
|
+
}
|
|
778
|
+
let resolvedYaml;
|
|
779
|
+
try {
|
|
780
|
+
resolvedYaml = yaml.dump(parsed, { noRefs: true, lineWidth: -1 });
|
|
781
|
+
} catch (e) {
|
|
782
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
783
|
+
}
|
|
784
|
+
const anchors = anchorDefs.map((a) => ({
|
|
785
|
+
name: a.name,
|
|
786
|
+
line: a.line,
|
|
787
|
+
referenced: referencedNames.has(a.name)
|
|
788
|
+
}));
|
|
789
|
+
const seenAnchorNames = [];
|
|
790
|
+
for (const a of anchorDefs) {
|
|
791
|
+
if (!seenAnchorNames.includes(a.name)) seenAnchorNames.push(a.name);
|
|
792
|
+
}
|
|
793
|
+
const unusedAnchors = seenAnchorNames.filter((name) => !referencedNames.has(name));
|
|
794
|
+
return { resolvedYaml, anchors, unusedAnchors, aliasCount };
|
|
795
|
+
}
|
|
796
|
+
server.tool(
|
|
797
|
+
"resolve_yaml_anchors",
|
|
798
|
+
"Expand YAML anchors, aliases, and merge keys inline, and list which anchors were referenced vs. left unused.",
|
|
799
|
+
{ yaml: z.string().describe("The YAML document to resolve") },
|
|
800
|
+
async ({ yaml: yamlText }) => {
|
|
801
|
+
const result = _resolveYamlAnchors(yamlText);
|
|
802
|
+
if ("error" in result) return errText(result);
|
|
803
|
+
return text(JSON.stringify(result, null, 2));
|
|
804
|
+
}
|
|
805
|
+
);
|
|
733
806
|
server.tool(
|
|
734
807
|
"toml_to_json",
|
|
735
808
|
"Convert a TOML string to JSON.",
|
|
@@ -1609,6 +1682,68 @@ server.tool(
|
|
|
1609
1682
|
return text(JSON.stringify(result, null, 2));
|
|
1610
1683
|
}
|
|
1611
1684
|
);
|
|
1685
|
+
var _SITEMAP_SPLITTER_MAX_URLS_PER_FILE = 5e4;
|
|
1686
|
+
function _escapeXmlUrlForSplitter(url) {
|
|
1687
|
+
return url.replace(/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1688
|
+
}
|
|
1689
|
+
function _splitSitemap(urlsText, baseUrlInput, urlsPerFileInput, filePrefixInput) {
|
|
1690
|
+
const urls = (urlsText ?? "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1691
|
+
if (urls.length === 0) return { error: "No URLs provided" };
|
|
1692
|
+
for (let i = 0; i < urls.length; i++) {
|
|
1693
|
+
if (!/^https?:\/\//i.test(urls[i])) {
|
|
1694
|
+
return { error: `Line ${i + 1}: "${urls[i]}" is not an absolute URL starting with http:// or https://` };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (!baseUrlInput || !/^https?:\/\//i.test(baseUrlInput)) {
|
|
1698
|
+
return { error: "baseUrl must be an absolute URL starting with http:// or https://" };
|
|
1699
|
+
}
|
|
1700
|
+
const baseUrl = baseUrlInput.replace(/\/+$/, "") + "/";
|
|
1701
|
+
let urlsPerFile = urlsPerFileInput ?? _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
|
|
1702
|
+
if (urlsPerFile <= 0) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
|
|
1703
|
+
if (urlsPerFile > _SITEMAP_SPLITTER_MAX_URLS_PER_FILE) urlsPerFile = _SITEMAP_SPLITTER_MAX_URLS_PER_FILE;
|
|
1704
|
+
const filePrefix = (filePrefixInput || "sitemap").replace(/[^A-Za-z0-9_-]/g, "") || "sitemap";
|
|
1705
|
+
const files = [];
|
|
1706
|
+
for (let i = 0; i < urls.length; i += urlsPerFile) {
|
|
1707
|
+
const chunk = urls.slice(i, i + urlsPerFile);
|
|
1708
|
+
const fileIndex = files.length + 1;
|
|
1709
|
+
const filename = `${filePrefix}-${fileIndex}.xml`;
|
|
1710
|
+
const urlLines = chunk.map((url) => ` <url>
|
|
1711
|
+
<loc>${_escapeXmlUrlForSplitter(url)}</loc>
|
|
1712
|
+
</url>`).join("\n");
|
|
1713
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1714
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
1715
|
+
${urlLines}
|
|
1716
|
+
</urlset>
|
|
1717
|
+
`;
|
|
1718
|
+
files.push({ filename, urlCount: chunk.length, xml });
|
|
1719
|
+
}
|
|
1720
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1721
|
+
const indexEntries = files.map((f) => ` <sitemap>
|
|
1722
|
+
<loc>${_escapeXmlUrlForSplitter(baseUrl + f.filename)}</loc>
|
|
1723
|
+
<lastmod>${today}</lastmod>
|
|
1724
|
+
</sitemap>`).join("\n");
|
|
1725
|
+
const indexXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1726
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
1727
|
+
${indexEntries}
|
|
1728
|
+
</sitemapindex>
|
|
1729
|
+
`;
|
|
1730
|
+
return { files, indexXml, totalUrls: urls.length };
|
|
1731
|
+
}
|
|
1732
|
+
server.tool(
|
|
1733
|
+
"split_sitemap",
|
|
1734
|
+
"Split a URL list exceeding the sitemaps.org 50,000-URL cap into multiple sitemap.xml files plus a sitemap index file.",
|
|
1735
|
+
{
|
|
1736
|
+
urls: z.string().describe("Newline-separated list of absolute URLs"),
|
|
1737
|
+
baseUrl: z.string().describe("Base URL the generated sitemap files will be hosted at"),
|
|
1738
|
+
urlsPerFile: z.number().optional().describe("URLs per generated file, clamped to 50000 (default 50000)"),
|
|
1739
|
+
filePrefix: z.string().optional().describe('Filename prefix for generated files (default "sitemap")')
|
|
1740
|
+
},
|
|
1741
|
+
async ({ urls, baseUrl, urlsPerFile, filePrefix }) => {
|
|
1742
|
+
const result = _splitSitemap(urls, baseUrl, urlsPerFile, filePrefix);
|
|
1743
|
+
if ("error" in result) return errText(result);
|
|
1744
|
+
return text(JSON.stringify(result, null, 2));
|
|
1745
|
+
}
|
|
1746
|
+
);
|
|
1612
1747
|
var _KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
|
|
1613
1748
|
function _validateRobotsTxt(text2) {
|
|
1614
1749
|
if (!text2 || !text2.trim()) return { error: "Input is empty" };
|
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.49.0",
|
|
4
|
+
"description": "MCP server exposing 188 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",
|
|
@@ -54,9 +54,11 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
56
56
|
"@utilix-tech/sdk": "^0.3.0",
|
|
57
|
+
"js-yaml": "^4.1.0",
|
|
57
58
|
"zod": "^3.23.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
61
|
+
"@types/js-yaml": "^4.0.9",
|
|
60
62
|
"@types/node": "^20.0.0",
|
|
61
63
|
"tsup": "^8.0.0",
|
|
62
64
|
"typescript": "^5.4.0"
|