@utilix-tech/mcp 0.16.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 +6 -0
- package/README.md +12 -5
- package/dist/index.js +287 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
+
|
|
8
|
+
## 0.17.0
|
|
9
|
+
- Added Simple Interest Calculator (`calculate_simple_interest`) and Base58 / Base62 Encoder (`base58_encode`, `base58_decode`, `base62_encode`, `base62_decode`).
|
|
10
|
+
|
|
5
11
|
## 0.16.0
|
|
6
12
|
- Added MIME Type Lookup (`lookup_mime`) and Tip / Bill Split Calculator (`calculate_tip_split`).
|
|
7
13
|
|
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
|
|
|
@@ -92,7 +92,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
92
92
|
| `json_to_python` | Generate Python dataclasses from JSON |
|
|
93
93
|
| `json_to_zod` | Generate a Zod schema from JSON |
|
|
94
94
|
|
|
95
|
-
### Encoding (
|
|
95
|
+
### Encoding (12 tools)
|
|
96
96
|
| Tool | Description |
|
|
97
97
|
|------|-------------|
|
|
98
98
|
| `base64_encode` | Encode a string to Base64 |
|
|
@@ -103,6 +103,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
103
103
|
| `html_entities_decode` | Decode HTML entities |
|
|
104
104
|
| `base32_encode` | Encode a string to Base32 |
|
|
105
105
|
| `base32_decode` | Decode a Base32 string |
|
|
106
|
+
| `base58_encode` | Encode text as Base58 (Bitcoin-address style, no ambiguous 0/O/I/l characters) |
|
|
107
|
+
| `base58_decode` | Decode a Base58 string back to text |
|
|
108
|
+
| `base62_encode` | Encode text as Base62 (alphanumeric, URL-safe) |
|
|
109
|
+
| `base62_decode` | Decode a Base62 string back to text |
|
|
106
110
|
|
|
107
111
|
### Hashing (4 tools)
|
|
108
112
|
| Tool | Description |
|
|
@@ -145,7 +149,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
145
149
|
| `date_diff` | Calculate the difference between two dates |
|
|
146
150
|
| `convert_timezone` | Convert a date/time between timezones |
|
|
147
151
|
|
|
148
|
-
### Units (
|
|
152
|
+
### Units (11 tools)
|
|
149
153
|
| Tool | Description |
|
|
150
154
|
|------|-------------|
|
|
151
155
|
| `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
|
|
@@ -158,8 +162,9 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
158
162
|
| `calculate_compound_interest` | Compute the future value of a principal plus an optional recurring contribution under compound interest |
|
|
159
163
|
| `validate_isbn` | Validate an ISBN-10 or ISBN-13 checksum and convert it to the other format where possible |
|
|
160
164
|
| `calculate_tip_split` | Compute a tip on a bill amount and split the bill, tip, and total evenly across N people |
|
|
165
|
+
| `calculate_simple_interest` | Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff |
|
|
161
166
|
|
|
162
|
-
### API / Network (
|
|
167
|
+
### API / Network (9 tools)
|
|
163
168
|
| Tool | Description |
|
|
164
169
|
|------|-------------|
|
|
165
170
|
| `decode_jwt` | Decode a JWT: header, payload, and expiry |
|
|
@@ -170,6 +175,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
170
175
|
| `parse_har` | Parse a DevTools .har network export into requests and summary stats |
|
|
171
176
|
| `validate_sitemap` | Validate a sitemap.xml or sitemap index against the sitemaps.org protocol |
|
|
172
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 |
|
|
173
179
|
|
|
174
180
|
### Color (4 tools)
|
|
175
181
|
| Tool | Description |
|
|
@@ -179,7 +185,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
179
185
|
| `color_palette` | Generate a color palette from a base color |
|
|
180
186
|
| `color_shades` | Generate shades or tints from a base color |
|
|
181
187
|
|
|
182
|
-
### Code (
|
|
188
|
+
### Code (9 tools)
|
|
183
189
|
| Tool | Description |
|
|
184
190
|
|------|-------------|
|
|
185
191
|
| `format_sql` | Format a SQL query with proper indentation |
|
|
@@ -190,6 +196,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
|
|
|
190
196
|
| `parse_env` | Parse a .env file and return all key-value pairs |
|
|
191
197
|
| `parse_docker_image` | Parse a Docker image reference into its components |
|
|
192
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 |
|
|
193
200
|
|
|
194
201
|
### Data (6 tools)
|
|
195
202
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -2267,6 +2267,114 @@ server.tool(
|
|
|
2267
2267
|
return text(JSON.stringify(result, null, 2));
|
|
2268
2268
|
}
|
|
2269
2269
|
);
|
|
2270
|
+
function _calculateSimpleInterestMcp(principal, annualRatePercent, years) {
|
|
2271
|
+
if (!Number.isFinite(principal) || principal < 0) return { error: "Principal must be zero or a positive number" };
|
|
2272
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) return { error: "Annual interest rate must be zero or a positive number" };
|
|
2273
|
+
if (!Number.isFinite(years) || years <= 0) return { error: "Years must be a positive number" };
|
|
2274
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
2275
|
+
const interest = round2(principal * (annualRatePercent / 100) * years);
|
|
2276
|
+
return {
|
|
2277
|
+
principal: round2(principal),
|
|
2278
|
+
annualRatePercent,
|
|
2279
|
+
years,
|
|
2280
|
+
interest,
|
|
2281
|
+
totalPayoff: round2(principal + interest)
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
server.tool(
|
|
2285
|
+
"calculate_simple_interest",
|
|
2286
|
+
"Compute simple (non-compounding) interest as principal x rate x time, plus the total payoff. Distinct from calculate_compound_interest, which reinvests interest each period.",
|
|
2287
|
+
{
|
|
2288
|
+
principal: z.number().describe("Starting principal amount"),
|
|
2289
|
+
annualRatePercent: z.number().describe("Annual interest rate as a percent, e.g. 5"),
|
|
2290
|
+
years: z.number().describe("Term in years, may be fractional")
|
|
2291
|
+
},
|
|
2292
|
+
async ({ principal, annualRatePercent, years }) => {
|
|
2293
|
+
const result = _calculateSimpleInterestMcp(principal, annualRatePercent, years);
|
|
2294
|
+
if ("error" in result) return errText(result);
|
|
2295
|
+
return text(JSON.stringify(result, null, 2));
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
var _BASE58_ALPHABET_MCP = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2299
|
+
var _BASE62_ALPHABET_MCP = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2300
|
+
function _encodeBytesToBaseMcp(bytes, alphabet) {
|
|
2301
|
+
const base = BigInt(alphabet.length);
|
|
2302
|
+
let num = 0n;
|
|
2303
|
+
for (const byte of bytes) num = num << 8n | BigInt(byte);
|
|
2304
|
+
let out = "";
|
|
2305
|
+
while (num > 0n) {
|
|
2306
|
+
out = alphabet[Number(num % base)] + out;
|
|
2307
|
+
num = num / base;
|
|
2308
|
+
}
|
|
2309
|
+
let leadingZeros = 0;
|
|
2310
|
+
for (const byte of bytes) {
|
|
2311
|
+
if (byte === 0) leadingZeros++;
|
|
2312
|
+
else break;
|
|
2313
|
+
}
|
|
2314
|
+
return alphabet[0].repeat(leadingZeros) + out;
|
|
2315
|
+
}
|
|
2316
|
+
function _decodeBaseToBytesMcp(input, alphabet) {
|
|
2317
|
+
const base = BigInt(alphabet.length);
|
|
2318
|
+
let num = 0n;
|
|
2319
|
+
for (const char of input) {
|
|
2320
|
+
const idx = alphabet.indexOf(char);
|
|
2321
|
+
if (idx === -1) return { error: `Invalid character '${char}' for this alphabet` };
|
|
2322
|
+
num = num * base + BigInt(idx);
|
|
2323
|
+
}
|
|
2324
|
+
const bytes = [];
|
|
2325
|
+
while (num > 0n) {
|
|
2326
|
+
bytes.unshift(Number(num % 256n));
|
|
2327
|
+
num = num / 256n;
|
|
2328
|
+
}
|
|
2329
|
+
let leadingZeros = 0;
|
|
2330
|
+
for (const char of input) {
|
|
2331
|
+
if (char === alphabet[0]) leadingZeros++;
|
|
2332
|
+
else break;
|
|
2333
|
+
}
|
|
2334
|
+
return new Uint8Array([...new Array(leadingZeros).fill(0), ...bytes]);
|
|
2335
|
+
}
|
|
2336
|
+
server.tool(
|
|
2337
|
+
"base58_encode",
|
|
2338
|
+
"Encode text as Base58 (Bitcoin-address style, no ambiguous 0/O/I/l characters).",
|
|
2339
|
+
{ input: z.string().describe("The text to encode") },
|
|
2340
|
+
async ({ input }) => text(_encodeBytesToBaseMcp(new TextEncoder().encode(input), _BASE58_ALPHABET_MCP))
|
|
2341
|
+
);
|
|
2342
|
+
server.tool(
|
|
2343
|
+
"base58_decode",
|
|
2344
|
+
"Decode a Base58 string back to text.",
|
|
2345
|
+
{ input: z.string().describe("The Base58 string to decode") },
|
|
2346
|
+
async ({ input }) => {
|
|
2347
|
+
if (!input) return errText({ error: "No input provided" });
|
|
2348
|
+
const result = _decodeBaseToBytesMcp(input, _BASE58_ALPHABET_MCP);
|
|
2349
|
+
if (!(result instanceof Uint8Array)) return errText(result);
|
|
2350
|
+
try {
|
|
2351
|
+
return text(new TextDecoder("utf-8", { fatal: true }).decode(result));
|
|
2352
|
+
} catch {
|
|
2353
|
+
return errText({ error: "Decoded bytes are not valid UTF-8 text" });
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
);
|
|
2357
|
+
server.tool(
|
|
2358
|
+
"base62_encode",
|
|
2359
|
+
"Encode text as Base62 (alphanumeric, URL-safe, no punctuation).",
|
|
2360
|
+
{ input: z.string().describe("The text to encode") },
|
|
2361
|
+
async ({ input }) => text(_encodeBytesToBaseMcp(new TextEncoder().encode(input), _BASE62_ALPHABET_MCP))
|
|
2362
|
+
);
|
|
2363
|
+
server.tool(
|
|
2364
|
+
"base62_decode",
|
|
2365
|
+
"Decode a Base62 string back to text.",
|
|
2366
|
+
{ input: z.string().describe("The Base62 string to decode") },
|
|
2367
|
+
async ({ input }) => {
|
|
2368
|
+
if (!input) return errText({ error: "No input provided" });
|
|
2369
|
+
const result = _decodeBaseToBytesMcp(input, _BASE62_ALPHABET_MCP);
|
|
2370
|
+
if (!(result instanceof Uint8Array)) return errText(result);
|
|
2371
|
+
try {
|
|
2372
|
+
return text(new TextDecoder("utf-8", { fatal: true }).decode(result));
|
|
2373
|
+
} catch {
|
|
2374
|
+
return errText({ error: "Decoded bytes are not valid UTF-8 text" });
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
);
|
|
2270
2378
|
server.tool(
|
|
2271
2379
|
"format_ndjson",
|
|
2272
2380
|
"Validate newline-delimited JSON (NDJSON/JSON Lines), report per-line errors, and pretty-print each valid line.",
|
|
@@ -3120,5 +3228,184 @@ server.tool("diff_env_example", "Diff a .env file against its .env.example templ
|
|
|
3120
3228
|
if ("error" in result) return errText(result);
|
|
3121
3229
|
return text(JSON.stringify(result, null, 2));
|
|
3122
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
|
+
);
|
|
3123
3410
|
var transport = new StdioServerTransport();
|
|
3124
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",
|