gogcli-mcp 2.23.2 → 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.
package/dist/lib.js CHANGED
@@ -23042,7 +23042,7 @@ function activeExecutor() {
23042
23042
  return runExecutor.getStore() ?? defaultExecutor;
23043
23043
  }
23044
23044
  var TIMEOUT_MS = 3e4;
23045
- var MIN_GOG_VERSION = "0.35.0";
23045
+ var MIN_GOG_VERSION = "0.37.0";
23046
23046
  function readonlyEnvEnabled() {
23047
23047
  return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
23048
23048
  }
@@ -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
  }
@@ -23712,6 +23739,7 @@ function registerApiTools(server) {
23712
23739
  // src/tools/auth.ts
23713
23740
  function registerAuthToolsWith(server, defaultServices) {
23714
23741
  const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
23742
+ const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
23715
23743
  server.registerTool("gog_auth_list", {
23716
23744
  description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
23717
23745
  annotations: { readOnlyHint: true },
@@ -23761,11 +23789,14 @@ function registerAuthToolsWith(server, defaultServices) {
23761
23789
  annotations: { destructiveHint: true },
23762
23790
  inputSchema: {
23763
23791
  email: external_exports.string().describe("Google account email to authorize"),
23764
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
23792
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
23793
+ extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
23765
23794
  }
23766
- }, async ({ email: email3, services = defaultServices }) => {
23795
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
23767
23796
  try {
23768
- return rawTextResult(await run(["auth", "add", email3, "--services", services], {
23797
+ const args = ["auth", "add", email3, "--services", services];
23798
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
23799
+ return rawTextResult(await run(args, {
23769
23800
  interactive: true,
23770
23801
  timeout: 3e5
23771
23802
  }));
@@ -23777,14 +23808,14 @@ function registerAuthToolsWith(server, defaultServices) {
23777
23808
  description: "Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any browser \u2014 no local server or terminal on the gogcli host is needed, so this works over the hosted connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, the browser is redirected to a localhost URL that fails to load \u2014 that is expected. They copy that full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to gog_auth_add_complete or the second step will not match this one.",
23778
23809
  inputSchema: {
23779
23810
  email: external_exports.string().describe("Google account email to authorize"),
23780
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
23811
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
23812
+ extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
23781
23813
  }
23782
- }, async ({ email: email3, services = defaultServices }) => {
23814
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
23783
23815
  try {
23784
- return rawTextResult(await run(
23785
- ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"],
23786
- { redactMode: "tokens" }
23787
- ));
23816
+ const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
23817
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
23818
+ return rawTextResult(await run(args, { redactMode: "tokens" }));
23788
23819
  } catch (err) {
23789
23820
  return errorResult(errorText(err));
23790
23821
  }
@@ -23799,25 +23830,28 @@ function registerAuthToolsWith(server, defaultServices) {
23799
23830
  ),
23800
23831
  services: external_exports.string().optional().default(defaultServices).describe(
23801
23832
  `Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
23833
+ ),
23834
+ extraScopes: external_exports.string().optional().describe(
23835
+ "Extra OAuth scope URIs \u2014 MUST match the value passed to gog_auth_add_url, for the same reason `services` must: the two steps have to describe the same grant."
23802
23836
  )
23803
23837
  }
23804
- }, async ({ email: email3, redirectUrl, services = defaultServices }) => {
23838
+ }, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
23805
23839
  try {
23806
- return rawTextResult(await run(
23807
- [
23808
- "auth",
23809
- "add",
23810
- email3,
23811
- "--remote",
23812
- "--step",
23813
- "2",
23814
- "--auth-url",
23815
- redirectUrl,
23816
- "--services",
23817
- services,
23818
- "--force-consent"
23819
- ]
23820
- ));
23840
+ const args = [
23841
+ "auth",
23842
+ "add",
23843
+ email3,
23844
+ "--remote",
23845
+ "--step",
23846
+ "2",
23847
+ "--auth-url",
23848
+ redirectUrl,
23849
+ "--services",
23850
+ services,
23851
+ "--force-consent"
23852
+ ];
23853
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
23854
+ return rawTextResult(await run(args));
23821
23855
  } catch (err) {
23822
23856
  return errorResult(errorText(err));
23823
23857
  }
@@ -23839,13 +23873,19 @@ function authToolsFor(defaultServices) {
23839
23873
  // src/tools/calendar.ts
23840
23874
  function registerCalendarTools(server) {
23841
23875
  server.registerTool("gog_calendar_events", {
23842
- description: 'List calendar events. Filters can be combined (e.g. --from + --to for a range, or --today for just today). gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
23876
+ description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
23843
23877
  annotations: { readOnlyHint: true },
23844
23878
  inputSchema: {
23845
23879
  calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
23846
23880
  from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
23847
- to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
23848
- today: external_exports.boolean().optional().describe("Only show today's events"),
23881
+ to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
23882
+ // gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
23883
+ // a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
23884
+ // silently threw --from away and answered for today instead — at exit 0,
23885
+ // in a well-formed table. It is only exposed here now that it means what
23886
+ // it says.
23887
+ days: external_exports.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
23888
+ today: external_exports.boolean().optional().describe("Only show today's events. A complete window on its own \u2014 mutually exclusive with from, to and days."),
23849
23889
  query: external_exports.string().optional().describe("Free text search within events"),
23850
23890
  max: external_exports.number().int().optional().describe("Max events to return. gog defaults to 10, which silently hides the rest \u2014 raise it, or page with pageToken."),
23851
23891
  pageToken: pageTokenParam,
@@ -23855,11 +23895,12 @@ function registerCalendarTools(server) {
23855
23895
  timezone: external_exports.string().optional().describe(`Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event's timezone, then its calendar's timezone.`),
23856
23896
  account: accountParam
23857
23897
  }
23858
- }, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
23898
+ }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
23859
23899
  const args = ["calendar", "events"];
23860
23900
  if (calendarId) args.push(calendarId);
23861
23901
  if (from) args.push(`--from=${from}`);
23862
23902
  if (to) args.push(`--to=${to}`);
23903
+ if (days !== void 0) args.push(`--days=${days}`);
23863
23904
  if (today) args.push("--today");
23864
23905
  if (query) args.push(`--query=${query}`);
23865
23906
  if (max !== void 0) args.push(`--max=${max}`);
@@ -24824,6 +24865,85 @@ function finish(base, itemsKey, merged, token) {
24824
24865
  return rawTextResult(JSON.stringify(out));
24825
24866
  }
24826
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
+
24827
24947
  // src/tools/gmail.ts
24828
24948
  function registerGmailTools(server) {
24829
24949
  server.registerTool("gog_gmail_search", {
@@ -24858,20 +24978,26 @@ function registerGmailTools(server) {
24858
24978
  });
24859
24979
  });
24860
24980
  server.registerTool("gog_gmail_get", {
24861
- description: "Get a Gmail message by ID.",
24981
+ description: "Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.",
24862
24982
  annotations: { readOnlyHint: true },
24863
24983
  inputSchema: {
24864
24984
  messageId: external_exports.string().describe("Message ID"),
24865
24985
  format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
24986
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
24987
+ // carried the headers and body TWICE — once inside `message`, once
24988
+ // copied to the top level — so the flag meant to shrink the payload
24989
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
24990
+ sanitizeContent: external_exports.boolean().optional().describe("Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy \u2014 omit this when you need to follow a link out of the message."),
24866
24991
  account: accountParam
24867
24992
  }
24868
- }, async ({ messageId, format, account }) => {
24993
+ }, async ({ messageId, format, sanitizeContent, account }) => {
24869
24994
  const args = ["gmail", "get", messageId];
24870
24995
  if (format) args.push(`--format=${format}`);
24996
+ if (sanitizeContent) args.push("--sanitize-content");
24871
24997
  return runOrDiagnose(args, { account });
24872
24998
  });
24873
24999
  server.registerTool("gog_gmail_send", {
24874
- 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.',
24875
25001
  annotations: { destructiveHint: true },
24876
25002
  inputSchema: {
24877
25003
  to: external_exports.string().describe("Recipient(s), comma-separated"),
@@ -24881,16 +25007,19 @@ function registerGmailTools(server) {
24881
25007
  bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
24882
25008
  replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
24883
25009
  threadId: external_exports.string().optional().describe("Thread ID to reply within"),
24884
- 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,
24885
25012
  account: accountParam
24886
25013
  }
24887
- }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
25014
+ }, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
24888
25015
  const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
24889
25016
  if (cc) args.push(`--cc=${cc}`);
24890
25017
  if (bcc) args.push(`--bcc=${bcc}`);
24891
25018
  if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
24892
25019
  if (threadId) args.push(`--thread-id=${threadId}`);
24893
25020
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
25021
+ const inline = inlineAttachmentArgs("attach", attachInline, args);
25022
+ args.push(...inline);
24894
25023
  return runOrDiagnose(args, { account });
24895
25024
  });
24896
25025
  registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
@@ -25220,7 +25349,7 @@ function registerTasksTools(server) {
25220
25349
  }
25221
25350
 
25222
25351
  // src/server.ts
25223
- var VERSION = true ? "2.23.2" : "0.0.0";
25352
+ var VERSION = true ? "2.25.0" : "0.0.0";
25224
25353
  var BASE_TOOL_REGISTRARS = [
25225
25354
  registerApiTools,
25226
25355
  registerAuthTools,
@@ -25720,17 +25849,25 @@ function useRemoteGogRunner(env = process.env) {
25720
25849
  }
25721
25850
  export {
25722
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,
25723
25856
  MIN_GOG_VERSION,
25724
25857
  PAYLOAD_INLINE_MAX,
25725
25858
  VERSION,
25726
25859
  accountParam,
25727
25860
  annotateTruncatedList,
25861
+ attachInlineParam,
25728
25862
  authToolsFor,
25729
25863
  diagnose,
25730
25864
  errorText,
25731
25865
  fetchGmailPages,
25732
25866
  finalizeGmailSearch,
25733
25867
  ids,
25868
+ inlineAttachmentArgs,
25869
+ inlineAttachmentSchema,
25870
+ inlineFileArg,
25734
25871
  isGogFileArg,
25735
25872
  normalizeTimestamps,
25736
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.23.2",
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.23.2",
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.23.2",
10
+ "version": "2.25.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.23.2",
15
+ "version": "2.25.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },