@utilix-tech/mcp 0.30.0 → 0.32.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 CHANGED
@@ -2,6 +2,14 @@
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.32.0
6
+ - Add `calculate_emi` tool: equated monthly installment, total interest, and a year-by-year principal/interest breakdown from principal, annual rate, and tenure in years or months.
7
+ - Add `parse_markdown_front_matter` tool: split a Markdown document into its YAML front matter fields and body content, parsing scalars, inline arrays, and block lists.
8
+
9
+ ## 0.31.0
10
+ - Add `json_schema_to_typescript` tool: convert a JSON Schema document into TypeScript interfaces/types, reading `required`/`enum`/`oneOf`/`anyOf`/`allOf`/`$ref` directly instead of inferring from an example payload.
11
+ - Add `estimate_password_entropy` tool: bits of entropy from a password's character pool, a strength label, and crack-time estimates across four guess-rate scenarios.
12
+
5
13
  ## 0.30.0
6
14
  - Add `read_pdf_page_dimensions` tool: each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree and inheriting from ancestor nodes, plus a common paper size match (Letter, A4, etc.).
7
15
  - Add `parse_ipv6_address` tool: expand an IPv6 address to its full 8-group form or compress it to its RFC 5952 canonical shortest form, classify its address type, and handle an embedded IPv4 suffix or zone ID.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 146 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 150 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
 
@@ -75,7 +75,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
75
75
 
76
76
  ## Tools
77
77
 
78
- ### JSON (14 tools)
78
+ ### JSON (15 tools)
79
79
  | Tool | Description |
80
80
  |------|-------------|
81
81
  | `format_json` | Pretty-print and validate a JSON string |
@@ -92,6 +92,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
92
92
  | `json_to_go` | Generate Go structs from JSON |
93
93
  | `json_to_python` | Generate Python dataclasses from JSON |
94
94
  | `json_to_zod` | Generate a Zod schema from JSON |
95
+ | `json_schema_to_typescript` | Generate TypeScript interfaces/types from a JSON Schema document (reads required/enum/$ref directly) |
95
96
 
96
97
  ### Encoding (12 tools)
97
98
  | Tool | Description |
@@ -117,12 +118,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
117
118
  | `bcrypt_hash` | Hash a password using bcrypt |
118
119
  | `bcrypt_verify` | Verify a password against a bcrypt hash |
119
120
 
120
- ### Generators (5 tools)
121
+ ### Generators (6 tools)
121
122
  | Tool | Description |
122
123
  |------|-------------|
123
124
  | `generate_uuid` | Generate UUID v4 or v7 |
124
125
  | `generate_ulid` | Generate a ULID |
125
126
  | `generate_password` | Generate a secure random password |
127
+ | `estimate_password_entropy` | Estimate a password's entropy in bits, strength label, and crack-time estimates |
126
128
  | `generate_qr` | Generate a QR code as an SVG string |
127
129
  | `generate_random_data` | Generate random mock data: names, emails, UUIDs, IPs, etc. |
128
130
 
@@ -153,7 +155,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
153
155
  | `convert_timezone` | Convert a date/time between timezones |
154
156
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
155
157
 
156
- ### Units (20 tools)
158
+ ### Units (21 tools)
157
159
  | Tool | Description |
158
160
  |------|-------------|
159
161
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -176,6 +178,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
176
178
  | `calculate_break_even` | Compute the break-even unit count and revenue from fixed costs, variable cost per unit, and price per unit |
177
179
  | `calculate_video_bitrate` | Calculate a video's file size from a bitrate and duration, or the bitrate needed to hit a target file size |
178
180
  | `calculate_rule_of_72` | Estimate years to double an investment from a rate, or the rate needed to double in a given number of years, via the Rule of 72 shortcut and the exact logarithmic result |
181
+ | `calculate_emi` | Compute the Equated Monthly Installment for a loan from principal, annual rate, and tenure in years or months, with a year-by-year principal/interest breakdown |
179
182
 
180
183
  ### API / Network (12 tools)
181
184
  | Tool | Description |
@@ -215,7 +218,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
215
218
  | `parse_github_url` | Parse a github.com URL (repo, pull request, issue, commit, file, or release) into structured components |
216
219
  | `detect_redos` | Statically flag catastrophic-backtracking (ReDoS) risk in a regex: nested quantifiers, overlapping alternation, adjacent overlapping quantifiers |
