@utilix-tech/mcp 0.17.0 → 0.18.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 +3 -0
- package/README.md +5 -3
- package/dist/index.js +179 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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.18.0
|
|
6
|
+
- Added `parse_user_agent` and `parse_github_url` tools.
|
|
7
|
+
|
|
5
8
|
## 0.17.0
|
|
6
9
|
- Added Simple Interest Calculator (`calculate_simple_interest`) and Base58 / Base62 Encoder (`base58_encode`, `base58_decode`, `base62_encode`, `base62_decode`).
|
|
7
10
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing
|
|
3
|
+
MCP server exposing 125 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
|
|
|
@@ -164,7 +164,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
164
164
|
| `calculate_tip_split` | Compute a tip on a bill amount and split the bill, tip, and total evenly across N people |
|
|
165
165
|
| `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
|
|
166
166
|
|
|
167
|
-
### API / Network (
|
|
167
|
+
### API / Network (9 tools)
|
|
168
168
|
| Tool | Description |
|
|
169
169
|
|------|-------------|
|
|
170
170
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -175,6 +175,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
175
175
|
| `parse_har` | Parse a DevTools .har network export into requests and summary stats |
|
|
176
176
|
| `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
|
|
177
177
|
| `validate_robots_txt` | Validate a robots.txt file: User-agent groups, rules, and directive syntax |
|
|
178
|
+
| `parse_user_agent` | Parse a browser User-Agent string into browser, engine, OS, and device type |
|
|
178
179
|
|
|
179
180
|
### Color (4 tools)
|
|
180
181
|
| Tool | Description |
|
|
@@ -184,7 +185,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
184
185
|
| `color_palette` | Generate a color palette from a base color |
|
|
185
186
|
| `color_shades` | Generate shades or tints from a base color |
|
|
186
187
|
|
|
187
|
-
### Code (
|
|
188
|
+
### Code (9 tools)
|
|
188
189
|
| Tool | Description |
|
|
189
190
|
|------|-------------|
|
|
190
191
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -195,6 +196,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
195
196
|
| `parse_env` | Parse a .env file and return all key-value pairs |
|
|
196
197
|
| `parse_docker_image` | Parse a Docker image reference into its components |
|
|
197
198
|
| `lookup_mime` | Look up the MIME type for a file extension, or the extension(s) for a MIME type |
|
|
199
|
+
| `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
|
|
198
200
|
|
|
199
201
|
### Data (6 tools)
|
|
200
202
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -3228,5 +3228,184 @@ server.tool("diff_env_example", "Diff a .env file against its .env.example templ
|
|
|
3228
3228
|
if ("error" in result) return errText(result);
|
|
3229
3229
|
return text(JSON.stringify(result, null, 2));
|
|
3230
3230
|
});
|
|
3231
|
+
var _UA_BOT_PATTERNS_MCP = [
|
|
3232
|
+
[/googlebot/i, "Googlebot"],
|
|
3233
|
+
[/bingbot/i, "Bingbot"],
|
|
3234
|
+
[/slurp/i, "Slurp"],
|
|
3235
|
+
[/duckduckbot/i, "DuckDuckBot"],
|
|
3236
|
+
[/baiduspider/i, "Baiduspider"],
|
|
3237
|
+
[/yandexbot/i, "YandexBot"],
|
|
3238
|
+
[/facebookexternalhit/i, "Facebot"],
|
|
3239
|
+
[/twitterbot/i, "Twitterbot"],
|
|
3240
|
+
[/applebot/i, "Applebot"],
|
|
3241
|
+
[/discordbot/i, "Discordbot"],
|
|
3242
|
+
[/slackbot/i, "Slackbot"],
|
|
3243
|
+
[/telegrambot/i, "TelegramBot"],
|
|
3244
|
+
[/whatsapp/i, "WhatsApp"],
|
|
3245
|
+
[/ahrefsbot/i, "AhrefsBot"],
|
|
3246
|
+
[/semrushbot/i, "SemrushBot"],
|
|
3247
|
+
[/crawler|spider|\bbot\b/i, "Bot"]
|
|
3248
|
+
];
|
|
3249
|
+
function _detectUaBrowserMcp(s) {
|
|
3250
|
+
let m;
|
|
3251
|
+
if (m = s.match(/Edg(?:A|iOS)?\/([\d.]+)/)) return { name: "Edge", version: m[1] };
|
|
3252
|
+
if (m = s.match(/Edge\/([\d.]+)/)) return { name: "Edge (Legacy)", version: m[1] };
|
|
3253
|
+
if (m = s.match(/OPR\/([\d.]+)/)) return { name: "Opera", version: m[1] };
|
|
3254
|
+
if (m = s.match(/Opera\/([\d.]+)/)) return { name: "Opera", version: m[1] };
|
|
3255
|
+
if (m = s.match(/SamsungBrowser\/([\d.]+)/)) return { name: "Samsung Internet", version: m[1] };
|
|
3256
|
+
if (m = s.match(/FxiOS\/([\d.]+)/)) return { name: "Firefox", version: m[1] };
|
|
3257
|
+
if (m = s.match(/Firefox\/([\d.]+)/)) return { name: "Firefox", version: m[1] };
|
|
3258
|
+
if (m = s.match(/CriOS\/([\d.]+)/)) return { name: "Chrome", version: m[1] };
|
|
3259
|
+
if (m = s.match(/Chrome\/([\d.]+)/)) return { name: "Chrome", version: m[1] };
|
|
3260
|
+
if (m = s.match(/Version\/([\d.]+).*Safari\//)) return { name: "Safari", version: m[1] };
|
|
3261
|
+
if (/Safari\//.test(s)) return { name: "Safari", version: "" };
|
|
3262
|
+
if (m = s.match(/MSIE ([\d.]+)/)) return { name: "Internet Explorer", version: m[1] };
|
|
3263
|
+
if (m = s.match(/Trident\/.*rv:([\d.]+)/)) return { name: "Internet Explorer", version: m[1] };
|
|
3264
|
+
return { name: "Unknown", version: "" };
|
|
3265
|
+
}
|
|
3266
|
+
function _detectUaEngineMcp(s, browserName) {
|
|
3267
|
+
let m;
|
|
3268
|
+
if (/Chrome\/|CriOS\/|Edg\/|OPR\/|SamsungBrowser\//.test(s)) {
|
|
3269
|
+
m = s.match(/Chrome\/([\d.]+)/) || s.match(/CriOS\/([\d.]+)/);
|
|
3270
|
+
return { name: "Blink", version: m ? m[1] : "" };
|
|
3271
|
+
}
|
|
3272
|
+
if (/Firefox\/|FxiOS\//.test(s)) {
|
|
3273
|
+
m = s.match(/rv:([\d.]+)/);
|
|
3274
|
+
return { name: "Gecko", version: m ? m[1] : "" };
|
|
3275
|
+
}
|
|
3276
|
+
if (m = s.match(/AppleWebKit\/([\d.]+)/)) return { name: "WebKit", version: m[1] };
|
|
3277
|
+
if (m = s.match(/Trident\/([\d.]+)/)) return { name: "Trident", version: m[1] };
|
|
3278
|
+
if (browserName === "Edge (Legacy)") return { name: "EdgeHTML", version: "" };
|
|
3279
|
+
return { name: "Unknown", version: "" };
|
|
3280
|
+
}
|
|
3281
|
+
var _UA_WINDOWS_VERSIONS_MCP = {
|
|
3282
|
+
"10.0": "10",
|
|
3283
|
+
"6.3": "8.1",
|
|
3284
|
+
"6.2": "8",
|
|
3285
|
+
"6.1": "7",
|
|
3286
|
+
"6.0": "Vista",
|
|
3287
|
+
"5.1": "XP"
|
|
3288
|
+
};
|
|
3289
|
+
function _detectUaOsMcp(s) {
|
|
3290
|
+
if (/Windows NT/.test(s)) return "Windows";
|
|
3291
|
+
if (/CrOS/.test(s)) return "ChromeOS";
|
|
3292
|
+
if (/Android/.test(s)) return "Android";
|
|
3293
|
+
if (/iPhone|iPad|iPod/.test(s)) return "iOS";
|
|
3294
|
+
if (/Mac OS X/.test(s)) return "macOS";
|
|
3295
|
+
if (/Linux/.test(s)) return "Linux";
|
|
3296
|
+
return "Unknown";
|
|
3297
|
+
}
|
|
3298
|
+
function _detectUaOsVersionMcp(s) {
|
|
3299
|
+
let m;
|
|
3300
|
+
if (m = s.match(/Windows NT ([\d.]+)/)) return _UA_WINDOWS_VERSIONS_MCP[m[1]] ?? m[1];
|
|
3301
|
+
if (/CrOS/.test(s) && (m = s.match(/CrOS \S+ ([\d.]+)/))) return m[1];
|
|
3302
|
+
if (m = s.match(/Android ([\d.]+)/)) return m[1];
|
|
3303
|
+
if (m = s.match(/(?:iPhone|CPU) OS ([\d_]+)/)) return m[1].replace(/_/g, ".");
|
|
3304
|
+
if (m = s.match(/Mac OS X ([\d_]+)/)) return m[1].replace(/_/g, ".");
|
|
3305
|
+
return "";
|
|
3306
|
+
}
|
|
3307
|
+
function _detectUaDeviceTypeMcp(s) {
|
|
3308
|
+
if (/iPad/.test(s)) return "tablet";
|
|
3309
|
+
if (/Android/.test(s) && !/Mobile/.test(s)) return "tablet";
|
|
3310
|
+
if (/Mobile|iPhone|iPod|Android/.test(s)) return "mobile";
|
|
3311
|
+
return "desktop";
|
|
3312
|
+
}
|
|
3313
|
+
function _parseUserAgentMcp(ua) {
|
|
3314
|
+
if (!ua || typeof ua !== "string" || ua.trim() === "") return { error: "Empty user-agent string" };
|
|
3315
|
+
const s = ua.trim();
|
|
3316
|
+
for (const [re, name] of _UA_BOT_PATTERNS_MCP) {
|
|
3317
|
+
if (re.test(s)) {
|
|
3318
|
+
const versionMatch = s.match(new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/([\\d.]+)`, "i"));
|
|
3319
|
+
return {
|
|
3320
|
+
browser: { name, version: versionMatch ? versionMatch[1] : "" },
|
|
3321
|
+
engine: { name: "Unknown", version: "" },
|
|
3322
|
+
os: { name: _detectUaOsMcp(s), version: _detectUaOsVersionMcp(s) },
|
|
3323
|
+
device: { type: "bot" },
|
|
3324
|
+
raw: s
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
const browser = _detectUaBrowserMcp(s);
|
|
3329
|
+
const engine = _detectUaEngineMcp(s, browser.name);
|
|
3330
|
+
const os = { name: _detectUaOsMcp(s), version: _detectUaOsVersionMcp(s) };
|
|
3331
|
+
const device = { type: _detectUaDeviceTypeMcp(s) };
|
|
3332
|
+
return { browser, engine, os, device, raw: s };
|
|
3333
|
+
}
|
|
3334
|
+
server.tool(
|
|
3335
|
+
"parse_user_agent",
|
|
3336
|
+
"Parse a browser User-Agent string into browser, engine, OS, and device type via pattern matching against well-known UA token formats. Covers major desktop/mobile browsers and common search-engine/social crawlers.",
|
|
3337
|
+
{ ua: z.string().describe("The User-Agent header value to parse") },
|
|
3338
|
+
async ({ ua }) => {
|
|
3339
|
+
const result = _parseUserAgentMcp(ua);
|
|
3340
|
+
if ("error" in result) return errText(result);
|
|
3341
|
+
return text(JSON.stringify(result, null, 2));
|
|
3342
|
+
}
|
|
3343
|
+
);
|
|
3344
|
+
function _parseGithubLineRangeMcp(hash) {
|
|
3345
|
+
const m = hash.match(/^#?L(\d+)(?:-L?(\d+))?$/i);
|
|
3346
|
+
if (!m) return {};
|
|
3347
|
+
const startLine = parseInt(m[1], 10);
|
|
3348
|
+
const endLine = m[2] ? parseInt(m[2], 10) : void 0;
|
|
3349
|
+
return endLine !== void 0 ? { startLine, endLine } : { startLine };
|
|
3350
|
+
}
|
|
3351
|
+
function _parseGithubUrlMcp(input) {
|
|
3352
|
+
if (!input || typeof input !== "string" || input.trim() === "") return { error: "Empty URL" };
|
|
3353
|
+
const trimmed = input.trim();
|
|
3354
|
+
const normalized = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
3355
|
+
let url;
|
|
3356
|
+
try {
|
|
3357
|
+
url = new URL(normalized);
|
|
3358
|
+
} catch {
|
|
3359
|
+
return { error: "Invalid URL" };
|
|
3360
|
+
}
|
|
3361
|
+
if (!/^(www\.)?github\.com$/i.test(url.hostname)) {
|
|
3362
|
+
return { error: `Not a github.com URL (got host "${url.hostname}")` };
|
|
3363
|
+
}
|
|
3364
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
3365
|
+
if (segments.length < 2) return { error: "URL must include an owner and a repository" };
|
|
3366
|
+
const [owner, repo, ...rest] = segments;
|
|
3367
|
+
if (rest.length === 0) return { owner, repo, type: "repo" };
|
|
3368
|
+
const kind = rest[0];
|
|
3369
|
+
if (kind === "pull" || kind === "issues") {
|
|
3370
|
+
const number = parseInt(rest[1], 10);
|
|
3371
|
+
if (!rest[1] || Number.isNaN(number)) {
|
|
3372
|
+
return { error: `Missing ${kind === "pull" ? "pull request" : "issue"} number` };
|
|
3373
|
+
}
|
|
3374
|
+
return { owner, repo, type: kind === "pull" ? "pull" : "issue", number };
|
|
3375
|
+
}
|
|
3376
|
+
if (kind === "commit") {
|
|
3377
|
+
if (!rest[1]) return { error: "Missing commit SHA" };
|
|
3378
|
+
return { owner, repo, type: "commit", ref: rest[1] };
|
|
3379
|
+
}
|
|
3380
|
+
if (kind === "blob") {
|
|
3381
|
+
const ref = rest[1];
|
|
3382
|
+
const path = rest.slice(2).join("/");
|
|
3383
|
+
if (!ref || !path) return { error: "File URL must include a ref and a file path (/blob/<ref>/<path>)" };
|
|
3384
|
+
return { owner, repo, type: "file", ref, path, ..._parseGithubLineRangeMcp(url.hash) };
|
|
3385
|
+
}
|
|
3386
|
+
if (kind === "tree") {
|
|
3387
|
+
const ref = rest[1];
|
|
3388
|
+
if (!ref) return { error: "Missing branch or tag ref (/tree/<ref>)" };
|
|
3389
|
+
const path = rest.slice(2).join("/");
|
|
3390
|
+
return { owner, repo, type: "tree", ref, ...path ? { path } : {} };
|
|
3391
|
+
}
|
|
3392
|
+
if (kind === "releases") {
|
|
3393
|
+
if (rest[1] !== "tag" || !rest[2]) {
|
|
3394
|
+
return { error: "Unsupported releases URL (expected /releases/tag/<name>)" };
|
|
3395
|
+
}
|
|
3396
|
+
return { owner, repo, type: "release", ref: rest[2] };
|
|
3397
|
+
}
|
|
3398
|
+
return { error: `Unsupported GitHub URL path: /${rest.join("/")}` };
|
|
3399
|
+
}
|
|
3400
|
+
server.tool(
|
|
3401
|
+
"parse_github_url",
|
|
3402
|
+
"Parse a github.com URL (repo, pull request, issue, commit, file, directory, or release tag) into structured components, including an optional #L10-L20 line-range anchor for file URLs. SSH remotes and other Git hosts are out of scope.",
|
|
3403
|
+
{ url: z.string().describe("The github.com URL to parse, e.g. https://github.com/owner/repo/pull/123") },
|
|
3404
|
+
async ({ url }) => {
|
|
3405
|
+
const result = _parseGithubUrlMcp(url);
|
|
3406
|
+
if ("error" in result) return errText(result);
|
|
3407
|
+
return text(JSON.stringify(result, null, 2));
|
|
3408
|
+
}
|
|
3409
|
+
);
|
|
3231
3410
|
var transport = new StdioServerTransport();
|
|
3232
3411
|
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.18.0",
|
|
4
|
+
"description": "MCP server exposing 125 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",
|