@mgsoftwarebv/mcp-server-bridge 3.5.3 → 3.5.5

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
@@ -105954,7 +105954,7 @@ var TOOLS = [
105954
105954
  },
105955
105955
  {
105956
105956
  name: "upload-ticket-attachment",
105957
- 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.",
105958
105958
  inputSchema: {
105959
105959
  type: "object",
105960
105960
  properties: {
@@ -105965,11 +105965,15 @@ var TOOLS = [
105965
105965
  },
105966
105966
  filePath: {
105967
105967
  type: "string",
105968
- 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."
105969
105969
  },
105970
105970
  imageUrl: {
105971
105971
  type: "string",
105972
- 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."
105973
105977
  },
105974
105978
  base64Data: {
105975
105979
  type: "string",
@@ -105987,6 +105991,22 @@ var TOOLS = [
105987
105991
  required: ["ticketId"]
105988
105992
  }
105989
105993
  },
105994
+ {
105995
+ name: "delete-ticket-attachment",
105996
+ description: "Safely remove a ticket or comment attachment by id. Validates provider/team access and that the attachment belongs to the requested ticket. Hard-deletes the DB row and vault file, and logs ticket activity with the filename. Find attachment ids via get-ticket-by-id.",
105997
+ inputSchema: {
105998
+ type: "object",
105999
+ properties: {
106000
+ teamId: teamIdProp,
106001
+ ticketId: ticketIdentifierProp,
106002
+ attachmentId: {
106003
+ type: "string",
106004
+ description: "Attachment ID from get-ticket-by-id (ticket or comment attachment)."
106005
+ }
106006
+ },
106007
+ required: ["ticketId", "attachmentId"]
106008
+ }
106009
+ },
105990
106010
  {
105991
106011
  name: "get-customers",
105992
106012
  description: "Get customers with optional search. Each result includes its ID (UUID), name, email, website, phone, status, archived flag, and created date. Archived customers are hidden by default; pass status 'archived' or 'all' to include them.",
@@ -120394,8 +120414,6 @@ var storage = new Proxy({}, {
120394
120414
  return Reflect.get(_storage, prop, _storage);
120395
120415
  }
120396
120416
  });
120397
-
120398
- // src/tools/ticket-attachments.ts
120399
120417
  var ALLOWED_IMAGE_TYPES = [
120400
120418
  "image/jpeg",
120401
120419
  "image/png",
@@ -120434,14 +120452,243 @@ var EXT_MIME = {
120434
120452
  ppt: "application/vnd.ms-powerpoint",
120435
120453
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
120436
120454
  };
120437
- function textResponse5(text3) {
120438
- return { content: [{ type: "text", text: text3 }] };
120439
- }
120455
+ var HERMES_CACHE_PATH = /(?:^|[\\/])\.hermes[\\/](.+)$/i;
120440
120456
  function mimeFromName(name21) {
120441
120457
  if (!name21) return null;
120442
120458
  const ext = name21.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
120443
120459
  return ext && EXT_MIME[ext] ? EXT_MIME[ext] : null;
120444
120460
  }
120461
+ function isHermesCachePath(filePath) {
120462
+ return HERMES_CACHE_PATH.test(filePath.replace(/\\/g, "/"));
120463
+ }
120464
+ function hermesCacheRelativePath(filePath) {
120465
+ const normalized = filePath.replace(/\\/g, "/");
120466
+ const match = HERMES_CACHE_PATH.exec(normalized);
120467
+ return match?.[1] ?? null;
120468
+ }
120469
+ function getHermesMediaBaseUrl() {
120470
+ const base = process.env.HERMES_MEDIA_BASE_URL?.trim() || process.env.HERMES_GATEWAY_MEDIA_URL?.trim();
120471
+ return base ? base.replace(/\/+$/, "") : null;
120472
+ }
120473
+ function resolveHermesCacheMediaUrl(filePath) {
120474
+ const base = getHermesMediaBaseUrl();
120475
+ const relative = hermesCacheRelativePath(filePath);
120476
+ if (!base || !relative) return null;
120477
+ return `${base}/${relative.replace(/^\/+/, "")}`;
120478
+ }
120479
+ function parseMcpStagingStorageKey(uploadId, teamId, userId) {
120480
+ const normalized = uploadId.replace(/\\/g, "/").replace(/^\/+/, "");
120481
+ const prefix = `${teamId}/mcp-staging/${userId}/`;
120482
+ if (!normalized.startsWith(prefix)) return null;
120483
+ const remainder = normalized.slice(prefix.length);
120484
+ if (!remainder || remainder.includes("..")) return null;
120485
+ return normalized;
120486
+ }
120487
+ function formatFilePathEnoentError(filePath) {
120488
+ 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).";
120489
+ 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.`;
120490
+ }
120491
+ function hermesMediaFetchHeaders() {
120492
+ const token = process.env.HERMES_MEDIA_AUTH_TOKEN?.trim() || process.env.HERMES_MEDIA_BEARER_TOKEN?.trim();
120493
+ if (!token) return void 0;
120494
+ return { Authorization: `Bearer ${token}` };
120495
+ }
120496
+ async function fetchAttachmentUrl(url3, fallbackName, mimeOverride) {
120497
+ const res = await fetch(url3, { headers: hermesMediaFetchHeaders() });
120498
+ if (!res.ok) {
120499
+ return {
120500
+ ok: false,
120501
+ message: `Could not download from URL: HTTP ${res.status}.`
120502
+ };
120503
+ }
120504
+ const headerType = res.headers.get("content-type")?.split(";")[0]?.trim();
120505
+ const buffer2 = Buffer.from(await res.arrayBuffer());
120506
+ const fileName = fallbackName || url3.split("/").pop()?.split(/[?#]/)[0] || `attachment_${Date.now()}`;
120507
+ const mimeType = mimeOverride?.trim() || (headerType && headerType !== "application/octet-stream" ? headerType : null) || mimeFromName(url3) || mimeFromName(fileName) || "application/octet-stream";
120508
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120509
+ }
120510
+ async function resolveFromFilePath(filePath, fileNameOverride, mimeOverride) {
120511
+ try {
120512
+ const buffer2 = await readFile(filePath);
120513
+ const fileName = fileNameOverride?.trim() || basename(filePath) || "attachment";
120514
+ const mimeType = mimeOverride?.trim() || mimeFromName(fileName) || "application/octet-stream";
120515
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120516
+ } catch (error49) {
120517
+ const code = error49 && typeof error49 === "object" && "code" in error49 ? String(error49.code) : "";
120518
+ if (code !== "ENOENT") {
120519
+ return {
120520
+ ok: false,
120521
+ message: `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
120522
+ };
120523
+ }
120524
+ const hermesUrl = resolveHermesCacheMediaUrl(filePath);
120525
+ if (hermesUrl) {
120526
+ const fetched = await fetchAttachmentUrl(
120527
+ hermesUrl,
120528
+ fileNameOverride?.trim() || basename(filePath) || void 0,
120529
+ mimeOverride
120530
+ );
120531
+ if (fetched.ok) {
120532
+ return {
120533
+ ok: true,
120534
+ buffer: fetched.buffer,
120535
+ fileName: fetched.fileName,
120536
+ mimeType: fetched.mimeType
120537
+ };
120538
+ }
120539
+ return {
120540
+ ok: false,
120541
+ message: `${formatFilePathEnoentError(filePath)} Hermes media fetch also failed: ${fetched.message}`
120542
+ };
120543
+ }
120544
+ return { ok: false, message: formatFilePathEnoentError(filePath) };
120545
+ }
120546
+ }
120547
+ async function resolveFromUploadId(uploadId, teamId, userId, fileNameOverride, mimeOverride) {
120548
+ const storageKey = parseMcpStagingStorageKey(uploadId, teamId, userId);
120549
+ if (!storageKey) {
120550
+ return {
120551
+ ok: false,
120552
+ message: "Invalid uploadId. Use the uploadId returned by POST /mcp/attachment-upload for your API key user."
120553
+ };
120554
+ }
120555
+ let downloaded;
120556
+ try {
120557
+ downloaded = await storage.download({ bucket: "vault", path: storageKey });
120558
+ } catch (error49) {
120559
+ return {
120560
+ ok: false,
120561
+ message: `Staged upload not found or expired (${uploadId}): ${error49 instanceof Error ? error49.message : String(error49)}`
120562
+ };
120563
+ }
120564
+ const buffer2 = Buffer.from(await downloaded.blob.arrayBuffer());
120565
+ const defaultName = storageKey.split("/").pop() || "attachment";
120566
+ const fileName = fileNameOverride?.trim() || defaultName;
120567
+ const mimeType = mimeOverride?.trim() || downloaded.contentType || mimeFromName(fileName) || "application/octet-stream";
120568
+ return {
120569
+ ok: true,
120570
+ buffer: buffer2,
120571
+ fileName,
120572
+ mimeType,
120573
+ stagingStorageKey: storageKey
120574
+ };
120575
+ }
120576
+ async function resolveAttachmentSource(input) {
120577
+ const sources = [
120578
+ input.filePath,
120579
+ input.imageUrl,
120580
+ input.base64Data,
120581
+ input.uploadId
120582
+ ].filter((v2) => typeof v2 === "string" && v2.trim().length > 0);
120583
+ if (sources.length === 0) {
120584
+ return {
120585
+ ok: false,
120586
+ message: "Provide exactly one source: filePath, imageUrl, uploadId (from POST /mcp/attachment-upload), or base64Data."
120587
+ };
120588
+ }
120589
+ if (sources.length > 1) {
120590
+ return {
120591
+ ok: false,
120592
+ message: "Provide only one source (filePath, imageUrl, uploadId, or base64Data), not several."
120593
+ };
120594
+ }
120595
+ if (input.uploadId) {
120596
+ return resolveFromUploadId(
120597
+ input.uploadId.trim(),
120598
+ input.teamId,
120599
+ input.userId,
120600
+ input.fileName,
120601
+ input.mimeType
120602
+ );
120603
+ }
120604
+ if (input.filePath) {
120605
+ return resolveFromFilePath(input.filePath, input.fileName, input.mimeType);
120606
+ }
120607
+ if (input.imageUrl) {
120608
+ const fetched = await fetchAttachmentUrl(
120609
+ input.imageUrl,
120610
+ input.fileName,
120611
+ input.mimeType
120612
+ );
120613
+ if (!fetched.ok) return fetched;
120614
+ return {
120615
+ ok: true,
120616
+ buffer: fetched.buffer,
120617
+ fileName: fetched.fileName,
120618
+ mimeType: fetched.mimeType
120619
+ };
120620
+ }
120621
+ let b64 = input.base64Data;
120622
+ let mimeType = input.mimeType?.trim() ?? "";
120623
+ const dataUri = b64.match(/^data:([^;]+);base64,(.*)$/s);
120624
+ if (dataUri) {
120625
+ if (!mimeType) mimeType = dataUri[1] ?? "";
120626
+ b64 = dataUri[2] ?? "";
120627
+ } else if (b64.includes(",")) {
120628
+ b64 = b64.split(",")[1] || b64;
120629
+ }
120630
+ const buffer2 = Buffer.from(b64, "base64");
120631
+ const fileName = input.fileName?.trim() || `attachment_${Date.now()}`;
120632
+ if (!mimeType) {
120633
+ mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120634
+ }
120635
+ return { ok: true, buffer: buffer2, fileName, mimeType };
120636
+ }
120637
+ function validateAttachmentBuffer(buffer2, mimeType) {
120638
+ if (buffer2.byteLength === 0) {
120639
+ return { ok: false, message: "The file is empty (0 bytes); nothing to upload." };
120640
+ }
120641
+ if (buffer2.byteLength > MAX_FILE_SIZE) {
120642
+ return {
120643
+ ok: false,
120644
+ message: `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
120645
+ 1
120646
+ )} MB). Max: 25 MB.`
120647
+ };
120648
+ }
120649
+ if (!ALLOWED_MIME_TYPES.has(mimeType)) {
120650
+ return {
120651
+ ok: false,
120652
+ message: `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
120653
+ };
120654
+ }
120655
+ return null;
120656
+ }
120657
+
120658
+ // src/tools/ticket-attachment-delete-util.ts
120659
+ function validateDeleteAttachmentInput(attachmentId) {
120660
+ if (!attachmentId?.trim()) {
120661
+ return "missing_attachment_id";
120662
+ }
120663
+ return null;
120664
+ }
120665
+ function validateAttachmentBelongsToTicket(attachmentTicketId, requestedTicketId) {
120666
+ return attachmentTicketId === requestedTicketId;
120667
+ }
120668
+ function buildDeleteAttachmentResult(input) {
120669
+ return {
120670
+ deleted: true,
120671
+ attachmentId: input.attachmentId,
120672
+ ticketId: input.ticketId,
120673
+ fileName: input.fileName,
120674
+ source: input.source
120675
+ };
120676
+ }
120677
+ function formatDeleteAttachmentRefusal(reason, context2) {
120678
+ switch (reason) {
120679
+ case "missing_attachment_id":
120680
+ return "attachmentId is required. Use get-ticket-by-id to list attachment ids.";
120681
+ case "attachment_not_found":
120682
+ return `Attachment not found: ${context2.attachmentId}. Use get-ticket-by-id on ${context2.ticketNumber} to list valid attachment ids.`;
120683
+ case "wrong_ticket":
120684
+ return `Attachment ${context2.attachmentId} (${context2.fileName ?? "unknown file"}) belongs to ticket ${context2.actualTicketId}, not ${context2.ticketNumber}. Pass the correct ticketId or choose an attachment id from that ticket.`;
120685
+ }
120686
+ }
120687
+
120688
+ // src/tools/ticket-attachments.ts
120689
+ function textResponse5(text3) {
120690
+ return { content: [{ type: "text", text: text3 }] };
120691
+ }
120445
120692
  async function findAttachment(attachmentId) {
120446
120693
  const [ticketAtt] = await db.select({
120447
120694
  ticketId: schema_exports.ticketAttachments.ticketId,
@@ -120510,82 +120757,30 @@ ${url3}`
120510
120757
  };
120511
120758
  }
120512
120759
  async function handleUploadTicketAttachment(input) {
120513
- const ctx = getAuthContext();
120514
- const sources = [input.filePath, input.imageUrl, input.base64Data].filter(
120515
- (v2) => typeof v2 === "string" && v2.trim().length > 0
120516
- );
120517
- if (sources.length === 0) {
120518
- return textResponse5(
120519
- "Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
120520
- );
120521
- }
120522
- if (sources.length > 1) {
120523
- return textResponse5(
120524
- "Provide only one source (filePath, imageUrl, or base64Data), not several."
120525
- );
120760
+ const ctx = getAuthContext() ?? authContext;
120761
+ if (!ctx) {
120762
+ return textResponse5("Error: Not authenticated.");
120526
120763
  }
120527
120764
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
120528
120765
  if (!access.ok) return access.response;
120529
120766
  const ticket = access.ticket;
120530
- let buffer2;
120531
- let fileName = input.fileName?.trim() ?? "";
120532
- let mimeType = input.mimeType?.trim() ?? "";
120533
- try {
120534
- if (input.filePath) {
120535
- buffer2 = await readFile(input.filePath);
120536
- if (!fileName) fileName = basename(input.filePath) || "attachment";
120537
- if (!mimeType) {
120538
- mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120539
- }
120540
- } else if (input.imageUrl) {
120541
- const res = await fetch(input.imageUrl);
120542
- if (!res.ok) {
120543
- return textResponse5(
120544
- `Could not download from URL: HTTP ${res.status}.`
120545
- );
120546
- }
120547
- const headerType = res.headers.get("content-type")?.split(";")[0]?.trim();
120548
- buffer2 = Buffer.from(await res.arrayBuffer());
120549
- if (!fileName) {
120550
- fileName = input.imageUrl.split("/").pop()?.split(/[?#]/)[0] || `attachment_${Date.now()}`;
120551
- }
120552
- if (!mimeType) {
120553
- mimeType = (headerType && headerType !== "application/octet-stream" ? headerType : null) ?? mimeFromName(input.imageUrl) ?? mimeFromName(fileName) ?? "application/octet-stream";
120554
- }
120555
- } else {
120556
- let b64 = input.base64Data;
120557
- const dataUri = b64.match(/^data:([^;]+);base64,(.*)$/s);
120558
- if (dataUri) {
120559
- if (!mimeType) mimeType = dataUri[1] ?? "";
120560
- b64 = dataUri[2] ?? "";
120561
- } else if (b64.includes(",")) {
120562
- b64 = b64.split(",")[1] || b64;
120563
- }
120564
- buffer2 = Buffer.from(b64, "base64");
120565
- if (!fileName) fileName = `attachment_${Date.now()}`;
120566
- if (!mimeType) {
120567
- mimeType = mimeFromName(fileName) ?? "application/octet-stream";
120568
- }
120569
- }
120570
- } catch (error49) {
120571
- return textResponse5(
120572
- `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
120573
- );
120574
- }
120575
- if (buffer2.byteLength === 0) {
120576
- return textResponse5("The file is empty (0 bytes); nothing to upload.");
120577
- }
120578
- if (buffer2.byteLength > MAX_FILE_SIZE) {
120579
- return textResponse5(
120580
- `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
120581
- 1
120582
- )} MB). Max: 25 MB.`
120583
- );
120767
+ const resolved = await resolveAttachmentSource({
120768
+ filePath: input.filePath,
120769
+ imageUrl: input.imageUrl,
120770
+ base64Data: input.base64Data,
120771
+ uploadId: input.uploadId,
120772
+ fileName: input.fileName,
120773
+ mimeType: input.mimeType,
120774
+ teamId: ctx.teamId,
120775
+ userId: ctx.userId
120776
+ });
120777
+ if (!resolved.ok) {
120778
+ return textResponse5(resolved.message);
120584
120779
  }
120585
- if (!ALLOWED_MIME_TYPES.has(mimeType)) {
120586
- return textResponse5(
120587
- `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
120588
- );
120780
+ const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
120781
+ const validationError = validateAttachmentBuffer(buffer2, mimeType);
120782
+ if (validationError) {
120783
+ return textResponse5(validationError.message);
120589
120784
  }
120590
120785
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
120591
120786
  try {
@@ -120600,6 +120795,12 @@ async function handleUploadTicketAttachment(input) {
120600
120795
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
120601
120796
  );
120602
120797
  }
120798
+ if (stagingStorageKey) {
120799
+ try {
120800
+ await storage.remove({ bucket: "vault", paths: [stagingStorageKey] });
120801
+ } catch {
120802
+ }
120803
+ }
120603
120804
  const [row] = await db.insert(schema_exports.ticketAttachments).values({
120604
120805
  ticketId: ticket.id,
120605
120806
  teamId: ticket.teamId,
@@ -120630,6 +120831,72 @@ Download URL (valid 1 hour):
120630
120831
  ${url3}` : "")
120631
120832
  );
120632
120833
  }
120834
+ async function handleDeleteTicketAttachment(input) {
120835
+ const ctx = getAuthContext() ?? authContext;
120836
+ if (!ctx) {
120837
+ return textResponse5("Error: Not authenticated.");
120838
+ }
120839
+ const inputError = validateDeleteAttachmentInput(input.attachmentId);
120840
+ if (inputError) {
120841
+ return textResponse5(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
120842
+ }
120843
+ const access = await loadAccessibleTicket(input.teamId, input.ticketId);
120844
+ if (!access.ok) return access.response;
120845
+ const ticket = access.ticket;
120846
+ const attachment = await findAttachment(input.attachmentId);
120847
+ if (!attachment) {
120848
+ return textResponse5(
120849
+ formatDeleteAttachmentRefusal("attachment_not_found", {
120850
+ attachmentId: input.attachmentId,
120851
+ ticketNumber: ticket.ticketNumber
120852
+ })
120853
+ );
120854
+ }
120855
+ if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
120856
+ return textResponse5(
120857
+ formatDeleteAttachmentRefusal("wrong_ticket", {
120858
+ attachmentId: input.attachmentId,
120859
+ ticketNumber: ticket.ticketNumber,
120860
+ fileName: attachment.fileName,
120861
+ actualTicketId: attachment.ticketId
120862
+ })
120863
+ );
120864
+ }
120865
+ const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
120866
+ const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
120867
+ if (!deletedRow) {
120868
+ return textResponse5(
120869
+ `Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
120870
+ );
120871
+ }
120872
+ try {
120873
+ await storage.remove({
120874
+ bucket: "vault",
120875
+ paths: [attachment.storageKey]
120876
+ });
120877
+ } catch {
120878
+ }
120879
+ await db.insert(schema_exports.ticketActivity).values({
120880
+ ticketId: ticket.id,
120881
+ teamId: ticket.teamId,
120882
+ userId: ctx.userId,
120883
+ activityType: "attachment_removed",
120884
+ metadata: {
120885
+ attachmentId: input.attachmentId,
120886
+ fileName: attachment.fileName,
120887
+ source: attachment.source,
120888
+ removedBy: "mcp"
120889
+ }
120890
+ });
120891
+ await db.update(schema_exports.tickets).set({ updatedAt: sql`NOW()`, updatedBy: ctx.userId }).where(eq(schema_exports.tickets.id, ticket.id));
120892
+ const result = buildDeleteAttachmentResult({
120893
+ attachmentId: input.attachmentId,
120894
+ ticketId: ticket.id,
120895
+ fileName: attachment.fileName,
120896
+ source: attachment.source
120897
+ });
120898
+ return textResponse5(JSON.stringify(result, null, 2));
120899
+ }
120633
120900
 
120634
120901
  // src/tools/tiptap-text.ts
120635
120902
  function renderNode(node) {
@@ -122624,7 +122891,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
122624
122891
  }
122625
122892
 
122626
122893
  // src/server.ts
122627
- var SERVER_VERSION = "3.5.1";
122894
+ var SERVER_VERSION = "3.5.4";
122628
122895
  function createMcpServer() {
122629
122896
  const server = new Server(
122630
122897
  {
@@ -122706,6 +122973,10 @@ function createMcpServer() {
122706
122973
  return await handleUploadTicketAttachment(
122707
122974
  asToolArgs(toolArgs)
122708
122975
  );
122976
+ case "delete-ticket-attachment":
122977
+ return await handleDeleteTicketAttachment(
122978
+ asToolArgs(toolArgs)
122979
+ );
122709
122980
  case "get-customers":
122710
122981
  return await handleGetCustomers(asToolArgs(toolArgs));
122711
122982
  case "create-customer":