gogcli-mcp-gmail 2.23.2 → 2.24.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > [!WARNING]
4
4
  > **AI-developed project.** This codebase was built and is actively maintained by [Claude Code](https://www.anthropic.com/claude). Review all code and tool permissions before use.
5
5
 
6
- Extended Gmail [MCP](https://modelcontextprotocol.io) server via [gogcli](https://github.com/openclaw/gogcli). Includes auth tools plus 46 additional dedicated Gmail tools for threads, labels, drafts, attachments, forwarding, autoreply, and bulk operations.
6
+ Extended Gmail [MCP](https://modelcontextprotocol.io) server via [gogcli](https://github.com/openclaw/gogcli). Includes auth tools plus 49 additional dedicated Gmail tools for threads, labels, drafts, attachments, forwarding, autoreply, and bulk operations.
7
7
 
8
8
  ## Requirements
9
9
 
@@ -44,9 +44,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
44
44
  claude mcp add gogcli-gmail -- gogcli-mcp-gmail
45
45
  ```
46
46
 
47
- ## Extra Gmail Tools (46)
47
+ ## Extra Gmail Tools (49)
48
48
 
49
- Plus 8 auth tools and 4 base Gmail tools (search, get, send, run) — 58 in all.
49
+ Plus 8 auth tools and 4 base Gmail tools (search, get, send, run) — 61 in all.
50
50
 
51
51
  ### Read
52
52
 
@@ -101,6 +101,9 @@ Plus 8 auth tools and 4 base Gmail tools (search, get, send, run) — 58 in all.
101
101
  | `gog_gmail_drafts_delete` | Delete a draft |
102
102
  | `gog_gmail_drafts_send` | Send an existing draft (a 404 comes back diagnosed — `DRAFT_FORKED`, or `GOOGLE_404_NOT_THE_DRAFT` when the draft is still listed) |
103
103
  | `gog_gmail_drafts_diff` | Diff two named drafts — divergent body lines (with untruncated `onlyInACount`/`onlyInBCount`), threading loss, and a conservative fork verdict (2 gog calls) |
104
+ | `gog_gmail_drafts_reply` | Save a reply as a draft — inherited recipients, subject and quote; never sends |
105
+ | `gog_gmail_drafts_reply_all` | Save a reply-all as a draft; never sends |
106
+ | `gog_gmail_drafts_forward` | Save a forward as a draft; recipients optional, so it can be staged without any |
104
107
 
105
108
  #### When a draft you created stops resolving
106
109
 
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Use when the user asks to read, organize, draft, forward, autoreply
5
5
 
6
6
  # gogcli-mcp-gmail
7
7
 
8
- Extended Gmail MCP server via [gogcli](https://github.com/openclaw/gogcli) — 58 tools: 8 auth + 4 base Gmail + 46 extra dedicated Gmail tools.
8
+ Extended Gmail MCP server via [gogcli](https://github.com/openclaw/gogcli) — 61 tools: 8 auth + 4 base Gmail + 49 extra dedicated Gmail tools.
9
9
 
10
10
  - **Source:** [github.com/chrischall/gogcli-mcp](https://github.com/chrischall/gogcli-mcp)
11
11
 
@@ -80,6 +80,9 @@ Extended Gmail MCP server via [gogcli](https://github.com/openclaw/gogcli) — 5
80
80
  | `gog_gmail_drafts_delete` | Delete a draft |
81
81
  | `gog_gmail_drafts_send` | Send a draft (404 → `DRAFT_FORKED`, or `GOOGLE_404_NOT_THE_DRAFT` if the draft is still listed) |
82
82
  | `gog_gmail_drafts_diff` | Diff two drafts (body divergence, threading loss, fork verdict) |
83
+ | `gog_gmail_drafts_reply` | Save a reply as a draft — inherited recipients, subject and quote; never sends |
84
+ | `gog_gmail_drafts_reply_all` | Save a reply-all as a draft; never sends |
85
+ | `gog_gmail_drafts_forward` | Save a forward as a draft; recipients optional, so it can be staged without any |
83
86
 
84
87
  A draft edited in a mail client is replaced, not updated: the old id 404s. `drafts_update` / `drafts_send` answer that
85
88
  404 with a `DRAFT_FORKED` report (what happened, the drafts that exist, what to do) instead of a bare `notFound`, at a
package/dist/index.js CHANGED
@@ -31753,6 +31753,7 @@ function formatAuthHealth(raw, now) {
31753
31753
  // ../gogcli-mcp/src/tools/auth.ts
31754
31754
  function registerAuthToolsWith(server, defaultServices) {
31755
31755
  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.`;
31756
+ 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.";
31756
31757
  server.registerTool("gog_auth_list", {
31757
31758
  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.",
31758
31759
  annotations: { readOnlyHint: true },
@@ -31802,11 +31803,14 @@ function registerAuthToolsWith(server, defaultServices) {
31802
31803
  annotations: { destructiveHint: true },
31803
31804
  inputSchema: {
31804
31805
  email: external_exports.string().describe("Google account email to authorize"),
31805
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31806
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31807
+ extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
31806
31808
  }
31807
- }, async ({ email: email3, services = defaultServices }) => {
31809
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31808
31810
  try {
31809
- return rawTextResult(await run(["auth", "add", email3, "--services", services], {
31811
+ const args = ["auth", "add", email3, "--services", services];
31812
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
31813
+ return rawTextResult(await run(args, {
31810
31814
  interactive: true,
31811
31815
  timeout: 3e5
31812
31816
  }));
@@ -31818,14 +31822,14 @@ function registerAuthToolsWith(server, defaultServices) {
31818
31822
  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.",
31819
31823
  inputSchema: {
31820
31824
  email: external_exports.string().describe("Google account email to authorize"),
31821
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31825
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31826
+ extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
31822
31827
  }
31823
- }, async ({ email: email3, services = defaultServices }) => {
31828
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31824
31829
  try {
31825
- return rawTextResult(await run(
31826
- ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"],
31827
- { redactMode: "tokens" }
31828
- ));
31830
+ const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
31831
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31832
+ return rawTextResult(await run(args, { redactMode: "tokens" }));
31829
31833
  } catch (err) {
31830
31834
  return errorResult(errorText(err));
31831
31835
  }
@@ -31840,25 +31844,28 @@ function registerAuthToolsWith(server, defaultServices) {
31840
31844
  ),
31841
31845
  services: external_exports.string().optional().default(defaultServices).describe(
31842
31846
  `Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
31847
+ ),
31848
+ extraScopes: external_exports.string().optional().describe(
31849
+ "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."
31843
31850
  )
31844
31851
  }
31845
- }, async ({ email: email3, redirectUrl, services = defaultServices }) => {
31852
+ }, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
31846
31853
  try {
31847
- return rawTextResult(await run(
31848
- [
31849
- "auth",
31850
- "add",
31851
- email3,
31852
- "--remote",
31853
- "--step",
31854
- "2",
31855
- "--auth-url",
31856
- redirectUrl,
31857
- "--services",
31858
- services,
31859
- "--force-consent"
31860
- ]
31861
- ));
31854
+ const args = [
31855
+ "auth",
31856
+ "add",
31857
+ email3,
31858
+ "--remote",
31859
+ "--step",
31860
+ "2",
31861
+ "--auth-url",
31862
+ redirectUrl,
31863
+ "--services",
31864
+ services,
31865
+ "--force-consent"
31866
+ ];
31867
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31868
+ return rawTextResult(await run(args));
31862
31869
  } catch (err) {
31863
31870
  return errorResult(errorText(err));
31864
31871
  }
@@ -32000,16 +32007,22 @@ function registerGmailTools(server) {
32000
32007
  });
32001
32008
  });
32002
32009
  server.registerTool("gog_gmail_get", {
32003
- description: "Get a Gmail message by ID.",
32010
+ 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.",
32004
32011
  annotations: { readOnlyHint: true },
32005
32012
  inputSchema: {
32006
32013
  messageId: external_exports.string().describe("Message ID"),
32007
32014
  format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
32015
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
32016
+ // carried the headers and body TWICE — once inside `message`, once
32017
+ // copied to the top level — so the flag meant to shrink the payload
32018
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
32019
+ 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."),
32008
32020
  account: accountParam
32009
32021
  }
32010
- }, async ({ messageId, format, account }) => {
32022
+ }, async ({ messageId, format, sanitizeContent, account }) => {
32011
32023
  const args = ["gmail", "get", messageId];
32012
32024
  if (format) args.push(`--format=${format}`);
32025
+ if (sanitizeContent) args.push("--sanitize-content");
32013
32026
  return runOrDiagnose(args, { account });
32014
32027
  });
32015
32028
  server.registerTool("gog_gmail_send", {
@@ -32048,7 +32061,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
32048
32061
  );
32049
32062
 
32050
32063
  // ../gogcli-mcp/src/server.ts
32051
- var VERSION = true ? "2.23.2" : "0.0.0";
32064
+ var VERSION = true ? "2.24.0" : "0.0.0";
32052
32065
 
32053
32066
  // ../gogcli-mcp/src/auth-log.ts
32054
32067
  var FAILURES = /* @__PURE__ */ new Set([
@@ -34226,7 +34239,7 @@ function registerExtraGmailTools(server) {
34226
34239
  args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
34227
34240
  }
34228
34241
  server.registerTool("gog_gmail_reply", {
34229
- description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage a reply without sending use gog_gmail_drafts_create.',
34242
+ description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage this same reply without sending it use gog_gmail_drafts_reply, which composes exactly what this tool would send.',
34230
34243
  annotations: { destructiveHint: true },
34231
34244
  inputSchema: replySchema
34232
34245
  }, async ({ messageId, account, ...flags }) => {
@@ -34235,7 +34248,7 @@ function registerExtraGmailTools(server) {
34235
34248
  return runOrDiagnose(args, { account });
34236
34249
  });
34237
34250
  server.registerTool("gog_gmail_reply_all", {
34238
- description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.',
34251
+ description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it without sending use gog_gmail_drafts_reply_all.',
34239
34252
  annotations: { destructiveHint: true },
34240
34253
  inputSchema: replySchema
34241
34254
  }, async ({ messageId, account, ...flags }) => {
@@ -34243,6 +34256,46 @@ function registerExtraGmailTools(server) {
34243
34256
  appendReplyFlags(args, flags);
34244
34257
  return runOrDiagnose(args, { account });
34245
34258
  });
34259
+ const draftReplyNote = ' Composes exactly what gog_gmail_reply%s would send \u2014 inherited recipients, "Re:" subject and quoted original \u2014 but SAVES IT AS A DRAFT instead of sending. Nothing leaves the mailbox; send it later with gog_gmail_drafts_send, or edit it first with gog_gmail_drafts_update (which overwrites the whole body, quote included \u2014 read the draft back before editing).';
34260
+ server.registerTool("gog_gmail_drafts_reply", {
34261
+ description: "Save a reply to a Gmail message as a draft (to the original sender only)." + draftReplyNote.replace("%s", "") + " Prefer this over gog_gmail_drafts_create + replyToMessageId when the draft is a real reply: that route threads the draft but leaves recipients and quoting for you to reconstruct.",
34262
+ inputSchema: { ...replySchema, returnFull: draftWriteSchema.returnFull }
34263
+ }, async ({ messageId, account, returnFull, ...flags }) => {
34264
+ const args = ["gmail", "drafts", "reply", messageId];
34265
+ appendReplyFlags(args, flags);
34266
+ return writeDraft(args, account, returnFull);
34267
+ });
34268
+ server.registerTool("gog_gmail_drafts_reply_all", {
34269
+ description: "Save a reply-all to a Gmail message as a draft (sender plus every To/Cc recipient)." + draftReplyNote.replace("%s", "_all") + " Use the remove flag to drop recipients BEFORE the draft exists, rather than editing them out afterwards.",
34270
+ inputSchema: { ...replySchema, returnFull: draftWriteSchema.returnFull }
34271
+ }, async ({ messageId, account, returnFull, ...flags }) => {
34272
+ const args = ["gmail", "drafts", "reply-all", messageId];
34273
+ appendReplyFlags(args, flags);
34274
+ return writeDraft(args, account, returnFull);
34275
+ });
34276
+ server.registerTool("gog_gmail_drafts_forward", {
34277
+ description: "Save a forward of a Gmail message as a draft. Same composition as gog_gmail_forward \u2014 the original message quoted below an optional note, with its attachments carried over \u2014 but nothing is sent. Unlike gog_gmail_forward, `to` is OPTIONAL here: omit it to stage a recipient-less forward as an accidental-send guard, then add recipients with gog_gmail_drafts_update before gog_gmail_drafts_send.",
34278
+ inputSchema: {
34279
+ messageId: external_exports.string().describe("Gmail message ID to forward"),
34280
+ to: external_exports.string().optional().describe("Recipients (comma-separated). Optional for a draft \u2014 omit to stage the forward without recipients."),
34281
+ cc: external_exports.string().optional().describe("CC recipients (comma-separated)"),
34282
+ bcc: external_exports.string().optional().describe("BCC recipients (comma-separated)"),
34283
+ note: external_exports.string().optional().describe("Introductory text above the forwarded message"),
34284
+ from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
34285
+ skipAttachments: external_exports.boolean().optional().describe("Do not include original attachments"),
34286
+ returnFull: draftWriteSchema.returnFull,
34287
+ account: accountParam
34288
+ }
34289
+ }, async ({ messageId, to, cc, bcc, note, from, skipAttachments, returnFull, account }) => {
34290
+ const args = ["gmail", "drafts", "forward", messageId];
34291
+ if (to) args.push(`--to=${to}`);
34292
+ if (cc) args.push(`--cc=${cc}`);
34293
+ if (bcc) args.push(`--bcc=${bcc}`);
34294
+ if (note) args.push(payloadArg("note", "note-file", note));
34295
+ if (from) args.push(`--from=${from}`);
34296
+ if (skipAttachments) args.push("--skip-attachments");
34297
+ return writeDraft(args, account, returnFull);
34298
+ });
34246
34299
  server.registerTool("gog_gmail_autoreply", {
34247
34300
  description: "Reply once to all messages matching a Gmail search query. Use the label flag to dedupe across runs.",
34248
34301
  annotations: { destructiveHint: true },
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-gmail",
5
5
  "display_name": "gogcli (Gmail)",
6
- "version": "2.23.2",
6
+ "version": "2.24.0",
7
7
  "description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -213,6 +213,18 @@
213
213
  "name": "gog_gmail_drafts_diff",
214
214
  "description": "Diff two named drafts: what each body has that the other lost, how their threading differs, and (only on a link back to the original or on agreement over unquoted text, with evidence) whether one replaced the other"
215
215
  },
216
+ {
217
+ "name": "gog_gmail_drafts_reply",
218
+ "description": "Save a reply to a Gmail message as a draft (inherited recipients, subject and quote; never sends)"
219
+ },
220
+ {
221
+ "name": "gog_gmail_drafts_reply_all",
222
+ "description": "Save a reply-all to a Gmail message as a draft (never sends)"
223
+ },
224
+ {
225
+ "name": "gog_gmail_drafts_forward",
226
+ "description": "Save a forward of a Gmail message as a draft; recipients are optional (never sends)"
227
+ },
216
228
  {
217
229
  "name": "gog_gmail_import",
218
230
  "description": "Import an RFC822/EML message into the mailbox (keeps its original headers and date; does not send)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-gmail",
3
- "version": "2.23.2",
3
+ "version": "2.24.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-gmail",
5
5
  "description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -3225,7 +3225,7 @@ export function registerExtraGmailTools(server: McpServer): void {
3225
3225
  }
3226
3226
 
3227
3227
  server.registerTool('gog_gmail_reply', {
3228
- description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage a reply without sending use gog_gmail_drafts_create.',
3228
+ description: 'Reply to a Gmail message (sends to the original sender only). Threads off the message and inherits a "Re:" subject and the quoted original by default. For replying to every participant use gog_gmail_reply_all; to reply across many messages matching a query use gog_gmail_autoreply; to stage this same reply without sending it use gog_gmail_drafts_reply, which composes exactly what this tool would send.',
3229
3229
  annotations: { destructiveHint: true },
3230
3230
  inputSchema: replySchema,
3231
3231
  }, async ({ messageId, account, ...flags }) => {
@@ -3235,7 +3235,7 @@ export function registerExtraGmailTools(server: McpServer): void {
3235
3235
  });
3236
3236
 
3237
3237
  server.registerTool('gog_gmail_reply_all', {
3238
- description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all.',
3238
+ description: 'Reply to all participants of a Gmail message (sender plus every To/Cc recipient). Same inherited "Re:" subject and quoting as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it without sending use gog_gmail_drafts_reply_all.',
3239
3239
  annotations: { destructiveHint: true },
3240
3240
  inputSchema: replySchema,
3241
3241
  }, async ({ messageId, account, ...flags }) => {
@@ -3244,6 +3244,76 @@ export function registerExtraGmailTools(server: McpServer): void {
3244
3244
  return runOrDiagnose(args, { account });
3245
3245
  });
3246
3246
 
3247
+ // gog >= 0.36.0: the draft-side twins of reply / reply-all / forward. They
3248
+ // take the SAME flag set as the send-side commands and share the composition
3249
+ // path with them, so the schemas above are reused verbatim rather than
3250
+ // re-declared — the only difference is the subcommand and that NOTHING IS
3251
+ // SENT.
3252
+ //
3253
+ // These exist because staging a reply used to mean gog_gmail_drafts_create
3254
+ // with replyToMessageId/replyToThreadId, which threads the draft but does NOT
3255
+ // inherit the original's recipients or quote its body — the caller had to
3256
+ // rebuild both by hand, and a missed Cc is invisible until the draft goes
3257
+ // out. Here the inheritance is gog's, identical to what the send path would
3258
+ // have produced.
3259
+ const draftReplyNote =
3260
+ ' Composes exactly what gog_gmail_reply%s would send — inherited recipients, "Re:" subject and quoted ' +
3261
+ 'original — but SAVES IT AS A DRAFT instead of sending. Nothing leaves the mailbox; send it later with ' +
3262
+ 'gog_gmail_drafts_send, or edit it first with gog_gmail_drafts_update (which overwrites the whole body, ' +
3263
+ 'quote included — read the draft back before editing).';
3264
+
3265
+ server.registerTool('gog_gmail_drafts_reply', {
3266
+ description:
3267
+ 'Save a reply to a Gmail message as a draft (to the original sender only).' + draftReplyNote.replace('%s', '') +
3268
+ ' Prefer this over gog_gmail_drafts_create + replyToMessageId when the draft is a real reply: that route threads ' +
3269
+ 'the draft but leaves recipients and quoting for you to reconstruct.',
3270
+ inputSchema: { ...replySchema, returnFull: draftWriteSchema.returnFull },
3271
+ }, async ({ messageId, account, returnFull, ...flags }) => {
3272
+ const args: GogArg[] = ['gmail', 'drafts', 'reply', messageId];
3273
+ appendReplyFlags(args, flags);
3274
+ return writeDraft(args, account, returnFull);
3275
+ });
3276
+
3277
+ server.registerTool('gog_gmail_drafts_reply_all', {
3278
+ description:
3279
+ 'Save a reply-all to a Gmail message as a draft (sender plus every To/Cc recipient).' +
3280
+ draftReplyNote.replace('%s', '_all') +
3281
+ ' Use the remove flag to drop recipients BEFORE the draft exists, rather than editing them out afterwards.',
3282
+ inputSchema: { ...replySchema, returnFull: draftWriteSchema.returnFull },
3283
+ }, async ({ messageId, account, returnFull, ...flags }) => {
3284
+ const args: GogArg[] = ['gmail', 'drafts', 'reply-all', messageId];
3285
+ appendReplyFlags(args, flags);
3286
+ return writeDraft(args, account, returnFull);
3287
+ });
3288
+
3289
+ server.registerTool('gog_gmail_drafts_forward', {
3290
+ description:
3291
+ 'Save a forward of a Gmail message as a draft. Same composition as gog_gmail_forward — the original ' +
3292
+ 'message quoted below an optional note, with its attachments carried over — but nothing is sent. ' +
3293
+ 'Unlike gog_gmail_forward, `to` is OPTIONAL here: omit it to stage a recipient-less forward as an ' +
3294
+ 'accidental-send guard, then add recipients with gog_gmail_drafts_update before gog_gmail_drafts_send.',
3295
+ inputSchema: {
3296
+ messageId: z.string().describe('Gmail message ID to forward'),
3297
+ to: z.string().optional().describe('Recipients (comma-separated). Optional for a draft — omit to stage the forward without recipients.'),
3298
+ cc: z.string().optional().describe('CC recipients (comma-separated)'),
3299
+ bcc: z.string().optional().describe('BCC recipients (comma-separated)'),
3300
+ note: z.string().optional().describe('Introductory text above the forwarded message'),
3301
+ from: z.string().optional().describe('Send from this email address (must be a verified send-as alias)'),
3302
+ skipAttachments: z.boolean().optional().describe('Do not include original attachments'),
3303
+ returnFull: draftWriteSchema.returnFull,
3304
+ account: accountParam,
3305
+ },
3306
+ }, async ({ messageId, to, cc, bcc, note, from, skipAttachments, returnFull, account }) => {
3307
+ const args: GogArg[] = ['gmail', 'drafts', 'forward', messageId];
3308
+ if (to) args.push(`--to=${to}`);
3309
+ if (cc) args.push(`--cc=${cc}`);
3310
+ if (bcc) args.push(`--bcc=${bcc}`);
3311
+ if (note) args.push(payloadArg('note', 'note-file', note));
3312
+ if (from) args.push(`--from=${from}`);
3313
+ if (skipAttachments) args.push('--skip-attachments');
3314
+ return writeDraft(args, account, returnFull);
3315
+ });
3316
+
3247
3317
  server.registerTool('gog_gmail_autoreply', {
3248
3318
  description: 'Reply once to all messages matching a Gmail search query. Use the label flag to dedupe across runs.',
3249
3319
  annotations: { destructiveHint: true },
@@ -1669,6 +1669,139 @@ describe('gog_gmail_reply_all', () => {
1669
1669
  });
1670
1670
  });
1671
1671
 
1672
+ // gog 0.36.0 (openclaw/gogcli#977) added the draft-side twins of reply /
1673
+ // reply-all / forward. The point of these tests is the SUBCOMMAND: the flag
1674
+ // handling is the send path's, shared verbatim, and a copy of it here would
1675
+ // only re-assert what the reply tests above already pin. What is new — and what
1676
+ // a regression would silently break — is that these route to `drafts <verb>`
1677
+ // and therefore never send.
1678
+ describe('gog_gmail_drafts_reply', () => {
1679
+ it('routes to gmail drafts reply, not the sending reply', async () => {
1680
+ await harness.callTool('gog_gmail_drafts_reply', { messageId: 'm1', body: 'Thanks' });
1681
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1682
+ ['gmail', 'drafts', 'reply', 'm1', '--body=Thanks', '--auto-from-addressed-alias=false'],
1683
+ { account: undefined },
1684
+ );
1685
+ });
1686
+
1687
+ it('passes the shared reply flag set through unchanged', async () => {
1688
+ await harness.callTool('gog_gmail_drafts_reply', {
1689
+ messageId: 'm1',
1690
+ body: 'Hi',
1691
+ to: ['a@b.com'],
1692
+ cc: ['cc@x.com'],
1693
+ remove: ['old@x.com'],
1694
+ subject: 'New subject',
1695
+ noQuote: true,
1696
+ attach: ['/tmp/a.pdf'],
1697
+ from: 'me@x.com',
1698
+ signature: true,
1699
+ account: 'me@gmail.com',
1700
+ });
1701
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1702
+ [
1703
+ 'gmail', 'drafts', 'reply', 'm1',
1704
+ '--body=Hi',
1705
+ '--to=a@b.com',
1706
+ '--cc=cc@x.com',
1707
+ '--remove=old@x.com',
1708
+ '--subject=New subject',
1709
+ '--no-quote',
1710
+ '--attach=/tmp/a.pdf',
1711
+ '--from=me@x.com',
1712
+ '--signature',
1713
+ '--auto-from-addressed-alias=false',
1714
+ ],
1715
+ { account: 'me@gmail.com' },
1716
+ );
1717
+ });
1718
+
1719
+ it('returnFull re-fetches the saved draft and never reaches the CLI as a flag', async () => {
1720
+ vi.mocked(lib.runOrDiagnose)
1721
+ .mockResolvedValueOnce(rawTextResult('{"draftId":"d9"}'))
1722
+ .mockResolvedValueOnce(rawTextResult('{"id":"d9","message":{"subject":"Re: Hi"}}'));
1723
+ const result = await harness.callTool('gog_gmail_drafts_reply', {
1724
+ messageId: 'm1', body: 'Hi', returnFull: true,
1725
+ });
1726
+ expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
1727
+ ['gmail', 'drafts', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false'], { account: undefined });
1728
+ expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
1729
+ ['gmail', 'drafts', 'get', 'd9', '--use-indexed-attachment-ids=false'], { account: undefined });
1730
+ expect(result.content[0].text).toContain('"subject":"Re: Hi"');
1731
+ });
1732
+ });
1733
+
1734
+ describe('gog_gmail_drafts_reply_all', () => {
1735
+ it('routes to gmail drafts reply-all', async () => {
1736
+ await harness.callTool('gog_gmail_drafts_reply_all', { messageId: 'm1', body: 'Thanks all' });
1737
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1738
+ ['gmail', 'drafts', 'reply-all', 'm1', '--body=Thanks all', '--auto-from-addressed-alias=false'],
1739
+ { account: undefined },
1740
+ );
1741
+ });
1742
+
1743
+ it('carries repeatable recipient removals onto the draft', async () => {
1744
+ await harness.callTool('gog_gmail_drafts_reply_all', {
1745
+ messageId: 'm1', body: 'Hi', remove: ['drop@y.com', 'also@y.com'],
1746
+ });
1747
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1748
+ [
1749
+ 'gmail', 'drafts', 'reply-all', 'm1',
1750
+ '--body=Hi',
1751
+ '--remove=drop@y.com',
1752
+ '--remove=also@y.com',
1753
+ '--auto-from-addressed-alias=false',
1754
+ ],
1755
+ { account: undefined },
1756
+ );
1757
+ });
1758
+ });
1759
+
1760
+ describe('gog_gmail_drafts_forward', () => {
1761
+ it('omits --to entirely when no recipients are given', async () => {
1762
+ await harness.callTool('gog_gmail_drafts_forward', { messageId: 'm1' });
1763
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1764
+ ['gmail', 'drafts', 'forward', 'm1'],
1765
+ { account: undefined },
1766
+ );
1767
+ });
1768
+
1769
+ it('passes every forward flag', async () => {
1770
+ await harness.callTool('gog_gmail_drafts_forward', {
1771
+ messageId: 'm1',
1772
+ to: 'a@b.com,c@d.com',
1773
+ cc: 'cc@x.com',
1774
+ bcc: 'bcc@x.com',
1775
+ note: 'FYI',
1776
+ from: 'me@x.com',
1777
+ skipAttachments: true,
1778
+ account: 'me@gmail.com',
1779
+ });
1780
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1781
+ [
1782
+ 'gmail', 'drafts', 'forward', 'm1',
1783
+ '--to=a@b.com,c@d.com',
1784
+ '--cc=cc@x.com',
1785
+ '--bcc=bcc@x.com',
1786
+ '--note=FYI',
1787
+ '--from=me@x.com',
1788
+ '--skip-attachments',
1789
+ ],
1790
+ { account: 'me@gmail.com' },
1791
+ );
1792
+ });
1793
+
1794
+ it('returnFull re-fetches the saved forward draft', async () => {
1795
+ vi.mocked(lib.runOrDiagnose)
1796
+ .mockResolvedValueOnce(rawTextResult('{"draftId":"d7"}'))
1797
+ .mockResolvedValueOnce(rawTextResult('{"id":"d7","message":{"subject":"Fwd: Hi"}}'));
1798
+ const result = await harness.callTool('gog_gmail_drafts_forward', { messageId: 'm1', returnFull: true });
1799
+ expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(2,
1800
+ ['gmail', 'drafts', 'get', 'd7', '--use-indexed-attachment-ids=false'], { account: undefined });
1801
+ expect(result.content[0].text).toContain('"subject":"Fwd: Hi"');
1802
+ });
1803
+ });
1804
+
1672
1805
  describe('gog_gmail_autoreply', () => {
1673
1806
  it('calls runOrDiagnose with query and --body', async () => {
1674
1807
  await harness.callTool('gog_gmail_autoreply', { query: 'is:unread', body: 'Thanks' });