@mgsoftwarebv/mcp-server-bridge 3.5.2 → 3.5.4

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/dist/index.js CHANGED
@@ -97301,6 +97301,21 @@ var invoiceProducts = pgTable(
97301
97301
  // Array of ProductOptionGroup (see @refront/invoice/types). Null/empty for
97302
97302
  // plain catalog products.
97303
97303
  options: jsonb().$type(),
97304
+ // Structured catalog metadata for richer package/quote composition. All
97305
+ // nullable/defaulted so existing products keep working. Validated at the
97306
+ // app/MCP layer (see @refront/invoice/types constants).
97307
+ // "one_time" | "monthly" | "yearly"
97308
+ billingType: text("billing_type"),
97309
+ // "website" | "addon" | "hosting" | "support" | "other"
97310
+ category: text(),
97311
+ // List of included parts shown to the customer (e.g. "Basis SEO").
97312
+ includedItems: jsonb("included_items").$type(),
97313
+ // Whether this product is an optional add-on rather than a base package.
97314
+ optional: boolean3().default(false).notNull(),
97315
+ // Optional preset/tier label, e.g. start/growth/pro or bronze/silver/gold.
97316
+ tier: text(),
97317
+ // Manual ordering hint for catalog presentation (lower shows first).
97318
+ sortOrder: integer2("sort_order").default(0).notNull(),
97304
97319
  isActive: boolean3().default(true).notNull(),
97305
97320
  usageCount: integer2("usage_count").default(0).notNull(),
97306
97321
  lastUsedAt: timestamp("last_used_at", {
@@ -105939,7 +105954,7 @@ var TOOLS = [
105939
105954
  },
105940
105955
  {
105941
105956
  name: "upload-ticket-attachment",
105942
- description: "Attach a file (image or document) to a ticket. Provide exactly ONE source: `filePath` (absolute local path), `imageUrl` (public URL to download), or `base64Data` (raw or data: URI). To push an image the user pasted into Cursor chat onto the ticket: when the message is sent, Cursor writes the pasted image into the workspace `assets/` folder as `image-<uuid>.png` \u2014 locate the newest `assets/image-*.png` and pass its absolute path as `filePath`. Allowed types: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV. Max 25 MB. Returns the attachment id and a 1-hour download URL.",
105957
+ description: "Attach a file (image or document) to a ticket. Provide exactly ONE source: `filePath` (absolute local path on the MCP runtime), `imageUrl` (URL to download), `uploadId` (from POST /mcp/attachment-upload \u2014 use for Hermes/Telegram gateway cache files over HTTP MCP), or `base64Data` (raw or data: URI, small files only). For Cursor pasted images: locate the newest workspace `assets/image-*.png` and pass its absolute path as `filePath` (stdio MCP). For Hermes cache paths like `/root/.hermes/image_cache/...` over HTTP MCP: POST bytes to `/mcp/attachment-upload` with the same API key, then pass the returned `uploadId`. Optionally set `HERMES_MEDIA_BASE_URL` so Hermes cache `filePath` values are fetched remotely. Allowed types: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV. Max 25 MB. Returns the attachment id and a 1-hour download URL.",
105943
105958
  inputSchema: {
105944
105959
  type: "object",
105945
105960
  properties: {
@@ -105950,11 +105965,15 @@ var TOOLS = [
105950
105965
  },
105951
105966
  filePath: {
105952
105967
  type: "string",
105953
- description: "Absolute local path to the file. For a pasted image, use the newest workspace assets/image-*.png."
105968
+ description: "Absolute local path on the MCP server. Works for Cursor workspace files with stdio MCP; Hermes gateway paths require uploadId or HERMES_MEDIA_BASE_URL."
105954
105969
  },
105955
105970
  imageUrl: {
105956
105971
  type: "string",
105957
- description: "Public URL to download and attach."
105972
+ description: "Public or gateway URL to download and attach."
105973
+ },
105974
+ uploadId: {
105975
+ type: "string",
105976
+ description: "Staged upload id from POST /mcp/attachment-upload. Preferred for original Telegram/Hermes media over HTTP MCP."
105958
105977
  },
105959
105978
  base64Data: {
105960
105979
  type: "string",
@@ -106480,7 +106499,7 @@ var TOOLS = [
106480
106499
  },
106481
106500
  {
106482
106501
  name: "get-products",
106483
- description: "List catalog products used on invoices AND quotes (the shared `invoice_products` catalog). Each entry includes its ID (UUID), name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats. Editing or archiving a catalog product never changes existing invoices/quotes \u2014 those keep an immutable line-item snapshot; catalog changes only affect documents created afterwards.",
106502
+ description: "List catalog products used on invoices AND quotes (the shared `invoice_products` catalog). Each entry includes its ID (UUID), name, unit price, currency, unit, active/archived flag, configurable flag, usage stats, and structured package metadata (category, billingType, tier, optional add-on flag and includedItems). Editing or archiving a catalog product never changes existing invoices/quotes \u2014 those keep an immutable line-item snapshot; catalog changes only affect documents created afterwards.",
106484
106503
  inputSchema: {
106485
106504
  type: "object",
106486
106505
  properties: {
@@ -106506,7 +106525,7 @@ var TOOLS = [
106506
106525
  },
106507
106526
  {
106508
106527
  name: "get-product-by-id",
106509
- description: "Get a single catalog product by its ID (UUID), including name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats.",
106528
+ description: "Get a single catalog product by its ID (UUID), including name, unit price, currency, unit, active/archived flag, configurable flag, usage stats, and structured package metadata (category, billingType, tier, optional add-on flag and includedItems).",
106510
106529
  inputSchema: {
106511
106530
  type: "object",
106512
106531
  properties: {
@@ -106518,7 +106537,7 @@ var TOOLS = [
106518
106537
  },
106519
106538
  {
106520
106539
  name: "create-product",
106521
- description: "Create a catalog product for use on invoices and quotes. Stored in the shared `invoice_products` catalog. This only adds a reusable catalog entry; it does not place the product on any document. Returns the created product with its ID and normalized fields.",
106540
+ description: "Create a catalog product for use on invoices and quotes. Stored in the shared `invoice_products` catalog. This only adds a reusable catalog entry; it does not place the product on any document. Supports structured package metadata (billingType, category, includedItems, optional add-on flag, tier, sortOrder) so website packages, add-ons, hosting and support subscriptions can be modelled richly. Returns the created product with its ID and normalized fields.",
106522
106541
  inputSchema: {
106523
106542
  type: "object",
106524
106543
  properties: {
@@ -106533,6 +106552,34 @@ var TOOLS = [
106533
106552
  unit: {
106534
106553
  type: "string",
106535
106554
  description: "Unit label (e.g. hour, piece, month)"
106555
+ },
106556
+ billingType: {
106557
+ type: "string",
106558
+ enum: ["one_time", "monthly", "yearly"],
106559
+ description: "Billing cadence for this product"
106560
+ },
106561
+ category: {
106562
+ type: "string",
106563
+ enum: ["website", "addon", "hosting", "support", "other"],
106564
+ description: "Product category for catalog filtering"
106565
+ },
106566
+ includedItems: {
106567
+ type: "array",
106568
+ items: { type: "string" },
106569
+ description: "Parts included in the package (e.g. ['Basis SEO', 'Contactformulier'])"
106570
+ },
106571
+ optional: {
106572
+ type: "boolean",
106573
+ description: "Optional add-on (true) vs base package (false)"
106574
+ },
106575
+ tier: {
106576
+ type: "string",
106577
+ enum: ["start", "growth", "pro", "bronze", "silver", "gold"],
106578
+ description: "Optional preset/tier label"
106579
+ },
106580
+ sortOrder: {
106581
+ type: "number",
106582
+ description: "Manual ordering hint (lower shows first)"
106536
106583
  }
106537
106584
  },
106538
106585
  required: ["name"]
@@ -106540,7 +106587,7 @@ var TOOLS = [
106540
106587
  },
106541
106588
  {
106542
106589
  name: "update-product",
106543
- description: "Update a catalog product's editable fields (name, description, price, currency, unit) or reactivate it (isActive: true). Only provided fields change. IMPORTANT: updates apply only to FUTURE invoices/quotes. Existing/sent/accepted/paid documents keep their immutable line-item snapshot and are never mutated. Find the product id via get-products.",
106590
+ description: "Update a catalog product's editable fields (name, description, price, currency, unit), its package metadata (billingType, category, includedItems, optional, tier, sortOrder), or reactivate it (isActive: true). Only provided fields change. IMPORTANT: updates apply only to FUTURE invoices/quotes. Existing/sent/accepted/paid documents keep their immutable line-item snapshot and are never mutated. Find the product id via get-products.",
106544
106591
  inputSchema: {
106545
106592
  type: "object",
106546
106593
  properties: {
@@ -106557,6 +106604,34 @@ var TOOLS = [
106557
106604
  isActive: {
106558
106605
  type: "boolean",
106559
106606
  description: "Set true to reactivate an archived product"
106607
+ },
106608
+ billingType: {
106609
+ type: ["string", "null"],
106610
+ enum: ["one_time", "monthly", "yearly", null],
106611
+ description: "Billing cadence; null clears it"
106612
+ },
106613
+ category: {
106614
+ type: ["string", "null"],
106615
+ enum: ["website", "addon", "hosting", "support", "other", null],
106616
+ description: "Product category; null clears it"
106617
+ },
106618
+ includedItems: {
106619
+ type: ["array", "null"],
106620
+ items: { type: "string" },
106621
+ description: "Parts included in the package; null/[] clears it"
106622
+ },
106623
+ optional: {
106624
+ type: "boolean",
106625
+ description: "Optional add-on (true) vs base package (false)"
106626
+ },
106627
+ tier: {
106628
+ type: ["string", "null"],
106629
+ enum: ["start", "growth", "pro", "bronze", "silver", "gold", null],
106630
+ description: "Preset/tier label; null clears it"
106631
+ },
106632
+ sortOrder: {
106633
+ type: "number",
106634
+ description: "Manual ordering hint (lower shows first)"
106560
106635
  }
106561
106636
  },
106562
106637
  required: ["productId"]
@@ -114088,6 +114163,22 @@ The project had no tickets, hours, trips, or templates. Any project-scoped confi
114088
114163
 
114089
114164
  // src/tools/products.ts
114090
114165
  var PRODUCT_STATUSES = ["active", "archived", "all"];
114166
+ var BILLING_TYPES = ["one_time", "monthly", "yearly"];
114167
+ var CATEGORIES = [
114168
+ "website",
114169
+ "addon",
114170
+ "hosting",
114171
+ "support",
114172
+ "other"
114173
+ ];
114174
+ var TIERS = [
114175
+ "start",
114176
+ "growth",
114177
+ "pro",
114178
+ "bronze",
114179
+ "silver",
114180
+ "gold"
114181
+ ];
114091
114182
  var PRODUCT_COLUMNS = {
114092
114183
  id: schema_exports.invoiceProducts.id,
114093
114184
  teamId: schema_exports.invoiceProducts.teamId,
@@ -114097,6 +114188,12 @@ var PRODUCT_COLUMNS = {
114097
114188
  currency: schema_exports.invoiceProducts.currency,
114098
114189
  unit: schema_exports.invoiceProducts.unit,
114099
114190
  isConfigurable: schema_exports.invoiceProducts.isConfigurable,
114191
+ billingType: schema_exports.invoiceProducts.billingType,
114192
+ category: schema_exports.invoiceProducts.category,
114193
+ includedItems: schema_exports.invoiceProducts.includedItems,
114194
+ optional: schema_exports.invoiceProducts.optional,
114195
+ tier: schema_exports.invoiceProducts.tier,
114196
+ sortOrder: schema_exports.invoiceProducts.sortOrder,
114100
114197
  isActive: schema_exports.invoiceProducts.isActive,
114101
114198
  usageCount: schema_exports.invoiceProducts.usageCount,
114102
114199
  lastUsedAt: schema_exports.invoiceProducts.lastUsedAt,
@@ -114113,13 +114210,34 @@ function formatPrice(p3) {
114113
114210
  function formatProduct(p3) {
114114
114211
  const flags = [p3.isActive ? "active" : "archived"];
114115
114212
  if (p3.isConfigurable) flags.push("configurable");
114213
+ if (p3.optional) flags.push("add-on");
114214
+ const meta5 = [];
114215
+ if (p3.category) meta5.push(`category=${p3.category}`);
114216
+ if (p3.billingType) meta5.push(`billing=${p3.billingType}`);
114217
+ if (p3.tier) meta5.push(`tier=${p3.tier}`);
114218
+ const included = p3.includedItems && p3.includedItems.length > 0 ? `Included: ${p3.includedItems.join(", ")}
114219
+ ` : "";
114116
114220
  return `**${p3.name}** (${flags.join(", ")})
114117
114221
  ID: ${p3.id}
114118
114222
  Price: ${formatPrice(p3)}
114119
- ${p3.description ? `Description: ${p3.description}
114223
+ ${meta5.length > 0 ? `${meta5.join(", ")}
114224
+ ` : ""}` + included + `${p3.description ? `Description: ${p3.description}
114120
114225
  ` : ""}Used: ${p3.usageCount}x${p3.lastUsedAt ? ` (last ${new Date(p3.lastUsedAt).toLocaleDateString()})` : ""}
114121
114226
  `;
114122
114227
  }
114228
+ function validateEnum(label, value, allowed) {
114229
+ if (value === void 0 || value === null) return null;
114230
+ if (typeof value !== "string" || !allowed.includes(value)) {
114231
+ return `Error: invalid ${label} "${String(value)}". Allowed: ${allowed.join(", ")}.`;
114232
+ }
114233
+ return null;
114234
+ }
114235
+ function normalizeIncludedItems(value) {
114236
+ if (value === void 0) return void 0;
114237
+ if (value === null) return null;
114238
+ const cleaned = value.map((item) => typeof item === "string" ? item.trim() : "").filter((item) => item.length > 0);
114239
+ return cleaned.length > 0 ? cleaned : null;
114240
+ }
114123
114241
  async function handleGetProducts(input) {
114124
114242
  const { q: q3, currency, pageSize = 20 } = input;
114125
114243
  const status = input.status ?? "active";
@@ -114200,6 +114318,8 @@ async function handleCreateProduct(input) {
114200
114318
  if (!name21 || name21.trim().length === 0) {
114201
114319
  return textResponse3("Error: `name` is required.");
114202
114320
  }
114321
+ const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
114322
+ if (enumError) return textResponse3(enumError);
114203
114323
  const resolved = await resolveTeamId(input.teamId);
114204
114324
  if (!resolved.ok) return resolved.response;
114205
114325
  const [created] = await db.insert(schema_exports.invoiceProducts).values({
@@ -114209,6 +114329,12 @@ async function handleCreateProduct(input) {
114209
114329
  price: price ?? null,
114210
114330
  currency: currency ?? null,
114211
114331
  unit: unit ?? null,
114332
+ billingType: input.billingType ?? null,
114333
+ category: input.category ?? null,
114334
+ includedItems: normalizeIncludedItems(input.includedItems) ?? null,
114335
+ optional: input.optional ?? false,
114336
+ tier: input.tier ?? null,
114337
+ sortOrder: input.sortOrder ?? 0,
114212
114338
  isActive: true,
114213
114339
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
114214
114340
  }).returning(PRODUCT_COLUMNS);
@@ -114230,6 +114356,8 @@ async function handleUpdateProduct(input) {
114230
114356
  `Product ${productId} not found, or it is not owned by this team.`
114231
114357
  );
114232
114358
  }
114359
+ const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
114360
+ if (enumError) return textResponse3(enumError);
114233
114361
  const updates = {};
114234
114362
  if (input.name !== void 0) {
114235
114363
  if (!input.name || input.name.trim().length === 0) {
@@ -114242,9 +114370,17 @@ async function handleUpdateProduct(input) {
114242
114370
  if (input.currency !== void 0) updates.currency = input.currency;
114243
114371
  if (input.unit !== void 0) updates.unit = input.unit;
114244
114372
  if (input.isActive !== void 0) updates.isActive = input.isActive;
114373
+ if (input.billingType !== void 0) updates.billingType = input.billingType;
114374
+ if (input.category !== void 0) updates.category = input.category;
114375
+ if (input.includedItems !== void 0) {
114376
+ updates.includedItems = normalizeIncludedItems(input.includedItems);
114377
+ }
114378
+ if (input.optional !== void 0) updates.optional = input.optional;
114379
+ if (input.tier !== void 0) updates.tier = input.tier;
114380
+ if (input.sortOrder !== void 0) updates.sortOrder = input.sortOrder;
114245
114381
  if (Object.keys(updates).length === 0) {
114246
114382
  return textResponse3(
114247
- "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive."
114383
+ "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder."
114248
114384
  );
114249
114385
  }
114250
114386
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -114320,7 +114456,12 @@ function snapshotFromProduct(product, defaults, now2 = () => (/* @__PURE__ */ ne
114320
114456
  capturedAt: now2(),
114321
114457
  metadata: {
114322
114458
  isConfigurable: product.isConfigurable,
114323
- options: product.options ?? null
114459
+ options: product.options ?? null,
114460
+ billingType: product.billingType ?? null,
114461
+ category: product.category ?? null,
114462
+ includedItems: product.includedItems ?? null,
114463
+ optional: product.optional ?? false,
114464
+ tier: product.tier ?? null
114324
114465
  }
114325
114466
  };
114326
114467
  }
@@ -114444,7 +114585,12 @@ async function loadProductsInTeam(productIds, teamId) {
114444
114585
  currency: schema_exports.invoiceProducts.currency,
114445
114586
  unit: schema_exports.invoiceProducts.unit,
114446
114587
  isConfigurable: schema_exports.invoiceProducts.isConfigurable,
114447
- options: schema_exports.invoiceProducts.options
114588
+ options: schema_exports.invoiceProducts.options,
114589
+ billingType: schema_exports.invoiceProducts.billingType,
114590
+ category: schema_exports.invoiceProducts.category,
114591
+ includedItems: schema_exports.invoiceProducts.includedItems,
114592
+ optional: schema_exports.invoiceProducts.optional,
114593
+ tier: schema_exports.invoiceProducts.tier
114448
114594
  }).from(schema_exports.invoiceProducts).where(
114449
114595
  and(
114450
114596
  inArray(schema_exports.invoiceProducts.id, productIds),
@@ -114780,12 +114926,22 @@ async function handleAddProductToQuote(input) {
114780
114926
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
114781
114927
  }).where(eq(schema_exports.invoiceProducts.id, product.id));
114782
114928
  const snap = newItem.productSnapshot;
114929
+ const meta5 = snap.metadata;
114930
+ const metaParts = [];
114931
+ if (meta5.category) metaParts.push(`category=${meta5.category}`);
114932
+ if (meta5.billingType) metaParts.push(`billing=${meta5.billingType}`);
114933
+ if (meta5.tier) metaParts.push(`tier=${meta5.tier}`);
114934
+ if (meta5.optional) metaParts.push("optional add-on");
114935
+ if (meta5.includedItems && meta5.includedItems.length > 0) {
114936
+ metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
114937
+ }
114783
114938
  return textResponse4(
114784
114939
  `\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
114785
114940
 
114786
114941
  Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
114787
114942
  Snapshot: name="${snap.name}", unitPrice=${snap.unitPrice}, currency=${snap.currency}, vatRate=${snap.vatRate}%, unit=${snap.unit ?? "-"}
114788
-
114943
+ ${metaParts.length > 0 ? `Metadata: ${metaParts.join(", ")}
114944
+ ` : ""}
114789
114945
  New quote total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal}, VAT ${updated.vat})
114790
114946
  The snapshot is immutable: later catalog edits won't change this quote.`
114791
114947
  );
@@ -120242,8 +120398,6 @@ var storage = new Proxy({}, {
120242
120398
  return Reflect.get(_storage, prop, _storage);
120243
120399
  }
120244
120400
  });
120245
-
120246
- // src/tools/ticket-attachments.ts
120247
120401
  var ALLOWED_IMAGE_TYPES = [
120248
120402
  "image/jpeg",
120249
120403
  "image/png",
@@ -120282,14 +120436,213 @@ var EXT_MIME = {
120282
120436
  ppt: "application/vnd.ms-powerpoint",
120283
120437
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
120284
120438
  };
120285
- function textResponse5(text3) {
120286
- return { content: [{ type: "text", text: text3 }] };
120287
- }
120439
+ var HERMES_CACHE_PATH = /(?:^|[\\/])\.hermes[\\/](.+)$/i;
120288
120440
  function mimeFromName(name21) {
120289
120441
  if (!name21) return null;
120290
120442
  const ext = name21.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
120291
120443
  return ext && EXT_MIME[ext] ? EXT_MIME[ext] : null;
120292
120444
  }
120445
+ function isHermesCachePath(filePath) {
120446
+ return HERMES_CACHE_PATH.test(filePath.replace(/\\/g, "/"));
120447
+ }
120448
+ function hermesCacheRelativePath(filePath) {
120449
+ const normalized = filePath.replace(/\\/g, "/");
120450
+ const match = HERMES_CACHE_PATH.exec(normalized);
120451
+ return match?.[1] ?? null;
120452
+ }
120453
+ function getHermesMediaBaseUrl() {
120454
+ const base = process.env.HERMES_MEDIA_BASE_URL?.trim() || process.env.HERMES_GATEWAY_MEDIA_URL?.trim();
120455
+ return base ? base.replace(/\/+$/, "") : null;
120456
+ }
120457
+ function resolveHermesCacheMediaUrl(filePath) {
120458
+ const base = getHermesMediaBaseUrl();
120459
+ const relative = hermesCacheRelativePath(filePath);
120460
+ if (!base || !relative) return null;
120461
+ return `${base}/${relative.replace(/^\/+/, "")}`;
120462
+ }
120463
+ function parseMcpStagingStorageKey(uploadId, teamId, userId) {
120464
+ const normalized = uploadId.replace(/\\/g, "/").replace(/^\/+/, "");
120465
+ const prefix = `${teamId}/mcp-staging/${userId}/`;
120466
+ if (!normalized.startsWith(prefix)) return null;
120467
+ const remainder = normalized.slice(prefix.length);
120468
+ if (!remainder || remainder.includes("..")) return null;
120469
+ return normalized;
120470
+ }
120471
+ function formatFilePathEnoentError(filePath) {
120472
+ const hermesHint = isHermesCachePath(filePath) ? getHermesMediaBaseUrl() ? " Hermes cache paths are fetched via HERMES_MEDIA_BASE_URL when local read fails." : " For Hermes/Telegram cache files over HTTP MCP, POST the bytes to /mcp/attachment-upload and pass the returned uploadId, or set HERMES_MEDIA_BASE_URL so cache paths can be fetched remotely." : " filePath must exist on the MCP server filesystem (works for Cursor workspace paths with stdio MCP, not for gateway-local paths over HTTP MCP).";
120473
+ return `Failed to read the file at "${filePath}": path not found in the MCP runtime (ENOENT).${hermesHint} Alternatives: imageUrl (download URL), uploadId (from POST /mcp/attachment-upload), or base64Data for small files.`;
120474
+ }
120475
+ function hermesMediaFetchHeaders() {
120476
+ const token = process.env.HERMES_MEDIA_AUTH_TOKEN?.trim() || process.env.HERMES_MEDIA_BEARER_TOKEN?.trim();
120477
+ if (!token) return void 0;
120478
+ return { Authorization: `Bearer ${token}` };
120479
+ }
120480
+ async function fetchAttachmentUrl(url3, fallbackName, mimeOverride) {
120481
+ const res = await fetch(url3, { headers: hermesMediaFetchHeaders() });
120482
+ if (!res.ok) {
120483
+ return {
120484
+ ok: false,
120485
+ message: `Could not download from URL: HTTP ${res.status}.`
120486
+ };
120487
+ }
120488
+ const headerType = res.headers.get("content-type")?.split(";")[0]?.trim();
120489
+ const buffer2 = Buffer.from(await res.arrayBuffer());
120490
+ const fileName = fallbackName || url3.split("/").pop()?.split(/[?#]/)[0] || `attachment_${Date.now()}`;
120491
+ const mimeType = mimeOverride?.trim() || (headerType && headerType !== "application/octet-stream" ? headerType : null) || mimeFromName(url3) || mimeFromName(fileName) || "application/octet-stream";
120492
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120493
+ }
120494
+ async function resolveFromFilePath(filePath, fileNameOverride, mimeOverride) {
120495
+ try {
120496
+ const buffer2 = await readFile(filePath);
120497
+ const fileName = fileNameOverride?.trim() || basename(filePath) || "attachment";
120498
+ const mimeType = mimeOverride?.trim() || mimeFromName(fileName) || "application/octet-stream";
120499
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120500
+ } catch (error49) {
120501
+ const code = error49 && typeof error49 === "object" && "code" in error49 ? String(error49.code) : "";
120502
+ if (code !== "ENOENT") {
120503
+ return {
120504
+ ok: false,
120505
+ message: `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
120506
+ };
120507
+ }
120508
+ const hermesUrl = resolveHermesCacheMediaUrl(filePath);
120509
+ if (hermesUrl) {
120510
+ const fetched = await fetchAttachmentUrl(
120511
+ hermesUrl,
120512
+ fileNameOverride?.trim() || basename(filePath) || void 0,
120513
+ mimeOverride
120514
+ );
120515
+ if (fetched.ok) {
120516
+ return {
120517
+ ok: true,
120518
+ buffer: fetched.buffer,
120519
+ fileName: fetched.fileName,
120520
+ mimeType: fetched.mimeType
120521
+ };
120522
+ }
120523
+ return {
120524
+ ok: false,
120525
+ message: `${formatFilePathEnoentError(filePath)} Hermes media fetch also failed: ${fetched.message}`
120526
+ };
120527
+ }
120528
+ return { ok: false, message: formatFilePathEnoentError(filePath) };
120529
+ }
120530
+ }
120531
+ async function resolveFromUploadId(uploadId, teamId, userId, fileNameOverride, mimeOverride) {
120532
+ const storageKey = parseMcpStagingStorageKey(uploadId, teamId, userId);
120533
+ if (!storageKey) {
120534
+ return {
120535
+ ok: false,
120536
+ message: "Invalid uploadId. Use the uploadId returned by POST /mcp/attachment-upload for your API key user."
120537
+ };
120538
+ }
120539
+ let downloaded;
120540
+ try {
120541
+ downloaded = await storage.download({ bucket: "vault", path: storageKey });
120542
+ } catch (error49) {
120543
+ return {
120544
+ ok: false,
120545
+ message: `Staged upload not found or expired (${uploadId}): ${error49 instanceof Error ? error49.message : String(error49)}`
120546
+ };
120547
+ }
120548
+ const buffer2 = Buffer.from(await downloaded.blob.arrayBuffer());
120549
+ const defaultName = storageKey.split("/").pop() || "attachment";
120550
+ const fileName = fileNameOverride?.trim() || defaultName;
120551
+ const mimeType = mimeOverride?.trim() || downloaded.contentType || mimeFromName(fileName) || "application/octet-stream";
120552
+ return {
120553
+ ok: true,
120554
+ buffer: buffer2,
120555
+ fileName,
120556
+ mimeType,
120557
+ stagingStorageKey: storageKey
120558
+ };
120559
+ }
120560
+ async function resolveAttachmentSource(input) {
120561
+ const sources = [
120562
+ input.filePath,
120563
+ input.imageUrl,
120564
+ input.base64Data,
120565
+ input.uploadId
120566
+ ].filter((v2) => typeof v2 === "string" && v2.trim().length > 0);
120567
+ if (sources.length === 0) {
120568
+ return {
120569
+ ok: false,
120570
+ message: "Provide exactly one source: filePath, imageUrl, uploadId (from POST /mcp/attachment-upload), or base64Data."
120571
+ };
120572
+ }
120573
+ if (sources.length > 1) {
120574
+ return {
120575
+ ok: false,
120576
+ message: "Provide only one source (filePath, imageUrl, uploadId, or base64Data), not several."
120577
+ };
120578
+ }
120579
+ if (input.uploadId) {
120580
+ return resolveFromUploadId(
120581
+ input.uploadId.trim(),
120582
+ input.teamId,
120583
+ input.userId,
120584
+ input.fileName,
120585
+ input.mimeType
120586
+ );
120587
+ }
120588
+ if (input.filePath) {
120589
+ return resolveFromFilePath(input.filePath, input.fileName, input.mimeType);
120590
+ }
120591
+ if (input.imageUrl) {
120592
+ const fetched = await fetchAttachmentUrl(
120593
+ input.imageUrl,
120594
+ input.fileName,
120595
+ input.mimeType
120596
+ );
120597
+ if (!fetched.ok) return fetched;
120598
+ return {
120599
+ ok: true,
120600
+ buffer: fetched.buffer,
120601
+ fileName: fetched.fileName,
120602
+ mimeType: fetched.mimeType
120603
+ };
120604
+ }
120605
+ let b64 = input.base64Data;
120606
+ let mimeType = input.mimeType?.trim() ?? "";
120607
+ const dataUri = b64.match(/^data:([^;]+);base64,(.*)$/s);
120608
+ if (dataUri) {
120609
+ if (!mimeType) mimeType = dataUri[1] ?? "";
120610
+ b64 = dataUri[2] ?? "";
120611
+ } else if (b64.includes(",")) {
120612
+ b64 = b64.split(",")[1] || b64;
120613
+ }
120614
+ const buffer2 = Buffer.from(b64, "base64");
120615
+ const fileName = input.fileName?.trim() || `attachment_${Date.now()}`;
120616
+ if (!mimeType) {
120617
+ mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120618
+ }
120619
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120620
+ }
120621
+ function validateAttachmentBuffer(buffer2, mimeType) {
120622
+ if (buffer2.byteLength === 0) {
120623
+ return { ok: false, message: "The file is empty (0 bytes); nothing to upload." };
120624
+ }
120625
+ if (buffer2.byteLength > MAX_FILE_SIZE) {
120626
+ return {
120627
+ ok: false,
120628
+ message: `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
120629
+ 1
120630
+ )} MB). Max: 25 MB.`
120631
+ };
120632
+ }
120633
+ if (!ALLOWED_MIME_TYPES.has(mimeType)) {
120634
+ return {
120635
+ ok: false,
120636
+ message: `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
120637
+ };
120638
+ }
120639
+ return null;
120640
+ }
120641
+
120642
+ // src/tools/ticket-attachments.ts
120643
+ function textResponse5(text3) {
120644
+ return { content: [{ type: "text", text: text3 }] };
120645
+ }
120293
120646
  async function findAttachment(attachmentId) {
120294
120647
  const [ticketAtt] = await db.select({
120295
120648
  ticketId: schema_exports.ticketAttachments.ticketId,
@@ -120358,82 +120711,30 @@ ${url3}`
120358
120711
  };
120359
120712
  }
120360
120713
  async function handleUploadTicketAttachment(input) {
120361
- const ctx = getAuthContext();
120362
- const sources = [input.filePath, input.imageUrl, input.base64Data].filter(
120363
- (v2) => typeof v2 === "string" && v2.trim().length > 0
120364
- );
120365
- if (sources.length === 0) {
120366
- return textResponse5(
120367
- "Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
120368
- );
120369
- }
120370
- if (sources.length > 1) {
120371
- return textResponse5(
120372
- "Provide only one source (filePath, imageUrl, or base64Data), not several."
120373
- );
120714
+ const ctx = getAuthContext() ?? authContext;
120715
+ if (!ctx) {
120716
+ return textResponse5("Error: Not authenticated.");
120374
120717
  }
120375
120718
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
120376
120719
  if (!access.ok) return access.response;
120377
120720
  const ticket = access.ticket;
120378
- let buffer2;
120379
- let fileName = input.fileName?.trim() ?? "";
120380
- let mimeType = input.mimeType?.trim() ?? "";
120381
- try {
120382
- if (input.filePath) {
120383
- buffer2 = await readFile(input.filePath);
120384
- if (!fileName) fileName = basename(input.filePath) || "attachment";
120385
- if (!mimeType) {
120386
- mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120387
- }
120388
- } else if (input.imageUrl) {
120389
- const res = await fetch(input.imageUrl);
120390
- if (!res.ok) {
120391
- return textResponse5(
120392
- `Could not download from URL: HTTP ${res.status}.`
120393
- );
120394
- }
120395
- const headerType = res.headers.get("content-type")?.split(";")[0]?.trim();
120396
- buffer2 = Buffer.from(await res.arrayBuffer());
120397
- if (!fileName) {
120398
- fileName = input.imageUrl.split("/").pop()?.split(/[?#]/)[0] || `attachment_${Date.now()}`;
120399
- }
120400
- if (!mimeType) {
120401
- mimeType = (headerType && headerType !== "application/octet-stream" ? headerType : null) ?? mimeFromName(input.imageUrl) ?? mimeFromName(fileName) ?? "application/octet-stream";
120402
- }
120403
- } else {
120404
- let b64 = input.base64Data;
120405
- const dataUri = b64.match(/^data:([^;]+);base64,(.*)$/s);
120406
- if (dataUri) {
120407
- if (!mimeType) mimeType = dataUri[1] ?? "";
120408
- b64 = dataUri[2] ?? "";
120409
- } else if (b64.includes(",")) {
120410
- b64 = b64.split(",")[1] || b64;
120411
- }
120412
- buffer2 = Buffer.from(b64, "base64");
120413
- if (!fileName) fileName = `attachment_${Date.now()}`;
120414
- if (!mimeType) {
120415
- mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120416
- }
120417
- }
120418
- } catch (error49) {
120419
- return textResponse5(
120420
- `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
120421
- );
120422
- }
120423
- if (buffer2.byteLength === 0) {
120424
- return textResponse5("The file is empty (0 bytes); nothing to upload.");
120425
- }
120426
- if (buffer2.byteLength > MAX_FILE_SIZE) {
120427
- return textResponse5(
120428
- `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
120429
- 1
120430
- )} MB). Max: 25 MB.`
120431
- );
120721
+ const resolved = await resolveAttachmentSource({
120722
+ filePath: input.filePath,
120723
+ imageUrl: input.imageUrl,
120724
+ base64Data: input.base64Data,
120725
+ uploadId: input.uploadId,
120726
+ fileName: input.fileName,
120727
+ mimeType: input.mimeType,
120728
+ teamId: ctx.teamId,
120729
+ userId: ctx.userId
120730
+ });
120731
+ if (!resolved.ok) {
120732
+ return textResponse5(resolved.message);
120432
120733
  }
120433
- if (!ALLOWED_MIME_TYPES.has(mimeType)) {
120434
- return textResponse5(
120435
- `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
120436
- );
120734
+ const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
120735
+ const validationError = validateAttachmentBuffer(buffer2, mimeType);
120736
+ if (validationError) {
120737
+ return textResponse5(validationError.message);
120437
120738
  }
120438
120739
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
120439
120740
  try {
@@ -120448,6 +120749,12 @@ async function handleUploadTicketAttachment(input) {
120448
120749
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
120449
120750
  );
120450
120751
  }
120752
+ if (stagingStorageKey) {
120753
+ try {
120754
+ await storage.remove({ bucket: "vault", paths: [stagingStorageKey] });
120755
+ } catch {
120756
+ }
120757
+ }
120451
120758
  const [row] = await db.insert(schema_exports.ticketAttachments).values({
120452
120759
  ticketId: ticket.id,
120453
120760
  teamId: ticket.teamId,
@@ -121555,7 +121862,7 @@ function attemptedLockedFields(update) {
121555
121862
 
121556
121863
  // src/tools/trips.ts
121557
121864
  var TRIP_TYPES = ["private", "business"];
121558
- var BILLING_TYPES = TRIP_BILLING_TYPES;
121865
+ var BILLING_TYPES2 = TRIP_BILLING_TYPES;
121559
121866
  function textResponse7(text3) {
121560
121867
  return { content: [{ type: "text", text: text3 }] };
121561
121868
  }
@@ -121616,9 +121923,9 @@ async function handleGetTrips(input) {
121616
121923
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
121617
121924
  );
121618
121925
  }
121619
- if (input.billingType && !BILLING_TYPES.includes(input.billingType)) {
121926
+ if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
121620
121927
  return textResponse7(
121621
- `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES.join(", ")}.`
121928
+ `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
121622
121929
  );
121623
121930
  }
121624
121931
  const scope = await resolveTeamScope(input.teamId);
@@ -121738,9 +122045,9 @@ async function handleCreateTrip(input) {
121738
122045
  );
121739
122046
  }
121740
122047
  const billingType = input.billingType ?? "not_billable";
121741
- if (!BILLING_TYPES.includes(billingType)) {
122048
+ if (!BILLING_TYPES2.includes(billingType)) {
121742
122049
  return textResponse7(
121743
- `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES.join(", ")}.`
122050
+ `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
121744
122051
  );
121745
122052
  }
121746
122053
  const resolved = await resolveTeamId(input.teamId);
@@ -121819,9 +122126,9 @@ async function handleUpdateTrip(input) {
121819
122126
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
121820
122127
  );
121821
122128
  }
121822
- if (input.billingType && !BILLING_TYPES.includes(input.billingType)) {
122129
+ if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
121823
122130
  return textResponse7(
121824
- `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES.join(", ")}.`
122131
+ `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
121825
122132
  );
121826
122133
  }
121827
122134
  const resolved = await resolveTeamId(input.teamId);
@@ -122472,7 +122779,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
122472
122779
  }
122473
122780
 
122474
122781
  // src/server.ts
122475
- var SERVER_VERSION = "3.5.1";
122782
+ var SERVER_VERSION = "3.5.4";
122476
122783
  function createMcpServer() {
122477
122784
  const server = new Server(
122478
122785
  {