@utilix-tech/mcp 0.69.0 → 0.71.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,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.71.0
6
+ - Add `read_midi_info` tool: parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds accounting for tempo changes.
7
+
8
+ ## 0.70.0
9
+ - Add `generate_hotp`, `generate_totp` tools: generate an HOTP code (RFC 4226) for a counter value, or the current TOTP code (RFC 6238) plus seconds until rotation, from a base32-encoded shared secret, supporting SHA-1/SHA-256/SHA-512 and 6-8 digit codes.
10
+ - Add `calculate_tax_equivalent_yield` tool: convert between a tax-free municipal bond yield and its taxable equivalent given a marginal federal (plus optional state) tax rate, in both directions.
11
+
5
12
  ## 0.69.0
6
13
  - Add `parse_commit_trailers` tool: parse a commit message's trailer block (Co-authored-by, Signed-off-by, Fixes, Reviewed-by, etc.) per git's RFC 822-style trailer convention, returning each trailer's key and value plus the body with the trailer block removed.
7
14
  - Add `extract_markdown_footnotes` tool: extract every Markdown-Extra-style footnote reference and definition from a document, flagging references with no matching definition and definitions that are never referenced.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 233 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 237 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
 
@@ -179,6 +179,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
179
179
  | `calculate_time_value` | Convert between present and future value (time value of money) in either direction |
180
180
  | `calculate_percentage_change` | Compute the percentage change between two numbers, with absolute change, direction, and multiplier |
181
181
  | `convert_apr_apy` | Convert between a nominal APR and effective APY for a given compounding frequency, in both directions |
182
+ | `calculate_tax_equivalent_yield` | Convert between a tax-free municipal bond yield and its taxable equivalent given a marginal federal (plus optional state) tax rate, in both directions |
182
183
  | `calculate_margin_markup` | Given any two of cost, price, margin percent, and markup percent, solve for the other two plus gross profit |
183
184
  | `calculate_discount` | Solve a sale price, amount saved, or discount percent from any one of them, or stack successive discounts in order |
184
185
  | `convert_subtitles` | Convert subtitles between SubRip (.srt) and WebVTT (.vtt), with an optional time shift to resync every cue |
@@ -211,6 +212,8 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
211
212
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
212
213
  | `parse_jwks` | Parse a JWKS (or a single bare JWK): type, use, algorithm, key ID, and size per key |
213
214
  | `compute_jwk_thumbprint` | Compute the RFC 7638 canonical thumbprint of a single JWK (SHA-256/384/512) |
215
+ | `generate_hotp` | Generate an HOTP code (RFC 4226) for a counter value from a base32-encoded shared secret |
216
+ | `generate_totp` | Generate the current TOTP code (RFC 6238) from a base32-encoded shared secret, plus seconds until rotation |
214
217
  | `build_curl` | Build a cURL command from method, URL, headers, and body |
215
218
  | `curl_to_code` | Convert a cURL command to fetch, axios, Python, Go, PHP, or Ruby |
216
219
  | `cors_headers` | Generate CORS response headers for a configuration |
@@ -368,12 +371,13 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
368
371
  | `read_pdf_metadata` | Read title, author, dates, page count, and encryption flag from a PDF's trailer/Info dictionary: no PDF rendering library required |
369
372
  | `read_pdf_page_dimensions` | List each page's MediaBox width/height (points and inches), rotation, and orientation, walking the Root -> Pages -> Kids tree: no PDF rendering library required |
370
373
 
371
- ### Audio (3 tools)
374
+ ### Audio (4 tools)
372
375
  | Tool | Description |
373
376
  |------|-------------|
374
377
  | `read_wav_info` | Read sample rate, channel count, bit depth, and duration from a WAV file's RIFF/fmt/data chunk headers: no audio library required |
375
378
  | `read_id3_tags` | Read title, artist, album, year, genre, comment, and track number from an MP3's ID3 tags: no audio library required |
