gogcli-mcp 2.24.0 → 2.25.0

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
10
- "version": "2.24.0"
10
+ "version": "2.25.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli",
16
16
  "source": "./",
17
17
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
18
- "version": "2.24.0",
18
+ "version": "2.25.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
3
  "displayName": "gogcli",
4
- "version": "2.24.0",
4
+ "version": "2.25.0",
5
5
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31177,10 +31177,11 @@ function sanitizedEnv() {
31177
31177
  }
31178
31178
  return result;
31179
31179
  }
31180
+ var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
31180
31181
  var GOOGLE_TOKEN_PATTERNS = [
31181
- /ya29\.[A-Za-z0-9._\-]+/g,
31182
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
31182
31183
  // OAuth2 access tokens
31183
- /1\/\/[A-Za-z0-9._\-]+/g
31184
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
31184
31185
  // OAuth2 refresh tokens
31185
31186
  ];
31186
31187
  function redactGoogleTokens(text) {
@@ -31193,6 +31194,26 @@ function redactGoogleTokens(text) {
31193
31194
  function redactSecrets2(text) {
31194
31195
  return redactGoogleTokens(redactSecrets(text));
31195
31196
  }
31197
+ var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
31198
+ var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
31199
+ function redactPreservingOpaqueFields(text, fields, redact) {
31200
+ const lifted = [];
31201
+ let staged = text;
31202
+ for (const field of fields) {
31203
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31204
+ const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
31205
+ staged = staged.replace(re, (_m, open, value, close) => {
31206
+ lifted.push(value);
31207
+ return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
31208
+ });
31209
+ }
31210
+ if (lifted.length === 0) return redact(text);
31211
+ let redacted = redact(staged);
31212
+ lifted.forEach((value, i) => {
31213
+ redacted = redacted.split(opaquePlaceholder(i)).join(value);
31214
+ });
31215
+ return redacted;
31216
+ }
31196
31217
  function augmentedPath() {
31197
31218
  const home = process.env.HOME;
31198
31219
  const candidates = [
@@ -31223,19 +31244,24 @@ function formatTimeout(ms) {
31223
31244
  return `${ms}ms`;
31224
31245
  }
31225
31246
  async function spawnWithTempFiles(args, opts) {
31226
- const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
31247
+ const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
31227
31248
  const { tmpdir } = await import("node:os");
31228
31249
  const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
31229
31250
  try {
31230
31251
  const argv = [];
31252
+ let seq = 0;
31231
31253
  for (const arg of args) {
31232
31254
  if (!isGogFileArg(arg)) {
31233
31255
  argv.push(arg);
31234
31256
  continue;
31235
31257
  }
31236
- const path = join(dir, `${arg.flag}.${arg.ext ?? "txt"}`);
31237
- await writeFile(path, arg.contents, { encoding: "utf8", mode: 384 });
31238
- argv.push(`--${arg.flag}=${path}`);
31258
+ const sub = join(dir, String(seq));
31259
+ seq += 1;
31260
+ await mkdir(sub, { recursive: true, mode: 448 });
31261
+ const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
31262
+ const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
31263
+ await writeFile(path, data, { mode: 384 });
31264
+ argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
31239
31265
  }
31240
31266
  return await spawnGog(argv, opts);
31241
31267
  } finally {
@@ -31320,8 +31346,9 @@ function assembleArgs(args, opts) {
31320
31346
  return fullArgs;
31321
31347
  }
31322
31348
  async function run(args, options = {}) {
31323
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
31324
- const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31349
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
31350
+ const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31351
+ const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
31325
31352
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
31326
31353
  const store = activeExecutor();
31327
31354
  try {
@@ -31335,7 +31362,7 @@ async function run(args, options = {}) {
31335
31362
  }
31336
31363
  return redact(output);
31337
31364
  } catch (err) {
31338
- const message = redact(err instanceof Error ? err.message : String(err));
31365
+ const message = base(err instanceof Error ? err.message : String(err));
31339
31366
  if (isRunnerTransportError(err)) {
31340
31367
  throw new RunnerTransportError(message, err.kind, err.status);
31341
31368
  }
@@ -32950,6 +32977,85 @@ function finish(base, itemsKey, merged, token) {
32950
32977
  return rawTextResult(JSON.stringify(out));
32951
32978
  }
32952
32979
 
32980
+ // src/attachments.ts
32981
+ var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
32982
+ var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
32983
+ var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
32984
+ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
32985
+ var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
32986
+ function wireBytesOf(arg) {
32987
+ if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
32988
+ return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
32989
+ }
32990
+ var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
32991
+ var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
32992
+ var inlineAttachmentSchema = external_exports.object({
32993
+ filename: external_exports.string().min(1).describe(
32994
+ `Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
32995
+ ),
32996
+ contentBase64: external_exports.string().min(1).describe(
32997
+ "The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
32998
+ )
32999
+ });
33000
+ var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
33001
+ `Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
33002
+ );
33003
+ function validateFilename(filename, where) {
33004
+ if (/[/\\]/.test(filename)) {
33005
+ throw new Error(
33006
+ `${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
33007
+ );
33008
+ }
33009
+ if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
33010
+ throw new Error(
33011
+ `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
33012
+ );
33013
+ }
33014
+ }
33015
+ function decodedLength(contentBase64) {
33016
+ const buf = Buffer.from(contentBase64, "base64");
33017
+ return buf.toString("base64") === contentBase64 ? buf.length : null;
33018
+ }
33019
+ function inlineFileArg(flag, attachment, opts = {}) {
33020
+ const { filename, contentBase64 } = attachment;
33021
+ const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
33022
+ validateFilename(filename, where);
33023
+ const bytes = decodedLength(contentBase64);
33024
+ if (bytes === null) {
33025
+ throw new Error(
33026
+ `${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
33027
+ );
33028
+ }
33029
+ if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
33030
+ throw new Error(
33031
+ `${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
33032
+ );
33033
+ }
33034
+ const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
33035
+ if (opts.positional) arg.positional = true;
33036
+ return { arg, bytes };
33037
+ }
33038
+ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
33039
+ if (!attachments?.length) return [];
33040
+ const args = [];
33041
+ const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
33042
+ let attachmentWire = 0;
33043
+ let decodedTotal = 0;
33044
+ for (const attachment of attachments) {
33045
+ const { arg, bytes } = inlineFileArg(flag, attachment);
33046
+ attachmentWire += arg.contents.length;
33047
+ decodedTotal += bytes;
33048
+ if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
33049
+ const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
33050
+ throw new Error(
33051
+ `This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
33052
+ );
33053
+ }
33054
+ args.push(arg);
33055
+ }
33056
+ return args;
33057
+ }
33058
+
32953
33059
  // src/tools/gmail.ts
32954
33060
  function registerGmailTools(server) {
32955
33061
  server.registerTool("gog_gmail_search", {
@@ -33003,7 +33109,7 @@ function registerGmailTools(server) {
33003
33109
  return runOrDiagnose(args, { account });
33004
33110
  });
33005
33111
  server.registerTool("gog_gmail_send", {
33006
- description: "Send an email. When attach is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were found and embedded.",
33112
+ description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded.',
33007
33113
  annotations: { destructiveHint: true },
33008
33114
  inputSchema: {
33009
33115
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -33013,16 +33119,19 @@ function registerGmailTools(server) {
33013
33119
  bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
33014
33120
  replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
33015
33121
  threadId: external_exports.string().optional().describe("Thread ID to reply within"),
33016
- attach: external_exports.array(external_exports.string()).optional().describe("Local file paths to attach (repeatable). Each file is read on the gog server (not this client), base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Keep the total under Gmail's ~35 MB inline-upload limit."),
33122
+ attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment.`),
33123
+ attachInline: attachInlineParam,
33017
33124
  account: accountParam
33018
33125
  }
33019
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
33126
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
33020
33127
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
33021
33128
  if (cc) args.push(`--cc=${cc}`);
33022
33129
  if (bcc) args.push(`--bcc=${bcc}`);
33023
33130
  if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
33024
33131
  if (threadId) args.push(`--thread-id=${threadId}`);
33025
33132
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
33133
+ const inline = inlineAttachmentArgs("attach", attachInline, args);
33134
+ args.push(...inline);
33026
33135
  return runOrDiagnose(args, { account });
33027
33136
  });
33028
33137
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
@@ -33352,7 +33461,7 @@ function registerTasksTools(server) {
33352
33461
  }
33353
33462
 
33354
33463
  // src/server.ts
33355
- var VERSION = true ? "2.24.0" : "0.0.0";
33464
+ var VERSION = true ? "2.25.0" : "0.0.0";
33356
33465
  var BASE_TOOL_REGISTRARS = [
33357
33466
  registerApiTools,
33358
33467
  registerAuthTools,
package/dist/lib.js CHANGED
@@ -23056,10 +23056,11 @@ function sanitizedEnv() {
23056
23056
  }
23057
23057
  return result;
23058
23058
  }
23059
+ var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
23059
23060
  var GOOGLE_TOKEN_PATTERNS = [
23060
- /ya29\.[A-Za-z0-9._\-]+/g,
23061
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
23061
23062
  // OAuth2 access tokens
23062
- /1\/\/[A-Za-z0-9._\-]+/g
23063
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
23063
23064
  // OAuth2 refresh tokens
23064
23065
  ];
23065
23066
  function redactGoogleTokens(text) {
@@ -23072,6 +23073,26 @@ function redactGoogleTokens(text) {
23072
23073
  function redactSecrets2(text) {
23073
23074
  return redactGoogleTokens(redactSecrets(text));
23074
23075
  }
23076
+ var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
23077
+ var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
23078
+ function redactPreservingOpaqueFields(text, fields, redact) {
23079
+ const lifted = [];
23080
+ let staged = text;
23081
+ for (const field of fields) {
23082
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23083
+ const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
23084
+ staged = staged.replace(re, (_m, open, value, close) => {
23085
+ lifted.push(value);
23086
+ return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
23087
+ });
23088
+ }
23089
+ if (lifted.length === 0) return redact(text);
23090
+ let redacted = redact(staged);
23091
+ lifted.forEach((value, i) => {
23092
+ redacted = redacted.split(opaquePlaceholder(i)).join(value);
23093
+ });
23094
+ return redacted;
23095
+ }
23075
23096
  function augmentedPath() {
23076
23097
  const home = process.env.HOME;
23077
23098
  const candidates = [
@@ -23102,19 +23123,24 @@ function formatTimeout(ms) {
23102
23123
  return `${ms}ms`;
23103
23124
  }
23104
23125
  async function spawnWithTempFiles(args, opts) {
23105
- const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
23126
+ const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
23106
23127
  const { tmpdir } = await import("node:os");
23107
23128
  const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
23108
23129
  try {
23109
23130
  const argv = [];
23131
+ let seq = 0;
23110
23132
  for (const arg of args) {
23111
23133
  if (!isGogFileArg(arg)) {
23112
23134
  argv.push(arg);
23113
23135
  continue;
23114
23136
  }
23115
- const path = join(dir, `${arg.flag}.${arg.ext ?? "txt"}`);
23116
- await writeFile(path, arg.contents, { encoding: "utf8", mode: 384 });
23117
- argv.push(`--${arg.flag}=${path}`);
23137
+ const sub = join(dir, String(seq));
23138
+ seq += 1;
23139
+ await mkdir(sub, { recursive: true, mode: 448 });
23140
+ const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
23141
+ const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
23142
+ await writeFile(path, data, { mode: 384 });
23143
+ argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
23118
23144
  }
23119
23145
  return await spawnGog(argv, opts);
23120
23146
  } finally {
@@ -23199,8 +23225,9 @@ function assembleArgs(args, opts) {
23199
23225
  return fullArgs;
23200
23226
  }
23201
23227
  async function run(args, options = {}) {
23202
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
23203
- const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
23228
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
23229
+ const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
23230
+ const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
23204
23231
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
23205
23232
  const store = activeExecutor();
23206
23233
  try {
@@ -23214,7 +23241,7 @@ async function run(args, options = {}) {
23214
23241
  }
23215
23242
  return redact(output);
23216
23243
  } catch (err) {
23217
- const message = redact(err instanceof Error ? err.message : String(err));
23244
+ const message = base(err instanceof Error ? err.message : String(err));
23218
23245
  if (isRunnerTransportError(err)) {
23219
23246
  throw new RunnerTransportError(message, err.kind, err.status);
23220
23247
  }
@@ -24838,6 +24865,85 @@ function finish(base, itemsKey, merged, token) {
24838
24865
  return rawTextResult(JSON.stringify(out));
24839
24866
  }
24840
24867
 
24868
+ // src/attachments.ts
24869
+ var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
24870
+ var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
24871
+ var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
24872
+ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
24873
+ var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
24874
+ function wireBytesOf(arg) {
24875
+ if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
24876
+ return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
24877
+ }
24878
+ var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
24879
+ var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
24880
+ var inlineAttachmentSchema = external_exports.object({
24881
+ filename: external_exports.string().min(1).describe(
24882
+ `Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
24883
+ ),
24884
+ contentBase64: external_exports.string().min(1).describe(
24885
+ "The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
24886
+ )
24887
+ });
24888
+ var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
24889
+ `Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
24890
+ );
24891
+ function validateFilename(filename, where) {
24892
+ if (/[/\\]/.test(filename)) {
24893
+ throw new Error(
24894
+ `${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
24895
+ );
24896
+ }
24897
+ if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
24898
+ throw new Error(
24899
+ `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
24900
+ );
24901
+ }
24902
+ }
24903
+ function decodedLength(contentBase64) {
24904
+ const buf = Buffer.from(contentBase64, "base64");
24905
+ return buf.toString("base64") === contentBase64 ? buf.length : null;
24906
+ }
24907
+ function inlineFileArg(flag, attachment, opts = {}) {
24908
+ const { filename, contentBase64 } = attachment;
24909
+ const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
24910
+ validateFilename(filename, where);
24911
+ const bytes = decodedLength(contentBase64);
24912
+ if (bytes === null) {
24913
+ throw new Error(
24914
+ `${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
24915
+ );
24916
+ }
24917
+ if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
24918
+ throw new Error(
24919
+ `${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
24920
+ );
24921
+ }
24922
+ const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
24923
+ if (opts.positional) arg.positional = true;
24924
+ return { arg, bytes };
24925
+ }
24926
+ function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
24927
+ if (!attachments?.length) return [];
24928
+ const args = [];
24929
+ const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
24930
+ let attachmentWire = 0;
24931
+ let decodedTotal = 0;
24932
+ for (const attachment of attachments) {
24933
+ const { arg, bytes } = inlineFileArg(flag, attachment);
24934
+ attachmentWire += arg.contents.length;
24935
+ decodedTotal += bytes;
24936
+ if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
24937
+ const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
24938
+ throw new Error(
24939
+ `This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
24940
+ );
24941
+ }
24942
+ args.push(arg);
24943
+ }
24944
+ return args;
24945
+ }
24946
+
24841
24947
  // src/tools/gmail.ts
24842
24948
  function registerGmailTools(server) {
24843
24949
  server.registerTool("gog_gmail_search", {
@@ -24891,7 +24997,7 @@ function registerGmailTools(server) {
24891
24997
  return runOrDiagnose(args, { account });
24892
24998
  });
24893
24999
  server.registerTool("gog_gmail_send", {
24894
- description: "Send an email. When attach is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were found and embedded.",
25000
+ description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded.',
24895
25001
  annotations: { destructiveHint: true },
24896
25002
  inputSchema: {
24897
25003
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -24901,16 +25007,19 @@ function registerGmailTools(server) {
24901
25007
  bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
24902
25008
  replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
24903
25009
  threadId: external_exports.string().optional().describe("Thread ID to reply within"),
24904
- attach: external_exports.array(external_exports.string()).optional().describe("Local file paths to attach (repeatable). Each file is read on the gog server (not this client), base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Keep the total under Gmail's ~35 MB inline-upload limit."),
25010
+ attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment.`),
25011
+ attachInline: attachInlineParam,
24905
25012
  account: accountParam
24906
25013
  }
24907
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
25014
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
24908
25015
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
24909
25016
  if (cc) args.push(`--cc=${cc}`);
24910
25017
  if (bcc) args.push(`--bcc=${bcc}`);
24911
25018
  if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
24912
25019
  if (threadId) args.push(`--thread-id=${threadId}`);
24913
25020
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
25021
+ const inline = inlineAttachmentArgs("attach", attachInline, args);
25022
+ args.push(...inline);
24914
25023
  return runOrDiagnose(args, { account });
24915
25024
  });
24916
25025
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
@@ -25240,7 +25349,7 @@ function registerTasksTools(server) {
25240
25349
  }
25241
25350
 
25242
25351
  // src/server.ts
25243
- var VERSION = true ? "2.24.0" : "0.0.0";
25352
+ var VERSION = true ? "2.25.0" : "0.0.0";
25244
25353
  var BASE_TOOL_REGISTRARS = [
25245
25354
  registerApiTools,
25246
25355
  registerAuthTools,
@@ -25740,17 +25849,25 @@ function useRemoteGogRunner(env = process.env) {
25740
25849
  }
25741
25850
  export {
25742
25851
  BASE_TOOL_REGISTRARS,
25852
+ INLINE_ATTACHMENT_LIMITS_TEXT,
25853
+ MAX_INLINE_ATTACHMENT_BYTES,
25854
+ MAX_INLINE_ATTACHMENT_TOTAL_BYTES,
25855
+ MAX_REQUEST_PAYLOAD_WIRE_BYTES,
25743
25856
  MIN_GOG_VERSION,
25744
25857
  PAYLOAD_INLINE_MAX,
25745
25858
  VERSION,
25746
25859
  accountParam,
25747
25860
  annotateTruncatedList,
25861
+ attachInlineParam,
25748
25862
  authToolsFor,
25749
25863
  diagnose,
25750
25864
  errorText,
25751
25865
  fetchGmailPages,
25752
25866
  finalizeGmailSearch,
25753
25867
  ids,
25868
+ inlineAttachmentArgs,
25869
+ inlineAttachmentSchema,
25870
+ inlineFileArg,
25754
25871
  isGogFileArg,
25755
25872
  normalizeTimestamps,
25756
25873
  pageAliasParam,
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp",
5
5
  "display_name": "gogcli",
6
- "version": "2.24.0",
6
+ "version": "2.25.0",
7
7
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -110,7 +110,7 @@
110
110
  },
111
111
  {
112
112
  "name": "gog_gmail_send",
113
- "description": "Send an email"
113
+ "description": "Send an email, with attachments from server-side paths (attach) or from base64 bytes sent with the call (attachInline, for remote deployments with no shared filesystem)"
114
114
  },
115
115
  {
116
116
  "name": "gog_gmail_run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp",
5
5
  "description": "MCP server wrapping gogcli for Google service access",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp"
9
9
  },
10
- "version": "2.24.0",
10
+ "version": "2.25.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.24.0",
15
+ "version": "2.25.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },