hypermail-mcp 0.7.5 → 0.7.7

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.
Files changed (4) hide show
  1. package/README.md +176 -135
  2. package/dist/cli.js +14713 -670
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +4 -3
package/README.md CHANGED
@@ -3,12 +3,24 @@
3
3
  A **Model Context Protocol** server that lets an agent operate any of the user's
4
4
  inboxes through a single, unified tool surface.
5
5
 
6
+ > **v0.7.7** — Env-only configuration. Runtime config now comes from flat
7
+ > `HYPERMAIL_*` environment variables plus selected CLI overrides. Config files
8
+ > and legacy provider env names are no longer read. Hosted Gmail OAuth callbacks
9
+ > are supported via `HYPERMAIL_GMAIL_REDIRECT_URI`; local loopback and manual
10
+ > completion still work.
11
+ >
12
+ > **v0.7.6** — Gmail setup uses OAuth authorization URLs instead of Google's
13
+ > rejected device-code flow for Gmail API scopes. `complete_add_account` accepts
14
+ > a final redirected URL or raw `code`/`state`, and provider credentials use
15
+ > dedicated `HYPERMAIL_GMAIL_*` / `HYPERMAIL_OUTLOOK_*` env vars.
16
+ >
6
17
  > **v0.7.5** — Attachments via file path on `send_email`/`draft_email`
7
18
  > (`attachments` param). `edit_draft` gains `new_attachments` and
8
19
  > `remove_attachments` — `add_attachment_to_draft` is removed (23 tools now).
9
20
  > Draft editing uses multi-strategy thread boundary detection for more reliable
10
- > quoted-thread preservation. Watcher now supports script emission (`runScript`)
11
- > alongside webhook delivery.
21
+ > quoted-thread preservation. Watcher now supports shell-command notification
22
+ > alongside webhook delivery. Published CLI installs the MCP SDK dependency so
23
+ > global/npx runs do not fail on a missing SDK module.
12
24
  >
13
25
  > **v0.7.4** — `inReplyTo` is now a required parameter on `send_email` and
14
26
  > `draft_email` (was optional). Set it to `false` for a new email, or pass a
@@ -21,13 +33,12 @@ inboxes through a single, unified tool surface.
21
33
  > (above the spacer delimiter) is replaced.
22
34
  >
23
35
  > **v0.7.1** — Every config field is now settable via a dedicated