376
379
  | `read_flac_info` | Read sample rate, channels, bit depth, total samples, and duration from a FLAC file's STREAMINFO block, plus Vorbis comment tags if present: no audio library required |
380
+ | `read_midi_info` | Parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds: no MIDI playback or synthesis |
377
381
 
378
382
  ### Video (1 tool)
379
383
  | Tool | Description |
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  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
- import { createHash, randomBytes } from "crypto";
7
+ import { createHash, createHmac, randomBytes } from "crypto";
8
8
  import * as yaml from "js-yaml";
9
9
  import {
10
10
  parseJson,
@@ -8768,6 +8768,139 @@ server.tool(
8768
8768
  return text(JSON.stringify(result, null, 2));
8769
8769
  }
8770
8770
  );
8771
+ function _readU16BEMcp(bytes, offset) {
8772
+ return bytes[offset] << 8 | bytes[offset + 1];
8773
+ }
8774
+ function _readMidiVarLenMcp(bytes, pos) {
8775
+ let value = 0;
8776
+ let i = pos;
8777
+ for (let n = 0; n < 4; n++) {
8778
+ if (i >= bytes.length) return null;
8779
+ const b = bytes[i];
8780
+ value = value << 7 | b & 127;
8781
+ i++;
8782
+ if ((b & 128) === 0) return { value, next: i };
8783
+ }
8784
+ return null;
8785
+ }
8786
+ function _readMidiInfoMcp(bytes) {
8787
+ if (bytes.length < 14) return { error: "File is too small to be a valid MIDI file" };
8788
+ if (Buffer.from(bytes.subarray(0, 4)).toString("ascii") !== "MThd") {
8789
+ return { error: 'Not a valid MIDI file (missing "MThd" header chunk)' };
8790
+ }
8791
+ const headerLength = (bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
8792
+ if (headerLength < 6 || 8 + headerLength > bytes.length) return { error: "Truncated or invalid MThd header chunk" };
8793
+ const format = _readU16BEMcp(bytes, 8);
8794
+ if (format !== 0 && format !== 1 && format !== 2) return { error: `Unsupported MIDI format: ${format} (expected 0, 1, or 2)` };
8795
+ const trackCount = _readU16BEMcp(bytes, 10);
8796
+ if (trackCount < 1) return { error: "MIDI file declares zero tracks" };
8797
+ const divisionRaw = _readU16BEMcp(bytes, 12);
8798
+ const division = (divisionRaw & 32768) !== 0 ? { type: "smpte", framesPerSecond: -(divisionRaw >> 8 << 24 >> 24), ticksPerFrame: divisionRaw & 255 } : { type: "ppq", ticksPerQuarterNote: divisionRaw & 32767 };
8799
+ const tracks = [];
8800
+ const tempoChanges = [];
8801
+ let timeSignature = null;
8802
+ let maxEndTick = 0;
8803
+ let offset = 8 + headerLength;
8804
+ let tracksParsed = 0;
8805
+ while (tracksParsed < trackCount && offset + 8 <= bytes.length) {
8806
+ const chunkId = Buffer.from(bytes.subarray(offset, offset + 4)).toString("ascii");
8807
+ const chunkLength = (bytes[offset + 4] << 24 | bytes[offset + 5] << 16 | bytes[offset + 6] << 8 | bytes[offset + 7]) >>> 0;
8808
+ const dataStart = offset + 8;
8809
+ const dataEnd = Math.min(dataStart + chunkLength, bytes.length);
8810
+ if (chunkId !== "MTrk") {
8811
+ offset = dataEnd;
8812
+ continue;
8813
+ }
8814
+ let pos = dataStart;
8815
+ let runningStatus = 0;
8816
+ let absoluteTick = 0;
8817
+ let eventCount = 0;
8818
+ let noteOnCount = 0;
8819
+ let name = null;
8820
+ while (pos < dataEnd) {
8821
+ const delta = _readMidiVarLenMcp(bytes, pos);
8822
+ if (!delta) break;
8823
+ pos = delta.next;
8824
+ absoluteTick += delta.value;
8825
+ if (pos >= dataEnd) break;
8826
+ let statusByte = bytes[pos];
8827
+ if (statusByte & 128) {
8828
+ runningStatus = statusByte;
8829
+ pos++;
8830
+ }
8831
+ statusByte = runningStatus;
8832
+ eventCount++;
8833
+ if (statusByte === 255) {
8834
+ const metaType = bytes[pos];
8835
+ pos++;
8836
+ const len = _readMidiVarLenMcp(bytes, pos);
8837
+ if (!len) break;
8838
+ pos = len.next;
8839
+ const data = bytes.subarray(pos, Math.min(pos + len.value, dataEnd));
8840
+ pos += len.value;
8841
+ if (metaType === 81 && data.length >= 3) {
8842
+ tempoChanges.push({ tick: absoluteTick, usPerQuarter: data[0] << 16 | data[1] << 8 | data[2] });
8843
+ } else if (metaType === 88 && data.length >= 2) {
8844
+ if (!timeSignature) timeSignature = { numerator: data[0], denominator: 2 ** data[1] };
8845
+ } else if (metaType === 3 && name === null) {
8846
+ name = Buffer.from(data).toString("utf8").trim() || null;
8847
+ } else if (metaType === 47) {
8848
+ break;
8849
+ }
8850
+ } else if (statusByte === 240 || statusByte === 247) {
8851
+ const len = _readMidiVarLenMcp(bytes, pos);
8852
+ if (!len) break;
8853
+ pos = len.next + len.value;
8854
+ } else if (statusByte >= 128) {
8855
+ const type = statusByte & 240;
8856
+ if (type === 192 || type === 208) {
8857
+ pos += 1;
8858
+ } else {
8859
+ if (type === 144 && (bytes[pos + 1] ?? 0) > 0) noteOnCount++;
8860
+ pos += 2;
8861
+ }
8862
+ } else {
8863
+ break;
8864
+ }
8865
+ }
8866
+ tracks.push({ index: tracksParsed, name, eventCount, noteOnCount });
8867
+ maxEndTick = Math.max(maxEndTick, absoluteTick);
8868
+ offset = dataEnd;
8869
+ tracksParsed++;
8870
+ }
8871
+ if (tracks.length === 0) return { error: "No MTrk track chunks found in this MIDI file" };
8872
+ const DEFAULT_US_PER_QUARTER = 5e5;
8873
+ const initialTempoBpm = Math.round(6e7 / (tempoChanges[0]?.usPerQuarter ?? DEFAULT_US_PER_QUARTER) * 100) / 100;
8874
+ let durationSeconds = null;
8875
+ if (division.type === "smpte") {
8876
+ const ticksPerSecond = division.framesPerSecond * division.ticksPerFrame;
8877
+ durationSeconds = ticksPerSecond > 0 ? Math.round(maxEndTick / ticksPerSecond * 1e3) / 1e3 : null;
8878
+ } else {
8879
+ const sorted = [...tempoChanges].sort((a, b) => a.tick - b.tick);
8880
+ let seconds = 0;
8881
+ let prevTick = 0;
8882
+ let currentUsPerQuarter = DEFAULT_US_PER_QUARTER;
8883
+ for (const change of sorted) {
8884
+ seconds += (change.tick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
8885
+ prevTick = change.tick;
8886
+ currentUsPerQuarter = change.usPerQuarter;
8887
+ }
8888
+ seconds += (maxEndTick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
8889
+ durationSeconds = Math.round(seconds * 1e3) / 1e3;
8890
+ }
8891
+ return { format, trackCount: tracks.length, division, initialTempoBpm, timeSignature, durationSeconds, tracks };
8892
+ }
8893
+ server.tool(
8894
+ "read_midi_info",
8895
+ "Parse a Standard MIDI File's header and every track chunk to report format (0/1/2), tick division, initial tempo, time signature, a per-track event/note-on count, and total duration in seconds, without any MIDI playback or synthesis.",
8896
+ { midi_base64: z.string().describe("Base64-encoded MIDI (.mid/.midi) file bytes, optionally prefixed with a data: URL scheme") },
8897
+ async ({ midi_base64 }) => {
8898
+ const bytes = new Uint8Array(Buffer.from(midi_base64.replace(/^data:.*;base64,/, ""), "base64"));
8899
+ const result = _readMidiInfoMcp(bytes);
8900
+ if ("error" in result) return errText(result);
8901
+ return text(JSON.stringify(result, null, 2));
8902
+ }
8903
+ );
8771
8904
  function _calculateCashRoundingMcp(total, roundingUnit, mode = "nearest") {
8772
8905
  const round2 = (n) => Math.round(n * 100) / 100;
8773
8906
  if (typeof total !== "number" || !Number.isFinite(total)) {
@@ -11120,5 +11253,149 @@ server.tool(
11120
11253
  return text(JSON.stringify(result, null, 2));
11121
11254
  }
11122
11255
  );
11256
+ function _calculateTaxEquivalentYieldMcp(inputYieldPercent, federalTaxRatePercent, stateTaxRatePercent, direction) {
11257
+ if (typeof inputYieldPercent !== "number" || !Number.isFinite(inputYieldPercent) || inputYieldPercent < 0) {
11258
+ return { error: "Yield must be zero or a positive number" };
11259
+ }
11260
+ if (typeof federalTaxRatePercent !== "number" || !Number.isFinite(federalTaxRatePercent) || federalTaxRatePercent < 0 || federalTaxRatePercent >= 100) {
11261
+ return { error: "Federal tax rate must be a percentage between 0 and 100 (exclusive of 100)" };
11262
+ }
11263
+ const stateRate = stateTaxRatePercent ?? 0;
11264
+ if (typeof stateRate !== "number" || !Number.isFinite(stateRate) || stateRate < 0 || stateRate >= 100) {
11265
+ return { error: "State tax rate must be a percentage between 0 and 100 (exclusive of 100)" };
11266
+ }
11267
+ if (direction !== "tax-free-to-taxable" && direction !== "taxable-to-tax-free") {
11268
+ return { error: 'Direction must be "tax-free-to-taxable" or "taxable-to-tax-free"' };
11269
+ }
11270
+ const combinedTaxRatePercent = federalTaxRatePercent + stateRate;
11271
+ if (combinedTaxRatePercent >= 100) {
11272
+ return { error: "Combined federal and state tax rate must be less than 100%" };
11273
+ }
11274
+ const round6 = (n) => Math.round(n * 1e6) / 1e6;
11275
+ const combinedRateDecimal = combinedTaxRatePercent / 100;
11276
+ let taxFreeYield;
11277
+ let taxableYield;
11278
+ if (direction === "tax-free-to-taxable") {
11279
+ taxFreeYield = inputYieldPercent;
11280
+ taxableYield = taxFreeYield / (1 - combinedRateDecimal);
11281
+ } else {
11282
+ taxableYield = inputYieldPercent;
11283
+ taxFreeYield = taxableYield * (1 - combinedRateDecimal);
11284
+ }
11285
+ return {
11286
+ direction,
11287
+ inputYield: round6(inputYieldPercent),
11288
+ federalTaxRatePercent: round6(federalTaxRatePercent),
11289
+ stateTaxRatePercent: round6(stateRate),
11290
+ combinedTaxRatePercent: round6(combinedTaxRatePercent),
11291
+ taxFreeYield: round6(taxFreeYield),
11292
+ taxableYield: round6(taxableYield),
11293
+ yieldDifference: round6(taxableYield - taxFreeYield)
11294
+ };
11295
+ }
11296
+ server.tool(
11297
+ "calculate_tax_equivalent_yield",
11298
+ 'Convert between a tax-free municipal bond yield and the equivalent taxable-bond yield given a marginal federal tax rate (plus an optional state rate, summed into a combined rate). With direction "tax-free-to-taxable", taxableYield = taxFreeYield / (1 - combinedRate); with "taxable-to-tax-free", taxFreeYield = taxableYield * (1 - combinedRate). Pure formula, no live bond pricing.',
11299
+ {
11300
+ inputYield: z.number().min(0).describe('The known yield as a percent: the tax-free yield when direction is "tax-free-to-taxable", or the taxable yield when direction is "taxable-to-tax-free"'),
11301
+ federalTaxRatePercent: z.number().min(0).max(99.999).describe("Marginal federal tax rate as a percent"),
11302
+ stateTaxRatePercent: z.number().min(0).max(99.999).optional().describe("Optional marginal state tax rate as a percent, summed with the federal rate"),
11303
+ direction: z.enum(["tax-free-to-taxable", "taxable-to-tax-free"]).describe("Which yield the input represents and which to compute")
11304
+ },
11305
+ async ({ inputYield, federalTaxRatePercent, stateTaxRatePercent, direction }) => {
11306
+ const result = _calculateTaxEquivalentYieldMcp(inputYield, federalTaxRatePercent, stateTaxRatePercent ?? 0, direction);
11307
+ if ("error" in result) return errText(result);
11308
+ return text(JSON.stringify(result, null, 2));
11309
+ }
11310
+ );
11311
+ var _OTP_ALG_TO_NODE_HASH_MCP = { SHA1: "sha1", SHA256: "sha256", SHA512: "sha512" };
11312
+ function _decodeBase32SecretMcp(secret) {
11313
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
11314
+ const cleaned = (secret ?? "").toUpperCase().replace(/[\s=]/g, "");
11315
+ if (!cleaned) return { error: "Secret must not be empty" };
11316
+ const bytes = [];
11317
+ let buffer = 0;
11318
+ let bitsLeft = 0;
11319
+ for (const char of cleaned) {
11320
+ const val = alphabet.indexOf(char);
11321
+ if (val === -1) return { error: `Invalid base32 character '${char}' in secret` };
11322
+ buffer = buffer << 5 | val;
11323
+ bitsLeft += 5;
11324
+ if (bitsLeft >= 8) {
11325
+ bitsLeft -= 8;
11326
+ bytes.push(buffer >> bitsLeft & 255);
11327
+ }
11328
+ }
11329
+ return Buffer.from(bytes);
11330
+ }
11331
+ function _hotpFromCounterMcp(keyBytes, counter, digits, algorithm) {
11332
+ const counterBuffer = Buffer.alloc(8);
11333
+ counterBuffer.writeUInt32BE(Math.floor(counter / 4294967296), 0);
11334
+ counterBuffer.writeUInt32BE(counter % 4294967296, 4);
11335
+ const hmac = createHmac(_OTP_ALG_TO_NODE_HASH_MCP[algorithm], keyBytes).update(counterBuffer).digest();
11336
+ const offset = hmac[hmac.length - 1] & 15;
11337
+ const binCode = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
11338
+ return (binCode % 10 ** digits).toString().padStart(digits, "0");
11339
+ }
11340
+ function _generateHotpMcp(secret, counter, digits, algorithm) {
11341
+ if (!Number.isInteger(counter) || counter < 0) {
11342
+ return { error: "Counter must be a non-negative whole number" };
11343
+ }
11344
+ if (!Number.isInteger(digits) || digits < 6 || digits > 8) {
11345
+ return { error: "Digits must be an integer between 6 and 8" };
11346
+ }
11347
+ if (!(algorithm in _OTP_ALG_TO_NODE_HASH_MCP)) {
11348
+ return { error: "Algorithm must be one of SHA1, SHA256, SHA512" };
11349
+ }
11350
+ const keyBytes = _decodeBase32SecretMcp(secret);
11351
+ if ("error" in keyBytes) return keyBytes;
11352
+ return { code: _hotpFromCounterMcp(keyBytes, counter, digits, algorithm), counter, digits, algorithm };
11353
+ }
11354
+ function _generateTotpMcp(secret, digits, algorithm, periodSeconds, timestamp) {
11355
+ const period = periodSeconds ?? 30;
11356
+ const ts = timestamp ?? Math.floor(Date.now() / 1e3);
11357
+ if (!Number.isFinite(period) || period <= 0) {
11358
+ return { error: "Period must be a positive number of seconds" };
11359
+ }
11360
+ if (!Number.isFinite(ts) || ts < 0) {
11361
+ return { error: "Timestamp must be a non-negative number of seconds" };
11362
+ }
11363
+ const counter = Math.floor(ts / period);
11364
+ const hotp = _generateHotpMcp(secret, counter, digits, algorithm);
11365
+ if ("error" in hotp) return hotp;
11366
+ const secondsRemaining = period - ts % period;
11367
+ return { ...hotp, periodSeconds: period, secondsRemaining };
11368
+ }
11369
+ server.tool(
11370
+ "generate_hotp",
11371
+ "Generate an HOTP code (RFC 4226) for a given counter value from a base32-encoded shared secret. Supports 6-8 digit codes and SHA-1/SHA-256/SHA-512 (SHA-1 is the default per the RFC). The secret is never stored, only used for this one HMAC computation.",
11372
+ {
11373
+ secret: z.string().min(1).describe("The base32-encoded shared secret (RFC 4648, case-insensitive, padding optional)"),
11374
+ counter: z.number().int().min(0).describe("The HOTP counter value"),
11375
+ digits: z.number().int().min(6).max(8).optional().describe("Code length, default 6"),
11376
+ algorithm: z.enum(["SHA1", "SHA256", "SHA512"]).optional().describe("HMAC algorithm, default SHA1")
11377
+ },
11378
+ async ({ secret, counter, digits, algorithm }) => {
11379
+ const result = _generateHotpMcp(secret, counter, digits ?? 6, algorithm ?? "SHA1");
11380
+ if ("error" in result) return errText(result);
11381
+ return text(JSON.stringify(result, null, 2));
11382
+ }
11383
+ );
11384
+ server.tool(
11385
+ "generate_totp",
11386
+ "Generate the current TOTP code (RFC 6238) from a base32-encoded shared secret, plus how many seconds remain before it rotates. Defaults (30s period, 6 digits, SHA-1) match what Google Authenticator and most other authenticator apps use. Pass an explicit timestamp for deterministic/testable output.",
11387
+ {
11388
+ secret: z.string().min(1).describe("The base32-encoded shared secret (RFC 4648, case-insensitive, padding optional)"),
11389
+ digits: z.number().int().min(6).max(8).optional().describe("Code length, default 6"),
11390
+ algorithm: z.enum(["SHA1", "SHA256", "SHA512"]).optional().describe("HMAC algorithm, default SHA1"),
11391
+ periodSeconds: z.number().int().min(1).optional().describe("Time-step window in seconds, default 30"),
11392
+ timestamp: z.number().int().min(0).optional().describe("Unix timestamp in seconds to compute the code for; defaults to the current time")
11393
+ },
11394
+ async ({ secret, digits, algorithm, periodSeconds, timestamp }) => {
11395
+ const result = _generateTotpMcp(secret, digits ?? 6, algorithm ?? "SHA1", periodSeconds ?? 30, timestamp);
11396
+ if ("error" in result) return errText(result);
11397
+ return text(JSON.stringify(result, null, 2));
11398
+ }
11399
+ );
11123
11400
  var transport = new StdioServerTransport();
11124
11401
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "MCP server exposing 198 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",