hypermail-mcp 0.7.6 → 0.7.8

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