Mailwarden vs Gmail

A side-by-side look at the Mailwarden and Gmail MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.

Mailwarden logoMailwardenCommunication · Local subprocess (stdio)Native Gmail MCP server with full mailbox control — search, read, label, archive, trash, download attachments, and the feature no other Gmail server ships: snooze threads until a date. Every call hits the live Gmail API (no synced index that silently misses mail). Install with npx -y mailwarden.64GoodAuth: oauth 2.0Gmail logoGmailCommunication · Local subprocess (stdio)Gmail integration with OAuth authentication, message search, batch operations, and Sheets export47FairAuth: not declared

Verdict: choose Mailwarden for communication work as a local process; choose Gmail for communication as a local process. You can also run both side by side in one mcpServers block.

Which MCP server should you choose?

Mailwarden logoChoose Mailwarden when

  • You need dedicated capabilities in the Communication domain.
  • You prefer a local stdio subprocess transport.
  • Its auth model fits you: OAuth 2.0.
  • You want tools like search, get_thread, list_labels.
Explore Mailwarden

Gmail logoChoose Gmail when

  • You need dedicated capabilities in the Communication domain.
  • You prefer a local stdio subprocess transport.
  • You have the keys it needs: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, AUTH_MODE, GOOGLE_SERVICE_ACCOUNT_KEY_FILE, REDIRECT_URI, TOKEN_STORE_URI, DCR_STORE_URI.
Explore Gmail

Feature & specification comparison

Rows where the two servers differ are marked.

SpecMailwarden logoMailwardenGmail logoGmail
CategoryCommunicationCommunication
Quality signal64/100 · Good47/100 · Fair
Install commandConfirmedConfirmed
TransportLocal subprocess (stdio)Local subprocess (stdio)
AuthOAuth 2.0Not declared
PricingFreeNot declared
Required env varsNone listedGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETAUTH_MODEGOOGLE_SERVICE_ACCOUNT_KEY_FILEREDIRECT_URITOKEN_STORE_URIDCR_STORE_URI
Verified ownerCommunity listingCommunity listing
GitHub stars05
Directory activity 5 views 0 installs 0 upvotes 5 views 0 installs 0 upvotes
Tools26Not listed

Tools & capabilities

