# @spideriq/mcp-mail

MCP server for SpiderMail — mailboxes, messages, threads, send, compose, templates,
plus the **send tier**: bring-your-own sending domain, the sending pool and its
reputation, and broadcasts.

## Two layers, and picking the wrong one is the common mistake

| You want to… | Use | Scope |
|---|---|---|
| Send **one** message through **one** registered mailbox | `send_email` | a mailbox |
| Read an inbox, thread, or search mail | `get_inbox` · `get_thread` · `search_mail` | a mailbox |
| Onboard a sending domain you own | `send_check_domain` → `send_enroll_domain` | the fleet |
| See the sending pool, caps, warm-up, reputation | `send_list_sources` · `send_get_deliverability` | the fleet |
| Mail **many** people | `send_create_broadcast` → `send_queue_broadcast` | the fleet |

### Three contracts worth reading before you call anything

1. **A draft is not a send.** `send_create_broadcast` creates a draft, and setting
   `scheduled_at` records intent — it does **not** send. Only `send_queue_broadcast`
   puts mail on the wire, and rows already enqueued are not withdrawn by a later cancel.
2. **A `null` DNS verdict is UNDETERMINABLE, never a pass.** Each check in
   `send_check_domain` is tri-state; `verified` is true only when all three are true.
   When one fails, its `expected` field names the exact record to publish.
3. **`already_enqueued > 0` and `provider_configured: false` are not failures.** The
   first is a resumed fan-out (the enqueue is idempotent per broadcast+address, so a
   retry fills gaps instead of double-sending); the second means the messages are
   queued and will hold until credentials exist — a deliberate dormant state.

All send-tier tools are tenant-scoped by your PAT. None takes a `client_id`.

## Install

```bash
npm install @spideriq/mcp-mail --registry https://npm.spideriq.ai
```

## Configure

```json
{
  "mcpServers": {
    "spideriq-mail": {
      "command": "npx",
      "args": ["-y", "@spideriq/mcp-mail"],
      "env": {
        "SPIDERIQ_WORKSPACE": "cli_…"
      }
    }
  }
}
```

## Which tenant will this write to?

Resolution order — first hit wins. Every response reports which rung won as `_tenancy.via`.

| # | Source | How |
|---|--------|-----|
| 1 | explicit | `workspace: "cli_…"` argument on the call — **always wins, works per-call** |
| 2 | environment | `SPIDERIQ_WORKSPACE=cli_…` |
| 3 | binding | a `spideriq.json` in cwd or an ancestor (`SPIDERIQ_WORKSPACE_CWD` overrides where we look) |
| 4 | sole credential | automatic when exactly one is stored — reported as `via: "sole-credential"`, never silent |
| 5 | — | **REFUSED.** With several tenants and no binding we do not guess. The error carries `retry_with` — merge it into your retry. |

**Running an agency / multiple clients?** Pass `workspace` on every call. `cwd` is a
per-process fact; tenancy is a per-call fact, so one session can drive many tenants
with no `cd`, restart, or reconfiguration.

```js
content_list_pages({ workspace: "cli_clientA" })
content_create_page({ workspace: "cli_clientB", title: "…" })
```

> ⚠️ **Some IDEs spawn MCP servers with `cwd=/`** (Antigravity does). Nothing
> cwd-derived is reachable from there, so set `SPIDERIQ_WORKSPACE` or
> `SPIDERIQ_WORKSPACE_CWD` in the server's `env` block, or pass `workspace` per call.

Lost? Call `get_auth_status({topic:"tenancy"})` — it returns the active tenant, why it
was chosen, every workspace you can reach, and how to switch.