217
220
 
218
- ### Data (6 tools)
221
+ ### Data (7 tools)
219
222
  | Tool | Description |
220
223
  |------|-------------|
221
224
  | `validate_yaml` | Validate YAML syntax |
@@ -224,6 +227,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
224
227
  | `parse_csv` | Parse a CSV string and return the data as JSON |
225
228
  | `format_ndjson` | Validate and pretty-print newline-delimited JSON (NDJSON) |
226
229
  | `diff_env_example` | Diff a .env file against its .env.example template: flags keys missing from either side, plus empty placeholder values |
230
+ | `parse_markdown_front_matter` | Split a Markdown document into its YAML front matter fields and body content |
227
231
 
228
232
  ### CSS (2 tools)
229
233
  | Tool | Description |
package/dist/index.js CHANGED
@@ -5177,5 +5177,374 @@ server.tool("read_video_info", "Read duration, resolution, and video/audio codec
5177
5177
  if ("error" in result) return errText(result);
5178
5178
  return text(JSON.stringify(result, null, 2));
5179
5179
  });
5180
+ function _tsCapitalizeMcp(str) {
5181
+ if (!str) return str;
5182
+ return str.charAt(0).toUpperCase() + str.slice(1);
5183
+ }
5184
+ function _schemaRefNameMcp(ref) {
5185
+ const parts = ref.split("/");
5186
+ return _tsCapitalizeMcp(parts[parts.length - 1] || "Ref");
5187
+ }
5188
+ function _schemaLiteralTypeMcp(value) {
5189
+ if (typeof value === "string") return JSON.stringify(value);
5190
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
5191
+ if (value === null) return "null";
5192
+ return JSON.stringify(value);
5193
+ }
5194
+ function _safePropertyKeyMcp(key) {
5195
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
5196
+ }
5197
+ function _schemaToTsTypeMcp(schema, name, interfaces) {
5198
+ if (schema.$ref) return _schemaRefNameMcp(schema.$ref);
5199
+ if (schema.const !== void 0) return _schemaLiteralTypeMcp(schema.const);
5200
+ if (schema.enum) return schema.enum.map(_schemaLiteralTypeMcp).join(" | ");
5201
+ if (schema.oneOf) return schema.oneOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Variant${i}`, interfaces)).join(" | ");
5202
+ if (schema.anyOf) return schema.anyOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Variant${i}`, interfaces)).join(" | ");
5203
+ if (schema.allOf) return schema.allOf.map((s, i) => _schemaToTsTypeMcp(s, `${name}Part${i}`, interfaces)).join(" & ");
5204
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
5205
+ const nullable = types.includes("null");
5206
+ const nonNullTypes = types.filter((t) => t !== "null");
5207
+ const primaryType = nonNullTypes[0] ?? (schema.properties || schema.additionalProperties !== void 0 ? "object" : void 0);
5208
+ let result;
5209
+ switch (primaryType) {
5210
+ case "string":
5211
+ result = "string";
5212
+ break;
5213
+ case "number":
5214
+ case "integer":
5215
+ result = "number";
5216
+ break;
5217
+ case "boolean":
5218
+ result = "boolean";
5219
+ break;
5220
+ case "array": {
5221
+ const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
5222
+ const itemType = itemsSchema ? _schemaToTsTypeMcp(itemsSchema, `${name}Item`, interfaces) : "unknown";
5223
+ result = `${itemType}[]`;
5224
+ break;
5225
+ }
5226
+ case "object": {
5227
+ _buildSchemaInterfaceMcp(schema, name, interfaces);
5228
+ result = name;
5229
+ break;
5230
+ }
5231
+ default:
5232
+ result = "unknown";
5233
+ }
5234
+ return nullable ? `${result} | null` : result;
5235
+ }
5236
+ function _buildSchemaInterfaceMcp(schema, name, interfaces) {
5237
+ if (interfaces.has(name)) return;
5238
+ const required = new Set(schema.required ?? []);
5239
+ const lines = [`interface ${name} {`];
5240
+ for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
5241
+ if (propSchema.description) lines.push(` /** ${propSchema.description} */`);
5242
+ const optional = required.has(key) ? "" : "?";
5243
+ const propType = _schemaToTsTypeMcp(propSchema, _tsCapitalizeMcp(key), interfaces);
5244
+ lines.push(` ${_safePropertyKeyMcp(key)}${optional}: ${propType};`);
5245
+ }
5246
+ if (schema.additionalProperties === true) {
5247
+ lines.push(" [key: string]: unknown;");
5248
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
5249
+ const addType = _schemaToTsTypeMcp(schema.additionalProperties, `${name}Value`, interfaces);
5250
+ lines.push(` [key: string]: ${addType};`);
5251
+ }
5252
+ lines.push("}");
5253
+ interfaces.set(name, lines.join("\n"));
5254
+ }
5255
+ function _jsonSchemaToTypescriptMcp(schemaJson, rootName) {
5256
+ let schema;
5257
+ try {
5258
+ schema = JSON.parse(schemaJson);
5259
+ } catch {
5260
+ return { error: "Invalid JSON input" };
5261
+ }
5262
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
5263
+ return { error: "Schema must be a JSON object" };
5264
+ }
5265
+ const typedSchema = schema;
5266
+ const interfaces = /* @__PURE__ */ new Map();
5267
+ const defs = { ...typedSchema.$defs ?? {}, ...typedSchema.definitions ?? {} };
5268
+ for (const [defName, defSchema] of Object.entries(defs)) {
5269
+ const defTypeName = _tsCapitalizeMcp(defName);
5270
+ const defType = _schemaToTsTypeMcp(defSchema, defTypeName, interfaces);
5271
+ if (!interfaces.has(defTypeName)) {
5272
+ interfaces.set(defTypeName, `type ${defTypeName} = ${defType};`);
5273
+ }
5274
+ }
5275
+ const rootType = _schemaToTsTypeMcp(typedSchema, rootName, interfaces);
5276
+ const parts = [...interfaces.values()];
5277
+ if (!interfaces.has(rootName)) {
5278
+ parts.push(`type ${rootName} = ${rootType};`);
5279
+ }
5280
+ return { output: parts.join("\n\n") };
5281
+ }
5282
+ server.tool(
5283
+ "json_schema_to_typescript",
5284
+ "Convert a JSON Schema document into TypeScript interfaces/types. Unlike json_to_typescript (which infers types from an example data payload), this reads the schema's declared type/enum/required keywords directly, so optional vs. required fields and enum/union types come through accurately. Understands local $ref into $defs/definitions, items for arrays, additionalProperties, and renders description as a JSDoc comment.",
5285
+ {
5286
+ schema: z.string().describe("The JSON Schema document, as a JSON string"),
5287
+ root_name: z.string().default("Root").describe("Name for the root generated interface/type")
5288
+ },
5289
+ async ({ schema, root_name }) => {
5290
+ const result = _jsonSchemaToTypescriptMcp(schema, root_name);
5291
+ if ("error" in result) return errText(result);
5292
+ return text(result.output);
5293
+ }
5294
+ );
5295
+ var _SYMBOL_POOL_CHARS_PW = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
5296
+ var _SYMBOL_POOL_SET_PW = new Set(_SYMBOL_POOL_CHARS_PW.split(""));
5297
+ function _hasSymbolCharPw(password) {
5298
+ for (const ch of password) {
5299
+ if (_SYMBOL_POOL_SET_PW.has(ch)) return true;
5300
+ }
5301
+ return false;
5302
+ }
5303
+ function _hasSequentialRunPw(password, minRun) {
5304
+ let ascRun = 1;
5305
+ let descRun = 1;
5306
+ for (let i = 1; i < password.length; i++) {
5307
+ const prev = password.charCodeAt(i - 1);
5308
+ const curr = password.charCodeAt(i);
5309
+ if (curr === prev + 1) {
5310
+ ascRun++;
5311
+ descRun = 1;
5312
+ } else if (curr === prev - 1) {
5313
+ descRun++;
5314
+ ascRun = 1;
5315
+ } else {
5316
+ ascRun = 1;
5317
+ descRun = 1;
5318
+ }
5319
+ if (ascRun >= minRun || descRun >= minRun) return true;
5320
+ }
5321
+ return false;
5322
+ }
5323
+ function _formatCrackDurationPw(seconds) {
5324
+ if (!isFinite(seconds)) return "effectively never";
5325
+ if (seconds < 1) return "instantly";
5326
+ const units = [
5327
+ ["century", 315576e4],
5328
+ ["year", 31557600],
5329
+ ["month", 2629800],
5330
+ ["day", 86400],
5331
+ ["hour", 3600],
5332
+ ["minute", 60],
5333
+ ["second", 1]
5334
+ ];
5335
+ for (const [name, unitSeconds] of units) {
5336
+ if (seconds >= unitSeconds) {
5337
+ const value = seconds / unitSeconds;
5338
+ if (name === "century" && value > 1e3) return "longer than the age of the universe";
5339
+ const rounded = value >= 100 ? Math.round(value) : Math.round(value * 10) / 10;
5340
+ const plural = name === "century" ? "centuries" : `${name}s`;
5341
+ return `${rounded} ${rounded === 1 ? name : plural}`;
5342
+ }
5343
+ }
5344
+ return "instantly";
5345
+ }
5346
+ function _estimatePasswordEntropyMcp(password) {
5347
+ if (!password) return { error: "Password is required" };
5348
+ const hasLowercase = /[a-z]/.test(password);
5349
+ const hasUppercase = /[A-Z]/.test(password);
5350
+ const hasDigits = /[0-9]/.test(password);
5351
+ const hasSymbols = _hasSymbolCharPw(password);
5352
+ const hasOther = /[^\x20-\x7e]/.test(password);
5353
+ let poolSize = 0;
5354
+ if (hasLowercase) poolSize += 26;
5355
+ if (hasUppercase) poolSize += 26;
5356
+ if (hasDigits) poolSize += 10;
5357
+ if (hasSymbols) poolSize += _SYMBOL_POOL_CHARS_PW.length;
5358
+ if (hasOther) poolSize += 100;
5359
+ const entropyBits = Math.round(password.length * Math.log2(poolSize) * 100) / 100;
5360
+ let strength;
5361
+ if (entropyBits < 28) strength = "very-weak";
5362
+ else if (entropyBits < 36) strength = "weak";
5363
+ else if (entropyBits < 60) strength = "fair";
5364
+ else if (entropyBits < 128) strength = "strong";
5365
+ else strength = "very-strong";
5366
+ const averageGuesses = Math.pow(2, entropyBits) / 2;
5367
+ const scenarios = [
5368
+ { scenario: "Online, throttled (100 guesses/hour)", guessesPerSecond: 100 / 3600 },
5369
+ { scenario: "Online, unthrottled (10 guesses/sec)", guessesPerSecond: 10 },
5370
+ { scenario: "Offline, slow hash e.g. bcrypt (10k guesses/sec)", guessesPerSecond: 1e4 },
5371
+ { scenario: "Offline, fast hash / GPU cluster (10B guesses/sec)", guessesPerSecond: 1e10 }
5372
+ ];
5373
+ const crackTimes = scenarios.map((s) => ({
5374
+ scenario: s.scenario,
5375
+ guessesPerSecond: s.guessesPerSecond,
5376
+ humanReadable: _formatCrackDurationPw(averageGuesses / s.guessesPerSecond)
5377
+ }));
5378
+ const warnings = [];
5379
+ if (password.length < 8) {
5380
+ warnings.push("Shorter than 8 characters \u2014 most sites and cracking tools treat this as trivially weak regardless of entropy.");
5381
+ }
5382
+ if (/(.)\1{2,}/.test(password)) {
5383
+ warnings.push('Contains a repeated character run (e.g. "aaa"), which is easier to guess than the entropy figure implies.');
5384
+ }
5385
+ if (_hasSequentialRunPw(password, 3)) {
5386
+ warnings.push('Contains a sequential run (e.g. "abc" or "321"), which is easier to guess than the entropy figure implies.');
5387
+ }
5388
+ if (poolSize <= 26) {
5389
+ warnings.push("Uses only one character class \u2014 mixing in uppercase, digits, or symbols raises the character pool and entropy.");
5390
+ }
5391
+ return {
5392
+ length: password.length,
5393
+ charset: { hasLowercase, hasUppercase, hasDigits, hasSymbols, hasOther, poolSize },
5394
+ entropyBits,
5395
+ strength,
5396
+ crackTimes,
5397
+ warnings
5398
+ };
5399
+ }
5400
+ server.tool(
5401
+ "estimate_password_entropy",
5402
+ "Estimate a password's entropy in bits from the character classes it actually draws from (lowercase, uppercase, digits, symbols, other/Unicode), using the pool-size x length method. Returns a strength label and crack-time estimates at four guess-rate scenarios, plus warnings for repeated runs, sequential runs, short length, and single-character-class passwords. Measures resistance to brute force over the theoretical character space, not real-world guessability against a breach database.",
5403
+ { password: z.string().describe("The password to analyze") },
5404
+ async ({ password }) => {
5405
+ const result = _estimatePasswordEntropyMcp(password);
5406
+ if ("error" in result) return errText(result);
5407
+ return text(JSON.stringify(result, null, 2));
5408
+ }
5409
+ );
5410
+ function _calculateEmiMcp(principal, annualRatePercent, tenure, tenureUnit) {
5411
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
5412
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
5413
+ return { error: "Annual interest rate must be zero or a positive number" };
5414
+ }
5415
+ if (tenureUnit !== "years" && tenureUnit !== "months") {
5416
+ return { error: 'tenureUnit must be "years" or "months"' };
5417
+ }
5418
+ if (!Number.isFinite(tenure) || tenure <= 0) return { error: "Tenure must be a positive number" };
5419
+ const tenureMonths = Math.round(tenureUnit === "years" ? tenure * 12 : tenure);
5420
+ if (tenureMonths <= 0) return { error: "Tenure must resolve to at least 1 month" };
5421
+ const monthlyRate = annualRatePercent / 100 / 12;
5422
+ let emi;
5423
+ if (monthlyRate === 0) {
5424
+ emi = principal / tenureMonths;
5425
+ } else {
5426
+ const factor = Math.pow(1 + monthlyRate, tenureMonths);
5427
+ emi = principal * monthlyRate * factor / (factor - 1);
5428
+ }
5429
+ const yearlyBreakdown = [];
5430
+ let balance = _roundMoneyMcp(principal);
5431
+ let totalPayment = 0;
5432
+ let yearPrincipal = 0;
5433
+ let yearInterest = 0;
5434
+ let year = 1;
5435
+ for (let month = 1; month <= tenureMonths; month++) {
5436
+ const interest = monthlyRate === 0 ? 0 : _roundMoneyMcp(balance * monthlyRate);
5437
+ let principalPortion = _roundMoneyMcp(emi) - interest;
5438
+ if (month === tenureMonths || principalPortion > balance) principalPortion = balance;
5439
+ const payment = _roundMoneyMcp(principalPortion + interest);
5440
+ balance = Math.max(_roundMoneyMcp(balance - principalPortion), 0);
5441
+ totalPayment += payment;
5442
+ yearPrincipal += principalPortion;
5443
+ yearInterest += interest;
5444
+ const isYearEnd = month % 12 === 0 || month === tenureMonths;
5445
+ if (isYearEnd) {
5446
+ yearlyBreakdown.push({ year, principalPaid: _roundMoneyMcp(yearPrincipal), interestPaid: _roundMoneyMcp(yearInterest), balance });
5447
+ year += 1;
5448
+ yearPrincipal = 0;
5449
+ yearInterest = 0;
5450
+ }
5451
+ }
5452
+ return {
5453
+ emi: _roundMoneyMcp(emi),
5454
+ totalInterest: _roundMoneyMcp(totalPayment - principal),
5455
+ totalPayment: _roundMoneyMcp(totalPayment),
5456
+ tenureMonths,
5457
+ yearlyBreakdown
5458
+ };
5459
+ }
5460
+ server.tool(
5461
+ "calculate_emi",
5462
+ "Compute the Equated Monthly Installment (EMI) for a loan given principal, annual interest rate (as a percent), and tenure in years or months. Same reducing-balance math as calculate_loan, but reports a year-by-year principal/interest breakdown instead of a full monthly schedule.",
5463
+ {
5464
+ principal: z.number().describe("Loan principal amount"),
5465
+ annualRatePercent: z.number().describe("Annual interest rate as a percent, e.g. 8.5"),
5466
+ tenure: z.number().describe("Loan tenure, in the unit given by tenureUnit"),
5467
+ tenureUnit: z.enum(["years", "months"]).optional().describe('Unit for the tenure field, defaults to "years"')
5468
+ },
5469
+ async ({ principal, annualRatePercent, tenure, tenureUnit }) => {
5470
+ const result = _calculateEmiMcp(principal, annualRatePercent, tenure, tenureUnit ?? "years");
5471
+ if ("error" in result) return errText(result);
5472
+ return text(JSON.stringify(result, null, 2));
5473
+ }
5474
+ );
5475
+ function _parseFrontMatterScalarMcp(value) {
5476
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
5477
+ return value.slice(1, -1);
5478
+ }
5479
+ if (value === "true") return true;
5480
+ if (value === "false") return false;
5481
+ if (value === "null" || value === "~") return null;
5482
+ if (value !== "" && !Number.isNaN(Number(value))) return Number(value);
5483
+ return value;
5484
+ }
5485
+ function _parseFrontMatterMcp(input) {
5486
+ if (typeof input !== "string" || input.length === 0) return { error: "input is required" };
5487
+ const normalized = input.replace(/\r\n/g, "\n");
5488
+ if (!normalized.startsWith("---\n") && normalized !== "---") {
5489
+ return { frontMatter: {}, body: input, hasFrontMatter: false, raw: "" };
5490
+ }
5491
+ const closingIdx = normalized.indexOf("\n---", 4);
5492
+ if (closingIdx === -1) {
5493
+ return { error: 'Front matter opening delimiter found but no closing "---" delimiter' };
5494
+ }
5495
+ const raw = normalized.slice(4, closingIdx);
5496
+ let afterClose = normalized.slice(closingIdx + 4);
5497
+ if (afterClose.startsWith("\n")) afterClose = afterClose.slice(1);
5498
+ const frontMatter = {};
5499
+ const rawLines = raw.split("\n");
5500
+ let i = 0;
5501
+ while (i < rawLines.length) {
5502
+ const line = rawLines[i];
5503
+ if (line.trim() === "" || line.trim().startsWith("#")) {
5504
+ i++;
5505
+ continue;
5506
+ }
5507
+ const colonIdx = line.indexOf(":");
5508
+ if (colonIdx === -1) return { error: `Invalid front matter line ${i + 1}: expected "key: value"` };
5509
+ const key = line.slice(0, colonIdx).trim();
5510
+ if (!key) return { error: `Invalid front matter line ${i + 1}: empty key` };
5511
+ const rest = line.slice(colonIdx + 1).trim();
5512
+ if (rest === "") {
5513
+ const listItems = [];
5514
+ let j = i + 1;
5515
+ while (j < rawLines.length && /^\s*-\s+/.test(rawLines[j])) {
5516
+ listItems.push(rawLines[j].replace(/^\s*-\s+/, "").trim());
5517
+ j++;
5518
+ }
5519
+ if (listItems.length > 0) {
5520
+ frontMatter[key] = listItems.map(_parseFrontMatterScalarMcp);
5521
+ i = j;
5522
+ continue;
5523
+ }
5524
+ frontMatter[key] = null;
5525
+ i++;
5526
+ continue;
5527
+ }
5528
+ if (rest.startsWith("[") && rest.endsWith("]")) {
5529
+ const inner = rest.slice(1, -1).trim();
5530
+ frontMatter[key] = inner === "" ? [] : inner.split(",").map((s) => _parseFrontMatterScalarMcp(s.trim()));
5531
+ i++;
5532
+ continue;
5533
+ }
5534
+ frontMatter[key] = _parseFrontMatterScalarMcp(rest);
5535
+ i++;
5536
+ }
5537
+ return { frontMatter, body: afterClose, hasFrontMatter: true, raw };
5538
+ }
5539
+ server.tool(
5540
+ "parse_markdown_front_matter",
5541
+ "Split a Markdown document into its YAML front matter (delimited by --- lines at the top) and body content, parsing simple scalar, list, and inline-array front matter values. Not a full YAML parser: nested mappings and multi-line block scalars are not supported.",
5542
+ { input: z.string().describe("The Markdown document contents") },
5543
+ async ({ input }) => {
5544
+ const result = _parseFrontMatterMcp(input);
5545
+ if ("error" in result) return errText(result);
5546
+ return text(JSON.stringify(result, null, 2));
5547
+ }
5548
+ );
5180
5549
  var transport = new StdioServerTransport();
5181
5550
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "MCP server exposing 143 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",