24
- > `HYPERMAIL_*` env var. Legacy env vars (`MS_CLIENT_ID`, `MS_TENANT_ID`,
25
- > `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) still work as fallbacks. See
36
+ > `HYPERMAIL_*` env var. Legacy provider env vars are no longer accepted. See
26
37
  > [Environment Variables](#environment-variables) for the full reference.
27
38
  >
28
39
  > **v0.7.0** — Email watch mode: background poll loop detects new inbox
29
40
  > messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in —
30
- > disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true` or config.
41
+ > disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true`.
31
42
  > Works in both stdio and HTTP transport modes.
32
43
  >
33
44
  > **v0.6.3** — Unify stdio and HTTP modes into a single feature set. Removed
@@ -54,13 +65,14 @@ inboxes through a single, unified tool surface.
54
65
  > union output schemas that caused `validateToolOutput` crashes.
55
66
 
56
67
  The agent doesn't care whether an address is a work Outlook account, a personal
57
- Microsoft account, or (soon) a personal IMAP mailbox — it just calls
68
+ Microsoft account, a personal IMAP mailbox, or Gmail — it just calls
58
69
  `list_emails`, `search_emails`, `read_email`, `send_email` and passes the email
59
70
  address as the `account` argument. The server routes to the right backend.
60
71
 
61
72
  **v1 status:** Outlook / Microsoft 365 (personal + work) fully supported via
62
73
  Microsoft Graph. IMAP (any IMAP server) supported via `imapflow` + `nodemailer`.
63
- Gmail supported via Google OAuth device-code flow.
74
+ Gmail supported via Google OAuth authorization-code flow with local loopback or
75
+ hosted callbacks plus remote-safe manual completion.
64
76
 
65
77
  ## Why
66
78
 
@@ -106,8 +118,8 @@ hypermail-mcp --http --port 3000 --host 0.0.0.0
106
118
  # endpoint: http://<host>:3000/mcp (Streamable HTTP transport, session-aware)
107
119
  ```
108
120
 
109
- When hosted you **must** set `HYPERMAIL_MCP_KEY` so the account file is
110
- reproducibly decryptable.
121
+ When hosted, set `HYPERMAIL_KEY` so the account file is reproducibly
122
+ decryptable across restarts and redeploys.
111
123
 
112
124
  ### Docker
113
125
 
@@ -116,18 +128,20 @@ reproducibly decryptable.
116
128
  docker build -t hypermail-mcp .
117
129
 
118
130
  # Run
131
+ # Pass secret values from your shell or deployment environment; do not commit them.
119
132
  docker run -d \
120
133
  --name hypermail-mcp \
121
134
  -p 3000:3000 \
122
- -e HYPERMAIL_MCP_KEY=<32-byte-key> \
123
- -e MS_CLIENT_ID=<your-client-id> \
124
- -e MS_TENANT_ID=<your-tenant-id> \
125
- -v hypermail-data:/data \
135
+ -e HYPERMAIL_KEY \
136
+ -e HYPERMAIL_OUTLOOK_CLIENT_ID \
137
+ -e HYPERMAIL_OUTLOOK_TENANT_ID \
138
+ -v hypermail-data:/var/lib/mcp \
126
139
  hypermail-mcp
127
140
  ```
128
141
 
129
142
  The image runs the server in HTTP mode on port 3000 with a 30-second
130
- HEALTHCHECK against `/mcp`. Data is persisted via a Docker volume at `/data`.
143
+ HEALTHCHECK against `/mcp`. Data is persisted via a Docker volume at
144
+ `/var/lib/mcp`.
131
145
 
132
146
  ### Development
133
147
 
@@ -137,91 +151,81 @@ To test the HTTP server locally:
137
151
  # Terminal 1: auto-rebuild TypeScript on save
138
152
  pnpm dev
139
153
 
140
- # Terminal 2: start HTTP server with dev config
154
+ # Terminal 2: start HTTP server with env/CLI config
141
155
  pnpm dev:http
142
156
  ```
143
157
 
144
- The server listens on `http://127.0.0.1:3000/mcp`. Pi connects via the
145
- `.pi/mcp.json` config (read by `pi-mcp-adapter`). Tools appear as
146
- `hypermail_http_*`.
158
+ The server listens on `http://127.0.0.1:3000/mcp`.
147
159
 
148
- ## Add-account flow (Outlook)
160
+ ## Runtime and provider configuration
149
161
 
150
- | Env var | Purpose | Default |
151
- | --- | --- | --- |
152
- | `HYPERMAIL_MCP_DATA_DIR` | Where to keep the encrypted accounts blob | `~/.hypermail-mcp` |
153
- | `HYPERMAIL_MCP_KEY` | 32-byte AES-256-GCM key (hex, base64, or any passphrase — derived via SHA-256). Required for hosted deployments. Auto-generated for stdio. | auto-generated, stored via OS keychain (`keytar`) or a local `master.key` file |
154
- | `MS_CLIENT_ID` | Azure Entra public client (application) id used for device-code login | placeholder — **set your own for production** |
155
- | `MS_TENANT_ID` | Tenant for the authority URL | `common` |
162
+ Hypermail uses flat `HYPERMAIL_*` environment variables as the source of truth.
163
+ There is no runtime config file. CLI flags only override transport, host, port,
164
+ and data directory for a single invocation.
156
165
 
157
- CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--read-only`, `--help`.
166
+ CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--help`.
158
167
 
159
- Subcommands: `hypermail-mcp generate-key` — generate an `hm_sk_` API key.
168
+ Subcommands: `hypermail-mcp generate-key` — generate a base64 32-byte key for
169
+ `HYPERMAIL_KEY`.
160
170
 
161
- ### Configuration
171
+ ### Local CLI / env example
172
+
173
+ ```bash
174
+ export HYPERMAIL_KEY="$(hypermail-mcp generate-key)"
175
+ export HYPERMAIL_DATA_DIR="$HOME/.local/share/hypermail-mcp"
176
+ export HYPERMAIL_OUTLOOK_CLIENT_ID="<your-client-id>"
177
+ hypermail-mcp
178
+ ```
162
179
 
163
- Instead of (or in addition to) CLI flags and env vars, you can configure the
164
- server with a `hypermail-config.json` file next to the server binary. The server
165
- looks for it in the same directory as `cli.js`.
180
+ ### Generic MCP client JSON example
166
181
 
167
182
  ```jsonc
168
183
  {
169
- "http": { "enabled": true, "port": 3000, "host": "0.0.0.0" },
170
- "dataDir": "/path/to/data",
171
- "tools": {
172
- // allowlist: only these tools are registered
173
- "enabled": ["list_emails", "search_emails", "read_email", "send_email"],
174
- // blocklist: these tools are NOT registered
175
- // "disabled": ["add_account", "remove_account"]
176
- },
177
- "providers": {
178
- "outlook": { "clientId": "...", "tenantId": "..." }
179
- },
180
- "watch": {
181
- "enabled": true,
182
- "pollIntervalSeconds": 10,
183
- "webhook": {
184
- "url": "http://your-agent:3000/api/email-webhook",
185
- "retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
184
+ "mcpServers": {
185
+ "hypermail": {
186
+ "command": "npx",
187
+ "args": ["-y", "hypermail-mcp"],
188
+ "env": {
189
+ "HYPERMAIL_KEY": "${HYPERMAIL_KEY}",
190
+ "HYPERMAIL_DATA_DIR": "${HYPERMAIL_DATA_DIR}",
191
+ "HYPERMAIL_OUTLOOK_CLIENT_ID": "${HYPERMAIL_OUTLOOK_CLIENT_ID}"
192
+ }
186
193
  }
187
194
  }
188
195
  }
189
196
  ```
190
197
 
191
- Per-tool filtering (`tools.enabled` / `tools.disabled`) lets operators ship
192
- minimal agent-facing surfaces — e.g. a read-only assistant that can only list
193
- and read emails.
194
-
195
- ## Environment Variables
196
-
197
- Every config field can be set via a dedicated `HYPERMAIL_*` env var, following
198
- a dotted-path naming convention (`HYPERMAIL_HTTP_PORT`,
199
- `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID`, etc.). Legacy env vars
200
- (`MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`)
201
- still work as fallbacks for backward compatibility.
198
+ ### Environment Variables
202
199
 
203
- | Env var | Config path | Type |
200
+ | Env var | Purpose | Default / behavior |
204
201
  | --- | --- | --- |
205
- | `HYPERMAIL_HTTP_ENABLED` | `http.enabled` | `bool` |
206
- | `HYPERMAIL_HTTP_PORT` | `http.port` | `int` |
207
- | `HYPERMAIL_HTTP_HOST` | `http.host` | `string` |
208
- | `HYPERMAIL_TOOLS_ENABLED` | `tools.enabled` | comma-sep strings |
209
- | `HYPERMAIL_TOOLS_DISABLED` | `tools.disabled` | comma-sep strings |
210
- | `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID` | `providers.outlook.clientId` | `string` |
211
- | `HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID` | `providers.outlook.tenantId` | `string` |
212
- | `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID` | `providers.gmail.clientId` | `string` |
213
- | `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET` | `providers.gmail.clientSecret` | `string` |
214
- | `HYPERMAIL_WATCH_ENABLED` | `watch.enabled` | `bool` |
215
- | `HYPERMAIL_WATCH_POLL_INTERVAL` | `watch.pollIntervalSeconds` | `int` |
216
- | `HYPERMAIL_WATCH_WEBHOOK_URL` | `watch.webhook.url` | `string` |
217
- | `HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS` | `watch.webhook.retry.maxAttempts` | `int` |
218
- | `HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS` | `watch.webhook.retry.baseDelayMs` | `int` |
219
- | `HYPERMAIL_WATCH_SCRIPT_COMMAND` | `watch.script.command` | `string` |
220
- | `HYPERMAIL_WATCH_SCRIPT_TIMEOUT_MS` | `watch.script.timeoutMs` | `int` |
221
- | `HYPERMAIL_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS` | `watch.script.retry.maxAttempts` | `int` |
222
- | `HYPERMAIL_WATCH_SCRIPT_RETRY_BASE_DELAY_MS` | `watch.script.retry.baseDelayMs` | `int` |
223
-
224
- **Priority order:** CLI flags > config file > `HYPERMAIL_*` env var > hardcoded default.
202
+ | `HYPERMAIL_DATA_DIR` | Account/token store location | `${XDG_DATA_HOME:-~/.local/share}/hypermail-mcp` |
203
+ | `HYPERMAIL_KEY` | 32-byte AES-256-GCM key as hex/base64, or any passphrase derived via SHA-256 | If unset, generates and persists a local key and prints a startup warning |
204
+ | `HYPERMAIL_TRANSPORT` | Runtime transport: `stdio` or `http` | `stdio`; `--http` overrides to `http` |
205
+ | `HYPERMAIL_HTTP_PORT` | HTTP bind port | `3000`; invalid HTTP-mode values warn and fall back |
206
+ | `HYPERMAIL_HTTP_HOST` | HTTP bind host | `127.0.0.1`; invalid HTTP-mode values warn and fall back |
207
+ | `HYPERMAIL_OUTLOOK_CLIENT_ID` | Optional custom Azure/Entra public client ID | Built-in public client |
208
+ | `HYPERMAIL_OUTLOOK_TENANT_ID` | Optional Outlook tenant/authority selector | `common` |
209
+ | `HYPERMAIL_GMAIL_CLIENT_ID` | Google OAuth client ID | Required when adding a Gmail account |
210
+ | `HYPERMAIL_GMAIL_CLIENT_SECRET` | Google OAuth client secret, when issued by the client type | unset |
211
+ | `HYPERMAIL_GMAIL_REDIRECT_URI` | Hosted Gmail OAuth callback URI | Local loopback callback when unset |
212
+ | `HYPERMAIL_TOOLS_ENABLED` | Comma-separated tool allowlist | Empty/unset means no filtering |
213
+ | `HYPERMAIL_TOOLS_DISABLED` | Comma-separated tool blocklist | Empty/unset means no filtering |
214
+ | `HYPERMAIL_WATCH_ENABLED` | Enable inbox polling: `true` or `false` | `false` |
215
+ | `HYPERMAIL_WATCH_POLL_SECONDS` | Watcher polling cadence | `10` |
216
+ | `HYPERMAIL_WATCH_WEBHOOK_URL` | Webhook delivery target | Required if watch is enabled and no notify command is set |
217
+ | `HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS` | Webhook retry attempts | `5` |
218
+ | `HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS` | Webhook exponential-backoff base delay | `1000` |
219
+ | `HYPERMAIL_WATCH_NOTIFY_COMMAND` | Shell command run with `EmailFull` JSON on stdin | Required if watch is enabled and no webhook is set |
220
+ | `HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS` | Notify-command execution timeout | `30000` |
221
+ | `HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS` | Notify-command retry attempts | `5` |
222
+ | `HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS` | Notify-command exponential-backoff base delay | `1000` |
223
+
224
+ **Priority order:** selected CLI flags > `HYPERMAIL_*` env vars > hardcoded defaults.
225
+
226
+ Per-tool filtering (`HYPERMAIL_TOOLS_ENABLED` / `HYPERMAIL_TOOLS_DISABLED`) lets
227
+ operators ship minimal agent-facing surfaces. If both non-empty lists are set,
228
+ or either list contains an unknown tool name, startup fails.
225
229
 
226
230
  ## Tools
227
231
 
@@ -232,65 +236,48 @@ account store.
232
236
  | Tool | Inputs | Notes |
233
237
  | --- | --- | --- |
234
238
  | `list_accounts` | — | Returns registered emails + provider, no secrets. |
235
- | `add_account` | `provider`, `email?`, `config?` | Starts device-code (Outlook). Returns `{handle, verification:{userCode, verificationUri, expiresAt}}`. |
236
- | `complete_add_account` | `provider`, `handle` | Returns `pending` / `ready` / `expired` / `error`. |
239
+ | `add_account` | `provider`, `email?`, `config?` | Starts the provider add flow. Outlook returns a device code; Gmail returns an OAuth URL. Returns `{handle, verification:{type, userCode, verificationUri, expiresAt, message}}`. |
240
+ | `complete_add_account` | `provider`, `handle`, `authorizationResponse?`, `code?`, `state?` | Returns `pending` / `ready` / `expired` / `error`. Gmail accepts a pasted final redirected URL or raw code/state for remote-safe completion. |
237
241
  | `get_account_settings` | `account` | Get signature (HTML) and style preferences for an account. |
238
- | `set_account_settings` | `account`, `signature?`, `signaturePath?`, `style?` | Set signature HTML (inline or via file path) and font preferences. Disabled under `--read-only`. |
242
+ | `set_account_settings` | `account`, `signature?`, `signaturePath?`, `style?` | Set signature HTML (inline or via file path) and font preferences. |
239
243
  | `remove_account` | `email` | Deletes tokens for the account. |
240
244
  | `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. |
241
245
  | `search_emails` | `account`, `query`, `limit?` | KQL on Outlook. |
242
246
  | `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. |
243
247
  | `read_attachment` | `account`, `messageId`, `attachmentId` | Download an attachment to a temporary file and return its path. |
244
- | `archive_email` | `account`, `id` | Move a message to the Archive folder. Disabled under `--read-only`. |
245
- | `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). Disabled under `--read-only`. |
246
- | `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. Disabled under `--read-only`. |
247
- | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. `inReplyTo` is required — set to `false` for new emails. `attachments` is an optional array of `{filePath, name?}` — files are read from disk and encoded automatically. Disabled under `--read-only`. |
248
- | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Save as draft instead of sending. Same params as `send_email` including `attachments`. Returns the draft message ID and HTML body (`draftHtml`). `inReplyTo` is required — set to `false` for new emails. Disabled under `--read-only`. |
249
- | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Only provided fields are updated. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. Disabled under `--read-only`. |
250
- | `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. Disabled under `--read-only`. |
248
+ | `archive_email` | `account`, `id` | Move a message to the Archive folder. |
249
+ | `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
250
+ | `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. |
251
+ | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. `inReplyTo` is required — set to `false` for new emails. `attachments` is an optional array of `{filePath, name?}` — files are read from disk and encoded automatically. |
252
+ | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Save as draft instead of sending. Same params as `send_email` including `attachments`. Returns the draft message ID and HTML body (`draftHtml`). `inReplyTo` is required — set to `false` for new emails. |
253
+ | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Only provided fields are updated. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. |
254
+ | `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. |
251
255
  | `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
252
- | `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. Disabled under `--read-only`. |
253
- | `delete_folder` | `account`, `folderId` | Delete a mail folder by ID. Disabled under `--read-only`. |
254
- | `rename_folder` | `account`, `folderId`, `newName` | Rename an existing mail folder. Disabled under `--read-only`. |
255
- | `mark_read` | `account`, `id` | Mark a message as read. Disabled under `--read-only`. |
256
- | `mark_unread` | `account`, `id` | Mark a message as unread. Disabled under `--read-only`. |
256
+ | `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. |
257
+ | `delete_folder` | `account`, `folderId` | Delete a mail folder by ID. |
258
+ | `rename_folder` | `account`, `folderId`, `newName` | Rename an existing mail folder. |
259
+ | `mark_read` | `account`, `id` | Mark a message as read. |
260
+ | `mark_unread` | `account`, `id` | Mark a message as unread. |
257
261
 
258
262
  ## Email Watch
259
263
 
260
264
  When enabled, hypermail-mcp runs a background poll loop that scans inboxes for
261
- new messages and delivers each one via webhook POST and/or script execution.
262
- Intended for push-based email triage — downstream agents (e.g. Mastra) receive
263
- full email content without polling.
265
+ new messages and delivers each one via webhook POST and/or shell command.
266
+ Intended for push-based email triage — downstream agents receive full email
267
+ content without polling.
264
268
 
265
- ```jsonc
266
- {
267
- "watch": {
268
- "enabled": true,
269
- "pollIntervalSeconds": 10,
270
- "webhook": {
271
- "url": "http://localhost:3000/api/email-webhook",
272
- "retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
273
- },
274
- "script": {
275
- "command": "node /path/to/email-handler.js",
276
- "timeoutMs": 30000,
277
- "retry": { "maxAttempts": 3, "baseDelayMs": 1000 }
278
- }
279
- }
280
- }
269
+ ```bash
270
+ HYPERMAIL_WATCH_ENABLED=true \
271
+ HYPERMAIL_WATCH_POLL_SECONDS=10 \
272
+ HYPERMAIL_WATCH_WEBHOOK_URL=http://localhost:3000/api/email-webhook \
273
+ HYPERMAIL_WATCH_NOTIFY_COMMAND='node /path/to/email-handler.js' \
274
+ hypermail-mcp
281
275
  ```
282
276
 
283
- | Setting | Default | Notes |
284
- | --- | --- | --- |
285
- | `watch.enabled` | `false` | Toggle via config or `HYPERMAIL_WATCH_ENABLED=true` env var |
286
- | `watch.pollIntervalSeconds` | `10` | Min 10s, max 3600s |
287
- | `watch.webhook.url` | — | Endpoint that receives `POST` with `EmailFull` JSON |
288
- | `watch.webhook.retry.maxAttempts` | `5` | Max delivery attempts (1–10) |
289
- | `watch.webhook.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
290
- | `watch.script.command` | — | Shell command spawned with email JSON on stdin |
291
- | `watch.script.timeoutMs` | `30000` | Max script runtime before SIGKILL |
292
- | `watch.script.retry.maxAttempts` | `3` | Max script delivery attempts |
293
- | `watch.script.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
277
+ **Validation:** If `HYPERMAIL_WATCH_ENABLED=true`, startup requires at least one
278
+ of `HYPERMAIL_WATCH_WEBHOOK_URL` or `HYPERMAIL_WATCH_NOTIFY_COMMAND`. Webhook
279
+ URLs are syntax-validated, and notify commands must be non-empty. Startup does
280
+ not test network reachability or execute the command.
294
281
 
295
282
  **Behavior:**
296
283
  - Polls **all accounts** in the store, **inbox only**.
@@ -298,21 +285,22 @@ full email content without polling.
298
285
  account file — no duplicate emits across restarts.
299
286
  - Two delivery modes (can be used together):
300
287
  - **Webhook:** One `POST` per email (full body as `EmailFull` JSON).
301
- - **Script:** Spawns a shell command with the `EmailFull` JSON piped to
302
- stdin. The script receives `conversationId`, `subject`, `from`, `toRecipients`,
303
- `body`, `isRead`, `sentDateTime`, `attachments`, plus an `account` field.
288
+ - **Notify command:** Executes `HYPERMAIL_WATCH_NOTIFY_COMMAND` through the
289
+ platform shell with the `EmailFull` JSON piped to stdin.
304
290
  - Both modes use exponential backoff (`baseDelay × 2^attempt`). Retries on
305
- failures (non-2xx for webhook, non-zero exit for script). Logs and moves on
291
+ failures (non-2xx for webhook, non-zero exit for command). Logs and moves on
306
292
  after `maxAttempts` exhausted — never blocks the poll loop.
307
- - Each delivery mode is fire-and-forget — the poll loop continues while
308
- delivery runs in the background.
293
+ - Command delivery is fire-and-forget — the poll loop continues while delivery
294
+ runs in the background.
309
295
  - Works in both **stdio** and **HTTP** transport modes — the poll interval
310
296
  fires normally alongside MCP message handling.
311
297
 
312
298
  **Rate limits:** Polling every 10s on a single inbox = 6 req/min = 0.6% of
313
299
  Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
314
300
 
315
- ## Add-account flow (Outlook)
301
+ ## Add-account flows
302
+
303
+ ### Outlook
316
304
 
317
305
  1. Agent calls `add_account({ provider: "outlook" })`.
318
306
  2. Server returns:
@@ -321,6 +309,7 @@ Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
321
309
  "status": "pending",
322
310
  "handle": "…uuid…",
323
311
  "verification": {
312
+ "type": "device_code",
324
313
  "userCode": "ABCD-EFGH",
325
314
  "verificationUri": "https://microsoft.com/devicelogin",
326
315
  "expiresAt": "2025-…",
@@ -333,6 +322,57 @@ Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
333
322
  it returns `{ "status": "ready", "account": {...} }`.
334
323
  5. From then on, any tool can be called with `account: "<that-email>"`.
335
324
 
325
+ ### Gmail
326
+
327
+ Gmail uses Google OAuth 2.0, matching the official Gmail MCP model. Google's
328
+ device-code endpoint rejects Gmail API scopes, so Hypermail uses an authorization
329
+ URL with a real callback. Service accounts are only suitable for Google
330
+ Workspace domain-wide delegation; they don't grant server-to-server access to
331
+ consumer `@gmail.com` inboxes.
332
+
333
+ For local stdio/Desktop OAuth clients, Hypermail starts a temporary
334
+ `127.0.0.1` loopback callback server automatically. For hosted HTTP deployments,
335
+ set `HYPERMAIL_GMAIL_REDIRECT_URI` and register the exact URI in Google Auth
336
+ Platform, for example:
337
+
338
+ ```bash
339
+ HYPERMAIL_TRANSPORT=http
340
+ HYPERMAIL_GMAIL_REDIRECT_URI=https://mail.example.com/oauth/gmail/callback
341
+ ```
342
+
343
+ 1. Configure `HYPERMAIL_GMAIL_CLIENT_ID` and, when issued by your Google client
344
+ type, `HYPERMAIL_GMAIL_CLIENT_SECRET`. Use a Desktop client for local
345
+ loopback, or a Web client for hosted HTTP callbacks.
346
+ 2. Agent calls `add_account({ provider: "gmail" })`.
347
+ 3. Server returns an OAuth URL:
348
+ ```json
349
+ {
350
+ "status": "pending",
351
+ "handle": "…uuid…",
352
+ "verification": {
353
+ "type": "oauth_url",
354
+ "userCode": "",
355
+ "verificationUri": "https://accounts.google.com/o/oauth2/v2/auth?...",
356
+ "expiresAt": "2025-…",
357
+ "message": "Open this URL in a browser to authorize Gmail access..."
358
+ }
359
+ }
360
+ ```
361
+ 4. The user opens `verificationUri` and grants access. If the configured
362
+ callback is reachable, the browser shows a small success page and the agent
363
+ can poll `complete_add_account({ provider: "gmail", handle })` until ready.
364
+ 5. If the browser cannot reach the callback, the manual fallback still works:
365
+ copy the final redirected URL from the browser address bar and call:
366
+ ```json
367
+ {
368
+ "provider": "gmail",
369
+ "handle": "…uuid…",
370
+ "authorizationResponse": "http://127.0.0.1:54321/oauth2callback?code=...&state=..."
371
+ }
372
+ ```
373
+ 6. `complete_add_account` validates state, exchanges the code for tokens, stores
374
+ the account, and returns `{ "status": "ready", "account": {...} }`.
375
+
336
376
  ## Roadmap
337
377
 
338
378
  - Threading / conversations.
@@ -345,7 +385,7 @@ src/
345
385
  cli.ts # arg parsing + entry
346
386
  server.ts # MCP server, stdio + HTTP transports, session management
347
387
  version.ts # version constant
348
- config.ts # hypermail-config.json schema + resolution
388
+ config.ts # env-only config types + resolution
349
389
  store/
350
390
  account-store.ts # encrypted multi-account store (AES-256-GCM)
351
391
  crypto.ts # AES-256-GCM encrypt/decrypt, key resolution, atomic writes
@@ -358,19 +398,20 @@ src/
358
398
  index.ts # OutlookProvider implementation
359
399
  imap/index.ts # IMAP provider (imapflow + nodemailer)
360
400
  gmail/
361
- auth.ts # Google OAuth device-code flow
401
+ auth.ts # Google OAuth authorization-code flow
362
402
  client.ts # Gmail API (googleapis)
363
403
  index.ts # GmailProvider implementation
364
404
  shared/ # shared utilities across providers
365
405
  watcher/
366
406
  manager.ts # WatcherManager — inbox poll loop + dedup
367
407
  webhook.ts # HTTP POST with exponential backoff retry
408
+ script.ts # shell-command delivery with retry/timeout
368
409
  index.ts # barrel export
369
410
  tools/
370
411
  index.ts # MCP tool registrations
371
412
  accounts.ts # list/add/remove/complete-add account tools
372
413
  browse.ts # list/search/read email tools
373
- compose.ts # send/draft/edit/send-draft/add-attachment tools
414
+ compose.ts # send/draft/edit/send-draft tools
374
415
  folders.ts # list/create/delete/rename folder tools
375
416
  organize.ts # archive/trash/move/mark-read/mark-unread tools
376
417
  shared.ts # shared tool helpers