@utilix-tech/mcp 0.62.0 → 0.64.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.64.0
6
+ - Add `calculate_loan_refinance` tool: compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs.
7
+ - Add `parse_multipart_form_data` tool: parse a raw multipart/form-data request body into its individual parts (field name, filename, headers, decoded value) per RFC 7578, with an optional Content-Type header value to extract the boundary from.
8
+
9
+ ## 0.63.0
10
+ - Add `plan_debt_payoff` tool: simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest.
11
+ - Add `audit_dependency_ranges` tool: classify each dependency's version specifier range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections.
12
+
5
13
  ## 0.62.0
6
14
  - Add `build_priority_header`, `parse_priority_header` tools: build an HTTP Priority request/response-header value from an urgency (0-7) and/or an incremental flag per RFC 9218, or parse an existing one into the same shape.
7
15
  - Add `lookup_close_code`, `list_close_codes` tools: look up what a WebSocket close code means per RFC 6455 and the IANA reserved ranges, or list every well-known code.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/mcp
2
2
 
3
- MCP server exposing 216 developer utility tools to Claude, Cursor, VS Code Copilot, and any [Model Context Protocol](https://modelcontextprotocol.io) compatible AI assistant.
3
+ MCP server exposing 220 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
 
@@ -162,7 +162,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
162
162
  | `convert_timezone` | Convert a date/time between timezones |
163
163
  | `convert_iso8601_duration` | Parse an ISO 8601 duration into components and total seconds, or format seconds into a duration string |
164
164
 
165
- ### Units (37 tools)
165
+ ### Units (39 tools)
166
166
  | Tool | Description |
167
167
  |------|-------------|
168
168
  | `convert_bytes` | Convert between B, KB, MB, GB, TB, KiB, MiB, GiB, TiB |
@@ -202,8 +202,10 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
202
202
  | `calculate_blended_loan_rate` | Compute the single weighted-average interest rate across a list of loan balances, weighted by each loan's share of the combined balance, plus combined monthly and annual interest accrual |
203
203
  | `calculate_freelancer_rate` | Solve for the minimum hourly rate a freelancer must charge to hit a target annual income, given billable hours, weeks worked, overhead, and a profit margin |
204
204
  | `calculate_extra_payment_payoff` | Compute how much faster a fixed-rate loan pays off, and how much interest is saved, when a fixed extra amount is added to every monthly payment |
205
+ | `plan_debt_payoff` | Simulate paying off a list of debts under both the snowball (smallest balance first) and avalanche (highest rate first) strategies given a fixed extra monthly payment, and compare months to payoff and total interest |
206
+ | `calculate_loan_refinance` | Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative savings recoup the closing costs |
205
207
 
206
- ### API / Network (37 tools)
208
+ ### API / Network (38 tools)
207
209
  | Tool | Description |
208
210
  |------|-------------|
209
211
  | `decode_jwt` | Decode a JWT: header, payload, and expiry |
@@ -245,6 +247,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
245
247
  | `parse_priority_header` | Parse an existing Priority header value into its urgency and incremental fields, with spec defaults applied |
246
248
  | `lookup_close_code` | Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges |
247
249
  | `list_close_codes` | List every well-known WebSocket close code (1000-1015) |
250
+ | `parse_multipart_form_data` | Parse a raw multipart/form-data request body into its individual parts: field name, filename, headers, and decoded value, per RFC 7578 |
248
251
 
249
252
  ### Color (4 tools)
250
253
  | Tool | Description |
@@ -254,7 +257,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
254
257
  | `color_palette` | Generate a color palette from a base color |
255
258
  | `color_shades` | Generate shades or tints from a base color |
256
259
 
257
- ### Code (15 tools)
260
+ ### Code (16 tools)
258
261
  | Tool | Description |
259
262
  |------|-------------|
260
263
  | `format_sql` | Format a SQL query with proper indentation |
@@ -272,6 +275,7 @@ claude mcp add utilix -- npx -y @utilix-tech/mcp
272
275
  | `validate_editorconfig` | Validate an .editorconfig file: root flag, glob sections, and structural mistakes against the EditorConfig spec |
273
276
  | `validate_tsconfig` | Validate a tsconfig.json file (JSONC-aware): unknown keys, unknown compilerOptions, and type mismatches |
274
277
  | `validate_package_exports` | Validate a package.json "exports" field: dual-package (ESM/CJS) misconfigurations like a missing default fallback or a require/import pointing at the wrong extension |
278
+ | `audit_dependency_ranges` | Classify each package.json dependency's version range type and flag wildcard ranges, the "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections |
275
279
 
276
280
  ### Data (14 tools)
277
281
  | Tool | Description |
package/dist/index.js CHANGED
@@ -9968,5 +9968,359 @@ server.tool(
9968
9968
  return text(JSON.stringify({ codes }, null, 2));
9969
9969
  }
9970
9970
  );