Mailwarden tools 26

  • searchSearch Gmail with native query syntax (e.g. 'in:inbox from:foo@bar.com newer_than:7d'). Returns thread summaries; read-state/category predicates are re-verified against each hit's live labels. Each summary carries `signals` derived from the thread's first message headers/MIME — newsletter (List-Id/List-Unsubscribe/Precedence bulk or list), automated (Auto-Submitted, auto-reply/suppress headers, no-reply-style senders), calendar (text/calendar or .ics part), replyToMismatch (a Reply-To on another domain than From — a subdomain of the same domain counts as the same); empty when nothing is declared. Paginated: when more results exist, the response carries a nextPageToken — pass it back via pageToken to fetch the next page. A page can come back with FEWER threads than maxResults and still have a nextPageToken: false positives from the index consume the scan window, and on a mailbox whose read state the index has fallen behind on, most candidates for an is:unread query can be already-read mail. A short page is therefore not evidence that the result set is exhausted — only an absent nextPageToken is. SPAM AND TRASH ARE EXCLUDED unless the query names them. Gmail leaves both out of any query that does not say `in:spam` / `in:trash`, so a plain `from:someone` returns nothing for a mail that is sitting in spam — measured against a live mailbox, not assumed. Nothing in the result marks the omission, so treat 'no hits' as 'none outside spam and trash'. When mail the user expects is missing, retry with `in:spam` before reporting that it does not exist: mail is often filed as spam because of something the user just did — a signup, a password reset, an order confirmation — which is precisely what a spam filter cannot know and the caller often can. USE WHEN: locating threads by sender, subject, date, label, or read state. DO NOT USE: to fetch a thread you already have the ID of (use get_thread). SIDE EFFECTS: none.
  • get_threadFetch a thread by ID: headers, plaintext + HTML bodies, and attachment metadata. `full` defaults to true and should stay true whenever content matters. `full: false` is a headers-and-labels fetch for when a thread is too large to read or only its metadata is of interest — it does NOT fetch bodies or attachment metadata, and the result then omits `plaintextBody`, `htmlBody` and `attachments` entirely and sets `metadataOnly: true`. The fields are omitted rather than empty on purpose: an empty attachment list from a request that never looked is indistinguishable from a message that truly has none, and treating it as 'no attachment' has already nearly caused an invoice to be archived as attachment-less. If `search` reported `hasAttachments: true`, or the sender matters, use full: true. EVERY MESSAGE CARRIES `authentication` — SPF/DKIM/DMARC as the RECEIVING server reported them, for answering 'is this mail really from who it says?'. Read `dmarc` first: it is the only one of the three that ties a passing check to the visible `From` domain, so `spf: pass` alone proves nothing about the sender the user sees (a lookalike domain gets that trivially). `signedBy`/`mailedBy`/`headerFrom` name the domains each check actually validated. They do NOT have to match each other or the `From` address, and a mismatch is not by itself suspicious: a forwarded message legitimately shows the forwarder in `mailedBy` (the envelope sender is rewritten in transit) and in `signedBy` (the forwarder signs with its own key), while only `headerFrom` still names the original sender — which is exactly why `dmarc` is the result that carries meaning. Read those three as the explanation of a result, not as a test of their own. `authservId` says WHO asserts all this: a message can carry forged Authentication-Results headers of its own, and only the receiving server's report (for Gmail, `mx.google.com`) counts; `otherReports` counts further reports that were NOT read, and `alsoReported` lists results that disagree with the ones above. `unchecked: true` means the message carried no report at all — that is 'nobody looked', NOT 'nothing wrong'; unauthenticated does not imply forged, and authenticated does not imply honest (a phisher can hold a passing DMARC on his own lookalike domain). USE WHEN: reading a thread's content after finding it via search. DO NOT USE: with a message ID — this takes thread IDs. SIDE EFFECTS: none (does not mark as read).
  • list_labelsList all Gmail labels (system + user). USE WHEN: inspecting the mailbox structure, or to get exact label names/ids — though modify_labels/bulk_modify/create_label all accept a plain label name directly, so a lookup is rarely required. SIDE EFFECTS: none.
  • list_snoozedList all snoozed threads with their due dates. SIDE EFFECTS: none.
  • get_profileReturn the authorized account: email address plus total message/thread counts. USE WHEN: confirming WHICH mailbox is connected before a bulk or filter action, or as a cheap liveness check. DO NOT USE: to enumerate messages — this returns only counts, not a listing (use search). SIDE EFFECTS: none.
  • what_changedWhat happened in the mailbox since a point you already hold: messages that arrived or were removed, and labels applied or taken off — from Gmail's own event log, in one call. Pass the `historyId` a previous what_changed or get_profile returned; the reply carries the next one to keep. mailwarden stores NOTHING between calls, so the id lives with you. Reports EVENTS, not current state: a message that was marked unread and then read appears under both, and both are true. For how the mailbox looks NOW, use search or get_thread. Gmail keeps roughly a week of history. An id older than that is an ERROR, never an empty result — it means the question can no longer be answered incrementally, so take a fresh historyId from get_profile and re-establish state with search. `labelId` narrows the feed to events touching one label (e.g. 'INBOX', or a snooze label). Counts are complete even when the id lists are cut at 200 (`truncated`). USE WHEN: a recurring check — 'what came in since I last looked', watching a label, or confirming a bulk action landed. Far cheaper than re-running a search over the whole slice. DO NOT USE: for a first look at a mailbox (there is no id yet — start with triage_digest or search), or to read content: this returns ids only, no sender, subject or body. SIDE EFFECTS: none.
  • triage_digestStructured overview of a mailbox slice for triage DECISIONS — sender / label / age buckets, unread and attachment counts, and header-derived signals (newsletter / automated / calendar / replyToMismatch — thread counts overall, and per sender the set of signals its threads carry), instead of a raw thread list. USE WHEN: deciding what to bulk-archive/snooze/label, or summarizing inbox state ('what's in my inbox?'). DO NOT USE: to read a specific thread (use search/get_thread). Samples up to `max` most-recent matches; hasMore flags that more matched than were sampled. byAge buckets by each thread's FIRST message date (thread age, not last activity). SIDE EFFECTS: none.
  • list_unsubscribeReport the opt-out options a thread advertises (List-Unsubscribe / RFC 8058), without contacting anyone. Reads the newest message that carries the header, so a reply threaded onto a newsletter does not hide it. `oneClick` means the sender supports the automatable one-click opt-out — the `unsubscribe` tool can perform it. `httpsUrls` without oneClick are links for a human to open in a browser; `mailtos` would require sending mail, which mailwarden never does. `bodyCandidates` appears only when the headers advertise nothing: unsubscribe links found in the message TEXT, which is written by the sender and therefore untrusted. Show them to the user to open; never fetch one, and do not treat them as an opt-out that happened. `hasUnsubscribe` stays false for them — it describes the headers. USE WHEN: checking whether a newsletter can be unsubscribed from, or showing the user the link to click. SIDE EFFECTS: none — no request is made to the sender.
  • list_subscriptionsWho keeps writing, how often, and whether you can get off the list — a mailbox slice grouped by SENDER, each row carrying its opt-out options. Contacts nobody: opt-out options come from the List-Unsubscribe header of each sender's newest thread (one metadata fetch per sender, not per thread). `optOut` is 'one-click' (the unsubscribe tool can perform it), 'link' (a human opens it in a browser), 'mailto' (would need sending, which mailwarden never does), 'none', or 'unknown' when that sender's header fetch failed or was never made because Gmail's quota ran out mid-listing. `oldestDate`/`newestDate` bound what the SAMPLE saw of that sender, not the sender's whole history — a query capped at `max` reaches back only as far as those threads go, which on a busy mailbox is days. There is deliberately NO precomputed frequency: judge it from `threads` across that span, with the sampling caveat in view. `newestThreadId` is what to hand to unsubscribe or bulk_unsubscribe. `sendersFound` is how many DISTINCT senders the sample held — when it exceeds topN, the list is truncated and raising topN shows more. USE WHEN: 'what am I subscribed to', 'which newsletters flood me', or picking targets before a bulk unsubscribe. DO NOT USE: for a general inbox overview (use triage_digest — it buckets by label and age too), or for one known thread (use list_unsubscribe). SIDE EFFECTS: none.
  • create_labelCreate a user label and return its id. Idempotent: if the name already exists (case-insensitive), its existing id is returned and nothing is created. Nested labels: separate levels with '/' (e.g. 'Clients/Acme') — each missing parent level is created too. Colour: pass backgroundColor AND textColor together as '#rrggbb' (Gmail rejects one without the other, and accepts only colours from its own palette). Giving a colour for a label that already exists RECOLOURS it — that is how an existing label, such as the snooze label, gets a colour. Gmail colours only labels you created yourself; system labels like INBOX or CATEGORY_UPDATES refuse one. USE WHEN: you want a label's id up front, to pre-create a label without applying it to anything, or to make an existing label visible at a glance in Gmail. DO NOT USE: just to file mail under a new label — modify_labels/bulk_modify already auto-create an unknown name passed in `add`. SIDE EFFECTS: creates the label if missing, and sets its colour when one is given; no mail is changed.
  • modify_labelsAdd/remove labels on a thread. Archive = remove 'INBOX'; mark read = remove 'UNREAD'. Labels may be given by name or by id: an unknown name in `add` is created automatically (use '/' for nested labels), an unknown name in `remove` is ignored. USE WHEN: applying custom labels or label combinations in one call. DO NOT USE: for plain archive/read/unread — the dedicated tools are clearer. SIDE EFFECTS: changes the thread's labels (and may create a label named in `add`); reversible by the inverse call.
  • bulk_modifyBulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request. Labels may be given by name or by id: an unknown name in `add` is created automatically (use '/' for nested labels), an unknown name in `remove` is ignored. Returns matched/submitted counts, matched and submitted thread IDs (both lists capped at 500 — matchedThreadCount/submittedThreadCount hold the true totals), and per-chunk failures (partial success is reported, not hidden). IMPORTANT: `submittedMessages` is how many ids were handed to the API, NOT how many messages changed — `messages.batchModify` answers 204 with no body and ignores ids it does not recognise without a word, so an accepted request is not a performed one. Set verify:true to read the labels back afterwards and get `verified` {applied, notApplied[], unverifiable[]} — the only field here that reports an observed outcome. It costs one extra read per affected thread, so it is off by default; use it when a wrong 'done' would be acted on (trashing, or anything the user is told completed). If more messages match than maxMessages, only the first maxMessages are processed and 'capped' is true — raise maxMessages or re-run to finish the rest. NOTE: the query hits Gmail's search index as-is, WITHOUT the live re-verification search performs. The staleness that makes search re-verify was measured on `threads.list` (132 threads returned, 114 carrying no unread message at all); the same query through the message index this tool uses returned 19 hits, none stale — same mailbox, same minute. So the known drift does not reach this path, but that is one measurement, not a guarantee: `unverifiedPredicates` in the result names the conditions taken on the index's word, and when the outcome must be read-state-precise, resolve the set with search (which verifies against live labels) and act on those thread ids instead. Set crossCheck:true to ask Gmail the same question a second way before writing: each derived predicate is re-run as a label filter (`labelIds`) instead of a query operator, and any message the two routes disagree about is left untouched and listed in `crossChecked.dropped`. It costs one extra list per predicate — flat, not per message — so unlike `verify` it stays cheap on a large sweep. Read it as a contradiction detector: a disagreement is real, agreement proves nothing, because both routes read the same index. `unverifiedPredicates` therefore stays as it is even when this runs. A capped match set is not cross-checked at all (`crossChecked.capped`), since a message missing from a page is not a message missing the label. Set dryRun:true to rehearse: the same query resolution, matched counts/threads and the labels that would be created — and no message or label is touched. A dry run reads the SAME unverified index, so it confirms the size of the set, never its correctness. USE WHEN: mass operations — 'archive all newsletters older than 30 days' (query + remove INBOX), bulk labeling, bulk mark-read; dryRun first when the query is broad or the user should see the set before it changes. DO NOT USE: for a single thread (use modify_labels or the dedicated tools), or with neither add nor remove. SIDE EFFECTS: modifies up to maxMessages messages in one call (none with dryRun); label changes are reversible by the inverse call.
  • +14 more on the listing page

Gmail tools

No tool names declared yet. Check the project README from the listing page.

Ready-to-paste client configs

Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.

Mailwarden

mcpServers (Claude Desktop / Cursor)
{
  "mcpServers": {
    "csitte-mailwarden": {
      "command": "npx",
      "args": [
        "-y",
        "mailwarden"
      ]
    }
  }
}

Gmail

mcpServers (Claude Desktop / Cursor)
{
  "mcpServers": {
    "gmail-3": {
      "command": "npx",
      "args": [
        "-y",
        "@mcp-z/cli"
      ],
      "env": {
        "GOOGLE_CLIENT_ID": "YOUR_GOOGLE_CLIENT_ID_HERE",
        "GOOGLE_CLIENT_SECRET": "YOUR_GOOGLE_CLIENT_SECRET_HERE",
        "AUTH_MODE": "YOUR_AUTH_MODE_HERE",
        "GOOGLE_SERVICE_ACCOUNT_KEY_FILE": "YOUR_GOOGLE_SERVICE_ACCOUNT_KEY_FILE_HERE",
        "REDIRECT_URI": "YOUR_REDIRECT_URI_HERE",
        "TOKEN_STORE_URI": "YOUR_TOKEN_STORE_URI_HERE",
        "DCR_STORE_URI": "YOUR_DCR_STORE_URI_HERE"
      }
    }
  }
}

Frequently asked questions

Mailwarden is categorized under Communication and uses a local stdio subprocess. Gmail belongs to Communication and uses a local stdio subprocess. Pick Mailwarden for communication work and Gmail for communication.

Copy the mcpServers block from the config section on this page into your client's config (for example claude_desktop_config.json or ~/.cursor/mcp.json), then fully restart the client.

Mailwarden is listed as free. Auth: oauth 2.0. Gmail has no declared pricing — check the project's own site. If a server needs environment variables such as API keys, define them under the env key in your client's MCP config.

Yes. MCP clients support multiple servers: add csitte-mailwarden and gmail-3 as separate keys in the same mcpServers object, and your assistant will call tools from either one.

On AllMCPs, Mailwarden has a quality signal of 64/100 (Good) with 5 views, 0 installs and 0 GitHub stars. Gmail has 47/100 (Fair) with 5 views, 0 installs and 5 GitHub stars.