gogcli-mcp-gmail 2.22.0 → 2.23.1
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 +91 -6
- package/SKILL.md +32 -4
- package/dist/index.js +989 -30
- package/manifest.json +8 -4
- package/package.json +1 -1
- package/src/tools/gmail-extra.ts +2114 -21
- package/tests/tools/draft-diff-arithmetic.test.ts +57 -0
- package/tests/tools/draft-fork-signature.test.ts +67 -0
- package/tests/tools/draft-fork.test.ts +995 -0
- package/tests/tools/draft-write-vs-refetch.test.ts +118 -0
- package/tests/tools/gmail-extra.test.ts +1295 -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 46 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 (46)
|
|
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) — 58 in all.
|
|
50
50
|
|
|
51
51
|
### Read
|
|
52
52
|
|
|
@@ -94,12 +94,97 @@ Plus 8 auth tools and 4 base Gmail tools (search, get, send, run) — 57 in all.
|
|
|
94
94
|
|
|
95
95
|
| Tool | Description |
|
|
96
96
|
|------|-------------|
|
|
97
|
-
| `gog_gmail_drafts_list` | List drafts |
|
|
97
|
+
| `gog_gmail_drafts_list` | List drafts (free `origin` + `rootsOwnThread`; `enrich` adds subject/from/date for one extra call) |
|
|
98
98
|
| `gog_gmail_drafts_get` | Get a draft by ID |
|
|
99
99
|
| `gog_gmail_drafts_create` | Create a new draft |
|
|
100
|
-
| `gog_gmail_drafts_update` | Update
|
|
100
|
+
| `gog_gmail_drafts_update` | Update a draft — and re-thread it in place with `replyToThreadId`, keeping the same id, with a `threadingVerification` block reporting what gog actually wrote. `forkSiblingDraftId` refuses the write when the new body would drop text the other copy still holds |
|
|
101
101
|
| `gog_gmail_drafts_delete` | Delete a draft |
|
|
102
|
-
| `gog_gmail_drafts_send` | Send an existing draft |
|
|
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
|
+
| `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
|
+
|
|
105
|
+
#### When a draft you created stops resolving
|
|
106
|
+
|
|
107
|
+
A draft edited in a real mail client is not updated in place: the client writes a **new** draft and abandons the
|
|
108
|
+
original, so the id you were given starts returning `Google API error (404 notFound)` — indistinguishable, at first
|
|
109
|
+
glance, from "someone deleted it".
|
|
110
|
+
|
|
111
|
+
`gog_gmail_drafts_update` and `gog_gmail_drafts_send` turn that 404 into a `DRAFT_FORKED` report: what happened, the
|
|
112
|
+
drafts that *do* exist (with the free `origin` / `rootsOwnThread` fields, plus subject/from/date), the other
|
|
113
|
+
explanations that produce the same 404 (deleted, already sent), and what to do next. It **names no replacement** — the
|
|
114
|
+
404'd draft can no longer be fetched, so there is nothing to establish lineage against, and without lineage no pairing
|
|
115
|
+
verdict is possible. Naming a pair and running `gog_gmail_drafts_diff` is the only path here that issues a fork verdict.
|
|
116
|
+
|
|
117
|
+
**"Not listed" is not "does not exist".** That listing is capped at 20 drafts by construction — it is a failure path and
|
|
118
|
+
must not grow with the mailbox — so the report states, under `listingEvidence`, what its own evidence can carry. Only a
|
|
119
|
+
listing that came back *short* of the window covered the whole Drafts folder and earns the sentence "draft X no longer
|
|
120
|
+
resolves" (`basis: complete-listing`, `establishesTheDraftIsGone: true`). A window that came back *full*
|
|
121
|
+
(`capped-listing`) or a listing that *failed* (`listing-unavailable`) says so in words and claims nothing about the
|
|
122
|
+
draft: on a mailbox with more than 20 drafts, absence of evidence must not become the fork story by default. Widen it
|
|
123
|
+
with `gog_gmail_drafts_list` before concluding anything. When the call named a reply target, that target is echoed on
|
|
124
|
+
the report and its explanation is listed **first** — the branch with the least evidence about the draft is the last one
|
|
125
|
+
that should stay silent about the leading alternative.
|
|
126
|
+
|
|
127
|
+
**The 404 is attributed, not assumed.** `gmail drafts update` resolves up to three Google entities — the draft, the
|
|
128
|
+
thread behind `replyToThreadId`, and the message behind `replyToMessageId` — and gog renders all three 404s with the
|
|
129
|
+
same string. The listing the report already pays for settles it: if the draft id is still listed, the answer is
|
|
130
|
+
`GOOGLE_404_NOT_THE_DRAFT`, which claims no fork, names the reply target as the remaining explanation, and tells you
|
|
131
|
+
not to go hunting for a replacement draft. `DRAFT_FORKED` is only used when the draft really has stopped resolving.
|
|
132
|
+
|
|
133
|
+
**What counts as lineage** (`gog_gmail_drafts_diff`): only a link from the candidate *to the original* — the original
|
|
134
|
+
draft's own `Message-Id` inside the candidate's `In-Reply-To`/`References`, or agreement on text **neither draft
|
|
135
|
+
quoted**. A **shared reply root is not lineage**: it links both drafts to a common *ancestor*, which every reply in a
|
|
136
|
+
thread has, so it is reported as corroboration and can raise the answer no higher than an explicitly weak `candidate`.
|
|
137
|
+
Quoted lines are excluded from the agreement metric because Apple Mail quotes on every reply — two unrelated replies
|
|
138
|
+
into one thread carry the same 30-line block, which scores 0.79 on a whole-body line metric while proving nothing.
|
|
139
|
+
|
|
140
|
+
**Client boilerplate is excluded for the same reason.** The salutation, the closing formula, the name under it and the
|
|
141
|
+
signature block are reproduced identically on every message a client composes, whatever the message says —
|
|
142
|
+
`Sent from my iPhone` is Apple Mail's own default. `Hi Jennifer,` + `Thanks,` + `Chris` + `Sent from my iPhone` is 4
|
|
143
|
+
lines and 43 characters, enough on its own to clear a line-and-character threshold, so counting it paired two genuinely
|
|
144
|
+
unrelated one-sentence notes as `confirmed` — and short confirmation plus a signature is the dominant shape of this
|
|
145
|
+
mailbox. `bodyAgreement` reports `quotedLinesIgnored` and `boilerplateLinesIgnored` separately so the arithmetic can be
|
|
146
|
+
redone. The divergence report still counts those lines: a merge that drops the signature really did drop it. The cost
|
|
147
|
+
of the tighter metric is that a genuine fork of a *one-sentence* note now comes back `candidate` rather than
|
|
148
|
+
`confirmed`, with `missing` naming the shortfall — the deliberate direction, since a missed fork costs a re-check and a
|
|
149
|
+
wrong one sends the wrong text to the wrong thread.
|
|
150
|
+
|
|
151
|
+
Cost is bounded and constant: **at most 2 extra gog invocations**, only on a call that already failed, never scaling
|
|
152
|
+
with the number of drafts. A non-404 failure spends nothing.
|
|
153
|
+
|
|
154
|
+
Once you know which draft survived, `gog_gmail_drafts_update` with `replyToThreadId` adopts it back onto the original
|
|
155
|
+
conversation **in one call, keeping its draft id** — gog resolves `In-Reply-To`/`References` from that thread's latest
|
|
156
|
+
message and reports them back under `threadingVerification` (`ok`, the effective headers, and a note). Two things that
|
|
157
|
+
block make that safe to rely on: an explicit reply target *replaces* the draft's stored lineage rather than merging with
|
|
158
|
+
it, and gog requires a body on every update, so the call **overwrites the whole body**. Diff first, merge by hand, then
|
|
159
|
+
write.
|
|
160
|
+
|
|
161
|
+
#### Guarding the merge: `forkSiblingDraftId`
|
|
162
|
+
|
|
163
|
+
Because gog requires a body on every update there is **no header-only edit**, so adopting a draft back onto its thread is
|
|
164
|
+
always a full body overwrite — the exact operation that destroys the paragraph living only in the other copy. In the
|
|
165
|
+
observed fork *neither* copy was a superset: the mail-client copy had lost a paragraph and the Gmail copy had gained
|
|
166
|
+
sentences.
|
|
167
|
+
|
|
168
|
+
The line-based comparison cuts the other way too: a paragraph re-wrapped at a different width no longer matches line
|
|
169
|
+
for line, so `none` means *no evidence was found*, never *proven unrelated*, and the notes say so.
|
|
170
|
+
|
|
171
|
+
Pass `forkSiblingDraftId` (the id of the other copy) and `gog_gmail_drafts_update` reads that draft **before** writing and
|
|
172
|
+
refuses the write — `DRAFT_CONTENT_LOSS`, nothing changed — if your body omits any line the sibling still holds, naming the
|
|
173
|
+
exact lines. Merge them in and retry, or pass `acceptContentLoss: true` to write anyway (the lines are still reported, and
|
|
174
|
+
the sibling itself is never touched). A check that cannot be *run* — sibling unfetchable, unparseable, or with no readable
|
|
175
|
+
body — refuses too, as `DRAFT_CONTENT_LOSS_UNCHECKED`: an unrun check is not a passed check.
|
|
176
|
+
|
|
177
|
+
An override reports the write that **actually happened**, never a predicted one: `contentLossCheck.written` is set from
|
|
178
|
+
the result, so a write that failed comes back saying it was attempted and saved nothing, and says the listed lines are
|
|
179
|
+
still in the sibling. Believing the opposite is the destructive case — a caller who thinks the merged body is stored may
|
|
180
|
+
delete or overwrite the copy that now holds the only version of those lines.
|
|
181
|
+
|
|
182
|
+
It **makes no fork claim**. You name the sibling; nothing searches for it, identical bodies would not prove a pairing and
|
|
183
|
+
divergent ones would not disprove one. `contentLossCheck.forkClaim` is always `null` and points at
|
|
184
|
+
`gog_gmail_drafts_diff`, which is the only tool here that weighs identity, lineage and ordering.
|
|
185
|
+
|
|
186
|
+
**Cost:** opt-in, exactly **one** extra gog invocation (a `drafts get` on the id you named — never a scan, never scaling
|
|
187
|
+
with the mailbox), and **zero** when the param is absent. A refusal spends that one call and skips the write entirely.
|
|
103
188
|
|
|
104
189
|
### Write
|
|
105
190
|
|
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) — 58 tools: 8 auth + 4 base Gmail + 46 extra dedicated Gmail tools.
|
|
9
9
|
|
|
10
10
|
- **Source:** [github.com/chrischall/gogcli-mcp](https://github.com/chrischall/gogcli-mcp)
|
|
11
11
|
|
|
@@ -73,12 +73,40 @@ Extended Gmail MCP server via [gogcli](https://github.com/openclaw/gogcli) — 5
|
|
|
73
73
|
### Drafts
|
|
74
74
|
| Tool | What it does |
|
|
75
75
|
|------|-------------|
|
|
76
|
-
| `gog_gmail_drafts_list` | List drafts |
|
|
76
|
+
| `gog_gmail_drafts_list` | List drafts (+ free `origin`/`rootsOwnThread`) |
|
|
77
77
|
| `gog_gmail_drafts_get` | Get a draft |
|
|
78
78
|
| `gog_gmail_drafts_create` | Create a draft |
|
|
79
|
-
| `gog_gmail_drafts_update` | Update a draft |
|
|
79
|
+
| `gog_gmail_drafts_update` | Update a draft; `replyToThreadId` re-threads it in place (same id) + `threadingVerification`; `forkSiblingDraftId` blocks a body overwrite that would drop the other copy's text |
|
|
80
80
|
| `gog_gmail_drafts_delete` | Delete a draft |
|
|
81
|
-
| `gog_gmail_drafts_send` | Send a draft |
|
|
81
|
+
| `gog_gmail_drafts_send` | Send a draft (404 → `DRAFT_FORKED`, or `GOOGLE_404_NOT_THE_DRAFT` if the draft is still listed) |
|
|
82
|
+
| `gog_gmail_drafts_diff` | Diff two drafts (body divergence, threading loss, fork verdict) |
|
|
83
|
+
|
|
84
|
+
A draft edited in a mail client is replaced, not updated: the old id 404s. `drafts_update` / `drafts_send` answer that
|
|
85
|
+
404 with a `DRAFT_FORKED` report (what happened, the drafts that exist, what to do) instead of a bare `notFound`, at a
|
|
86
|
+
bounded cost of ≤2 extra gog calls. It never names a replacement — the 404'd draft cannot be fetched, so no lineage
|
|
87
|
+
signal can exist; use `drafts_diff` on a named pair for that. The 404 is attributed first: `replyToThreadId` and
|
|
88
|
+
`replyToMessageId` resolve their own entities and 404 identically, so if the draft id is still in the listing the answer
|
|
89
|
+
is `GOOGLE_404_NOT_THE_DRAFT` — no fork claimed, reply target named. That listing is capped at 20, so `listingEvidence`
|
|
90
|
+
says what it can carry: only a `complete-listing` (the window came back short, i.e. it covered the folder) claims the
|
|
91
|
+
draft is gone; `capped-listing` and `listing-unavailable` say in words that they establish nothing. A reply target, when
|
|
92
|
+
one was passed, is echoed and explained first.
|
|
93
|
+
|
|
94
|
+
`drafts_diff` confirms a pairing only on a link from the candidate **to the original** (its `Message-Id` in the
|
|
95
|
+
candidate's `In-Reply-To`/`References`) or on agreement over text **neither draft quoted and neither client generated**
|
|
96
|
+
— the salutation, closing formula, name and signature block are excluded alongside quoting, because a client writes them
|
|
97
|
+
identically on every message (`Hi Jennifer,` + `Thanks,` + `Chris` + `Sent from my iPhone` is 4 lines and 43 characters
|
|
98
|
+
of pure apparatus). A shared reply root is corroboration only — every reply in a thread has one — and `none` means no
|
|
99
|
+
evidence was found, not "unrelated". To adopt the survivor back onto the thread, call
|
|
100
|
+
`drafts_update` with `replyToThreadId`: same draft id, gog resolves the reply headers, `threadingVerification` reports
|
|
101
|
+
whether it worked. It rewrites the whole body, so diff and merge first.
|
|
102
|
+
|
|
103
|
+
gog requires a body on every update, so there is no header-only edit and every adoption overwrites the body. Pass
|
|
104
|
+
`forkSiblingDraftId` (the other copy's id) and the update reads that draft first — one extra gog call, zero when the param
|
|
105
|
+
is absent — and refuses to write (`DRAFT_CONTENT_LOSS`, nothing changed) if your body drops a line the sibling holds,
|
|
106
|
+
naming the lines; `acceptContentLoss: true` overrides, and `contentLossCheck.written` then reports whether the write it
|
|
107
|
+
authorised actually succeeded — a failed write says so and says nothing was saved. A check that could not run refuses too
|
|
108
|
+
(`DRAFT_CONTENT_LOSS_UNCHECKED`). It compares text only and never claims one draft replaced another — that is
|
|
109
|
+
`drafts_diff`'s job.
|
|
82
110
|
|
|
83
111
|
### Write
|
|
84
112
|
| Tool | What it does |
|