@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 +351 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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` (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
120515
|
-
(
|
|
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
|
-
|
|
120531
|
-
|
|
120532
|
-
|
|
120533
|
-
|
|
120534
|
-
|
|
120535
|
-
|
|
120536
|
-
|
|
120537
|
-
|
|
120538
|
-
|
|
120539
|
-
|
|
120540
|
-
|
|
120541
|
-
|
|
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
|
-
|
|
120586
|
-
|
|
120587
|
-
|
|
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.
|
|
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":
|