nexus-agymail 0.8.0__tar.gz
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.
- nexus_agymail-0.8.0/PKG-INFO +225 -0
- nexus_agymail-0.8.0/README.md +200 -0
- nexus_agymail-0.8.0/pyproject.toml +88 -0
- nexus_agymail-0.8.0/pyproject.toml.orig +64 -0
- nexus_agymail-0.8.0/src/nexus_agymail/__init__.py +9 -0
- nexus_agymail-0.8.0/src/nexus_agymail/__main__.py +3 -0
- nexus_agymail-0.8.0/src/nexus_agymail/auth.py +299 -0
- nexus_agymail-0.8.0/src/nexus_agymail/cli.py +126 -0
- nexus_agymail-0.8.0/src/nexus_agymail/client.py +855 -0
- nexus_agymail-0.8.0/src/nexus_agymail/dpapi.py +183 -0
- nexus_agymail-0.8.0/src/nexus_agymail/fichiers.py +173 -0
- nexus_agymail-0.8.0/src/nexus_agymail/garde-fous.json +176 -0
- nexus_agymail-0.8.0/src/nexus_agymail/garde_fous.py +449 -0
- nexus_agymail-0.8.0/src/nexus_agymail/mime.py +430 -0
- nexus_agymail-0.8.0/src/nexus_agymail/plugin/plugin.json +5 -0
- nexus_agymail-0.8.0/src/nexus_agymail/plugin/skills/nexus-agymail/SKILL.md +60 -0
- nexus_agymail-0.8.0/src/nexus_agymail/register.py +118 -0
- nexus_agymail-0.8.0/src/nexus_agymail/server.py +824 -0
- nexus_agymail-0.8.0/src/nexus_agymail/token_store.py +172 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexus-agymail
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Gmail MCP server for AI agents: official Gmail API, Google's tool names, sending only after the user's yes, no permanent deletion
|
|
5
|
+
Keywords: mcp,mcp-server,gmail,email,ai-agents,antigravity,claude,codex,prompt-injection
|
|
6
|
+
Author: Julien Piron
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Dist: mcp>=2.3.0
|
|
17
|
+
Requires-Dist: google-api-python-client>=2.160.0
|
|
18
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
19
|
+
Requires-Dist: google-auth-oauthlib>=1.2.0
|
|
20
|
+
Requires-Python: >=3.14
|
|
21
|
+
Project-URL: Homepage, https://julienpiron.fr/nexusagymail/
|
|
22
|
+
Project-URL: Repository, https://github.com/pironjulien/NexusAGYmail
|
|
23
|
+
Project-URL: Changelog, https://github.com/pironjulien/NexusAGYmail/blob/main/CHANGELOG.md
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# NexusAGYmail
|
|
27
|
+
|
|
28
|
+
<!-- mcp-name: io.github.pironjulien/nexus-agymail -->
|
|
29
|
+
|
|
30
|
+
[](https://github.com/pironjulien/NexusAGYmail/releases)
|
|
31
|
+
[](https://pypi.org/project/nexus-agymail/)
|
|
32
|
+
[](https://github.com/pironjulien/NexusAGYmail/actions/workflows/tests.yml)
|
|
33
|
+
[](https://scorecard.dev/viewer/?uri=github.com/pironjulien/NexusAGYmail)
|
|
34
|
+
[](https://github.com/pironjulien/NexusAGYmail/blob/main/LICENSE)
|
|
35
|
+
[](https://github.com/pironjulien/NexusAGYmail/blob/main/pyproject.toml)
|
|
36
|
+
[](https://modelcontextprotocol.io)
|
|
37
|
+
|
|
38
|
+
**Gmail for AI agents that is safe to leave alone.** One local MCP server, `nexus-agymail`, gives Antigravity (whose
|
|
39
|
+
store has no Gmail connector) or any MCP client your Gmail through the official Gmail API: search, read, attachments,
|
|
40
|
+
drafts, labels, a reversible trash, and sending a draft **only after your explicit yes for that exact draft**.
|
|
41
|
+
Nothing is ever deleted for good.
|
|
42
|
+
|
|
43
|
+
- **Same tools as Google's official Gmail MCP server** (`search_threads`, `get_message`, `get_thread`,
|
|
44
|
+
`create_draft`, `label_message`...), so an agent that knows one knows the other, without enrolling in the
|
|
45
|
+
Workspace Developer Preview or enabling a remote MCP endpoint: it runs on your machine, with any personal Gmail.
|
|
46
|
+
- **Guardrails in the code, not in a prompt**: an email can never trigger a send, every email is framed as data,
|
|
47
|
+
instructions hidden in an email are flagged, attachments are saved safely and secrets are never attached.
|
|
48
|
+
- Windows (used daily) and Linux (unit tests in CI); macOS should work but is untested. The token is encrypted
|
|
49
|
+
with DPAPI on Windows, a file readable only by you elsewhere. Everything sent to the client is in English.
|
|
50
|
+
|
|
51
|
+
Part of the Nexus family: [NexusWindows](https://julienpiron.fr/nexuswindows/),
|
|
52
|
+
[NexusBrowser](https://julienpiron.fr/nexusbrowser/), [NexusAndroid](https://julienpiron.fr/nexusandroid/).
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
You need [uv](https://docs.astral.sh/uv/getting-started/installation/) and a Google account. About 10 minutes, once.
|
|
57
|
+
|
|
58
|
+
1. **Create your own Google OAuth client** (Gmail scopes are restricted, so every user brings their own client:
|
|
59
|
+
your mail never goes through anyone else's app).
|
|
60
|
+
1. In [Google Cloud Console](https://console.cloud.google.com/), create a project (any name).
|
|
61
|
+
2. [Enable the Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com) for it.
|
|
62
|
+
3. [Google Auth Platform > Branding](https://console.cloud.google.com/auth/branding): app name, your address;
|
|
63
|
+
**Audience**: External, then add your Gmail address as a test user, and click **Publish app** (in *Testing*,
|
|
64
|
+
Google expires the sign-in every 7 days; a published app for your own use needs no verification, Google only shows
|
|
65
|
+
a "Google hasn't verified this app" screen: *Advanced > Go to … (unsafe)*).
|
|
66
|
+
4. [Clients](https://console.cloud.google.com/auth/clients) > **Create client** > type **Desktop app** >
|
|
67
|
+
**Download JSON** (`client_secret_….json`).
|
|
68
|
+
2. **Sign in** (opens your browser on Google's consent page):
|
|
69
|
+
```bash
|
|
70
|
+
uvx --python 3.14 nexus-agymail auth --client-secret ~/Downloads/client_secret_XXXX.json
|
|
71
|
+
```
|
|
72
|
+
3. **Connect your AI app**, then restart it:
|
|
73
|
+
- **Antigravity**: `uvx --python 3.14 nexus-agymail setup` (adds the `nexus-agymail` plugin: server and skill).
|
|
74
|
+
- **Any other MCP client**: add the server by hand.
|
|
75
|
+
|
|
76
|
+
<details><summary>JSON (Claude Desktop, Cursor, VS Code, Antigravity by hand)</summary>
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"nexus-agymail": { "command": "uvx", "args": ["--python", "3.14", "nexus-agymail"] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
</details>
|
|
86
|
+
<details><summary>Codex (~/.codex/config.toml)</summary>
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[mcp_servers.nexus-agymail]
|
|
90
|
+
command = "uvx"
|
|
91
|
+
args = ["--python", "3.14", "nexus-agymail"]
|
|
92
|
+
startup_timeout_sec = 60
|
|
93
|
+
```
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
Then ask, for example: *"Summarize my unread emails from this week"*, *"Archive the newsletters older than a
|
|
97
|
+
month"*, *"Draft a reply to the last email from Alice saying I agree, and show it to me before sending"*.
|
|
98
|
+
|
|
99
|
+
`uvx --python 3.14 nexus-agymail status` shows where the token is and which scopes it carries (no secret shown).
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
```mermaid
|
|
104
|
+
flowchart LR
|
|
105
|
+
A[AI app<br/>Antigravity, Claude, Codex, Cursor...] -- MCP stdio --> S[nexus-agymail<br/>on your machine]
|
|
106
|
+
S -- HTTPS, official Gmail API v1 --> G[(Gmail)]
|
|
107
|
+
S -. token, encrypted .- T[~/.nexus/nexus-agymail]
|
|
108
|
+
S -. attachments .- D[Downloads/NexusAGYmail]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The server is a plain local process: no cloud relay, no browser automation. It talks to `gmail.googleapis.com` with
|
|
112
|
+
your own OAuth client and a refresh token stored on your machine, and to `oauth2.googleapis.com` to refresh it.
|
|
113
|
+
|
|
114
|
+
## Tools
|
|
115
|
+
|
|
116
|
+
Names and parameters follow [Google's official Gmail MCP server](https://developers.google.com/workspace/gmail/api/reference/mcp)
|
|
117
|
+
and its extensions (`update_draft`, `trash_*`), plus three of its own: `send_draft`, `download_attachment`,
|
|
118
|
+
`auth_status`.
|
|
119
|
+
|
|
120
|
+
| Tool | What it does |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `search_threads` | Search threads (Gmail syntax), one page per call: `pageSize` 1 to 50 (20), `pageToken`; each thread gives `messageCount` and a summary of its last 5 messages; trash with `includeTrash`, spam never |
|
|
123
|
+
| `get_message` | One message: headers, body as text (HTML converted), attachments listed (`part_id`, name, type, size); `maxChars` (8,090 by default) |
|
|
124
|
+
| `get_thread` | A thread in order, quotes of earlier replies removed and reported; `maxChars` shared (16,180 by default) |
|
|
125
|
+
| `download_attachment` | Saves an attachment (`partId`) to `Downloads/NexusAGYmail` or an absolute `directory`: sanitized name, never overwrites, marked as downloaded from the Internet; returns path, size, SHA-256 |
|
|
126
|
+
| `list_labels` | Labels (id, name, type), system labels first |
|
|
127
|
+
| `create_draft` | A draft; `to`, `cc`, `bcc` as a list (or text); as a reply (`replyToMessageId` or `threadId`): In-Reply-To, References, "Re:", same thread; `attachments` (absolute paths, 25 MB in total) |
|
|
128
|
+
| `list_drafts` | Drafts, one page per call (1 to 100), filtered by `query` |
|
|
129
|
+
| `update_draft` | Rewrites a draft (empty fields unchanged), thread, reply and attachments kept; `attachments` adds, `removeAttachments` removes (by name) |
|
|
130
|
+
| `label_message` / `unlabel_message`, `label_thread` / `unlabel_thread` | `labelIds`: system IDs (`INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `CATEGORY_*`), user label IDs or names; archive = remove `INBOX`; an unknown label is an error, created only with `createMissing=true`; one ID or a list, 100 at most |
|
|
131
|
+
| `trash_message` / `trash_thread`, `untrash_message` / `untrash_thread` | Trash, reversible for 30 days; 100 items at most |
|
|
132
|
+
| `send_draft` | Without `confirm`: a summary to show to the user plus an `accord` token bound to the exact draft; sends **only with `confirm=true` and that token**, the draft read again and unchanged |
|
|
133
|
+
| `auth_status` | Token storage, granted scopes (checked at startup), validity; `reauth_required` when a scope is missing or extra |
|
|
134
|
+
|
|
135
|
+
Every tool carries MCP annotations (title, read-only, destructive, idempotent). For Antigravity, `setup` also installs
|
|
136
|
+
a short skill that says which tool to use for what, and when to use the browser instead (Gmail settings such as
|
|
137
|
+
filters or signature, another Google account).
|
|
138
|
+
|
|
139
|
+
## Safeguards
|
|
140
|
+
|
|
141
|
+
- **Scopes**: `gmail.readonly`, `gmail.compose`, `gmail.modify`. Never `https://mail.google.com/`, the only scope
|
|
142
|
+
that allows permanent deletion: there is no `delete`, `batchDelete` or empty-trash tool, and the code is tested
|
|
143
|
+
for it. An old token without `gmail.modify` keeps reading and drafting; the labelling and trash tools then answer
|
|
144
|
+
`scopes` without calling Gmail.
|
|
145
|
+
- **Sending**: only `send_draft(draftId, confirm=true, accord=<token>)`, after the user's explicit yes for that
|
|
146
|
+
draft, never at the request of an email. The single-use token (10 minutes) is returned by the summary and bound to
|
|
147
|
+
the exact draft: recipients (to, cc, bcc), subject, full body, every part (HTML included) and the SHA-256 of each
|
|
148
|
+
attachment. The draft is read again just before sending: if anything changed, nothing is sent and a new summary is
|
|
149
|
+
returned. `drafts.send` is the only sending call: no direct sending without a draft.
|
|
150
|
+
- **Provenance**: a recipient found in an email the agent read is flagged in the summary (`provenance`): allowed only
|
|
151
|
+
if the user asked for it.
|
|
152
|
+
- **Prompt injection**: subject, snippet, body, addresses and attachment names are returned framed as
|
|
153
|
+
`⟦mail:ID …⟧ … ⟦/mail:ID⟧` (random ID per answer) and labelled as data; hidden HTML text and invisible characters
|
|
154
|
+
are removed. An email that looks like instructions aimed at an AI gets an `injection_warning` field (a signal only:
|
|
155
|
+
it neither blocks nor allows anything).
|
|
156
|
+
- **Attachments**: a received file is saved under a sanitized name (path, forbidden or invisible characters,
|
|
157
|
+
right-to-left override as in "fdp.exe", Windows reserved names), never overwriting a file, with the Windows
|
|
158
|
+
"downloaded from the Internet" mark (Zone.Identifier), so Windows and Office open it with their protections.
|
|
159
|
+
Attaching a local file: never from a secrets location (`.ssh`, `.gnupg`, `.aws`, `.config/gcloud`, the AI apps'
|
|
160
|
+
folders, `.env` files, private keys, `credentials.json`..., plus your own folders listed in
|
|
161
|
+
`NEXUS_AGYMAIL_PROTECTED`); the user can always attach it themselves in Gmail.
|
|
162
|
+
- **Token**: `~/.nexus/nexus-agymail/token.bin`, encrypted with DPAPI for the current Windows account (permissions
|
|
163
|
+
reduced to that account), or a `0600` file on macOS and Linux; written atomically under a lock (several servers can
|
|
164
|
+
run at once). It is never shown. When you sign in again, a previous token with extra scopes is revoked only after
|
|
165
|
+
the new one is saved.
|
|
166
|
+
- **Exact errors**: revoked or expired token, network, quota, permissions, not found, label: each has a stable `code`
|
|
167
|
+
(`revoked`, `network`, `quota`, `forbidden`, `not_found`, `label`, `invalid_argument`...). Every request is retried
|
|
168
|
+
with exponential backoff on 429 and 5xx; batched requests retry only the items refused for quota.
|
|
169
|
+
- **Bounded sizes**: bodies cut at `maxChars` (reported), paged searches, at most 100 items per labelling or trash
|
|
170
|
+
call.
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
| Variable | Purpose |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `NEXUS_AGYMAIL_HOME` | State folder (token, OAuth client, backups) instead of `~/.nexus/nexus-agymail` |
|
|
177
|
+
| `NEXUS_AGYMAIL_CLIENT_SECRET` | Path of the OAuth client JSON, instead of the copy made by `auth --client-secret` |
|
|
178
|
+
| `NEXUS_AGYMAIL_PROTECTED` | Extra folders whose files are never attached (separated by `;` on Windows, `:` elsewhere) |
|
|
179
|
+
| `NEXUS_AGYMAIL_ACCOUNT` | Address shown by `auth_status` when the token does not carry it |
|
|
180
|
+
|
|
181
|
+
## Privacy
|
|
182
|
+
|
|
183
|
+
NexusAGYmail runs on your machine. It has no account of its own, no telemetry, no analytics and no relay: its only
|
|
184
|
+
network requests go to Google (`gmail.googleapis.com`, `oauth2.googleapis.com`, and `accounts.google.com` in your
|
|
185
|
+
browser when you sign in), with your own OAuth client.
|
|
186
|
+
|
|
187
|
+
- **What it reads**: the emails, threads, drafts, labels and attachments the agent asks for, only when it calls the
|
|
188
|
+
matching tool; local files only when the agent attaches them to a draft.
|
|
189
|
+
- **Where it goes**: tool results go to the AI client you connected, which may send them to its model provider under
|
|
190
|
+
its own privacy policy. Drafts and sent emails go to Gmail. NexusAGYmail sends nothing anywhere else.
|
|
191
|
+
- **What it stores**, under `~/.nexus/nexus-agymail/` (or `NEXUS_AGYMAIL_HOME`): the refresh token (`token.bin`,
|
|
192
|
+
encrypted with DPAPI on Windows, a `0600` file elsewhere), its lock file, the copy of your OAuth client
|
|
193
|
+
(`client_secret.json`), and backups of the Antigravity config files `setup` edits (`backups/`). Attachments you ask
|
|
194
|
+
it to save go to `~/Downloads/NexusAGYmail` (or the folder given). In memory only, for the life of the server: the
|
|
195
|
+
pending send accords and the addresses seen in emails read (for the `provenance` warning). No email is written to
|
|
196
|
+
disk unless saved as an attachment; no log of emails is kept.
|
|
197
|
+
- **Removing it**: delete `~/.nexus/nexus-agymail/` and, for Antigravity, `~/.gemini/config/plugins/nexus-agymail/`;
|
|
198
|
+
revoke the access in your [Google account](https://myaccount.google.com/connections) and delete your Google Cloud
|
|
199
|
+
project if you no longer need it.
|
|
200
|
+
|
|
201
|
+
## Known limitations
|
|
202
|
+
|
|
203
|
+
- Each user creates their own Google OAuth client (step 1). A shared public client would need Google's restricted
|
|
204
|
+
scope verification and a security assessment.
|
|
205
|
+
- Plain-text bodies only when writing: `create_draft` and `update_draft` do not write HTML; `update_draft` replaces
|
|
206
|
+
the HTML part of a draft written in Gmail with the plain text.
|
|
207
|
+
- No Gmail settings (filters, signature, forwarding, vacation replies): use the Gmail web app.
|
|
208
|
+
- No spam reporting and no permanent deletion, on purpose.
|
|
209
|
+
- Used daily on Windows 11 with a real Gmail account; Linux runs the startup and unit tests in CI; macOS is
|
|
210
|
+
untested.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```powershell
|
|
215
|
+
./test.ps1 # ruff, pyright, unit tests against a fake Gmail API (no network, no Google account)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`test.ps1` gives each checkout its own test environment under `~/.nexus/nexus-agymail` and never downloads anything:
|
|
219
|
+
run `uv sync --frozen` once with network access to fill the uv cache. The fake Gmail API (`tests/fake_gmail.py`)
|
|
220
|
+
speaks HTTP to the real `google-api-python-client`, so batching, retries, uploads and paging are tested for real.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
[MIT](https://github.com/pironjulien/NexusAGYmail/blob/main/LICENSE). Not affiliated with Google; Gmail is a trademark
|
|
225
|
+
of Google LLC.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# NexusAGYmail
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.pironjulien/nexus-agymail -->
|
|
4
|
+
|
|
5
|
+
[](https://github.com/pironjulien/NexusAGYmail/releases)
|
|
6
|
+
[](https://pypi.org/project/nexus-agymail/)
|
|
7
|
+
[](https://github.com/pironjulien/NexusAGYmail/actions/workflows/tests.yml)
|
|
8
|
+
[](https://scorecard.dev/viewer/?uri=github.com/pironjulien/NexusAGYmail)
|
|
9
|
+
[](https://github.com/pironjulien/NexusAGYmail/blob/main/LICENSE)
|
|
10
|
+
[](https://github.com/pironjulien/NexusAGYmail/blob/main/pyproject.toml)
|
|
11
|
+
[](https://modelcontextprotocol.io)
|
|
12
|
+
|
|
13
|
+
**Gmail for AI agents that is safe to leave alone.** One local MCP server, `nexus-agymail`, gives Antigravity (whose
|
|
14
|
+
store has no Gmail connector) or any MCP client your Gmail through the official Gmail API: search, read, attachments,
|
|
15
|
+
drafts, labels, a reversible trash, and sending a draft **only after your explicit yes for that exact draft**.
|
|
16
|
+
Nothing is ever deleted for good.
|
|
17
|
+
|
|
18
|
+
- **Same tools as Google's official Gmail MCP server** (`search_threads`, `get_message`, `get_thread`,
|
|
19
|
+
`create_draft`, `label_message`...), so an agent that knows one knows the other, without enrolling in the
|
|
20
|
+
Workspace Developer Preview or enabling a remote MCP endpoint: it runs on your machine, with any personal Gmail.
|
|
21
|
+
- **Guardrails in the code, not in a prompt**: an email can never trigger a send, every email is framed as data,
|
|
22
|
+
instructions hidden in an email are flagged, attachments are saved safely and secrets are never attached.
|
|
23
|
+
- Windows (used daily) and Linux (unit tests in CI); macOS should work but is untested. The token is encrypted
|
|
24
|
+
with DPAPI on Windows, a file readable only by you elsewhere. Everything sent to the client is in English.
|
|
25
|
+
|
|
26
|
+
Part of the Nexus family: [NexusWindows](https://julienpiron.fr/nexuswindows/),
|
|
27
|
+
[NexusBrowser](https://julienpiron.fr/nexusbrowser/), [NexusAndroid](https://julienpiron.fr/nexusandroid/).
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
You need [uv](https://docs.astral.sh/uv/getting-started/installation/) and a Google account. About 10 minutes, once.
|
|
32
|
+
|
|
33
|
+
1. **Create your own Google OAuth client** (Gmail scopes are restricted, so every user brings their own client:
|
|
34
|
+
your mail never goes through anyone else's app).
|
|
35
|
+
1. In [Google Cloud Console](https://console.cloud.google.com/), create a project (any name).
|
|
36
|
+
2. [Enable the Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com) for it.
|
|
37
|
+
3. [Google Auth Platform > Branding](https://console.cloud.google.com/auth/branding): app name, your address;
|
|
38
|
+
**Audience**: External, then add your Gmail address as a test user, and click **Publish app** (in *Testing*,
|
|
39
|
+
Google expires the sign-in every 7 days; a published app for your own use needs no verification, Google only shows
|
|
40
|
+
a "Google hasn't verified this app" screen: *Advanced > Go to … (unsafe)*).
|
|
41
|
+
4. [Clients](https://console.cloud.google.com/auth/clients) > **Create client** > type **Desktop app** >
|
|
42
|
+
**Download JSON** (`client_secret_….json`).
|
|
43
|
+
2. **Sign in** (opens your browser on Google's consent page):
|
|
44
|
+
```bash
|
|
45
|
+
uvx --python 3.14 nexus-agymail auth --client-secret ~/Downloads/client_secret_XXXX.json
|
|
46
|
+
```
|
|
47
|
+
3. **Connect your AI app**, then restart it:
|
|
48
|
+
- **Antigravity**: `uvx --python 3.14 nexus-agymail setup` (adds the `nexus-agymail` plugin: server and skill).
|
|
49
|
+
- **Any other MCP client**: add the server by hand.
|
|
50
|
+
|
|
51
|
+
<details><summary>JSON (Claude Desktop, Cursor, VS Code, Antigravity by hand)</summary>
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"nexus-agymail": { "command": "uvx", "args": ["--python", "3.14", "nexus-agymail"] }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
</details>
|
|
61
|
+
<details><summary>Codex (~/.codex/config.toml)</summary>
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[mcp_servers.nexus-agymail]
|
|
65
|
+
command = "uvx"
|
|
66
|
+
args = ["--python", "3.14", "nexus-agymail"]
|
|
67
|
+
startup_timeout_sec = 60
|
|
68
|
+
```
|
|
69
|
+
</details>
|
|
70
|
+
|
|
71
|
+
Then ask, for example: *"Summarize my unread emails from this week"*, *"Archive the newsletters older than a
|
|
72
|
+
month"*, *"Draft a reply to the last email from Alice saying I agree, and show it to me before sending"*.
|
|
73
|
+
|
|
74
|
+
`uvx --python 3.14 nexus-agymail status` shows where the token is and which scopes it carries (no secret shown).
|
|
75
|
+
|
|
76
|
+
## How it works
|
|
77
|
+
|
|
78
|
+
```mermaid
|
|
79
|
+
flowchart LR
|
|
80
|
+
A[AI app<br/>Antigravity, Claude, Codex, Cursor...] -- MCP stdio --> S[nexus-agymail<br/>on your machine]
|
|
81
|
+
S -- HTTPS, official Gmail API v1 --> G[(Gmail)]
|
|
82
|
+
S -. token, encrypted .- T[~/.nexus/nexus-agymail]
|
|
83
|
+
S -. attachments .- D[Downloads/NexusAGYmail]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The server is a plain local process: no cloud relay, no browser automation. It talks to `gmail.googleapis.com` with
|
|
87
|
+
your own OAuth client and a refresh token stored on your machine, and to `oauth2.googleapis.com` to refresh it.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
Names and parameters follow [Google's official Gmail MCP server](https://developers.google.com/workspace/gmail/api/reference/mcp)
|
|
92
|
+
and its extensions (`update_draft`, `trash_*`), plus three of its own: `send_draft`, `download_attachment`,
|
|
93
|
+
`auth_status`.
|
|
94
|
+
|
|
95
|
+
| Tool | What it does |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `search_threads` | Search threads (Gmail syntax), one page per call: `pageSize` 1 to 50 (20), `pageToken`; each thread gives `messageCount` and a summary of its last 5 messages; trash with `includeTrash`, spam never |
|
|
98
|
+
| `get_message` | One message: headers, body as text (HTML converted), attachments listed (`part_id`, name, type, size); `maxChars` (8,090 by default) |
|
|
99
|
+
| `get_thread` | A thread in order, quotes of earlier replies removed and reported; `maxChars` shared (16,180 by default) |
|
|
100
|
+
| `download_attachment` | Saves an attachment (`partId`) to `Downloads/NexusAGYmail` or an absolute `directory`: sanitized name, never overwrites, marked as downloaded from the Internet; returns path, size, SHA-256 |
|
|
101
|
+
| `list_labels` | Labels (id, name, type), system labels first |
|
|
102
|
+
| `create_draft` | A draft; `to`, `cc`, `bcc` as a list (or text); as a reply (`replyToMessageId` or `threadId`): In-Reply-To, References, "Re:", same thread; `attachments` (absolute paths, 25 MB in total) |
|
|
103
|
+
| `list_drafts` | Drafts, one page per call (1 to 100), filtered by `query` |
|
|
104
|
+
| `update_draft` | Rewrites a draft (empty fields unchanged), thread, reply and attachments kept; `attachments` adds, `removeAttachments` removes (by name) |
|
|
105
|
+
| `label_message` / `unlabel_message`, `label_thread` / `unlabel_thread` | `labelIds`: system IDs (`INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `CATEGORY_*`), user label IDs or names; archive = remove `INBOX`; an unknown label is an error, created only with `createMissing=true`; one ID or a list, 100 at most |
|
|
106
|
+
| `trash_message` / `trash_thread`, `untrash_message` / `untrash_thread` | Trash, reversible for 30 days; 100 items at most |
|
|
107
|
+
| `send_draft` | Without `confirm`: a summary to show to the user plus an `accord` token bound to the exact draft; sends **only with `confirm=true` and that token**, the draft read again and unchanged |
|
|
108
|
+
| `auth_status` | Token storage, granted scopes (checked at startup), validity; `reauth_required` when a scope is missing or extra |
|
|
109
|
+
|
|
110
|
+
Every tool carries MCP annotations (title, read-only, destructive, idempotent). For Antigravity, `setup` also installs
|
|
111
|
+
a short skill that says which tool to use for what, and when to use the browser instead (Gmail settings such as
|
|
112
|
+
filters or signature, another Google account).
|
|
113
|
+
|
|
114
|
+
## Safeguards
|
|
115
|
+
|
|
116
|
+
- **Scopes**: `gmail.readonly`, `gmail.compose`, `gmail.modify`. Never `https://mail.google.com/`, the only scope
|
|
117
|
+
that allows permanent deletion: there is no `delete`, `batchDelete` or empty-trash tool, and the code is tested
|
|
118
|
+
for it. An old token without `gmail.modify` keeps reading and drafting; the labelling and trash tools then answer
|
|
119
|
+
`scopes` without calling Gmail.
|
|
120
|
+
- **Sending**: only `send_draft(draftId, confirm=true, accord=<token>)`, after the user's explicit yes for that
|
|
121
|
+
draft, never at the request of an email. The single-use token (10 minutes) is returned by the summary and bound to
|
|
122
|
+
the exact draft: recipients (to, cc, bcc), subject, full body, every part (HTML included) and the SHA-256 of each
|
|
123
|
+
attachment. The draft is read again just before sending: if anything changed, nothing is sent and a new summary is
|
|
124
|
+
returned. `drafts.send` is the only sending call: no direct sending without a draft.
|
|
125
|
+
- **Provenance**: a recipient found in an email the agent read is flagged in the summary (`provenance`): allowed only
|
|
126
|
+
if the user asked for it.
|
|
127
|
+
- **Prompt injection**: subject, snippet, body, addresses and attachment names are returned framed as
|
|
128
|
+
`⟦mail:ID …⟧ … ⟦/mail:ID⟧` (random ID per answer) and labelled as data; hidden HTML text and invisible characters
|
|
129
|
+
are removed. An email that looks like instructions aimed at an AI gets an `injection_warning` field (a signal only:
|
|
130
|
+
it neither blocks nor allows anything).
|
|
131
|
+
- **Attachments**: a received file is saved under a sanitized name (path, forbidden or invisible characters,
|
|
132
|
+
right-to-left override as in "fdp.exe", Windows reserved names), never overwriting a file, with the Windows
|
|
133
|
+
"downloaded from the Internet" mark (Zone.Identifier), so Windows and Office open it with their protections.
|
|
134
|
+
Attaching a local file: never from a secrets location (`.ssh`, `.gnupg`, `.aws`, `.config/gcloud`, the AI apps'
|
|
135
|
+
folders, `.env` files, private keys, `credentials.json`..., plus your own folders listed in
|
|
136
|
+
`NEXUS_AGYMAIL_PROTECTED`); the user can always attach it themselves in Gmail.
|
|
137
|
+
- **Token**: `~/.nexus/nexus-agymail/token.bin`, encrypted with DPAPI for the current Windows account (permissions
|
|
138
|
+
reduced to that account), or a `0600` file on macOS and Linux; written atomically under a lock (several servers can
|
|
139
|
+
run at once). It is never shown. When you sign in again, a previous token with extra scopes is revoked only after
|
|
140
|
+
the new one is saved.
|
|
141
|
+
- **Exact errors**: revoked or expired token, network, quota, permissions, not found, label: each has a stable `code`
|
|
142
|
+
(`revoked`, `network`, `quota`, `forbidden`, `not_found`, `label`, `invalid_argument`...). Every request is retried
|
|
143
|
+
with exponential backoff on 429 and 5xx; batched requests retry only the items refused for quota.
|
|
144
|
+
- **Bounded sizes**: bodies cut at `maxChars` (reported), paged searches, at most 100 items per labelling or trash
|
|
145
|
+
call.
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
| Variable | Purpose |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `NEXUS_AGYMAIL_HOME` | State folder (token, OAuth client, backups) instead of `~/.nexus/nexus-agymail` |
|
|
152
|
+
| `NEXUS_AGYMAIL_CLIENT_SECRET` | Path of the OAuth client JSON, instead of the copy made by `auth --client-secret` |
|
|
153
|
+
| `NEXUS_AGYMAIL_PROTECTED` | Extra folders whose files are never attached (separated by `;` on Windows, `:` elsewhere) |
|
|
154
|
+
| `NEXUS_AGYMAIL_ACCOUNT` | Address shown by `auth_status` when the token does not carry it |
|
|
155
|
+
|
|
156
|
+
## Privacy
|
|
157
|
+
|
|
158
|
+
NexusAGYmail runs on your machine. It has no account of its own, no telemetry, no analytics and no relay: its only
|
|
159
|
+
network requests go to Google (`gmail.googleapis.com`, `oauth2.googleapis.com`, and `accounts.google.com` in your
|
|
160
|
+
browser when you sign in), with your own OAuth client.
|
|
161
|
+
|
|
162
|
+
- **What it reads**: the emails, threads, drafts, labels and attachments the agent asks for, only when it calls the
|
|
163
|
+
matching tool; local files only when the agent attaches them to a draft.
|
|
164
|
+
- **Where it goes**: tool results go to the AI client you connected, which may send them to its model provider under
|
|
165
|
+
its own privacy policy. Drafts and sent emails go to Gmail. NexusAGYmail sends nothing anywhere else.
|
|
166
|
+
- **What it stores**, under `~/.nexus/nexus-agymail/` (or `NEXUS_AGYMAIL_HOME`): the refresh token (`token.bin`,
|
|
167
|
+
encrypted with DPAPI on Windows, a `0600` file elsewhere), its lock file, the copy of your OAuth client
|
|
168
|
+
(`client_secret.json`), and backups of the Antigravity config files `setup` edits (`backups/`). Attachments you ask
|
|
169
|
+
it to save go to `~/Downloads/NexusAGYmail` (or the folder given). In memory only, for the life of the server: the
|
|
170
|
+
pending send accords and the addresses seen in emails read (for the `provenance` warning). No email is written to
|
|
171
|
+
disk unless saved as an attachment; no log of emails is kept.
|
|
172
|
+
- **Removing it**: delete `~/.nexus/nexus-agymail/` and, for Antigravity, `~/.gemini/config/plugins/nexus-agymail/`;
|
|
173
|
+
revoke the access in your [Google account](https://myaccount.google.com/connections) and delete your Google Cloud
|
|
174
|
+
project if you no longer need it.
|
|
175
|
+
|
|
176
|
+
## Known limitations
|
|
177
|
+
|
|
178
|
+
- Each user creates their own Google OAuth client (step 1). A shared public client would need Google's restricted
|
|
179
|
+
scope verification and a security assessment.
|
|
180
|
+
- Plain-text bodies only when writing: `create_draft` and `update_draft` do not write HTML; `update_draft` replaces
|
|
181
|
+
the HTML part of a draft written in Gmail with the plain text.
|
|
182
|
+
- No Gmail settings (filters, signature, forwarding, vacation replies): use the Gmail web app.
|
|
183
|
+
- No spam reporting and no permanent deletion, on purpose.
|
|
184
|
+
- Used daily on Windows 11 with a real Gmail account; Linux runs the startup and unit tests in CI; macOS is
|
|
185
|
+
untested.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```powershell
|
|
190
|
+
./test.ps1 # ruff, pyright, unit tests against a fake Gmail API (no network, no Google account)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`test.ps1` gives each checkout its own test environment under `~/.nexus/nexus-agymail` and never downloads anything:
|
|
194
|
+
run `uv sync --frozen` once with network access to fill the uv cache. The fake Gmail API (`tests/fake_gmail.py`)
|
|
195
|
+
speaks HTTP to the real `google-api-python-client`, so batching, retries, uploads and paging are tested for real.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
[MIT](https://github.com/pironjulien/NexusAGYmail/blob/main/LICENSE). Not affiliated with Google; Gmail is a trademark
|
|
200
|
+
of Google LLC.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nexus-agymail"
|
|
3
|
+
version = "0.8.0"
|
|
4
|
+
description = "Gmail MCP server for AI agents: official Gmail API, Google's tool names, sending only after the user's yes, no permanent deletion"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
keywords = [
|
|
9
|
+
"mcp",
|
|
10
|
+
"mcp-server",
|
|
11
|
+
"gmail",
|
|
12
|
+
"email",
|
|
13
|
+
"ai-agents",
|
|
14
|
+
"antigravity",
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"prompt-injection",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Communications :: Email",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"mcp>=2.3.0",
|
|
31
|
+
"google-api-python-client>=2.160.0",
|
|
32
|
+
"google-auth-httplib2>=0.2.0",
|
|
33
|
+
"google-auth-oauthlib>=1.2.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[project.authors]]
|
|
37
|
+
name = "Julien Piron"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://julienpiron.fr/nexusagymail/"
|
|
41
|
+
Repository = "https://github.com/pironjulien/NexusAGYmail"
|
|
42
|
+
Changelog = "https://github.com/pironjulien/NexusAGYmail/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
nexus-agymail = "nexus_agymail.server:main"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["uv_build>=0.12.5,<0.14.0"]
|
|
49
|
+
build-backend = "uv_build"
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=9.1.1",
|
|
54
|
+
"ruff>=0.17.0",
|
|
55
|
+
"pyright==1.1.407",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 120
|
|
60
|
+
target-version = "py314"
|
|
61
|
+
cache-dir = "~/.nexus/nexus-agymail/cache/ruff"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = [
|
|
65
|
+
"E",
|
|
66
|
+
"F",
|
|
67
|
+
"W",
|
|
68
|
+
"I",
|
|
69
|
+
"B",
|
|
70
|
+
"UP",
|
|
71
|
+
]
|
|
72
|
+
ignore = ["E501"]
|
|
73
|
+
|
|
74
|
+
[tool.pyright]
|
|
75
|
+
include = [
|
|
76
|
+
"src",
|
|
77
|
+
"tests",
|
|
78
|
+
]
|
|
79
|
+
pythonVersion = "3.14"
|
|
80
|
+
pythonPlatform = "Windows"
|
|
81
|
+
extraPaths = [
|
|
82
|
+
"src",
|
|
83
|
+
"tests",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
testpaths = ["tests"]
|
|
88
|
+
addopts = "-p no:cacheprovider"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nexus-agymail"
|
|
3
|
+
version = "0.8.0"
|
|
4
|
+
description = "Gmail MCP server for AI agents: official Gmail API, Google's tool names, sending only after the user's yes, no permanent deletion"
|
|
5
|
+
authors = [{ name = "Julien Piron" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
keywords = ["mcp", "mcp-server", "gmail", "email", "ai-agents", "antigravity", "claude", "codex", "prompt-injection"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: Communications :: Email",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=2.3.0",
|
|
22
|
+
"google-api-python-client>=2.160.0",
|
|
23
|
+
"google-auth-httplib2>=0.2.0",
|
|
24
|
+
"google-auth-oauthlib>=1.2.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://julienpiron.fr/nexusagymail/"
|
|
29
|
+
Repository = "https://github.com/pironjulien/NexusAGYmail"
|
|
30
|
+
Changelog = "https://github.com/pironjulien/NexusAGYmail/blob/main/CHANGELOG.md"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
nexus-agymail = "nexus_agymail.server:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.12.5,<0.14.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=9.1.1",
|
|
42
|
+
"ruff>=0.17.0",
|
|
43
|
+
"pyright==1.1.407",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 120
|
|
48
|
+
target-version = "py314"
|
|
49
|
+
cache-dir = "~/.nexus/nexus-agymail/cache/ruff"
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = ["E", "F", "W", "I", "B", "UP"]
|
|
53
|
+
ignore = ["E501"]
|
|
54
|
+
|
|
55
|
+
[tool.pyright]
|
|
56
|
+
include = ["src", "tests"]
|
|
57
|
+
pythonVersion = "3.14"
|
|
58
|
+
pythonPlatform = "Windows"
|
|
59
|
+
# Les tests importent fake_gmail depuis leur propre dossier (sys.path).
|
|
60
|
+
extraPaths = ["src", "tests"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
addopts = "-p no:cacheprovider"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""NexusAGYmail - serveur MCP Gmail (lecture, brouillons, rangement, envoi confirmé) pour Antigravity."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Aucun __pycache__ dans les sources (dossier synchronisé) : les commandes de lancement passent aussi -B et
|
|
6
|
+
# PYTHONDONTWRITEBYTECODE, ceci couvre les sous-modules si Python est lancé sans eux.
|
|
7
|
+
sys.dont_write_bytecode = True
|
|
8
|
+
|
|
9
|
+
__version__ = "0.8.0"
|