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 +6 -3
- package/SKILL.md +4 -1
- package/dist/index.js +83 -30
- package/manifest.json +13 -1
- package/package.json +1 -1
- package/src/tools/gmail-extra.ts +72 -2
- package/tests/tools/gmail-extra.test.ts +133 -0
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
|
|
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 (
|
|
47
|
+
## Extra Gmail Tools (49)
|
|
48
48
|
|
|
49
|
-
Plus 8 auth tools and 4 base Gmail tools (search, get, send, run) —
|
|
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) —
|
|
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
|
-
|
|
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
|
-
|
|
31826
|
-
|
|
31827
|
-
|
|
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
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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>",
|
package/src/tools/gmail-extra.ts
CHANGED
|
@@ -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
|
|
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' });
|