← 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 →