[object Object]

← back to George Mcp

fix gmail_get_attachment: fetch raw bytes + save locally (remote George), was calling broken server-side /api/attachment-local

b2cbb3d86575bdbe9c2e931e6669d194e5e3b036 · 2026-07-09 08:56:46 -0700 · Steve Abrams

Files touched

Diff

commit b2cbb3d86575bdbe9c2e931e6669d194e5e3b036
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 9 08:56:46 2026 -0700

    fix gmail_get_attachment: fetch raw bytes + save locally (remote George), was calling broken server-side /api/attachment-local
---
 index.js | 42 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 38 insertions(+), 4 deletions(-)

diff --git a/index.js b/index.js
index deca998..54a051b 100755
--- a/index.js
+++ b/index.js
@@ -68,6 +68,23 @@ async function george(path, { method = "GET", query, body } = {}) {
   return data;
 }
 
+// Fetch raw binary bytes (attachments) from George. The JSON `george()` helper
+// mangles binary via res.text()/JSON.parse, so attachments use this instead.
+async function georgeRaw(path, { query } = {}) {
+  const fullPath = BASE_PATH_PREFIX + (path.startsWith("/") ? path : "/" + path);
+  const url = new URL(BASE_URL + fullPath);
+  if (query) {
+    for (const [k, v] of Object.entries(query)) {
+      if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
+    }
+  }
+  const res = await fetch(url, { headers: { Authorization: `Basic ${BASIC_AUTH}` } });
+  if (!res.ok) {
+    throw new Error(`George GET ${fullPath} → ${res.status} : ${(await res.text()).slice(0, 200)}`);
+  }
+  return Buffer.from(await res.arrayBuffer());
+}
+
 // George serves five Gmail accounts. Every tool below accepts an optional
 // `account` param; omitted => steve-office. Keys match George's resolveAccount().
 const ACCOUNTS = ["steve-office", "info", "steve-personal", "stevesclaude", "agentabrams"];
@@ -271,11 +288,28 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
           query: { account },
         });
         break;
-      case "gmail_get_attachment":
-        data = await george("/api/attachment-local", {
-          query: { account, messageId: args.id, filename: args.filename, save: args.save },
-        });
+      case "gmail_get_attachment": {
+        // George may be REMOTE (Kamatera), so we cannot ask it to save server-side.
+        // Resolve the attachmentId from the message, fetch raw bytes, write LOCALLY.
+        const msg = await george(`/api/messages/${encodeURIComponent(args.id)}`, { query: { account } });
+        const atts = msg.attachments || [];
+        const want = String(args.filename || "").toLowerCase();
+        const match = atts.find((a) => (a.filename || "").toLowerCase().includes(want));
+        if (!match) {
+          throw new Error(
+            `No attachment matching "${args.filename}" on message ${args.id}. Available: ${atts.map((a) => a.filename).join(", ") || "(none)"}`
+          );
+        }
+        // Account-aware attachment route (mirrors /api/messages/:id?account=…).
+        const buf = await georgeRaw(
+          `/api/messages/${encodeURIComponent(args.id)}/attachments/${encodeURIComponent(match.attachmentId)}`,
+          { query: { account } }
+        );
+        const fs = await import("node:fs");
+        fs.writeFileSync(args.save, buf);
+        data = { saved: args.save, bytes: buf.length, mimeType: match.mimeType, filename: match.filename };
         break;
+      }
       case "gmail_list_labels":
         data = await george("/api/labels", { query: { account } });
         break;

← dacdbfb feat: add gmail_get_attachment MCP tool (wraps George /api/a  ·  back to George Mcp  ·  add gmail_send_attachment tool: send email with local-file a d9bbb80 →