9971
+ var _DEBT_PAYOFF_MAX_MONTHS_MCP = 1200;
9972
+ function _simulateDebtPayoffMcp(debts, order, extraMonthlyPayment) {
9973
+ const round2 = (n) => Math.round(n * 100) / 100;
9974
+ const balances = debts.map((d) => d.balance);
9975
+ const totalMinPayment = debts.reduce((sum, d) => sum + d.minPayment, 0);
9976
+ const totalBudget = totalMinPayment + extraMonthlyPayment;
9977
+ let totalInterestPaid = 0;
9978
+ let totalPaid = 0;
9979
+ const schedule = [];
9980
+ let month = 0;
9981
+ while (balances.some((b) => b > 5e-3) && month < _DEBT_PAYOFF_MAX_MONTHS_MCP) {
9982
+ month++;
9983
+ for (let i = 0; i < balances.length; i++) {
9984
+ if (balances[i] <= 0) continue;
9985
+ const interest = balances[i] * debts[i].ratePercent / 100 / 12;
9986
+ balances[i] += interest;
9987
+ totalInterestPaid += interest;
9988
+ }
9989
+ let remainingBudget = totalBudget;
9990
+ for (let i = 0; i < balances.length; i++) {
9991
+ if (balances[i] <= 0) continue;
9992
+ const pay = Math.min(balances[i], debts[i].minPayment);
9993
+ balances[i] -= pay;
9994
+ remainingBudget -= pay;
9995
+ totalPaid += pay;
9996
+ }
9997
+ for (const i of order) {
9998
+ if (remainingBudget <= 0) break;
9999
+ if (balances[i] <= 0) continue;
10000
+ const pay = Math.min(balances[i], remainingBudget);
10001
+ balances[i] -= pay;
10002
+ remainingBudget -= pay;
10003
+ totalPaid += pay;
10004
+ }
10005
+ for (const i of order) {
10006
+ if (balances[i] <= 5e-3 && !schedule.some((s) => s.label === debts[i].label)) {
10007
+ schedule.push({ label: debts[i].label, monthPaidOff: month });
10008
+ }
10009
+ }
10010
+ }
10011
+ if (balances.some((b) => b > 5e-3)) return null;
10012
+ return {
10013
+ order: order.map((i) => debts[i].label),
10014
+ monthsToPayoff: month,
10015
+ totalInterestPaid: round2(totalInterestPaid),
10016
+ totalPaid: round2(totalPaid),
10017
+ schedule
10018
+ };
10019
+ }
10020
+ function _planDebtPayoffMcp(debtsInput, extraMonthlyPayment) {
10021
+ if (!Array.isArray(debtsInput) || debtsInput.length === 0) {
10022
+ return { error: "debts must be a non-empty array" };
10023
+ }
10024
+ if (typeof extraMonthlyPayment !== "number" || !Number.isFinite(extraMonthlyPayment) || extraMonthlyPayment < 0) {
10025
+ return { error: "extraMonthlyPayment must be a finite, non-negative number" };
10026
+ }
10027
+ const normalized = [];
10028
+ for (let i = 0; i < debtsInput.length; i++) {
10029
+ const d = debtsInput[i];
10030
+ if (typeof d.balance !== "number" || !Number.isFinite(d.balance) || d.balance <= 0) {
10031
+ return { error: `debt ${i + 1}: balance must be a finite number greater than zero` };
10032
+ }
10033
+ if (typeof d.ratePercent !== "number" || !Number.isFinite(d.ratePercent) || d.ratePercent < 0) {
10034
+ return { error: `debt ${i + 1}: ratePercent must be a finite, non-negative number` };
10035
+ }
10036
+ if (typeof d.minPayment !== "number" || !Number.isFinite(d.minPayment) || d.minPayment <= 0) {
10037
+ return { error: `debt ${i + 1}: minPayment must be a finite number greater than zero` };
10038
+ }
10039
+ normalized.push({ label: d.label?.trim() || `Debt ${i + 1}`, balance: d.balance, ratePercent: d.ratePercent, minPayment: d.minPayment });
10040
+ }
10041
+ const snowballOrder = normalized.map((_, i) => i).sort((a, b) => normalized[a].balance - normalized[b].balance);
10042
+ const avalancheOrder = normalized.map((_, i) => i).sort((a, b) => normalized[b].ratePercent - normalized[a].ratePercent);
10043
+ const snowball = _simulateDebtPayoffMcp(normalized, snowballOrder, extraMonthlyPayment);
10044
+ const avalanche = _simulateDebtPayoffMcp(normalized, avalancheOrder, extraMonthlyPayment);
10045
+ if (!snowball || !avalanche) {
10046
+ return { error: `debts cannot be paid off within ${_DEBT_PAYOFF_MAX_MONTHS_MCP} months at this payment level; increase a minimum payment or the extra monthly payment` };
10047
+ }
10048
+ const round2 = (n) => Math.round(n * 100) / 100;
10049
+ const snowballMonths = snowball.monthsToPayoff;
10050
+ const avalancheMonths = avalanche.monthsToPayoff;
10051
+ const snowballInterest = snowball.totalInterestPaid;
10052
+ const avalancheInterest = avalanche.totalInterestPaid;
10053
+ const fasterStrategy = snowballMonths === avalancheMonths ? "tie" : snowballMonths < avalancheMonths ? "snowball" : "avalanche";
10054
+ const cheaperStrategy = snowballInterest === avalancheInterest ? "tie" : snowballInterest < avalancheInterest ? "snowball" : "avalanche";
10055
+ return {
10056
+ debts: normalized,
10057
+ totalMonthlyBudget: round2(normalized.reduce((sum, d) => sum + d.minPayment, 0) + extraMonthlyPayment),
10058
+ extraMonthlyPayment,
10059
+ snowball,
10060
+ avalanche,
10061
+ comparison: {
10062
+ fasterStrategy,
10063
+ monthsDifference: Math.abs(snowballMonths - avalancheMonths),
10064
+ cheaperStrategy,
10065
+ interestDifference: round2(Math.abs(snowballInterest - avalancheInterest))
10066
+ }
10067
+ };
10068
+ }
10069
+ server.tool(
10070
+ "plan_debt_payoff",
10071
+ "Simulate paying off a list of debts under both the snowball strategy (smallest balance first) and the avalanche strategy (highest interest rate first), given a fixed extra monthly payment stacked on top of everyone's minimum payment, and compare months to payoff and total interest between the two.",
10072
+ {
10073
+ debts: z.array(z.object({
10074
+ label: z.string().optional().describe("Optional debt label"),
10075
+ balance: z.number().positive().describe("Current balance owed"),
10076
+ ratePercent: z.number().min(0).describe("Annual interest rate for this debt, as a percent"),
10077
+ minPayment: z.number().positive().describe("Minimum monthly payment required on this debt")
10078
+ })).min(1).describe("A non-empty list of debts"),
10079
+ extraMonthlyPayment: z.number().min(0).optional().describe("Extra amount paid each month on top of every debt's minimum payment, default 0")
10080
+ },
10081
+ async ({ debts, extraMonthlyPayment }) => {
10082
+ const result = _planDebtPayoffMcp(debts, extraMonthlyPayment ?? 0);
10083
+ if ("error" in result) return errText(result);
10084
+ return text(JSON.stringify(result, null, 2));
10085
+ }
10086
+ );
10087
+ var _DEPENDENCY_SECTIONS_MCP = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
10088
+ var _EXACT_SEMVER_RE_MCP = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
10089
+ function _classifyDependencyRangeMcp(version) {
10090
+ const v = version.trim();
10091
+ if (v === "" || v === "*" || v.toLowerCase() === "x") return "wildcard";
10092
+ if (v.startsWith("workspace:")) return "workspace";
10093
+ if (v.startsWith("file:")) return "file";
10094
+ if (v.startsWith("link:")) return "link";
10095
+ if (v.startsWith("http://") || v.startsWith("https://")) return "url";
10096
+ if (v.startsWith("git+") || v.startsWith("git://") || v.startsWith("github:") || v.endsWith(".git") || /^[\w-]+\/[\w.-]+$/.test(v)) return "git";
10097
+ if (v.startsWith("^")) return "caret";
10098
+ if (v.startsWith("~")) return "tilde";
10099
+ if (v.includes("||") || v.includes(" ") || v.startsWith(">") || v.startsWith("<") || v.startsWith("=")) return "range";
10100
+ if (_EXACT_SEMVER_RE_MCP.test(v)) return "exact";
10101
+ if (/^[A-Za-z][\w.-]*$/.test(v)) return "tag";
10102
+ return "unknown";
10103
+ }
10104
+ function _auditDependencyRangesMcp(packageJsonText) {
10105
+ if (!packageJsonText || !packageJsonText.trim()) return { error: "Input is empty" };
10106
+ let parsed;
10107
+ try {
10108
+ parsed = JSON.parse(packageJsonText);
10109
+ } catch (e) {
10110
+ return { error: `Invalid JSON: ${e.message}` };
10111
+ }
10112
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10113
+ return { error: "Root of package.json must be an object" };
10114
+ }
10115
+ const obj = parsed;
10116
+ const presentSections = _DEPENDENCY_SECTIONS_MCP.filter((s) => obj[s] && typeof obj[s] === "object");
10117
+ if (presentSections.length === 0) {
10118
+ return { error: "package.json has no dependencies, devDependencies, peerDependencies, or optionalDependencies field" };
10119
+ }
10120
+ const dependencies = [];
10121
+ const issues = [];
10122
+ const byName = /* @__PURE__ */ new Map();
10123
+ for (const section of presentSections) {
10124
+ const map = obj[section];
10125
+ for (const [name, versionRaw] of Object.entries(map)) {
10126
+ const version = typeof versionRaw === "string" ? versionRaw : String(versionRaw);
10127
+ const rangeType = _classifyDependencyRangeMcp(version);
10128
+ const entryIssues = [];
10129
+ if (rangeType === "wildcard") {
10130
+ entryIssues.push("no real version constraint; any version, including breaking ones, could be installed");
10131
+ }
10132
+ if (rangeType === "tag" && version.trim().toLowerCase() === "latest") {
10133
+ entryIssues.push('"latest" is not reproducible; a fresh install can pull in a different, potentially breaking version at any time');
10134
+ }
10135
+ if (rangeType === "git" || rangeType === "url") {
10136
+ entryIssues.push(`not resolved from the npm registry (${rangeType}); can't be checked against registry advisories the same way`);
10137
+ }
10138
+ if (rangeType === "file" || rangeType === "link") {
10139
+ entryIssues.push(`local ${rangeType === "file" ? "path" : "symlink"} dependency; not portable to a machine or CI runner without the same local path`);
10140
+ }
10141
+ const entry = { name, version, section, rangeType, issues: entryIssues };
10142
+ dependencies.push(entry);
10143
+ for (const issue of entryIssues) {
10144
+ issues.push({ severity: "warning", name, message: issue });
10145
+ }
10146
+ const existing = byName.get(name);
10147
+ if (existing) existing.push(entry);
10148
+ else byName.set(name, [entry]);
10149
+ }
10150
+ }
10151
+ const duplicates = [];
10152
+ for (const [name, entries] of byName) {
10153
+ if (entries.length < 2) continue;
10154
+ const versions = new Set(entries.map((e) => e.version));
10155
+ const sameVersion = versions.size === 1;
10156
+ duplicates.push({ name, sections: entries.map((e) => e.section), sameVersion });
10157
+ if (!sameVersion) {
10158
+ issues.push({ severity: "error", name, message: `listed with different version ranges in ${entries.map((e) => `${e.section} ("${e.version}")`).join(" and ")}` });
10159
+ } else {
10160
+ issues.push({ severity: "warning", name, message: `listed in more than one section (${entries.map((e) => e.section).join(", ")}) with the same range; redundant` });
10161
+ }
10162
+ }
10163
+ const rangeTypeCounts = {};
10164
+ for (const dep of dependencies) {
10165
+ const rt = dep.rangeType;
10166
+ rangeTypeCounts[rt] = (rangeTypeCounts[rt] ?? 0) + 1;
10167
+ }
10168
+ return {
10169
+ parsed: true,
10170
+ sections: presentSections,
10171
+ totalDependencies: dependencies.length,
10172
+ dependencies,
10173
+ duplicates,
10174
+ rangeTypeCounts,
10175
+ issues
10176
+ };
10177
+ }
10178
+ server.tool(
10179
+ "audit_dependency_ranges",
10180
+ `Parse a package.json's dependencies, devDependencies, peerDependencies, and optionalDependencies fields, classify each version specifier's range type, and flag wildcard ranges, the non-reproducible "latest" tag, git/URL/file/link dependencies, and packages listed with conflicting or redundant ranges across sections.`,
10181
+ {
10182
+ text: z.string().min(1).describe("The full package.json file contents")
10183
+ },
10184
+ async ({ text: packageJsonText }) => {
10185
+ const result = _auditDependencyRangesMcp(packageJsonText);
10186
+ if ("error" in result) return errText(result);
10187
+ return text(JSON.stringify(result, null, 2));
10188
+ }
10189
+ );
10190
+ function _calculateLoanForRefinanceMcp(principal, annualRatePercent, termMonths) {
10191
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
10192
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
10193
+ return { error: "Annual interest rate must be zero or a positive number" };
10194
+ }
10195
+ if (!Number.isInteger(termMonths) || termMonths <= 0) {
10196
+ return { error: "Term must be a positive whole number of months" };
10197
+ }
10198
+ const monthlyRate = annualRatePercent / 100 / 12;
10199
+ let monthlyPayment;
10200
+ if (monthlyRate === 0) {
10201
+ monthlyPayment = principal / termMonths;
10202
+ } else {
10203
+ const factor = Math.pow(1 + monthlyRate, termMonths);
10204
+ monthlyPayment = principal * monthlyRate * factor / (factor - 1);
10205
+ }
10206
+ let balance = _roundMoneyMcp(principal);
10207
+ let totalPayment = 0;
10208
+ for (let month = 1; month <= termMonths; month++) {
10209
+ const interest = monthlyRate === 0 ? 0 : _roundMoneyMcp(balance * monthlyRate);
10210
+ let principalPortion = _roundMoneyMcp(monthlyPayment) - interest;
10211
+ if (month === termMonths || principalPortion > balance) principalPortion = balance;
10212
+ const payment = _roundMoneyMcp(principalPortion + interest);
10213
+ balance = _roundMoneyMcp(balance - principalPortion);
10214
+ totalPayment += payment;
10215
+ }
10216
+ return {
10217
+ monthlyPayment: _roundMoneyMcp(monthlyPayment),
10218
+ totalPayment: _roundMoneyMcp(totalPayment),
10219
+ totalInterest: _roundMoneyMcp(totalPayment - principal),
10220
+ termMonths
10221
+ };
10222
+ }
10223
+ function _calculateLoanRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts) {
10224
+ const current = _calculateLoanForRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths);
10225
+ if ("error" in current) return current;
10226
+ const refinanced = _calculateLoanForRefinanceMcp(currentBalance, newRatePercent, newTermMonths);
10227
+ if ("error" in refinanced) return refinanced;
10228
+ if (!Number.isFinite(closingCosts) || closingCosts < 0) {
10229
+ return { error: "Closing costs must be zero or a positive number" };
10230
+ }
10231
+ const monthlySavings = _roundMoneyMcp(current.monthlyPayment - refinanced.monthlyPayment);
10232
+ const breakEvenMonths = monthlySavings > 0 ? Math.ceil(closingCosts / monthlySavings) : null;
10233
+ return {
10234
+ currentMonthlyPayment: current.monthlyPayment,
10235
+ newMonthlyPayment: refinanced.monthlyPayment,
10236
+ monthlySavings,
10237
+ closingCosts: _roundMoneyMcp(closingCosts),
10238
+ breakEvenMonths,
10239
+ currentTotalInterest: current.totalInterest,
10240
+ newTotalInterest: refinanced.totalInterest,
10241
+ totalInterestSavings: _roundMoneyMcp(current.totalInterest - refinanced.totalInterest)
10242
+ };
10243
+ }
10244
+ server.tool(
10245
+ "calculate_loan_refinance",
10246
+ "Compare a current loan's remaining payments against a new refinanced loan on the same outstanding balance, and compute the monthly savings and the break-even month where cumulative monthly savings recoup the closing costs.",
10247
+ {
10248
+ currentBalance: z.number().describe("Outstanding balance on the current loan"),
10249
+ currentRatePercent: z.number().describe("Current loan's annual interest rate as a percent, e.g. 6.5"),
10250
+ currentRemainingMonths: z.number().int().describe("Months remaining on the current loan"),
10251
+ newRatePercent: z.number().describe("New (refinanced) loan's annual interest rate as a percent, e.g. 5.5"),
10252
+ newTermMonths: z.number().int().describe("New loan's term in months"),
10253
+ closingCosts: z.number().describe("Total closing costs to refinance")
10254
+ },
10255
+ async ({ currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts }) => {
10256
+ const result = _calculateLoanRefinanceMcp(currentBalance, currentRatePercent, currentRemainingMonths, newRatePercent, newTermMonths, closingCosts);
10257
+ if ("error" in result) return errText(result);
10258
+ return text(JSON.stringify(result, null, 2));
10259
+ }
10260
+ );
10261
+ function _extractMultipartBoundaryMcp(contentType) {
10262
+ if (!contentType) return null;
10263
+ const match = contentType.match(/boundary=(?:"([^"]*)"|([^;\s]+))/i);
10264
+ if (!match) return null;
10265
+ return match[1] ?? match[2] ?? null;
10266
+ }
10267
+ function _parseMultipartFormDataMcp(body, boundary) {
10268
+ if (!body || !body.trim()) return { error: "Body is empty" };
10269
+ if (!boundary || !boundary.trim()) return { error: "Boundary is required" };
10270
+ const normalized = body.replace(/\r\n/g, "\n");
10271
+ const delimiter = `--${boundary}`;
10272
+ const segments = normalized.split(delimiter);
10273
+ if (segments.length < 3) {
10274
+ return { error: `No parts found for boundary "${boundary}"; check that it matches the boundary used in the body` };
10275
+ }
10276
+ const parts = [];
10277
+ for (let i = 1; i < segments.length; i++) {
10278
+ let segment = segments[i];
10279
+ if (segment.startsWith("--")) break;
10280
+ segment = segment.replace(/^\n/, "").replace(/\n$/, "");
10281
+ const headerEnd = segment.indexOf("\n\n");
10282
+ if (headerEnd === -1) continue;
10283
+ const headerBlock = segment.slice(0, headerEnd);
10284
+ const value = segment.slice(headerEnd + 2);
10285
+ const headers = {};
10286
+ for (const line of headerBlock.split("\n")) {
10287
+ const colon = line.indexOf(":");
10288
+ if (colon === -1) continue;
10289
+ headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
10290
+ }
10291
+ const disposition = headers["content-disposition"] ?? "";
10292
+ const nameMatch = disposition.match(/name="([^"]*)"/);
10293
+ const filenameMatch = disposition.match(/filename="([^"]*)"/);
10294
+ const partContentType = headers["content-type"] ?? "";
10295
+ const isBinary = !!filenameMatch && !partContentType.startsWith("text/") && !partContentType.includes("json") && !partContentType.includes("xml");
10296
+ parts.push({
10297
+ name: nameMatch ? nameMatch[1] : null,
10298
+ filename: filenameMatch ? filenameMatch[1] : null,
10299
+ headers,
10300
+ value,
10301
+ isBinary
10302
+ });
10303
+ }
10304
+ if (parts.length === 0) {
10305
+ return { error: `No parts found for boundary "${boundary}"; check that it matches the boundary used in the body` };
10306
+ }
10307
+ return { boundary, parts };
10308
+ }
10309
+ server.tool(
10310
+ "parse_multipart_form_data",
10311
+ "Parse a raw multipart/form-data request body into its individual parts: field name, filename (for file fields), headers, and decoded value, per RFC 7578. Give the boundary directly, or the request's Content-Type header value to extract it from automatically.",
10312
+ {
10313
+ body: z.string().min(1).describe("The raw multipart/form-data request body"),
10314
+ boundary: z.string().optional().describe("The multipart boundary (without the leading --), if known directly"),
10315
+ contentType: z.string().optional().describe("Alternative to boundary: the request's Content-Type header value, from which the boundary is extracted")
10316
+ },
10317
+ async ({ body, boundary, contentType }) => {
10318
+ const resolvedBoundary = boundary ?? (contentType ? _extractMultipartBoundaryMcp(contentType) : null);
10319
+ if (!resolvedBoundary) return errText({ error: "boundary is required (either directly, or via a contentType header containing a boundary parameter)" });
10320
+ const result = _parseMultipartFormDataMcp(body, resolvedBoundary);
10321
+ if ("error" in result) return errText(result);
10322
+ return text(JSON.stringify(result, null, 2));
10323
+ }
10324
+ );
9971
10325
  var transport = new StdioServerTransport();
9972
10326
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilix-tech/mcp",
3
- "version": "0.62.0",
3
+ "version": "0.64.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",