@mgsoftwarebv/mcp-server-bridge 3.5.3 → 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
@@ -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",
@@ -120394,8 +120398,6 @@ var storage = new Proxy({}, {
120394
120398
  return Reflect.get(_storage, prop, _storage);
120395
120399
  }
120396
120400
  });
120397
-
120398
- // src/tools/ticket-attachments.ts
120399
120401
  var ALLOWED_IMAGE_TYPES = [
120400
120402
  "image/jpeg",
120401
120403
  "image/png",
@@ -120434,14 +120436,213 @@ var EXT_MIME = {
120434
120436
  ppt: "application/vnd.ms-powerpoint",
120435
120437
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
120436
120438
  };
120437
- function textResponse5(text3) {
120438
- return { content: [{ type: "text", text: text3 }] };
120439
- }
120439
+ var HERMES_CACHE_PATH = /(?:^|[\\/])\.hermes[\\/](.+)$/i;
120440
120440
  function mimeFromName(name21) {
120441
120441
  if (!name21) return null;
120442
120442
  const ext = name21.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
120443
120443
  return ext && EXT_MIME[ext] ? EXT_MIME[ext] : null;
120444
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
+ }
120445
120646
  async function findAttachment(attachmentId) {
120446
120647
  const [ticketAtt] = await db.select({
120447
120648
  ticketId: schema_exports.ticketAttachments.ticketId,
@@ -120510,82 +120711,30 @@ ${url3}`
120510
120711
  };
120511
120712
  }
120512
120713
  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
- );
120714
+ const ctx = getAuthContext() ?? authContext;
120715
+ if (!ctx) {
120716
+ return textResponse5("Error: Not authenticated.");
120526
120717
  }
120527
120718
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
120528
120719
  if (!access.ok) return access.response;
120529
120720
  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
- );
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);
120584
120733
  }
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
- );
120734
+ const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
120735
+ const validationError = validateAttachmentBuffer(buffer2, mimeType);
120736
+ if (validationError) {
120737
+ return textResponse5(validationError.message);
120589
120738
  }
120590
120739
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
120591
120740
  try {
@@ -120600,6 +120749,12 @@ async function handleUploadTicketAttachment(input) {
120600
120749
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
120601
120750
  );
120602
120751
  }
120752
+ if (stagingStorageKey) {
120753
+ try {
120754
+ await storage.remove({ bucket: "vault", paths: [stagingStorageKey] });
120755
+ } catch {
120756
+ }
120757
+ }
120603
120758
  const [row] = await db.insert(schema_exports.ticketAttachments).values({
120604
120759
  ticketId: ticket.id,
120605
120760
  teamId: ticket.teamId,
@@ -122624,7 +122779,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
122624
122779
  }
122625
122780
 
122626
122781
  // src/server.ts
122627
- var SERVER_VERSION = "3.5.1";
122782
+ var SERVER_VERSION = "3.5.4";
122628
122783
  function createMcpServer() {
122629
122784
  const server = new Server(
122630
122785
  {