Skip to main content
Glama

mailwarden

npm license Node Website Smithery

A reliable, native Gmail MCP server — full mailbox triage for AI assistants, with the feature no other Gmail MCP server ships: mailbox-side snooze.

Highlights

  • Snooze — the only mailbox-side snooze in a Gmail MCP server. Archive a thread now, have it resurface in the inbox on a date. Built on dated labels + a sweep, so it works from any client, is visible in Gmail itself, and survives restarts. (Where another server offers a "snooze", it is a local reminder list — the mail never leaves or re-enters the inbox.)

  • Search you can trust. Gmail's threads.list — the call any thread search goes through — can answer is:unread from a stale thread-level read state: measured in one real mailbox, 86% of the threads it returned held no unread message at all; in a second mailbox, no drift whatsoever. You cannot tell which mailbox you are in without looking, so search re-verifies every hit against its live labels. Paginated via pageToken/nextPageToken. (the measurements)

  • Sender authentication, not sender spelling. get_thread reports the SPF/DKIM/DMARC results the receiving server recorded, so "is this really from my bank?" is answered from the message's own headers instead of from how the domain looks. It reads Gmail's own report only — the one written under the id mx.google.com; a message can carry forged ones of its own — says whether a passing DKIM signature belongs to the From domain, and says unchecked when nobody checked, because a missing check is not a passing one.

  • Bulk operations that scale. bulk_modify archives/labels everything matching a query at 1000 messages per API request — with per-chunk partial-success reporting instead of all-or-nothing. The snooze sweep uses the same batch path.

  • Structured outputs. Every tool declares an outputSchema and returns validated structuredContent alongside fenced JSON text — no parsing guesswork for clients. Failures are structured as well: a code and a retryable flag, so a client can tell "try again later" from "re-authorize" without reading prose.

  • Small attack surface. No send tools (no exfiltration path for prompt-injected mail), optional read-only mode, no telemetry, no open ports by default, symlink-safe download fencing, injection-fenced output. And no code path that could send: every Gmail request passes an egress checkpoint that refuses messages.send, every draft endpoint, permanent deletion and forwarding settings, whatever a compromised or careless caller asks for. One deliberate exception: unsubscribe / bulk_unsubscribe (manage tier) contact the opt-out endpoint named in a message's own header — the only non-Google host mailwarden ever reaches, and a read-tier deployment makes no outbound request at all. Details under Security & privacy and Unsubscribing.

  • Correct with real-world mail. RFC 2047 headers decoded (=?UTF-8?B?…?= → readable text), bodies decoded in their declared charset (no mojibake for ISO-8859-1/Shift_JIS mail), 429/5xx retried with exponential backoff.

Related MCP server: Gmail MCP

Why

Connectors that sync or cache your mailbox can lag behind it — and even Gmail's own search index is sometimes loose (see below). mailwarden talks straight to the live Gmail API (no cached snapshot) and re-verifies what the index returns, so what you see is what's actually there. It's a generic Gmail capability layer — keep your own rules/logic in your AI client, not in the server.

search goes one step further than the raw API: Gmail's threads.list index can answer read-state operators from a stale copy of that state, so is:unread returns threads you finished reading weeks ago — in one measured mailbox, the large majority of what came back. Since every hit is fetched live anyway, search re-checks the unambiguous predicates (is:unread/is:read/is:starred/in:inbox/category:…, with negation) against each thread's true labels and drops the index's false positives.

Compared to other Gmail MCP servers

Most Gmail MCP servers cover the same read/label/send surface. Two capabilities are still unique to mailwarden as far as we have looked (mailbox-side snooze, search re-verification), and one deliberate omission is a security feature, not a gap. A third, sender authentication, was unique when it shipped and no longer is: @agentcomms/gmail-mcp has read Authentication-Results since 18 September 2026, trusting only the report whose authserv-id is mx.google.com. It has its own column below since 13 October 2026. Google's own server is also narrower than it looks: draft-only, and no trash, filters or unsubscribe.

Capability

mailwarden

Google official

taylorwilsdon

a-bonus

aaronsb

klodr

agentcomms

Mailbox-side snooze — archive now, resurface in the inbox on a date/time or preset

✅

—

—

—

—

—

—

Search-result re-verification — drops the thread index's false positives against live labels

✅

—

—

—

—

—

—

Sender authentication — SPF/DKIM/DMARC as the receiving server recorded them, on every message

✅ Gmail's own report only, values token-validated, DKIM alignment

—

—

—

—

—

✅ Gmail's own report only, DKIM alignment

Sweep / bulk over a query — one action across every thread a search returns

✅ 1000/req, partial-success

—

⚠️ batch by explicit ids; filter apply relabels over a query, no re-check

—

⚠️ batch by explicit ids

⚠️ batch by explicit ids

⚠️ batch by explicit ids, exact per-message undo

Unsubscribe — per-sender overview + RFC 8058 one-click opt-out, no send scope needed

✅

—

⚠️ header shown, no action

—

—

—

—

Inbox triage overview — one call that buckets what is waiting

✅ sender/label/age + header signals

—

—

✅ heuristic flags + stats

⚠️ unread list, no buckets

—

—

Server-side filters — rules that keep triaging with no assistant in the loop

✅ never forwarding

—

✅

—

—

✅

—

No send tools — by design — a prompt-injected mail has no exfiltration path

✅ no compose at all

⚠️ draft-only

❌ sends

❌ sends

❌ sends; ⚠️ opt-in draft-only policy

❌ sends

❌ sends; ⚠️ per-message approval, in chat by default

Least-privilege tool tiers — OAuth scopes derived from the tools you enable

✅

⚠️ scope split

⚠️ --permissions narrows scopes per service; tiers narrow tools only

—

⚠️ per-account read/readwrite, at call time

⚠️ inverse: tools gated by granted scopes

⚠️ tier per mailbox picks the scopes; writes checked against the grant at call time

Token encryption at rest

✅ AES-256-GCM, opt-in (MAILWARDEN_TOKEN_PASSPHRASE)

n/a (hosted)

⚠️ file mode 0600; bucket CMEK on GCS

—

—

—

✅ OS keychain by default; owner-only file by choice

No vendor cloud — you operate the server

✅

❌ Google-hosted

✅

✅

✅

✅

✅

Structured outputs — every tool declares an outputSchema

✅

✅ documented per tool in its reference

—

—

—

⚠️ two tools (download_email, download_all_attachments), more planned

✅ every tool

Snapshot as of 10 October 2026 — the oldest of the six column checks, and so the only date the table as a whole can claim. Each repository column is brought forward by reading the diff against the revision recorded in docs/comparison-sources.json whenever npm run table-sources reports a move. On 10 October 2026 three of them moved at once: taylorwilsdon to 78973045 (v2.1.0, 170 commits on, and the one cell that changed: its filter tool gained an apply action that turns a filter's criteria into a search, pages messages.list and relabels every match through batchModify — a bulk over a query, labels only and with no re-check, so the sweep cell now says so while the re-verification cell stays a dash; the Gmail level table and the tool registry are untouched, so the least-privilege cell holds), aaronsb to 14c25e66 (v4.6.0: listDrafts, deleteDraft and createLabel, with the permanent draft deletion added to its no-delete policy — no cell moves) and klodr to ddb9da01 (three release commits and dependency bumps; the only source line that moved is the version string). a-bonus moved on 13 October 2026 to d9e9bb21: drafts gained file attachments, read only from directories the operator lists and refused while none are — an exfiltration boundary on a server that still sends, so no cell moves. Google's tool reference was re-read in full the same day, and that reading corrected us: every tool page documents an output schema, which the reading of 3 September 2026 missed although the pages date from July, so the structured-output cell now shows it as offered. The reference also lists eleven tools, not ten — create_label was there too. Still Developer Preview, and still no send, trash, filter, snooze, unsubscribe or query-driven tool. — = not offered / not documented. Columns are the servers a reader is most likely to reach for — Google's first-party one, plus the three largest community servers still under maintenance — and klodr, which comes closest to mailwarden's own least-privilege design. agentcomms (@agentcomms/gmail-mcp, read at 01cee751 on 13 October 2026) was added the same day for two reasons: it is the only other server we know of that reads Authentication-Results, and it pins that read to Gmail's own report, which is stricter than what mailwarden did until then and the reason mailwarden now does the same; and its npm reach overtook klodr's within a month of its first release. Its bulk path is per id, not per query, but every change returns an exact per-message undo — the design mailwarden's bulk_modify adopted as undo: true. It sends, behind a per-message approval whose default is a yes in the conversation; its own README says the software cannot tell that yes from the agent's, and offers a terminal confirmation (confirm) or no sending at all (never) instead. aaronsb was added on 8 September 2026, and the reason it was missing is worth stating plainly: the selection had been made by capability overlap, so a server an order of magnitude larger than the smallest column sat outside the table while that column stayed in it. It earns its place on the send row rather than on size. It was the first server here besides mailwarden that can be made to refuse sending at all — GWS_SAFETY_POLICY=draft-only-email blocks send, reply, replyAll and forward; agentcomms has since joined it with a never send policy — and the difference that keeps the row honest is where the promise lives: there, sending is the default and the refusal is an operator's environment variable; here, no tool to send was ever written. Its no-delete policy stands in the same relation to this project's trash-only rule. The filter row asks the same question from the other end, and a third project shows why it is one: c0webster/hardened-google-workspace-mcp, a fork of the taylorwilsdon column made for no reason but this one, removes create_gmail_filter and delete_gmail_filter outright because a filter can forward — which is exactly the risk, answered by dropping the capability. mailwarden keeps the capability and removes the risk instead: create_filter has no way to express a forward, and list_filters surfaces the ones that already exist so they can be audited. That fork also shows where a promise of this kind usually leaks, which is why the row above is about the tool surface and the egress guard rather than about scopes: it drops gmail.send but keeps gmail.compose — commented "for draft creation/editing only (NOT sending)" — and gmail.modify. Google documents the first as "Manage drafts and send emails" and the second as "Read, compose, and send emails", so the promise does not rest on one scope too many but on two, and gmail.modify alone would be enough — the same scope this project holds in its own manage tier, which is exactly why the row points at the tool surface. Its token can send; only its tool list cannot. The most installed Gmail server is absent for that reason and not by oversight: GongRzhe/Gmail-MCP-Server is archived, its last commit dating to August 2025, and it still drew 112,163 npm downloads in the month to 29 August 2026. Reach and currency are different questions, and a comparison of what a server does today can only answer the second. Send capability is listed as a security property: mailwarden's lack of it is intentional (see Security & privacy). The encryption row asks who holds the key: mailwarden encrypts the token itself from a passphrase you set — and does nothing without one, which is why the cell says opt-in rather than showing a bare tick; taylorwilsdon relies on file permissions locally and on the storage bucket's own CMEK when hosted on GCS — protection against a stolen file in the first case, against a stolen disk in the second. The last row asks who operates the server, not where it happens to run: self-hosting is common ground here, and every community server on this table offers some remote deployment except klodr (stdio only) — mailwarden via --http, taylorwilsdon over streamable HTTP with OAuth 2.1, a-bonus on Cloud Run. Running one of them on your own host is not a cloud copy; running it on the vendor's is.

The moat isn't any single row — it's snooze + live re-verification together: an actual inbox-workflow layer that acts on the mailbox's current state, not a cached snapshot. Where others have caught up it's noted honestly above: at-rest encryption (taylorwilsdon), scope-driven tool gating (klodr inversely; taylorwilsdon in our direction, and further than this row said until it was corrected — besides --read-only, which switches the OAuth flow to the read-only scope map, he has had per-service permission levels since 25 February 2026: --permissions gmail:organize builds the requested set from cumulative levels — readonly, organize, drafts, send, full — and his tool registry then disables every tool whose declared scopes fall outside them, deliberately skipping scope-hierarchy expansion so that organize really does exclude gmail.send. What separates that from this row's claim is the direction, not the strength: the level is picked per service rather than derived from the tools you enable, so --tool-tier core --tools gmail still asks for the full Gmail scopes unless a level is named, and neither mode has a guard at the request itself. Our August round read only the tier path and understated him here from 26 August on; re-read in his auth/permissions.py, auth/scopes.py and core/tool_registry.py at 54b1c56 on 7 September 2026 — the day we read, at the revision his repository stood at. The tier half was checked on 26 August 2026 and corrected the same day by csitte.at, who verified it against their own clone rather than taking our word for it), a richer per-message triage heuristic (a-bonus), and bulk organize over a mailbox (the hosted mcpemails.com, which has no snooze either). What none of them do is act on a query and check the mailbox's answer before acting on it — taylorwilsdon's filter apply now does the first half over messages.list, and not the second.

Running it next to a Workspace server

mailwarden is a Gmail server, not a Workspace suite — if you want Calendar, Drive, Docs and Sheets from one place, a broad server like taylorwilsdon/google_workspace_mcp covers ground this one never will, and the two are not mutually exclusive. Adding both is a reasonable setup, and the reason to is the token, not the tool count: a suite server that can send mail holds a credential that can send mail, for every mailbox it is pointed at. Giving Gmail to mailwarden instead means the mail half of your setup has no compose, reply, forward or send tool at all. Where that promise rests differs by tier, and the distinction matters: on read Google enforces it at the token (gmail.readonly, which the send endpoints reject), while on manage it rests on the tool surface — Gmail does accept gmail.modify for sending, so the scope alone is no guarantee. In both cases the egress guard refuses messages.send and every draft endpoint in the server itself, so an injected message in your inbox has no tool to reach for and no endpoint to reach.

Practical shape: point the suite server at the services you want and disable its Gmail tools (--disabled-tools, or a tier that omits them), and run mailwarden alongside for mail. Keep the tier rule in mind — at most one mailbox per client config should carry writing tiers.

Why re-verification matters — a concrete case

Ask an assistant to "archive the unread promotional mail that's already skipped my inbox" and it will reach for the obvious query, category:updates is:unread -in:inbox. A server that trusts Gmail's index now archives threads you had already read — mail you never meant to touch, gone in a bulk action you can't easily reverse.

Measured, not asserted. In one real mailbox (~70,000 messages), category:updates is:unread returned 131 threads through threads.list, and only 17 of them held an unread message — 87% stale. The same query, same mailbox, same minute, asked through messages.list instead: 19 messages, none stale. So this is not "Gmail search is unreliable" — the thread view of read state lags while the per-message view does not, and search goes through threads.list. A second mailbox, measured identically on the same day, drifted not at all.

Method, all three queries, the controls, and what the finding is not (it is not the index dropping the predicate, and not a quirk of exotic operator combinations): Gmail's thread index can answer is:unread from a stale read state — a standalone report, every figure traced to a recorded measurement.

Which is the whole point: a server cannot know which kind of mailbox it is in. Re-verification costs nothing where nothing drifts, and saves you where it does — in the measurement above, every thread search dropped was genuinely read, and it discarded no genuinely unread mail.

Where it is not free: the bulk tools. search re-verifies because it fetches every hit anyway; bulk_modify (and create_filter's applyToExisting sweep) is sized in thousands of messages, where one fetch per hit is a different order of cost. Both act on what the index returns — and bulk_modify reports unverifiedPredicates, the conditions from your query that were taken on the index's word (+UNREAD, -INBOX, …). Empty means there was nothing to distrust. Non-empty and the result has to be read-state-precise? Resolve the set with search first and act on those thread ids. A dryRun does not close this gap: it re-reads the same index, so it confirms how big the set is, never whether it is right.

The filter sweep does not report that field, deliberately. Its query is not yours — create_filter builds one from the criteria, parenthesising a query criterion and double-quoting every from/to/subject value, and both of those switch the predicate derivation off. The list would come back empty for every filter you can define, and an empty list here would read as "nothing to distrust" when it means "could not tell". So the caveat stays in prose, where it holds without exception: treat a sweep as acting on the index's answer, whatever it matched. Use verify: true to learn what the sweep actually changed, and search when the set has to be right before anything is written.

A cheaper half-measure, honestly labelled. bulk_modify also takes crossCheck: true, which asks Gmail the same question a second way before writing: every derived predicate is re-run as a label filter (labelIds) rather than as a query operator, and any message the two routes disagree about is left untouched and reported. The cost is one extra list call per predicate — flat, independent of how many messages match — where re-verification costs one fetch per hit. What it buys is bounded and worth stating plainly: a disagreement is real evidence, agreement is none at all, because both routes read the same index and an index can be consistently wrong. So unverifiedPredicates still reports what it always did, cross-check or not, and only search re-checks against the mailbox itself. Whether the two routes ever diverge in practice is unmeasured — node scripts/probe-crosscheck.mjs measures exactly that in your own mailbox, read-only and ids only.

mailwarden fetches every hit live anyway, so search re-checks the unambiguous predicates (is:unread, is:read, in:inbox, category:…, with negation) against each thread's true labels and drops the index's false positives before any tool sees them. The bulk action then runs on exactly the set you asked for. This is the difference between acting on what Gmail indexed and acting on what's actually in the mailbox right now — and it's why snooze/sweep are safe to hand to an assistant: the sweep resurfaces only threads whose snooze is genuinely due, verified against live labels at run time.

See it yourself — no Gmail account needed. From a clone of the repo (the demo is a repo-only verification script, not part of the npm package):

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node scripts/demo-reverify.mjs

There is a second script next to it, node scripts/probe-reverify.mjs, which measures the same thing in your mailbox instead of a fake one — read-only, metadata only (no subject, sender or body is fetched), printing counts and label names. It is how the numbers above were produced, and how you can check whether your mailbox drifts at all.

The demo drives the real search() against a fake Gmail API whose index is deliberately stale (returns a read thread for an is:unread query, exactly as Gmail does) and shows mailwarden dropping the false positive. It asserts the outcome, so it exits non-zero if the behavior ever regresses. The same case is locked by unit tests in test/gmail.test.ts ("drops index false positives via live-label re-verify").

Asking again without asking for everything

A recurring check — what came in since I last looked — is the expensive shape for a live server: the obvious way to answer it is to search the whole slice again and compare. what_changed (read tier) answers it from Gmail's own event log instead. Hand it the historyId a previous call or get_profile returned, and it comes back with what arrived, what left, and which labels went on or came off, plus the next id to keep.

This is not a cache, and the distinction is the whole design. The only thing that persists between calls is one number, and it persists in the caller. mailwarden still stores nothing about the mailbox, keeps no mirror and no index, and every call remains live against the Gmail API — the same rule as everywhere else here.

Two properties worth knowing before relying on it. It reports events, not state: a message marked unread and then read appears under both, and both are true — for how the mailbox looks now, ask search. And Gmail keeps roughly a week of history, after which an id is refused; mailwarden turns that refusal into an error rather than an empty result, because nothing changed and I can no longer tell you what changed call for opposite reactions and only one of them is safe to act on.

Judging a sender — what authentication answers, and what it doesn't

Every message from get_thread carries an authentication object: the SPF, DKIM and DMARC results the receiving server recorded, plus the domains each check actually validated.

{
  "spf": "pass",   "mailedBy":   "forwarder.example",        // envelope sender SPF checked
  "dkim": "pass",  "signedBy":   "routing.example",           // domain whose key signed it
  "dmarc": "pass", "headerFrom": "authority.example",        // the From domain DMARC evaluated
  "dkimAligned": false,                                      // the signer is not the From domain
  "authservId": "mx.google.com",                             // who asserts all of the above
  "returnPath": "srs0=…=authority.example=…@forwarder.example"
}

Read dmarc first. It is the only one of the three that ties a passing check to the From address a human sees. spf: "pass" on its own says an envelope sender was authorised to send — something a lookalike domain gets in minutes.

The three domains do not have to match, and a mismatch is not a finding. The object above is a real message from a public authority, forwarded through a custom domain on a mail-routing service before it reached the mailbox. Every domain differs from the others, and the mail is genuine: forwarding rewrites the envelope sender (mailedBy becomes the forwarder), the forwarder signs with its own key (signedBy), and only headerFrom still names the original sender — which is exactly why DMARC, not SPF, is the check that carries meaning here. Treat the domains as the explanation of a result, not as a test of their own.

What a pass does not mean. That the mail really came from that domain — not that the domain deserves anything. A phisher holds perfect SPF, DKIM and DMARC on the lookalike domain he registered this morning; authentication tells you who sent it, and the answer can be "exactly who it claims to be, and that is the problem".

dkimAligned answers the question a signature raises. Anyone can sign mail with a key for a domain they own, so dkim: "pass" alone says only that someone signed. dkimAligned says whether that someone is the From domain, or a parent or child of it. For two sibling subdomains it says nothing: news.bank.example and mail.bank.example are aligned, a.co.uk and b.co.uk are not, and without a public-suffix list the two cases look the same — so the field is absent rather than wrong. In the forwarded example above it is false, and correctly so: the forwarder signed. It is absent when DKIM did not pass. As with the domains, false is an explanation, not a verdict — dmarc remains the check to read first.

What unchecked: true means. The message carried no Authentication-Results header from Gmail — nobody looked. It is not a failure, and it is not a pass. The header is written by a server that receives a message, so anything that never arrived from outside — your own sent mail, for instance — should be expected to have none.

Forged reports. A message can carry Authentication-Results headers of its own — an attacker writes whatever he likes into the mail he sends. Only Gmail's own report is read: the topmost one whose authserv-id is mx.google.com, the trust boundary RFC 8601 tells a reader to rely on. A report under any other id is never read, however convincing it looks, and a message that has none from Gmail is unchecked even if it carries a forged one under another id. The limit: mail that reached the mailbox without passing Gmail's inbound mail server — fetched from another account over POP, or inserted by an app — may carry no Gmail report at all, and a forged one that claims mx.google.com is then indistinguishable from the real thing. Mail Gmail received itself always carries its own report on top, so there a forgery is never read; otherReports counts every report that was not read. (Through 0.22.1 the first report was read whatever its id — right wherever Gmail had written one, and wrong exactly where it had not.) Values are validated as tokens rather than passed through, so a field that reads like a verdict cannot carry a sentence. If two results for the same method disagree — a second DKIM signature that failed — the disagreement shows up in alsoReported instead of being swallowed.

Tools

Tool

What it does

search

Gmail query syntax → thread summaries (from/subject/date/labels/snippet); read-state/category predicates are re-verified against each hit's live labels; paginated via pageToken/nextPageToken. Each hit carries signals — newsletter (List-Id / List-Unsubscribe / Precedence bulk, junk or list), automated (Auto-Submitted, auto-reply/suppress headers, no-reply-style senders), calendar (text/calendar or .ics part), replyToMismatch (Reply-To on another domain than From; a subdomain of the same domain counts as the same) — read off the first message's headers/MIME, no extra call. Spam and trash are excluded unless the query says in:spam / in:trash — see Looking in spam

get_thread

Full thread: headers, plaintext + HTML bodies, attachment metadata. Every message also carries authentication — SPF/DKIM/DMARC as the receiving server reported them, plus the domains each check validated (signedBy/mailedBy/headerFrom), who asserts it (authservId), the returnPath, alsoReported for results that contradict each other, otherReports for reports that were not read, dkimAligned for whether the DKIM signer is the From domain, and unchecked: true when the message carried no report from Gmail — see Judging a sender. full: false fetches headers and labels only — it then omits plaintextBody/htmlBody/attachments and sets metadataOnly: true, rather than reporting them empty for a request that never looked

list_labels

All labels (system + user)

get_profile

Connected account's address, total message/thread counts and the mailbox's current historyId — confirm which mailbox is wired up before acting

what_changed

Mailbox events since a historyId you hold — arrivals, removals, labels on and off, in one call

triage_digest

Structured overview of a mailbox slice for decisions: top senders (each with the signals its threads carry), label and age buckets, unread + attachment counts, and how many threads are newsletters / automated / calendar invites / reply-to mismatches — instead of a raw thread list

follow_ups

Who is waiting on whom: threads whose last message is yours and unanswered for a few days (a note to your own address is left out; one other recipient in To or Cc keeps it), and inbox threads whose last message came from a person rather than a newsletter or a no-reply notice. Decided on each thread's last message and its live labels, so a thread the index still files under the inbox after it was archived is dropped

list_unsubscribe

What opt-out options a thread advertises (List-Unsubscribe), plus body links when it advertises none — contacts nobody

list_subscriptions

A mailbox slice grouped by sender: thread/unread counts, the date span each was seen over, and each one's opt-out options — one header fetch per sender, contacts nobody. sendersFound reports how many senders there were before topN truncated the list

create_label

Create a user label (idempotent; nested via Parent/Child) and return its id; an optional backgroundColor/textColor pair colours it, including one that already exists

modify_labels

Add/remove labels by name or id — an unknown name in add is auto-created (archive = remove INBOX, read = remove UNREAD)

bulk_modify

Batch label changes for every message matching a query — 1000 messages per API request, partial success reported per chunk (thread-id list capped at 500, submittedThreadCount has the total). Counts say submitted, because messages.batchModify answers 204 with no body and ignores unknown ids silently; verify: true reads the labels back and returns verified {applied, notApplied, unverifiable} — the only observed outcome on offer. Acts on the raw index, so unverifiedPredicates names the conditions it could not vouch for (see below). dryRun: true resolves the query and reports the matched threads and the labels it would create, touching nothing. undo: true reads each affected thread's labels live before writing and returns undo, the exact per-message inverse (up to 500 messages; with undo: true nothing is written unless the plan can be built)

undo_bulk_modify

Replays the undo plan a bulk_modify returned: each message gets back exactly the labels it lost and loses exactly the ones it gained, so mail that was already in the target state stays as it was — which swapping add and remove would not do. Takes label ids only, as the plan carries them; a label the bulk change created stays in the mailbox, empty

archive / mark_read / mark_unread

Convenience wrappers

trash / untrash

Move to / restore from Trash

download_attachment

Save an attachment to a local path (never overwrites — collisions get a numeric suffix)

export_thread

Write a whole thread to a local file instead of returning it — Markdown for people (headers and attachment names as code, so a preview never loads an image or follows a link the sender wrote; sender authentication; each body in a fence the mail text cannot close) or JSON as get_thread returns it. Same path rules as attachments

unsubscribe

One-click opt-out (RFC 8058) using the endpoint from the message's own header — the only tool that contacts a non-Google host (details)

bulk_unsubscribe

The same for several threads, sequentially and at most one request per sender (remembered across calls for as long as the server runs, so a retry contacts nobody twice); partial success reported per thread. dryRun: true runs the same header reads and dedupe and reports the endpoint each thread wouldCall — contacting nobody

snooze

Archive now, resurface on/after a date (YYYY-MM-DD), a date+time (2026-06-20 9am), or a preset (tomorrow, tomorrow 9am, weekend, next week, a weekday name, in N days, in N hours)

unsnooze

Cancel a snooze, return to inbox now

list_snoozed

All snoozed threads + due dates

sweep_snoozed

Resurface threads whose snooze is due (run on demand, via cron, or the daemon); batched, with partial-failure reporting. dryRun: true answers "what is due right now?" (dueLabels/dueThreads) without waking anything

list_filters

All Gmail filters (criteria + label actions); surfaces any forward address on existing filters for auditing

create_filter

Create a server-side auto-triage rule (criteria → label actions only; no forwarding — see below). Optionally applyToExisting to also sweep matching mail already in the mailbox

delete_filter

Delete a filter by id

All tools declare an outputSchema and return structured content (validated, machine-readable) alongside the same JSON as fenced text — clients never have to parse prose.

A failure is structured too: isError plus a fenced JSON body with a code (not_authorized, needs_reauth, insufficient_scope, forbidden_operation, not_found, rate_limited, upstream_unavailable, network_error, invalid_input, internal_error) and a retryable flag, alongside the sentence a human reads. So "wait and try again" versus "re-run mailwarden --auth" is something a client can decide, not something it has to infer from wording that may be reworded next release. (No structuredContent on errors: that is validated against the tool's outputSchema, which describes a success.) A rate_limited failure also carries retryAfterSeconds, because there the wait is the remedy. An insufficient_scope failure names the gap rather than the possibilities — which scope is missing, what it covers, and what the saved authorization grants instead — by reading the token's own scopes when it lands, and the server prints the same line on stderr at startup. It explains the refusal rather than preventing it: the call still goes to Google, and Google is what turns it down. That matters most with an encrypted token: the tier gate at registration cannot decrypt, so the surface is advertised in full and the gap would otherwise surface as a bare 403 mid-task.

Quotas: the one failure that fixes itself

Gmail bills each call against a per-user budget of 6,000 units per minute, and the units are not uniform — fetching a thread is the expensive part, in whatever format it is fetched:

Tool

Units per call

search

10 per list page, plus 40 for every candidate thread it fetches — about 1,000 at the default size, up to about 4,000 when the query carries a read-state or category condition

get_thread (full and metadata alike)

40

trash, download_attachment

20

export_thread

40

archive, mark_read, untrash

10

modify_labels, snooze

10, plus 1 for each label looked up by name and 5 for each label it has to create

bulk_modify

5 per 500 matching messages, plus 50 per 1,000 modified; verify: true adds 40 per thread, crossCheck 5 per condition and 500 matches, undo: true 40 per thread

undo_bulk_modify

50 per step, plus 40 per thread with verify: true

follow_ups

1 for the profile, 10 per list page, plus 40 per thread scanned — up to 50 threads at the defaults, about 2,000

list_labels, get_profile

1

mark_unread costs what mark_read does.

Two consequences worth knowing before you build on this. Parallel agents share one budget — they reach Gmail through a single server process as a single user, so four of them reading threads at once spend four times as fast. And bulk_modify is cheaper than looping from six threads on: up to 500 matching messages it costs 55 units, the price of five and a half archive calls, and it grows by the thousand messages, not by the thread. verify: true reverses that — reading the labels back costs 40 per thread, more than the loop it replaced.

If you do exceed it, mailwarden fails the call with rate_limited and retryAfterSeconds: 60. That failure is temporary and waiting genuinely fixes it — the budget is counted per minute. mailwarden absorbs one such wait (about 20 seconds) itself before reporting; past that it hands the decision back rather than holding your tool call open. That holds for the tools that fetch many threads in one call too: search fails with rate_limited rather than returning a list the quota cut short, and bulk_modify with verify and list_subscriptions stop fetching and report what they did not reach as unverifiable and unknown (for bulk_modify the change is already made, and failing would hide it). Note that Gmail reports this as a 403 as readily as a 429, so a client keying on the status alone will mistake it for a permission problem — key on the code.

How snooze works (no Gmail API snooze exists — we build it)

snooze removes INBOX and applies a dated label MCP/Snoozed/<key>, where the key is either YYYY-MM-DD (due all day) or YYYY-MM-DDTHHMM (due at that local minute). The until argument takes an explicit date, a date+time (2026-06-20 9am, …T17:00), or a preset resolved server-side — today, tomorrow, weekend (next Saturday), next week (next Monday), a weekday name (monday–sunday, next occurrence), in N days, or in N hours — and a date preset may carry a trailing time (tomorrow 9am, monday 8:30), so the caller never has to compute the moment itself. sweep_snoozed finds due labels and returns those threads to the inbox (marked unread); a timed snooze wakes at the first sweep on/after its minute, so wake latency equals your sweep interval. Run the sweep:

  • on demand (sweep_snoozed tool),

  • via cron: mailwarden --sweep,

  • or automatically: set MAILWARDEN_AUTO_SWEEP=1 (hourly sweep while the server runs).

Filters (persistent auto-triage rules)

create_filter sets up a Gmail server-side rule: mail matching the criteria automatically gets the given label actions — the mailbox keeps triaging itself with no assistant in the loop.

  • Criteria: from, to, subject, query (full Gmail search syntax), negatedQuery, hasAttachment, excludeChats, and size + sizeComparison (smaller/larger, given together). At least one is required.

  • Actions (label only): addLabels / removeLabels, by name or id (an unknown name in addLabels is auto-created, nested via /). Common recipes: skip the inbox → removeLabels: ["INBOX"]; auto-mark-read → removeLabels: ["UNREAD"]; auto-trash → addLabels: ["TRASH"]; star → addLabels: ["STARRED"]; never-spam → removeLabels: ["SPAM"]; file under a label → addLabels: ["Receipts"].

  • Existing mail: a filter only runs on messages arriving after it's created. Pass applyToExisting: true to also apply the same actions once to mail already in the mailbox — mailwarden builds a Gmail search from the criteria and runs a bulk modify (up to maxMessages, default 1000; same unverified-index caveat as bulk_modify, and the one-off pass excludes Spam/Trash). This requires at least one positive criterion (from/to/subject/query/hasAttachment:true/size): an exclusion-only rule (negatedQuery or hasAttachment:false) is refused for applyToExisting because it would match almost the whole mailbox — create such a filter without the flag. The outcome comes back under applied (the query used, matchedMessages/submittedMessages/submittedThreadCount counts, capped when the match set hit maxMessages, per-chunk failed, and an error string if the whole pass failed); it's null when applyToExisting was not set. submittedMessages is what was handed to the API, not what changed — pass verify: true alongside applyToExisting to read the labels back and get applied.verified {applied, notApplied, unverifiable}, the same check as bulk_modify's verify (one extra read per affected thread, so off by default). The filter is created first, so a partial or failed backlog pass is reported in applied, never raised — the rule still stands.

  • No forwarding — see Security & privacy.

  • Requires the gmail.settings.basic scope; re-run --auth once if you authorized an older version. Not available in read-only mode.

Unsubscribing — the one outbound request

list_unsubscribe (read tier) reports what the sender offers, without contacting anyone. It reads the newest message that actually carries a List-Unsubscribe header — a reply threaded onto a newsletter sits at the end and advertises nothing, which would otherwise read as "this list has no opt-out". When a thread advertises no opt-out header at all, list_unsubscribe looks in the message body and reports the unsubscribe links it finds there as bodyCandidates — plenty of senders put the link only in the footer, and answering "no opt-out options" for them is true about the headers and wrong about the mail. Those links are reported, never fetched, they cannot be handed to unsubscribe, and hasUnsubscribe stays false for them, because that flag has always described the headers. The search costs one full thread fetch and happens only in that case. list_subscriptions (read tier) does the same across a whole slice, grouped by sender, so you can see who keeps writing and which of them can actually be left — one header fetch per sender rather than per thread. unsubscribe and bulk_unsubscribe (manage tier) act on it — and that is the only place mailwarden ever talks to a host that isn't Google, so the rules are tight:

  • There is no URL parameter. The endpoint comes from the message's own header and nowhere else. A URL argument would let a prompt-injected mail turn the tool into an exfiltration channel (mailbox content in a query string); the header cannot carry data the model chose. This is also why a body link is only ever reported: it is a URL the sender wrote into the text, and fetching one would reintroduce exactly the request this rule exists to prevent — by a path that never passes the guard. Another Gmail server, navbuildz/gmail-mcp-server, does fetch them, following redirects, when the header is missing.

  • Only RFC 8058 one-click is performed — the sender must have opted in via List-Unsubscribe-Post. A plain https: link is meant for a human in a browser and is handed back, not fetched.

  • mailto: opt-outs are never performed. They would require sending mail, which mailwarden cannot do. The address is reported so you can act on it yourself.

  • Fixed request, discarded response. The POST body is always List-Unsubscribe=One-Click and is never derived from anything; the response body is cancelled unread. What returns to the model is the status code and the URL actually called — no content from the endpoint, so it cannot answer with instructions. (A 301/302/303 redirect is followed as a GET, i.e. with no body at all.)

  • One request per sender, sequentially, inside one budget. bulk_unsubscribe takes thread ids (never a query — a query-driven bulk would fire off a request per matched sender before anyone had looked). Threads from a sender whose request already went out are reported with duplicateOf and cost no second request: two threads from one list share an opt-out, and calling it twice only confirms your address twice. A sender is only recorded once a request actually reached an endpoint, so a refusal or a dropped connection still leaves the next thread its own try — and if the skipped thread advertises a different endpoint, the reason says so, since one sender can run several lists. That memory spans calls for as long as the server runs, and unsubscribe shares it: a call that times out is safe to repeat, and asking twice for the same newsletter contacts the sender once. Pass force: true to unsubscribe for a deliberate second attempt — after an endpoint answered 500, say. It is kept in memory only: persisting it would mean a second kind of local state beside the token, which this server deliberately does not keep, so a restart forgets. Capped at 25 threads and 60 seconds per call; whatever the budget doesn't cover comes back as skippedOutOfTime rather than silently undone. None of it can be reversed, which is why all three limits exist.

  • SSRF guards. https only, default port only, no credentials in the URL, and every hop — including redirects, followed at most 3 times — must resolve exclusively to globally reachable addresses. The check parses each address to its bytes and matches it against the IANA special-purpose registry, so every spelling of the same address gets the same verdict (::1 and 0:0:0:0:0:0:0:1 alike); an address that does not parse is refused. DNS resolution and all hops share one 10-second budget. Not rebinding-proof (fetch resolves again when it connects) — see SECURITY.md; what survives that gap is a blind POST whose response is never read.

Check it against your own mail before you trust it. From a repo clone (repo-only, not in the npm package), after npm run build and mailwarden --auth:

node scripts/probe-unsubscribe.mjs --vet          # category:promotions, 25 threads
node scripts/probe-unsubscribe.mjs "from:substack.com" --max 50 --vet

It prints each real List-Unsubscribe header next to what the parser made of it, and --vet also runs the endpoint through the URL vetting and the address guard — so you see both whether the parser understood the header and whether the guards would have let that opt-out through. Strictly read-only: no request is ever made to a sender, and nothing in the mailbox changes.

What it can't undo: the request tells the sender your address is live. A sender that ignores its own opt-out is beyond any client's reach — pair unsubscribe with create_filter or trash for those. Not offering an automatable option is reported as unsubscribed:false with the alternatives, not as an error. A read-only deployment gets list_unsubscribe and list_subscriptions, and never makes the request at all.

Looking in spam

A query that does not name a place never sees spam or trash. Gmail excludes both from any search that does not say in:spam / in:trash, so from:someone returns nothing for a mail that is sitting in the spam folder — and nothing in the answer says so. Measured against a live mailbox: the same from: query returned 0 hits by default and 1 with spam included.

This matters because of why mail gets misfiled. A spam filter judges a message on its own; it cannot know that you signed up for something a minute ago, requested a password reset, or placed an order — so the confirmation you are waiting for is exactly the kind of mail that lands there. You know what you just did. The filter does not.

So when mail someone expects is missing, ask again with the place named:

search("in:spam newer_than:2d")          # what got filed as spam recently
search("in:spam from:example.com")       # the confirmation that never arrived

A thread returns to the inbox with modify_labels (remove SPAM, add INBOX), and a sender that keeps being misjudged is best fixed for good with a never-spam rule — create_filter with removeLabels: ["SPAM"] (see Filters).

Two things this server deliberately does not do. It does not scan the spam folder and judge what belongs there: measured over one real spam folder, 89% of it carries no mailing-list machinery at all, so "looks unlike bulk mail" flags nearly the whole folder and filters nothing. And it does not act on that judgement by itself — releasing mail from spam is a decision, and the context that makes it obvious ("I just registered there") lives in the conversation, not in the mailbox.

Security & privacy

For the full threat model — trust boundary, per-threat mitigations, explicit non-goals, and how to report a vulnerability — see SECURITY.md. The highlights:

  • No telemetry. Nothing phones home — no analytics, no crash reporting, no tracking.

  • No open ports by default. stdio only. The optional --http listener binds to 127.0.0.1 (not the LAN) and refuses to start without a MAILWARDEN_TOKEN bearer token — set MAILWARDEN_ALLOW_NO_TOKEN=1 to override on a trusted, isolated network. On a loopback bind it also validates the Host header (DNS-rebinding defense). For remote hosting, set MAILWARDEN_HOST and front it with TLS.

  • No send tools — by design. mailwarden cannot compose, reply, or forward. A prompt-injected instruction inside an email has no exfiltration path through this server. create_filter follows the same rule: it can label, archive, trash, star or mark mail, but never creates a forwarding filter (which would be an exfiltration path). list_filters still surfaces any forwarding filter already on the account, so you can spot one. This holds because no such tool exists and none can be registered at runtime; for the stronger variant, where Google refuses to send rather than mailwarden declining to, see Read-only mode below. Read the promise as narrow, because it is: it closes the route out through this server, not the assistant's other routes — a client that can fetch a URL, write to a synced folder or run code still has everything an injected message needs, and this server is what puts that message in front of it. See non-goals.

  • One outbound host, no model-chosen URL. The unsubscribe tool is the only code path that contacts a non-Google host. Its endpoint is read from the message's List-Unsubscribe header — never from a tool argument — the request body is fixed and the response body is discarded, so it cannot become a data channel. https/default-port only, redirects re-validated, and any hop resolving to a private, loopback, link-local or metadata address is refused. See Unsubscribing.

  • Tool tiers (progressive disclosure + least scope). MAILWARDEN_TOOLS advertises only the tiers you name — read (the read tools), manage (mailbox mutations, snooze, downloads), filters (server-side filter CRUD, the only tier whose tools need gmail.settings.basic). Default is all three; e.g. read,manage gives a full triage surface without filter management. The OAuth scopes requested at --auth are derived from the enabled tiers — a read deployment asks only for gmail.readonly, and gmail.settings.basic is requested only when the filters tier is on. And the filter tools are hidden automatically when the stored token doesn't carry gmail.settings.basic (e.g. a token authorized before you enabled the tier) — re-run --auth to grant it. Older tokens without a recorded scope are advertised as before, with the runtime insufficient-scope message as the fallback.

  • Read-only mode. Set MAILWARDEN_READONLY=1 (shorthand for MAILWARDEN_TOOLS=read) and only the read tools (search, get_thread, list_labels, list_snoozed, get_profile, what_changed, triage_digest, follow_ups, list_unsubscribe, list_subscriptions) are registered — nothing that can change the mailbox or write files is even advertised to clients (the filter tools, which need the broader gmail.settings.basic scope, are excluded too). Recommended for shared/HTTP deployments that only triage. It is also the only tier whose no-send property Google enforces: it holds a gmail.readonly token, which Gmail's send endpoints reject outright. manage needs gmail.modify, and Gmail does accept that scope for sending — mailwarden simply exposes no tool that would. So a read deployment could not send even if this binary were replaced; a manage one cannot send because there is nothing to call. (There is no send-free write scope to switch to — see SECURITY.md, threat 1.)

  • Egress guard. "Nothing to call" is no longer only a statement about the tool list. Every authenticated Gmail request passes one checkpoint that allows exactly the endpoints mailwarden uses and refuses the rest — with messages.send, drafts.*, messages.import/insert, permanent deletion and every non-filter settings endpoint named in a deny list checked first, so a later edit to the allowlist cannot reopen them by accident — including through the /upload/gmail/v1/... route Google's client takes when a method is handed media. A request whose host was rewritten (GOOGLE_CLOUD_UNIVERSE_DOMAIN, a rootUrl option) is refused before the token leaves the process. Every method in Gmail's discovery document is tested against the guard. It guards this server, not the token: a stolen gmail.modify token can still send from elsewhere.

  • Fenced downloads. With MAILWARDEN_DOWNLOAD_DIR set, file writes — attachments and thread exports — are confined to that directory (realpath-canonicalized, symlink-aware) and never overwrite an existing file. Without it there is nothing to resolve the client-supplied path against, so download_attachment and export_thread can write anywhere this process can — which matters for --http, where the client is remote. Starting --http without the fence therefore prints a warning naming the exposure (it stays a warning, not a refusal: unlike a missing bearer token this needs an authorized client, and existing deployments depend on the current behaviour). A read-tier deployment is silent — it never registers the tool.

  • Untrusted-content fencing. Every tool result is wrapped in <untrusted-tool-output> markers and stripped of invisible/BiDi-override characters, so clients can tell quoted mail content from instructions. The strip also covers Unicode tag characters and the variation selectors supplement (invisible ASCII smuggling), and it applies to structuredContent as well as the text copy — a client reading the machine-readable half gets the same sanitized content.

  • Live API, no copy. No mailbox mirror or search index is stored anywhere. The only local state is your OAuth token in ~/.mailwarden/.

  • Optional token encryption at rest. token.json holds a refresh token; on disk it is protected only by mode 0o600 (a no-op on Windows). Set MAILWARDEN_TOKEN_PASSPHRASE to a passphrase and the token is stored AES-256-GCM-encrypted (scrypt-derived key), so a copy of the file — a backup, a synced folder, another machine — is useless without the passphrase. Re-run mailwarden --auth once after setting it to encrypt the existing token. Note the boundary: this defends against file theft, not against malware running as your user (which can read the passphrase from the environment too).

Quick start

claude mcp add mailwarden -- npx -y mailwarden

That's the whole install — npx fetches and runs the published package, no clone or build step. You only need Google OAuth credentials once (below).

Setup

First time setting up a Google OAuth app? Follow the step-by-step setup guide — it walks through the Google Cloud Console with exact click paths, explains the "unverified app" screen, and covers the trap that makes tokens die after 7 days. The short version:

  1. Google Cloud: create a project → enable the Gmail API → configure the OAuth consent screen and publish it to Production (in Testing status, Google expires refresh tokens after 7 days) → create an OAuth client ID of type Desktop app → download it as credentials.json.

  2. Put credentials.json in ~/.mailwarden/ (or set MAILWARDEN_CREDENTIALS=/path/to/credentials.json).

  3. Authorize once — opens a browser, stores a refresh token in ~/.mailwarden/token.json:

    npx -y mailwarden --auth

    Scopes requested: gmail.modify (read + label/archive/trash) and gmail.settings.basic (filter management only). If you authorized a version before filters existed, re-run --auth once to grant the added scope. To hold a token that Gmail itself refuses to send with, authorize with MAILWARDEN_TOOLS=read — see Read-only mode above.

  4. Verify the setup any time with the built-in doctor:

    npx -y mailwarden --check

    It checks credentials.json, whether a token exists (and if it's encrypted), whether the granted scopes cover your enabled tiers, and makes one live Gmail call to prove the token still works — printing a concrete fix for anything that's wrong, and exiting non-zero if so (handy in CI/health checks). Diagnoses the common traps: no/wrong credentials file, never authorized, an encrypted token with no MAILWARDEN_TOKEN_PASSPHRASE, a missing scope, or the 7-day "Testing"-consent token expiry.

Connect

Claude Code (local stdio):

claude mcp add mailwarden -- npx -y mailwarden

Claude Code plugin — the same server plus two skills: /mailwarden:setup walks you through the OAuth setup and diagnoses a broken one, and /mailwarden:triage carries the operating rules an assistant needs while working a mailbox — above all that search re-verifies and bulk_modify does not, so a non-empty unverifiedPredicates means resolving the set with search before acting on it. That rule protects the user only if the assistant follows it, and a tool description is read once per call while a skill is read before the plan. The repo root is the plugin (.claude-plugin/plugin.json), so from a clone:

claude --plugin-dir /path/to/mailwarden

It is submitted to Anthropic's community marketplace; once listed, /plugin marketplace add anthropics/claude-plugins-community then /plugin install mailwarden@claude-community does the same without a clone. The plugin starts the exact published version (npx -y mailwarden@<version>, bumped by the release script together with server.json), because the directory reviews one revision and refuses a launcher that would run whatever npm serves later. The plugin runs the full tool surface — for a narrower tier (MAILWARDEN_TOOLS=read) or a second account, use claude mcp add with the env you want instead (see Config and Multiple accounts). That route gets the server without the skills: the npm package ships the server alone, and the skills live in this repo's skills/ folder — to keep triage with a claude mcp add setup, copy skills/triage from a clone into ~/.claude/skills/.

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "mailwarden": { "command": "npx", "args": ["-y", "mailwarden"] }
  }
}

Or install the MCPB bundle (mailwarden-<version>.mcpb, attached to GitHub releases from 0.10.0 on) as a Desktop extension — Settings → Extensions → Install extension… — the same server, self-contained at run time (no npx; Claude Desktop brings the Node runtime), with the tool tiers as a setting. The bundle is built from the packed npm package (same file set as published; npm run mcpb, verified in CI: validated, unpacked and booted). The one-time npx -y mailwarden --auth still applies (Node needed once for that) — the bundle reads the same ~/.mailwarden/ token.

Smithery — listed in the directory as csitte/mailwarden. Smithery shuts down its CLI, toolbox, gateway and accounts on 31 October 2026 and stays on only as a public directory (announcement), so there is no Smithery install path any more: install with npx or the .mcpb above. Whether the listing itself stays after that date is up to Smithery.

Remote (Streamable HTTP) — for a VPS / claude.ai custom connector:

# Loopback + token required by default. For real hosting, bind outward and keep the token:
MAILWARDEN_TOKEN=<secret> MAILWARDEN_HOST=0.0.0.0 npx -y mailwarden --http   # :8787/mcp

Then in claude.ai: Settings → Connectors → Add custom connector → your https://your-host/mcp URL. In Claude Code: claude mcp add --transport http mailwarden https://your-host/mcp.

Multiple accounts

One OAuth app (one credentials.json) can authorize several Gmail accounts. Each account keeps its own refresh token in a separate file, selected by MAILWARDEN_ACCOUNT:

mailwarden --auth --account work        # stores token.work.json
mailwarden --auth --account personal    # stores token.personal.json

Run them side by side by registering the server once per account, each with its own MAILWARDEN_ACCOUNT. Every instance carries its own token, its own granted scopes and its own tool surface, and a tool call acts on the account of the entry that carries it and on no other:

{
  "mcpServers": {
    "gmail-work":     { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work" } },
    "gmail-personal": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "personal" } }
  }
}

Account names are case-insensitive — they become filenames, so Work and work would be the same file on Windows/macOS. mailwarden lower-cases them (--account Work → token.work.json) so a name always maps to exactly one mailbox.

Which file --auth writes depends only on --account / MAILWARDEN_ACCOUNT — never on the account you pick in the browser. Authorizing a second mailbox without --account would therefore aim straight at the first one's token file, so --auth checks first and refuses rather than replacing another mailbox's token; --force overrides it deliberately. The two knobs are not interchangeable: MAILWARDEN_ACCOUNT is the one for several mailboxes out of one config directory (it picks token.<name>.json), while MAILWARDEN_DIR moves the whole directory — useful to keep setups apart entirely, but it does not give you a second account inside one. npm run auth from a repo clone passes neither, i.e. it always serves the default account.

mailwarden --check shows the active account and lists the others it finds. With no MAILWARDEN_ACCOUNT set, everything uses the default token.json exactly as before — this is fully backward compatible.

More than two accounts

Two entries are the easy case. Past that, two properties of this design start to matter.

Each instance brings its own tools. The tier split is 10 read + 16 manage + 3 filters, so a full instance advertises 29 tools and four of them advertise 116. Clients that search their tool surface on demand absorb that; clients that hold every definition in context do not.

Several accounts in one client share one model context. The account boundary binds a call to one mailbox — it does not stop text read from one mailbox from prompting a call against another, because all of those tool surfaces are in front of the same model. That is a limit of the boundary, not a defect in it; see threat 8 in SECURITY.md.

One move answers both: give exactly one mailbox write tools and leave the rest on read.

{
  "mcpServers": {
    "gmail-main":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "main" } },
    "gmail-work":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-club":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "club",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-archive":{ "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "archive","MAILWARDEN_TOOLS": "read" } }
  }
}

Three things follow at once: the read entries only ever ask for gmail.readonly, the one scope in which no-send is enforced by Google rather than by mailwarden's tool surface; the four instances add up to 49 tools rather than 100; and an instruction injected into any of them finds no write tool for another mailbox to reach for. When one of the read-only mailboxes does need cleaning up, hand that entry manage for as long as the work takes instead of permanently.

Separate clients — or separate sessions — remove the shared context entirely, at the price of never having two mailboxes in view at once. Worth it when several mailboxes genuinely need write tools; otherwise the tier split is the cheaper boundary.

No tool reads across mailboxes. search, triage_digest and list_subscriptions each serve the one account their instance was configured with, so a question like "which newsletter writes to all four" is four calls whose answers the caller combines. In a setup this size it is worth calling get_profile before the first action that changes anything — it names the mailbox actually on the other end.

From source

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node dist/index.js --auth

A clone deliberately arrives without a CLAUDE.md: the working instructions for this repository are an internal document and are not tracked here. Nothing in the build, the tests or npm run smoke needs them. The rules a contributor — or a coding agent — actually has to know are in CONTRIBUTING.md (design rules that are not up for grabs, the build/test loop) and SECURITY.md (the threat model those rules come from).

Config (env)

Var

Meaning

MAILWARDEN_DIR

config dir (default ~/.mailwarden)

MAILWARDEN_CREDENTIALS

path to credentials.json

MAILWARDEN_ACCOUNT

select a named account (its token is token.<name>.json; names are lower-cased); unset = the default token.json. See Multiple accounts

MAILWARDEN_TOKEN_PASSPHRASE

passphrase → encrypt token.json at rest (AES-256-GCM); re-run --auth after setting

MAILWARDEN_AUTO_SWEEP

1 → snooze sweep at startup + hourly while running (writes labels — needs the manage/gmail.modify scope; a read-only grant can't sweep)

MAILWARDEN_DOWNLOAD_DIR

restrict download_attachment and export_thread to this directory. Unset, the client picks any path this process can write to — --http warns at startup unless the manage tier is off

MAILWARDEN_READONLY

1 → register only the read tools (search/get_thread/list_labels/list_snoozed/get_profile/what_changed/triage_digest/follow_ups/list_unsubscribe/list_subscriptions). Shorthand for MAILWARDEN_TOOLS=read

MAILWARDEN_TOOLS

comma-separated tool tiers to advertise: read, manage, filters (default: all). Also derives the OAuth scopes requested at --auth. E.g. read,manage drops the filter tools and their gmail.settings.basic scope

MAILWARDEN_DEBUG

1 → print full errors with stack traces instead of a one-line message (for bug reports)

PORT

HTTP port (default 8787)

MAILWARDEN_HOST

HTTP bind address (default 127.0.0.1; set e.g. 0.0.0.0 for remote hosting)

MAILWARDEN_TOKEN

bearer token for the HTTP endpoint — required for --http unless overridden

MAILWARDEN_ALLOW_NO_TOKEN

1 → allow --http without a token (trusted/isolated networks only)

MAILWARDEN_ALLOWED_HOSTS

extra comma-separated host:port values accepted by the loopback Host allowlist

Status

Working and used in daily mailbox automation. Core Gmail tools + snooze implemented against @googleapis/gmail, covered by a vitest suite (1300 tests — npm run coverage). Current version: see the npm badge above, the changelog, or releases. PRs welcome — CONTRIBUTING.md covers the build/test loop and the design rules that are not up for grabs.

License

MIT © C.Sitte Softwaretechnik

Available Tools

29 tools
archiveA
Idempotent

Archive a thread (remove it from the inbox). USE WHEN: inbox triage — the thread is handled and should leave the inbox. DO NOT USE: to delete (use trash) or to defer to a date (use snooze). SIDE EFFECTS: thread leaves the inbox; reversible via modify_labels add INBOX.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses side effect 'thread leaves the inbox' and reversibility via 'modify_labels add INBOX', adding value beyond annotations (idempotentHint=true, destructiveHint=false). No contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise, front-loaded with action, then usage, then side effects. Every sentence contributes meaning with no waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers purpose, usage guidelines, side effects, and reversibility; output schema exists so return values need not be explained. Complete for a simple tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter (threadId) with 0% schema coverage, but the description does not elaborate on it beyond the schema; the parameter is self-explanatory, so minimal added value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states 'Archive a thread (remove it from the inbox)' with specific verb and resource, and distinguishes from siblings 'trash' and 'snooze' by naming them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly provides when-to-use ('inbox triage') and when-not-to-use ('to delete use trash, to defer use snooze'), with alternatives named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

bulk_modifyA
DestructiveIdempotent

Bulk-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. Set undo:true to get undo, the EXACT inverse of this call: before writing, every affected thread's labels are read live, and the plan says per message what to add back and what to take off — a message that was already in the target state is left out, which a swapped add/remove would not do. Pass undo.steps unchanged to undo_bulk_modify to revert. It costs one extra read per affected thread. With undo:true the call writes NOTHING unless the plan can be built: more than 500 messages to change, or a prior state that could not be read, is an error before the first write — a change the caller asked to be reversible is never made irreversibly. A label the call created for a name in add is taken off again by the undo but stays in the mailbox, empty. SIDE EFFECTS: modifies up to maxMessages messages in one call (none with dryRun); label changes are reversible — exactly with undo:true, approximately by the inverse call.

NameRequiredDescriptionDefault
addNo
undoNo
queryYes
dryRunNo
removeNo
verifyNo
crossCheckNo
maxMessagesNo

Output Schema

NameRequiredDescription
undoNo
cappedYes
dryRunYes
failedYes
verifiedNo
crossCheckedNo
labelsToCreateNo
matchedThreadsYes
matchedMessagesYes
submittedThreadsYes
submittedMessagesYes
matchedThreadCountYes
submittedThreadCountYes
unverifiedPredicatesYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes far beyond the annotations (readOnly=false, destructive=true, idempotent=true): it discloses partial-success reporting, the 204-no-body behavior and that submittedMessages != messages changed, per-thread cost of verify/undo, the pre-write abort semantics of undo:true, the flat cost of crossCheck, and that capped sets are not cross-checked. This is exactly the kind of context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The opening sentence is front-loaded and the USE WHEN / DO NOT USE markers aid scanning, but the body is a very long wall of text. Most sentences earn their place given the tool's complexity, though the staleness-measurement digression (132 threads / 114 unread / 19 hits) is longer than strictly needed to justify unverifiedPredicates.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 8-parameter, multi-mode, destructive bulk tool with rich semantics, the description is genuinely complete: it covers every mode, the precedence and cost of each flag, failure/partial-success behavior, and the index-staleness caveat. An output schema exists, yet the extra return-shape context given (verified, crossChecked.dropped, undo.steps) is useful rather than redundant.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden and does so: it explains add (name or id, unknown name created, '/' for nested), remove (unknown name ignored), maxMessages (capping + 'capped' flag), dryRun, verify, crossCheck, and undo. Every one of the 8 parameters gains meaning beyond its type/default.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: 'Bulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request.' It clearly distinguishes itself from near-siblings like modify_labels (single-thread) and the dedicated tools, so an agent can route correctly without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Contains explicit USE WHEN ('mass operations... archive all newsletters older than 30 days') and DO NOT USE ('for a single thread (use modify_labels or the dedicated tools), or with neither add nor remove') sections, plus conditional guidance on when to prefer dryRun/verify/undo/crossCheck. Alternatives are named directly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

bulk_unsubscribeA
Idempotent

Unsubscribe from several mailing lists in one call, one thread id per list. Threads are processed SEQUENTIALLY, and at most ONE request is made per sender — a second thread from a sender whose request already went out is reported with duplicateOf and no request (and says so when it advertises a DIFFERENT endpoint, i.e. is probably a separate list from the same sender). The whole call shares a 60-second budget; threads left over when it runs out come back with skippedOutOfTime and a reason, so re-running with the remaining ids finishes the job. Like unsubscribe, there is no URL parameter: every endpoint comes from that thread's own List-Unsubscribe header. Only RFC 8058 one-click senders are contacted; the rest come back with their alternatives in options. Partial success is reported, never hidden: a thread that cannot be read or whose endpoint fails becomes an entry with a reason, and the remaining threads still run. Set dryRun:true to rehearse: same header reads, same per-sender dedupe (as a real run with every request succeeding), and each entry a real run would contact reports the endpoint it wouldCall; refusals and duplicates carry none — and nobody is contacted. USE WHEN: clearing out several newsletters at once — pair with list_subscriptions, which gives you the sender rows and their newestThreadId; dryRun first to show the user which senders would be contacted. DO NOT USE: for one thread (use unsubscribe), or to find candidates (use list_subscriptions — it contacts nobody). SIDE EFFECTS: up to one outbound HTTPS request per DISTINCT sender (plus up to 3 redirects each) — the only non-Google hosts mailwarden ever contacts (none with dryRun). Each confirms to that sender that the address is live, and none of it can be undone. The mailbox itself is not changed.

NameRequiredDescriptionDefault
dryRunNo
threadIdsYes

Output Schema

NameRequiredDescription
dryRunYes
resultsYes
requestsYes
attemptedYes
requestedYes
unsubscribedYes
skippedOutOfTimeYes
skippedDuplicatesYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses sequential processing, per-sender deduplication, a 60-second time budget, dryRun behavior, external HTTP requests (with redirects), confirmation of address liveness, and irreversibility. These details go well beyond the annotations (readOnlyHint:false, openWorldHint:true, idempotentHint:true) and provide necessary caveats about side effects. No contradiction with annotations is apparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is lengthy but well-structured with bolded section headers (USE WHEN, DO NOT USE, SIDE EFFECTS). It front-loads the core purpose and packs each sentence with meaningful details. Slightly verbose, but each part contributes to understanding a complex tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers all major edge cases: duplicateOf, skippedOutOfTime, options for non-RFC 8058, partial failure handling, dryRun output, and side effects. Given the complexity and the existence of an output schema, the description provides comprehensive operational knowledge for correct usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 0%, the description explains the meaning of threadIds ('one thread id per list') and dryRun ('rehearse' with no contact). It also clarifies that no URL parameter is used and that endpoint comes from List-Unsubscribe headers. This adds useful context beyond the raw schema, though it could be more explicit about max 25 items and preconditions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action (unsubscribe) and the scope (several mailing lists in one call, one thread id per list). It distinguishes itself from the sibling tool 'unsubscribe' by explicitly noting it is for multiple threads, and the phrase 'one thread id per list' adds specificity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit 'USE WHEN' and 'DO NOT USE' sections, naming alternative tools (list_subscriptions for candidate discovery, unsubscribe for single thread) and pairing recommendations with list_subscriptions. This gives agents clear decision criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_filterA
Destructive

Create a Gmail filter: matching incoming mail automatically gets the given label actions. Give at least one criterion and at least one action. Actions are label add/remove only (labels by name or id; an unknown name in addLabels is auto-created). Common recipes: skip the inbox → removeLabels ['INBOX']; auto-mark-read → removeLabels ['UNREAD']; auto-trash → addLabels ['TRASH']; star → addLabels ['STARRED']; file under a label → addLabels ['Receipts']. A filter only affects mail arriving AFTER it's created; set applyToExisting:true to ALSO apply the same actions once to mail already in the mailbox (builds a Gmail search from the criteria and runs a bulk modify — same unverified-index caveat as bulk_modify — the sweep acts on what the index returns, which can be badly stale on read state; up to maxMessages, default 1000). The sweep's applied.submittedMessages is how many ids were handed to the API, NOT how many messages changed; set verify:true alongside applyToExisting to read the labels back afterwards and get applied.verified {applied, notApplied[], unverifiable[]}. It costs one extra read per affected thread, so it is off by default; a read-back that fails lands in unverifiable and never fails the call — the filter stands either way. USE WHEN: setting up a persistent auto-triage rule (e.g. 'always archive + label newsletters from x'), optionally cleaning up the existing backlog too. NOTE: forwarding filters are intentionally not supported — mailwarden creates no send/exfiltration path. SIDE EFFECTS: adds a server-side rule affecting future mail (reversible via delete_filter); with applyToExisting also modifies existing messages. Requires gmail.settings.basic.

NameRequiredDescriptionDefault
toNo
fromNo
sizeNo
queryNo
verifyNo
subjectNo
addLabelsNo
maxMessagesNo
excludeChatsNo
negatedQueryNo
removeLabelsNo
hasAttachmentNo
sizeComparisonNo
applyToExistingNo

Output Schema

NameRequiredDescription
idYes
appliedYes
forwardNo
criteriaYes
addLabelIdsYes
removeLabelIdsYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the annotations. It explains that a filter affects only mail arriving after creation, that applyToExisting runs a bulk sweep with a stale-index caveat, that submittedMessages is not the number of changed messages, that verify costs an extra read and failures are non-fatal, that labels can auto-create unknown names, and that the operation requires gmail.settings.basic. This is rich, honest behavioral disclosure consistent with destructiveHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but nearly every sentence adds a distinct operational detail: core semantics, common recipes, applyToExisting caveats, verification behavior, use case, and side effects. It is front-loaded with the primary purpose and structured with labeled sections such as 'USE WHEN' and 'SIDE EFFECTS'. It could be tighter, but the density is justified for a tool with 14 parameters and subtle edge cases.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 14 parameters, no required fields, and an output schema, this description is exceptionally complete. It covers what happens on creation, how existing mail is handled, the semantics of verification, failure behavior, permission requirements, reversibility, and unsupported forwarding. An agent can safely construct a valid call and predict the consequences; the output schema can fill in remaining return-value details.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the semantic burden and does so well for the action parameters: it defines addLabels/removeLabels semantics, gives concrete recipes like removeLabels ['INBOX'] and addLabels ['TRASH'], and explains applyToExisting, maxMessages, and verify. The criteria parameters (from, to, subject, query, etc.) are not individually detailed, but their Gmail meanings are conventional and the overall parameter guidance is well above the minimum viable level.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb-resource pair — 'Create a Gmail filter' — and immediately states what the filter does: matching incoming mail automatically gets label actions. It also distinguishes the tool from siblings by emphasizing persistent auto-triage rules and explicitly noting that forwarding filters are not supported, so there is no confusion with bulk_modify, modify_labels, or other Gmail mutators.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is a clear 'USE WHEN' statement defining the intended scenario: setting up a persistent auto-triage rule, with the optional applyToExisting use case explained. It also gives an explicit 'not supported' exclusion for forwarding. It stops short of naming a sibling alternative such as bulk_modify for one-time bulk label changes, but the context is strong enough for an agent to choose this tool for filter creation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_labelA
Idempotent

Create 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.

NameRequiredDescriptionDefault
nameYes
textColorNo
backgroundColorNo

Output Schema

NameRequiredDescription
idYes
nameYes
colorNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations only declare idempotentHint=true, readOnlyHint=false, and destructiveHint=false. The description goes far beyond this by explaining case-insensitive name matching, nested label creation, recoloring existing labels, system label color restrictions, and the fact that no mail is changed. This is rich behavioral context with no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every section earns its place: core action, idempotency, nesting, color behavior, usage guidance, and side effects. It is front-loaded with the most important information and uses clear section markers to keep the density navigable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of label creation in Gmail, the description covers all relevant caveats: idempotency, color pairing, palette restrictions, recoloring, system label refusals, and side effects. Since an output schema exists, the return value need not be further explained. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden of explaining parameters. It explains name with nesting syntax, and describes that textColor and backgroundColor must be passed together as '#rrggbb' and must come from Gmail's palette. This is far more than the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Create a user label and return its id.' It also distinguishes itself from modify_labels by explicitly stating that filing mail under a new label should use modify_labels/bulk_modify, which auto-create unknown names.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes explicit USE WHEN and DO NOT USE sections, giving clear conditions for tool selection. It names the exact alternative tools (modify_labels/bulk_modify) and explains when they should be used instead, leaving no ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_filterA

Delete a Gmail filter by id (get ids from list_filters). USE WHEN: removing an auto-triage rule. SIDE EFFECTS: removes the server-side rule; future mail is no longer auto-processed by it. Requires gmail.settings.basic.

NameRequiredDescriptionDefault
idYes

Output Schema

NameRequiredDescription
okYes

TDQS

A3.8/5.0
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotation Contradiction: The description says 'removes the server-side rule' and the tool is named delete_filter, indicating destructive behavior, yet annotations declare destructiveHint=false. This is a direct contradiction and severely undermines trust in safety-critical metadata.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured, with labeled sections (USE WHEN, SIDE EFFECTS) that immediately convey the core action, when to use it, and consequences. Every sentence contributes valuable information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple single-parameter delete operation, the description covers purpose, usage context, side effects, and required permissions. An output schema exists so return values are not required. It lacks explicit error-handling/idempotency details, but annotations and simplicity compensate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With zero schema description coverage, the description adds essential meaning to the 'id' parameter by explaining it is a Gmail filter id obtainable from list_filters. This gives the agent a clear source and purpose, going beyond just the schema's string type.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with 'Delete a Gmail filter by id', a specific verb and resource, and directly distinguishes itself from sibling tools like create_filter and list_filters by noting how to obtain ids. It clearly states the tool's scope.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'USE WHEN: removing an auto-triage rule' provides a clear context. It also instructs the agent to get ids from list_filters, implicitly guiding when to use this tool vs its sibling. However, it does not explicitly state when not to use it or mention alternatives beyond listing filters.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

download_attachmentA

Download an attachment to a local file path. If MAILWARDEN_DOWNLOAD_DIR is set, destPath is resolved inside (and restricted to) that directory. USE WHEN: the user wants an attachment saved to disk (IDs come from get_thread's attachment metadata). SIDE EFFECTS: writes a local file; never overwrites — an existing file gets a numeric suffix (file-1.pdf). The response's 'saved' field is the path actually used. Mailbox unchanged.

NameRequiredDescriptionDefault
destPathYes
messageIdYes
attachmentIdYes

Output Schema

NameRequiredDescription
savedYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses critical behaviors beyond annotations: writes a local file, never overwrites (numeric suffix file-1.pdf), explains the MAILWARDEN_DOWNLOAD_DIR path restriction, confirms mailbox unchanged. The 'saved' field semantics are explained. Slight gap: doesn't mention permission/error behavior for the download operation, but coverage is strong given annotations already signal non-readOnly, non-destructive, non-idempotent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four tight sentences, each earning its place: definition, directory restriction, usage trigger, side effects with concrete example, and response field meaning. Zero waste, well front-loaded with the primary action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a non-readOnly, non-idempotent write tool with zero param documentation, the description provides strong behavioral disclosure: overwrite policy, path resolution, mailbox unaffected. An output schema exists, so return details needn't be enumerated. Minor gap: no mention of failure modes (unresolvable path, missing attachment) but solid overall.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and there are 3 required parameters with no descriptions. The description does explain destPath semantics well (directory resolution, suffix behavior) and touches on IDs coming from get_thread. However, messageId and attachmentId meanings are only implied via the get_thread reference, not explicitly defined. Description partially compensates but doesn't fully cover all three params.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clear verb+object: 'Download an attachment to a local file path.' It distinguishes from siblings by noting IDs come from get_thread's attachment metadata, explicitly differentiating from search/read-only tools. Purpose is immediately unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'USE WHEN' statement defines the trigger condition (user wants attachment saved to disk) and cites the exact source of input IDs (get_thread's attachment metadata). This gives strong selection guidance among the sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

export_threadA

Write a whole thread to a local file instead of returning it — for keeping, sharing, or reading a long conversation without loading it into the context. markdown (default): one section per message with From/To/Date/Subject and attachment names as inline code (never rendered as a link or image), sender authentication and the attachment list, and the body in a code fence that the message text cannot close (HTML-only mail is reduced to text). json: the parsed messages as get_thread returns them, HTML included. Both formats strip the same hidden characters (zero-width, bidi controls) as every tool result. Attachments are listed, not saved — use download_attachment for those. If MAILWARDEN_DOWNLOAD_DIR is set, destPath is resolved inside (and restricted to) that directory, as for attachments. The file holds mail text as the sender wrote it: untrusted content, now on disk. USE WHEN: the user wants a conversation saved or handed on, or a thread is too long to read in one piece. DO NOT USE: to read a thread (use get_thread). SIDE EFFECTS: writes a local file; never overwrites — an existing file gets a numeric suffix. The response's 'saved' field is the path actually used. Mailbox unchanged.

NameRequiredDescriptionDefault
formatNomarkdown
destPathYes
threadIdYes

Output Schema

NameRequiredDescription
bytesYes
savedYes
messageCountYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations: discloses per-format output behavior, hidden-character stripping, the never-overwrite numeric-suffix policy, destPath resolution restricted to MAILWARDEN_DOWNLOAD_DIR, that the response 'saved' field is the path used, and that the file holds untrusted sender content now on disk. This is rich behavioral context that annotations (readOnlyHint=false, destructiveHint=false, idempotentHint=false) only sketch.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose, then well-organized USE WHEN / DO NOT USE / SIDE EFFECTS blocks. It is long and dense, but essentially every sentence carries operative detail, so little is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, yet the description still covers the file-write side effects, overwrite behavior, directory restriction, and untrusted-content caveat. For a tool that writes to disk, an agent has everything needed to call it correctly and set expectations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does: it defines the two format values (markdown default with its section layout, json mirroring get_thread with HTML included) and explains destPath resolution and the MAILWARDEN_DOWNLOAD_DIR restriction. threadId is only implied via the get_thread reference, leaving one parameter slightly under-specified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — 'Write a whole thread to a local file instead of returning it' — and immediately contrasts with the returning behavior of get_thread. An agent can distinguish it from get_thread and download_attachment without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit USE WHEN (user wants a conversation saved/handed on, or thread too long to read) and DO NOT USE (to read a thread — use get_thread), plus a routing note that attachments are listed not saved and download_attachment should be used for those. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

follow_upsA
Read-only

Who is waiting on whom. waitingOnThem: threads whose LAST message is yours (SENT) and has had no answer for at least nudgeAfterDays — candidates for a nudge; a note to yourself (every To and Cc address is the account's own primary address, in any spelling Gmail delivers to it: +tags, dots, googlemail.com) is left out; one other recipient keeps it. waitingOnMe: threads still in the inbox whose last message came from someone else — a person, not a newsletter or an automated notice by its own headers — oldest first. Decided on each thread's last message and its LIVE labels: the search index only proposes candidates (in:sent / in:inbox within the last days), so a thread the index still files under the inbox after it was archived is dropped, as in search. Drafts, trash and spam messages do not count as a thread's last word. Scans at most max threads per side; hasMore says when more candidates existed. A thread with no recorded date is left out rather than given an invented age. USE WHEN: 'what am I waiting on?', 'who have I not answered?', 'what should I follow up on?' DO NOT USE: as an inbox overview (use triage_digest) or to read a thread (use get_thread). SIDE EFFECTS: none.

NameRequiredDescriptionDefault
maxNo
daysNo
nudgeAfterDaysNo

Output Schema

NameRequiredDescription
hasMoreYes
scannedYes
waitingOnMeYes
waitingOnThemYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, but the description goes well beyond them: it discloses exclusion rules (self-notes, newsletters, drafts/trash/spam), the live-labels-vs-search-index distinction, the max scan ceiling with hasMore signaling, the no-date-exclusion rule, and explicitly states 'SIDE EFFECTS: none'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core 'who is waiting on whom' framing and structured into per-bucket logic plus USE WHEN / DO NOT USE / SIDE EFFECTS sections. Dense and a touch long, but virtually every clause earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained, and the description still adds hasMore semantics. For a zero-required-param read tool with complex matching logic, the definition covers everything an agent needs to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must carry the params, and it does: nudgeAfterDays is tied to the 'no answer for at least nudgeAfterDays' condition, days bounds the in:sent/in:inbox index window, and max caps threads scanned per side. It lacks default/range details (e.g., defaults of 25/30/3) but the semantics are clear.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific concept ('Who is waiting on whom') and then defines its two output buckets, waitingOnThem and waitingOnMe, with precise conditions. This distinguishes it cleanly from list-oriented siblings like search and get_thread.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit USE WHEN triggers ('what am I waiting on?', 'who have I not answered?') and explicit DO NOT USE routing to triage_digest for inbox overviews and get_thread for reading a thread. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_profileA
Read-only

Return 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.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
historyIdYes
emailAddressYes
threadsTotalYes
messagesTotalYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description reinforces this with 'SIDE EFFECTS: none.' It adds useful behavioral context beyond the annotation by stating the tool returns only counts, not a message listing, and positions it as a liveness check.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured: the core function is first, followed by USE WHEN, DO NOT USE, and SIDE EFFECTS. Every sentence carries actionable information with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple no-parameter, read-only tool with an output schema present, the description provides all necessary context: what it returns, when to use it, when not to use it, and that there are no side effects. Nothing important is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters and full schema description coverage, so there are no parameter semantics to document. The description usefully clarifies what the response contains, satisfying the baseline for no-parameter tools.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('Return the authorized account') and specifies exactly what is returned: email address plus total message/thread counts. It also explicitly distinguishes itself from message enumeration by pointing to 'search' as the tool for listings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit USE WHEN guidance (confirming which mailbox is connected, cheap liveness check) and explicit DO NOT USE guidance (enumerating messages), with an alternative named. This gives clear decision criteria for an agent.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_threadA
Read-only

Fetch 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?'. That holds on a full: false fetch too — the metadata request asks for Authentication-Results by name, so checking whether a mail is really from who it says never costs a body. 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. dkimAligned says whether a passing DKIM signature belongs to the From domain or a parent or child of it; it is absent when that cannot be told without a public-suffix list (two sibling subdomains), so a missing field is not a mismatch. authservId says WHO asserts all this: a message can carry forged Authentication-Results headers of its own, and only Gmail's own report (mx.google.com) is ever read; otherReports counts the reports that were NOT read, and alsoReported lists results that disagree with the ones above. unchecked: true means the message carried no report from Gmail — 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).

NameRequiredDescriptionDefault
fullNo
threadIdYes

Output Schema

NameRequiredDescription
messagesYes
threadIdYes
metadataOnlyNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only cover readOnlyHint/openWorldHint, and the description adds substantial context beyond them: no side effects (does not mark as read), the exact fields omitted under `full: false` and why they are omitted rather than empty (with a concrete near-miss anecdote), `metadataOnly: true`, and the deliberate design rationale. It also explains the failure mode of treating a missing attachment list as 'no attachment'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loading is excellent — purpose, then the `full` semantics, then auth interpretation, then USE/DO NOT USE. But roughly two thirds of the text is a deep tutorial on SPF/DKIM/DMARC interpretation, `authservId`, `alsoReported` and `unchecked`, which is return-value interpretation for a tool that already ships an output schema. For a 2-parameter fetch this is heavily over-sized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter read tool with an output schema, nothing an agent needs is missing: identity of the ID type, the `full` trade-off, the omission convention, and the read-only guarantee are all stated. The auth prose is redundant with the output schema rather than a gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden — and does. `full` is given its default, its semantic difference (headers/labels only), the fields it drops, and the flag it sets; `threadId` is disambiguated from message IDs. No parameter is left to inference.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+payload: 'Fetch a thread by ID: headers, plaintext + HTML bodies, and attachment metadata.' Immediately separates itself from search_* siblings by requiring a thread ID and pointing back to search for discovery.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit USE WHEN ('reading a thread's content after finding it via search') and DO NOT USE ('with a message ID — this takes thread IDs'). It also gives a decision rule for the `full` flag: stay true when content matters, false only when the thread is too large or metadata-only is of interest, plus the `hasAttachments: true` trigger.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_filtersA
Read-only

List all Gmail filters — the server-side rules that auto-apply label actions to incoming mail. Shows each filter's criteria and label actions, and (for auditing) any forward address an existing filter carries. USE WHEN: reviewing existing automation, or to get a filter's id before delete_filter. SIDE EFFECTS: none. Requires the gmail.settings.basic scope — re-run mailwarden --auth if you authorized an earlier version.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
filtersYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explicitly declares 'SIDE EFFECTS: none' and adds the required scope ('gmail.settings.basic') with a re-authentication note ('re-run mailwarden --auth'). This goes beyond the readOnlyHint annotation by providing operational auth context and confirming no side effects, which is valuable for an agent deciding to invoke the tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, structured with clear segments (purpose, output details, usage, side effects/scope), and front-loaded with the core action. Every sentence earns its place without repetition, and the formatting improves readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters and an existing output schema, the description provides ample context: it explains what filters are, what information is returned, when to use the tool, that there are no side effects, and what scope is required. This is complete for a simple list tool with no input complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, so the baseline is 4 per the rubric. The description adds no parameter-specific details but instead clarifies what the tool returns (criteria, label actions, forward addresses), which compensates for the lack of parameters and enriches the agent's understanding of the output.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('List all Gmail filters') and the resource (filters), with a definition of what filters are and what information is shown (criteria, label actions, forward addresses). It distinguishes the tool from siblings like create_filter and delete_filter by focusing on listing and auditing existing filters.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit 'USE WHEN' guidance: reviewing existing automation or getting a filter's id before delete_filter. It gives clear context but does not explicitly name alternative tools for exclusion, so it falls short of the highest calibration example. The intent is clear enough for an agent to select this tool appropriately.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_labelsA
Read-only

List 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.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
labelsYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations include readOnlyHint=true, and the description adds the explicit 'SIDE EFFECTS: none' statement, reinforcing the safe read-only nature. It also provides behavioral context that the listing includes both system and user labels, which is not in the structured annotations. This adds value beyond the annotations without contradicting them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: two short segments ('List all Gmail labels (system + user)' and 'SIDE EFFECTS: none') plus a focused usage clause. It is front-loaded with the core action, with no wasted words, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (no parameters, read-only, output schema exists), the description covers all necessary context: purpose, usage, side effects, and scope. It explicitly mentions the labels' scope (system + user) and when to use it, making it fully complete for an agent to select and invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the schema is empty, so schema description coverage is 100%. With 0 params, the baseline is 4. The description doesn't discuss parameters because none exist, which is appropriate. No additional parameter semantics are needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'List all Gmail labels (system + user).' It uses a specific verb ('List') and resource ('Gmail labels'), and the scope is explicit. This distinguishes it from sibling tools like modify_labels, bulk_modify, and create_label, which perform mutations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes an explicit 'USE WHEN' clause, specifying appropriate contexts (inspecting mailbox structure, getting exact label names/ids). It also names alternative tools (modify_labels/bulk_modify/create_label) and notes that a lookup is rarely required because they accept plain label names. This is clear guidance on when to use the tool vs. alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_snoozedA
Read-only

List all snoozed threads with their due dates. SIDE EFFECTS: none.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
snoozedYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Explicitly states 'SIDE EFFECTS: none', which complements the readOnlyHint=true annotation and adds clarity beyond the annotation alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences with no fluff. Front-loads the main function and includes side-effect note concisely.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no parameters and an existing output schema, the description adequately covers the tool's behavior. Mentions 'due dates' hinting at output content.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so baseline is 4. The description adds no param info, but none is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists snoozed threads with due dates, distinguishing it from sibling tools like snooze or trash which perform actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus alternatives such as search or sweep_snoozed. The description is purely descriptive without usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_subscriptionsA
Read-only

Who 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.

NameRequiredDescriptionDefault
maxNo
topNNo
queryNocategory:promotions

Output Schema

NameRequiredDescription
queryYes
hasMoreYes
sampledYes
sendersFoundYes
subscriptionsYes

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations provide readOnlyHint=true, but the description goes well beyond that, disclosing that opt-out options come from metadata fetches, that Gmail quota exhaustion can produce 'unknown', that dates bound the sample not the sender's history, and that there is deliberately no precomputed frequency. These are real behavioral traits an agent needs to interpret results correctly.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense and front-loads the purpose, but it runs long with several sentences that could be tightened, and the flow moves between purpose, mechanics, field semantics, and usage guidance without clean visual separation. Every sentence earns its place informationally, but the size is above ideal conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values needn't be fully explained, and the description covers most of what's missing: field semantics for optOut, oldestDate/newestDate, newestThreadId, sendersFound, and sampling caveats. It's comprehensive for a 3-param list tool, though the query parameter's behavior could be explained slightly more.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does substantially: it explains that max bounds the sample ('a query capped at max reaches back only as far as those threads go'), that topN truncation happens when sendersFound exceeds topN, and implies the query parameter's role through the 'mailbox slice' framing. The query parameter itself ('category:promotions' default) isn't explicitly discussed, leaving a minor gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly establishes the resource (sender-grouped mailbox slice with opt-out options) and distinguishes itself from siblings by naming triage_digest and list_unsubscribe as alternatives. The opening is slightly metaphorical ('Who keeps writing, how often, whether you can get off the list'), but it resolves into a precise function. It's clear what the tool does, though the verb is less direct than an ideal 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'USE WHEN' and 'DO NOT USE' sections name the exact scenarios and the sibling tools to prefer in each case: triage_digest for general inbox overview, list_unsubscribe for one known thread. This is textbook when/when-not guidance with alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_unsubscribeA
Read-only

Report 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.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
fromYes
mailtosYes
subjectYes
oneClickYes
threadIdYes
httpsUrlsYes
messageIdYes
bodyCandidatesYes
hasUnsubscribeYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description strongly reinforces this with 'without contacting anyone' and 'SIDE EFFECTS: none — no request is made to the sender.' It also discloses important nuances: it reads the newest message carrying the header, bodyCandidates come from untrusted message text, and they must not be treated as an actual opt-out. This goes well beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every major section earns its place: field meanings, safety warnings, use cases, and side effects. It is front-loaded with the core purpose before diving into details. There is minor redundancy between 'without contacting anyone' and 'SIDE EFFECTS: none,' but overall the length is justified by the nuanced output behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's purpose, when to use it, how it behaves regarding headers and body text, safety constraints, side effects, and relationships to sibling tools. It also explains the special cases of `hasUnsubscribe`, `bodyCandidates`, and untrusted content. Given the output schema exists, no important gap remains for an agent to invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema only defines `threadId` as a required string with no description, so the description carries the burden. It adds meaning by explaining that the tool operates on a thread and reads the newest message in that thread, even noting how a reply threaded onto a newsletter is handled. This gives enough context for an agent to understand what `threadId` represents, though no explicit format guidance is provided.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Report the opt-out options a thread advertises (List-Unsubscribe / RFC 8058), without contacting anyone.' It clarifies this is a read-only inspection tool and distinguishes it from the sibling `unsubscribe` tool by explicitly noting that `unsubscribe` can perform the one-click opt-out.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides an explicit 'USE WHEN' section: 'checking whether a newsletter can be unsubscribed from, or showing the user the link to click.' It also gives alternative routing by pointing to the `unsubscribe` tool for oneClick cases and instructs the agent to never fetch bodyCandidates, establishing clear boundaries for safe use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

mark_readA
Idempotent

Mark a thread as read. SIDE EFFECTS: removes UNREAD; reversible via mark_unread.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes beyond annotations by detailing the specific side effect and reversibility, fully disclosing the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise with two sentences, front-loading the purpose and listing side effects efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter, clear annotations, and an output schema, the description covers all necessary context including side effects and reversibility.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter 'threadId' is self-explanatory given the tool name, and with 0% schema coverage the description adds no extra meaning, which is adequate for this simple case.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action 'Mark a thread as read', which directly matches the tool name and distinguishes it from siblings like 'mark_unread'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains the side effect (removes UNREAD) and reversibility via mark_unread, providing clear context for usage, though it does not explicitly state when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

mark_unreadA
Idempotent

Mark a thread as unread. SIDE EFFECTS: adds UNREAD; reversible via mark_read.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate idempotent and non-destructive behavior. The description adds value by explicitly stating the side effect 'adds UNREAD' and noting reversibility, which aligns with and supplements the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: one sentence plus a bullet for side effects. Every word is necessary, and the structure immediately conveys the core action and notable consequences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the simplicity of the tool (1 parameter, no enums, output schema present), the description covers the essential behavior. It could benefit from explaining what 'threadId' represents, but the context from sibling tools and the name likely suffices for an AI agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has one parameter 'threadId' with no description, and schema description coverage is 0%. The description does not explain the parameter meaning, format, or constraints. While the name is self-explanatory, the description should add more context to compensate for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: 'Mark a thread as unread.' The verb 'Mark' and resource 'thread' are specific. It distinguishes from sibling 'mark_read' implicitly through the opposite action, and the name itself clarifies the purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description mentions reversibility via 'mark_read,' which hints at when not to use it (if you want to undo). However, it does not provide explicit context on when to use this tool over alternatives like 'archive' or 'trash,' leaving some ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

modify_labelsA
Idempotent

Add/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.

NameRequiredDescriptionDefault
addNo
removeNo
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations, the description discloses side effects ('changes the thread's labels'), the auto-creation of labels in 'add', the ignoring of unknown names in 'remove', and reversibility via inverse call. This adds meaningful behavioral context not present in the idempotentHint or readOnlyHint annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tightly written with front-loaded purpose and structured segments (USE WHEN, DO NOT USE, SIDE EFFECTS). Every sentence conveys needed information without fluff, making it easy for an agent to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simple scope (single thread labels) and the presence of an output schema, the description covers all essential usage aspects: operation semantics, edge cases (unknown labels), side effects, and reversibility. It is complete enough to guide correct invocation without requiring external knowledge.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must bear the full weight of parameter explanation. It does so by explaining that labels can be given by name or id, describing the special INBOX/UNREAD mapping, and clarifying the behavior of unknown names in each array. This goes well beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Add/remove labels on a thread,' a specific verb and resource. It distinguishes itself from siblings by explicitly mapping archive to removing 'INBOX' and mark read to removing 'UNREAD,' and by pointing out that dedicated tools exist for simpler operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit 'USE WHEN' and 'DO NOT USE' guidance, naming alternatives ('dedicated tools') for plain archive/read/unread. This clearly tells an agent when to select this tool over similar ones like mark_read or archive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

snoozeA
Idempotent

Snooze a thread until a date or time: archives it now, resurfaces when it comes due and sweep_snoozed runs. until accepts an explicit date (YYYY-MM-DD), a date+time (YYYY-MM-DD HH:MM or e.g. '2026-06-20 9am'), OR a preset resolved server-side: today, tomorrow, weekend (next Saturday), next week (next Monday), a weekday name (monday–sunday, next occurrence), 'in N days', or 'in N hours'. A preset may carry a trailing time ('tomorrow 9am', 'monday 8:30'). A timed snooze wakes at the next sweep on/after that minute. USE WHEN: deferring a thread to a later date/time instead of leaving it in the inbox. DO NOT USE: for permanent removal (use archive or trash). SIDE EFFECTS: removes INBOX, adds a dated MCP/Snoozed label; reversible via unsnooze.

NameRequiredDescriptionDefault
untilYes
threadIdYes

Output Schema

NameRequiredDescription
threadIdYes
snoozedUntilYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Explains side effects in detail ('removes INBOX, adds a dated MCP/Snoozed label'), notes reversibility ('reversible via unsnooze'), and describes the resurfacing trigger ('sweep_snoozed runs'). This goes well beyond the annotations (readOnly=false, destructive=false, idempotent=true) and is consistent with them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Structured and front-loaded: one-sentence summary, then detailed `until` format, then usage guidance, then side effects. Every sentence adds value; the length is justified by the complexity of the `until` parameter.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers the operation, timing semantics, side effects, reversibility, and usage boundaries. With an output schema present, no return-value explanation is needed, and the description is sufficient for an agent to select and invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema has no descriptions for either parameter (0% coverage), but the description provides exhaustive detail for `until`, including explicit date/time formats, presets, and trailing times. `threadId` is self-explanatory from its name, so the description compensates fully.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly identifies the operation ('Snooze a thread') and its resource ('thread'), and explains the mechanism (archives now, resurfaces later). It also distinguishes from siblings by stating 'DO NOT USE for permanent removal (use archive or trash).'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use ('USE WHEN: deferring a thread to a later date/time instead of leaving it in the inbox') and when not to ('DO NOT USE: for permanent removal (use archive or trash)'), naming alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

sweep_snoozedA
Idempotent

Resurface all snoozed threads whose date is due (<= today), batched at 1000 messages per API request. Set dryRun:true to rehearse: reports the due labels and threads (dueLabels/dueThreads) as the sweep would find them (from the live label listing; a single snooze label with more than 5000 messages is under-counted in the rehearsal), and wakes nothing. USE WHEN: the user asks to process due snoozes, or as a scheduled maintenance call; dryRun to answer 'what is due right now?' without acting. SIDE EFFECTS: due threads return to the inbox marked unread (none with dryRun); safe to run repeatedly. failedCount/errors report messages a batch could not wake (their label is kept for the next sweep).

NameRequiredDescriptionDefault
dryRunNo

Output Schema

NameRequiredDescription
dateYes
wokenYes
dryRunYes
errorsYes
dueLabelsYes
dueThreadsYes
wokenCountYes
failedCountYes
dueThreadCountYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the annotations by disclosing concrete side effects: due threads return to the inbox marked unread, dryRun wakes none, and failures keep the label for the next sweep. It also discloses a real limitation (under-counting in rehearsal for labels with >5000 messages), the batch size, and idempotence, giving the agent an accurate behavioral model.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with labeled sections (USE WHEN, SIDE EFFECTS) and front-loaded with the core function. Every sentence adds value—batch size, dryRun behavior, side effects, and error handling—without fluff. It is longer than ideal but earns its length through density of useful information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity and the presence of an output schema, the description covers all critical aspects: behavior, batch limits, dryRun rehearsal details, side effects, idempotence, and failure handling ('failedCount/errors report messages a batch could not wake'). No significant gaps remain for the agent to make an informed invocation decision.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Even though the schema only defines a single boolean dryRun, the description thoroughly explains its purpose and outcome: setting dryRun:true produces a rehearsal that reports dueLabels/dueThreads and wakes nothing. This fully compensates for the schema's minimal parameter info, adding essential semantics for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb ('Resurface') and a precise target ('all snoozed threads whose date is due (<= today)'), making it unmistakable what the tool does. It clearly differentiates from sibling tools such as list_snoozed, snooze, and unsnooze by focusing on acting on due threads rather than listing or individual operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes an explicit 'USE WHEN' section stating clear triggers: 'the user asks to process due snoozes, or as a scheduled maintenance call,' and also covers the dryRun rehearsal use case for answering 'what is due right now?' without acting. It lacks an explicit 'when not to use' or direct alternative names, but the context is sufficiently clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

trashA
DestructiveIdempotent

Move a thread to Trash. USE WHEN: the thread should be discarded. DO NOT USE: for inbox cleanup of mail worth keeping (use archive). SIDE EFFECTS: thread moves to Trash; recoverable via untrash for ~30 days, then Gmail deletes it permanently.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate destructiveHint=true, but the description adds critical context: thread is recoverable via untrash for ~30 days, then permanently deleted. This goes beyond annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the action, and every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter and an output schema present, the description covers purpose, usage guidelines, and side effects completely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 0% description coverage for the single required string parameter 'threadId'. While the description doesn't detail its format, the tool's context makes it clear. A format hint (e.g., 'Gmail thread ID') would improve clarity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states 'Move a thread to Trash' with a specific verb and resource, and distinguishes from the sibling 'archive' tool by noting when not to use it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear USE WHEN ('the thread should be discarded') and DO NOT USE conditions ('for inbox cleanup of mail worth keeping') with explicit alternative (archive). This is exemplary guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

triage_digestA
Read-only

Structured 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.

NameRequiredDescriptionDefault
maxNo
topNNo
queryNoin:inbox

Output Schema

NameRequiredDescription
byAgeYes
queryYes
unreadYes
hasMoreYes
sampledYes
signalsYes
topLabelsYes
topSendersYes
withAttachmentsYes

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, but the description adds valuable behavioral details: sampling up to 'max' with hasMore, age bucketing by first message date, and explicit 'SIDE EFFECTS: none'. This goes beyond annotations and answers likely agent questions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense yet well-organized into clear sections: overview, use cases, behavior, and side effects. Every sentence contributes new information, with no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's purpose, usage, sampling, age bucketing, and side effects, while an output schema likely handles return-value documentation. However, it leaves parameter semantics incomplete (especially topN/query), a notable gap given the low schema coverage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain parameters. It only mentions 'max' (sampling limit) but leaves 'topN' and 'query' undefined. This forces the agent to infer their meaning, which is insufficient for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it provides a structured overview of a mailbox slice for triage decisions, listing specific aggregated fields (sender/label/age buckets, counts, signals) and explicitly contrasts with a raw thread list. It also distinguishes from reading a specific thread by referencing search/get_thread.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Includes explicit 'USE WHEN' and 'DO NOT USE' sections, naming alternatives (search/get_thread) and giving concrete examples like bulk-archiving/snoozing/labeling. This provides clear guidance on when to choose this tool over siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

undo_bulk_modifyA
DestructiveIdempotent

Revert a bulk_modify exactly, from the undo plan it returned (bulk_modify with undo:true). Pass steps unchanged: each step adds back the labels its messages lost and takes off the labels they gained, so a message that was already in the target state before the bulk change is left alone. Steps name labels by id, as the plan does; a label name is refused, because an unknown name would be created rather than restored. The plan restores the state read just before the bulk change. A label changed by hand since then is changed again by the undo, and a label deleted since then makes its step fail — reported in failed, the other steps still run. A label the bulk change created stays in the mailbox, empty. Set verify:true to read the labels back afterwards — one read per affected thread, however many steps name it. USE WHEN: the user wants a bulk change taken back, and the bulk_modify result carried undo. DO NOT USE: without a plan from bulk_modify (use bulk_modify or modify_labels), or to change labels in a new way — this applies exactly the steps it is given. SIDE EFFECTS: label changes on the listed messages; reversible again by the inverse.

NameRequiredDescriptionDefault
stepsYes
verifyNo

Output Schema

NameRequiredDescription
failedYes
verifiedNo
submittedMessagesYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare destructive/idempotent/not-readOnly. The description goes well beyond: hand-changed labels get re-changed, deleted labels cause a step to fail but other steps still run, a label created by the original change is left empty, and verify:true costs one read per affected thread. This is rich pre-call behavioral disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded and the USE WHEN / DO NOT USE / SIDE EFFECTS blocks are scannable. It is long, but given a no-coverage schema and complex semantics almost every sentence carries load; still on the verbose side of tight.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be enumerated, yet the description still flags the `failed` field. For a destructive, plan-driven mutation tool with a zero-coverage schema, the description covers everything an agent needs to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must carry parameter meaning, and it largely does: `steps` must be passed unchanged, labels are named by id (names are refused and would be created instead of restored), and `verify` triggers a read-back. It does not explicitly clarify the messageIds-vs-threadIds split, a minor remaining gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Revert a bulk_modify exactly') and immediately ties it to the artifact it consumes (the `undo` plan). It explicitly distinguishes itself from bulk_modify and modify_labels, so the agent can route without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit USE WHEN ('the user wants a bulk change taken back, and the bulk_modify result carried `undo`') and DO NOT USE ('without a plan from bulk_modify, use bulk_modify or modify_labels', or 'to change labels in a new way'). Alternatives and the selecting condition are named outright.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unsnoozeA
Idempotent

Cancel a snooze: return the thread to the inbox now. SIDE EFFECTS: removes the snooze label, restores INBOX.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
threadIdYes
unsnoozedYes

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses side effects beyond annotations: 'removes the snooze label, restores INBOX'. Annotations already indicate idempotentHint=true and destructiveHint=false, and the description aligns with these, adding valuable behavioral context without contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with two sentences. The first sentence front-loads the primary action, and the second lists side effects. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple one-parameter tool with an output schema (though not shown), the description covers the essential effect. However, it omits any mention of error conditions or prerequisites (e.g., thread must be snoozed). Still, given low complexity and presence of output schema, it is largely complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter threadId has no description in the schema (0% coverage) and the tool description does not add any meaning beyond the name. It fails to explain what threadId represents or its format, which is necessary for correct usage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: 'cancel a snooze' and 'return the thread to the inbox', with a specific verb and resource. It distinguishes from sibling tools like snooze (the reverse operation).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to use the tool (to cancel a snooze) and mentions side effects, but does not explicitly state when not to use it or list alternatives like mark_read or archive. However, the sibling list includes snooze as the inverse, making usage clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unsubscribeA
Idempotent

Unsubscribe from the mailing list a thread came from, via the sender's RFC 8058 one-click endpoint. There is deliberately no URL parameter: the endpoint is taken from the message's own List-Unsubscribe header and nowhere else. Only https one-click endpoints are called (fixed request body, response body discarded); a plain link is reported for the user to open, and a mailto: opt-out is never performed because mailwarden cannot send mail. If the sender offers nothing automatable this returns unsubscribed:false with the alternatives in options — it is not an error. USE WHEN: the user wants off a newsletter. Pair with archive/trash or create_filter to deal with mail already in the mailbox. DO NOT USE: to check whether unsubscribing is possible (use list_unsubscribe — it contacts nobody). A sender already contacted in this session is reported with duplicateOf and NOT contacted again — safe to retry after a timeout. Pass force:true for a deliberate second attempt (e.g. the endpoint answered 500). SIDE EFFECTS: makes an outbound HTTPS request to the sender's unsubscribe endpoint (plus up to 3 redirects) — the only non-Google host mailwarden ever contacts. This confirms to the sender that the address is live, and it cannot be undone. The mailbox itself is not changed.

NameRequiredDescriptionDefault
forceNo
threadIdYes

Output Schema

NameRequiredDescription
urlNo
fromYes
reasonNo
statusNo
optionsYes
threadIdYes
messageIdYes
duplicateOfNo
unsubscribedYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false), the description discloses crucial side effects: makes an outbound HTTPS request, confirms the address is live, cannot be undone, and never modifies the mailbox. It also details the duplicateOf behavior and force:true for retries. No contradictions with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but well-organized with 'USE WHEN', 'DO NOT USE', and 'SIDE EFFECTS' sections. Every sentence adds value: security protocol, lacking URL parameter, redirect limits, non-error on failure. Despite length, it remains front-loaded and efficiently structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (external network call, alternative handling, duplicate prevention), the description covers all aspects: side effects, alternatives, failure modes, and output semantics (duplicateOf, options). An output schema exists, so return values are covered. No significant gaps remain.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must explain parameters. It implicitly defines threadId as the thread containing the mailing list email. It explicitly explains force:true for a deliberate second attempt. While it doesn't specify threadId's exact format, it adds meaningful context beyond the bare schema. Slight room for more explicit parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action: 'Unsubscribe from the mailing list a thread came from, via the sender's RFC 8058 one-click endpoint.' It clearly distinguishes from sibling tools (e.g., list_unsubscribe) by emphasizing it actually performs the subscription removal. The scope and method are precise.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit guidance is given: 'USE WHEN: the user wants off a newsletter' and 'DO NOT USE: to check whether unsubscribing is possible (use list_unsubscribe — it contacts nobody).' It also suggests pairing with archive/trash or create_filter for post-processing. This fully covers when and when not to use, with alternative tools named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

untrashA
Idempotent

Restore a thread from Trash. SIDE EFFECTS: removes the TRASH label; user labels are preserved, but INBOX is NOT re-added — use modify_labels (add INBOX) to return it to the inbox.

NameRequiredDescriptionDefault
threadIdYes

Output Schema

NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations (readOnlyHint=false, destructiveHint=false) already indicate mutation and non-destructive nature. Description adds critical context: side effects on labels (TRASH removal, INBOX not re-added). No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: first provides core action, second details side effects and follow-up. No superfluous words. Efficient and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Output schema exists, so return values are documented externally. Description covers side effects, usage, and follow-up. With one simple param and clear annotations, the description is fully adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter 'threadId' with 0% schema description coverage. Description does not explicitly detail the parameter, but context from 'Restore a thread from Trash' implies its purpose. Adequate for a simple string param, but could be more explicit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states 'Restore a thread from Trash.' verb+resource pair. Distinguishes from sibling tools like 'trash' (opposite action) and 'archive' (different destination).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly notes side effects: removes TRASH label, preserves user labels, but does NOT re-add INBOX. Recommends using 'modify_labels (add INBOX)' to return to inbox. Provides clear when-to-use and follow-up action.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

what_changedA
Read-only

What 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.

NameRequiredDescriptionDefault
maxNo
labelIdNo
sinceHistoryIdYes

Output Schema

NameRequiredDescription
addedYes
deletedYes
recordsYes
historyIdYes
truncatedYes
labelsAddedYes
labelsRemovedYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes far beyond the annotations: explains events vs current state, history retention limits, error-on-stale-id behavior, truncation semantics, count completeness, and side-effect-free behavior. No contradiction with readOnlyHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every sentence earns its place: mechanics, semantics, error handling, use cases, and exclusions are all present. Critical context is front-loaded before the extended guidance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a delta-reporting tool with one required parameter, the description covers return shape, state management, error behavior, filter behavior, and alternatives. Even with an output schema present, nothing important is left ambiguous.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description adds deep semantics for sinceHistoryId (how to obtain, carry, and refresh it) and labelId (narrowing to label events). max is not explicitly described, though its schema constraints make it largely self-explanatory.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states exactly what the tool reports — mailbox changes (arrivals, removals, label changes) from Gmail's event log since a held historyId. It explicitly contrasts with search and get_thread, so an agent can distinguish this from siblings immediately.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit USE WHEN guidance for recurring checks and DO NOT USE guidance for first looks and content reading, naming alternatives like triage_digest, search, and get_thread. This is exemplary routing information.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 5 tool updatesv0.23.0
    • Changedbulk_modify2 fields changed
      • addedInput schema / properties / undo
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / undo
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "steps": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "add": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          },
        +          "messageIds": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          },
        +          "remove": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          },
        +          "threadIds": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          }
        +        },
        +        "required": [
        +          "add",
        +          "remove",
        +          "messageIds",
        +          "threadIds"
        +        ],
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "unknown": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "steps",
        +    "unknown"
        +  ],
        +  "type": "object"
        +}
    • Addedexport_thread
    • Addedfollow_ups
    • Changedget_thread1 field changed
      • addedOutput schema / properties / messages / items / properties / authentication / properties / dkimAligned
        Added value: +{
        +  "type": "boolean"
        +}
    • Addedundo_bulk_modify
  2. 1 tool updatev0.21.0
    • Changedcreate_filter2 fields changed
      • addedInput schema / properties / verify
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • changedOutput schema / properties / applied / anyOf
        Previous value: -[
        -  {
        -    "additionalProperties": false,
        -    "properties": {
        -      "capped": {
        -        "type": "boolean"
        -      },
        -      "error": {
        -        "type": "string"
        -      },
        -      "failed": {
        -        "items": {
        -          "additionalProperties": false,
        -          "properties": {
        -            "error": {
        -              "type": "string"
        -            },
        -            "messageIds": {
        -              "items": {
        -                "type": "string"
        -              },
        -              "type": "array"
        -            }
        -          },
        -          "required": [
        -            "messageIds",
        -            "error"
        -          ],
        -          "type": "object"
        -        },
        -        "type": "array"
        -      },
        -      "matchedMessages": {
        -        "type": "number"
        -      },
        -      "query": {
        -        "type": "string"
        -      },
        -      "submittedMessages": {
        -        "type": "number"
        -      },
        -      "submittedThreadCount": {
        -        "type": "number"
        -      }
        -    },
        -    "required": [
        -      "query",
        -      "matchedMessages",
        -      "submittedMessages",
        -      "submittedThreadCount",
        -      "capped",
        -      "failed"
        -    ],
        -    "type": "object"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]New value: +[
        +  {
        +    "additionalProperties": false,
        +    "properties": {
        +      "capped": {
        +        "type": "boolean"
        +      },
        +      "error": {
        +        "type": "string"
        +      },
        +      "failed": {
        +        "items": {
        +          "additionalProperties": false,
        +          "properties": {
        +            "error": {
        +              "type": "string"
        +            },
        +            "messageIds": {
        +              "items": {
        +                "type": "string"
        +              },
        +              "type": "array"
        +            }
        +          },
        +          "required": [
        +            "messageIds",
        +            "error"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      },
        +      "matchedMessages": {
        +        "type": "number"
        +      },
        +      "query": {
        +        "type": "string"
        +      },
        +      "submittedMessages": {
        +        "type": "number"
        +      },
        +      "submittedThreadCount": {
        +        "type": "number"
        +      },
        +      "verified": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "applied": {
        +            "type": "number"
        +          },
        +          "notApplied": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          },
        +          "unverifiable": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          }
        +        },
        +        "required": [
        +          "applied",
        +          "notApplied",
        +          "unverifiable"
        +        ],
        +        "type": "object"
        +      }
        +    },
        +    "required": [
        +      "query",
        +      "matchedMessages",
        +      "submittedMessages",
        +      "submittedThreadCount",
        +      "capped",
        +      "failed"
        +    ],
        +    "type": "object"
        +  },
        +  {
        +    "type": "null"
        +  }
        +]
  3. 5 tool updatesv0.18.0
    • Changedbulk_modify2 fields changed
      • addedInput schema / properties / crossCheck
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / crossChecked
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "capped": {
        +      "type": "boolean"
        +    },
        +    "dropped": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "id": {
        +            "type": "string"
        +          },
        +          "predicate": {
        +            "type": "string"
        +          },
        +          "threadId": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "id",
        +          "threadId",
        +          "predicate"
        +        ],
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "droppedMessages": {
        +      "type": "number"
        +    },
        +    "predicates": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "skipped": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "predicates",
        +    "droppedMessages",
        +    "dropped",
        +    "capped"
        +  ],
        +  "type": "object"
        +}
    • Changedcreate_label3 fields changed
      • addedInput schema / properties / backgroundColor
        Added value: +{
        +  "type": "string"
        +}
      • addedInput schema / properties / textColor
        Added value: +{
        +  "type": "string"
        +}
      • addedOutput schema / properties / color
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "backgroundColor": {
        +      "type": "string"
        +    },
        +    "textColor": {
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "backgroundColor",
        +    "textColor"
        +  ],
        +  "type": "object"
        +}
    • Changedget_profile2 fields changed
      • addedOutput schema / properties / historyId
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "emailAddress",
        -  "messagesTotal",
        -  "threadsTotal"
        -]New value: +[
        +  "emailAddress",
        +  "messagesTotal",
        +  "threadsTotal",
        +  "historyId"
        +]
    • Changedlist_unsubscribe2 fields changed
      • addedOutput schema / properties / bodyCandidates
        Added value: +{
        +  "items": {
        +    "additionalProperties": false,
        +    "properties": {
        +      "evidence": {
        +        "enum": [
        +          "link-text",
        +          "url"
        +        ],
        +        "type": "string"
        +      },
        +      "text": {
        +        "type": "string"
        +      },
        +      "url": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "url",
        +      "evidence",
        +      "text"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "threadId",
        -  "messageId",
        -  "from",
        -  "subject",
        -  "hasUnsubscribe",
        -  "oneClick",
        -  "httpsUrls",
        -  "mailtos"
        -]New value: +[
        +  "threadId",
        +  "messageId",
        +  "from",
        +  "subject",
        +  "hasUnsubscribe",
        +  "bodyCandidates",
        +  "oneClick",
        +  "httpsUrls",
        +  "mailtos"
        +]
    • Addedwhat_changed
  4. 1 tool updatev0.17.0
    • Changedget_thread2 fields changed
      • addedOutput schema / properties / messages / items / properties / authentication
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "alsoReported": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "authservId": {
        +      "type": "string"
        +    },
        +    "dkim": {
        +      "type": "string"
        +    },
        +    "dmarc": {
        +      "type": "string"
        +    },
        +    "headerFrom": {
        +      "type": "string"
        +    },
        +    "mailedBy": {
        +      "type": "string"
        +    },
        +    "otherReports": {
        +      "type": "integer"
        +    },
        +    "returnPath": {
        +      "type": "string"
        +    },
        +    "signedBy": {
        +      "type": "string"
        +    },
        +    "spf": {
        +      "type": "string"
        +    },
        +    "unchecked": {
        +      "const": true,
        +      "type": "boolean"
        +    }
        +  },
        +  "type": "object"
        +}
      • changedOutput schema / properties / messages / items / required
        Previous value: -[
        -  "id",
        -  "threadId",
        -  "labelIds",
        -  "from",
        -  "to",
        -  "subject",
        -  "date",
        -  "snippet"
        -]New value: +[
        +  "id",
        +  "threadId",
        +  "labelIds",
        +  "from",
        +  "to",
        +  "subject",
        +  "date",
        +  "snippet",
        +  "authentication"
        +]
  5. 3 tool updatesv0.15.1
    • Changedbulk_modify9 fields changed
      • addedInput schema / properties / verify
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • removedOutput schema / properties / modifiedMessages
        Removed value: -{
        -  "type": "number"
        -}
      • removedOutput schema / properties / modifiedThreadCount
        Removed value: -{
        -  "type": "number"
        -}
      • removedOutput schema / properties / modifiedThreads
        Removed value: -{
        -  "items": {
        -    "type": "string"
        -  },
        -  "type": "array"
        -}
      • addedOutput schema / properties / submittedMessages
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / submittedThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / submittedThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / verified
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "applied": {
        +      "type": "number"
        +    },
        +    "notApplied": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "unverifiable": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "applied",
        +    "notApplied",
        +    "unverifiable"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "dryRun",
        -  "matchedMessages",
        -  "matchedThreadCount",
        -  "matchedThreads",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "capped",
        -  "unverifiedPredicates",
        -  "failed"
        -]New value: +[
        +  "dryRun",
        +  "matchedMessages",
        +  "matchedThreadCount",
        +  "matchedThreads",
        +  "submittedMessages",
        +  "submittedThreadCount",
        +  "submittedThreads",
        +  "capped",
        +  "unverifiedPredicates",
        +  "failed"
        +]
    • Changedcreate_filter1 field changed
      • changedOutput schema / properties / applied / anyOf
        Previous value: -[
        -  {
        -    "additionalProperties": false,
        -    "properties": {
        -      "capped": {
        -        "type": "boolean"
        -      },
        -      "error": {
        -        "type": "string"
        -      },
        -      "failed": {
        -        "items": {
        -          "additionalProperties": false,
        -          "properties": {
        -            "error": {
        -              "type": "string"
        -            },
        -            "messageIds": {
        -              "items": {
        -                "type": "string"
        -              },
        -              "type": "array"
        -            }
        -          },
        -          "required": [
        -            "messageIds",
        -            "error"
        -          ],
        -          "type": "object"
        -        },
        -        "type": "array"
        -      },
        -      "matchedMessages": {
        -        "type": "number"
        -      },
        -      "modifiedMessages": {
        -        "type": "number"
        -      },
        -      "modifiedThreadCount": {
        -        "type": "number"
        -      },
        -      "query": {
        -        "type": "string"
        -      }
        -    },
        -    "required": [
        -      "query",
        -      "matchedMessages",
        -      "modifiedMessages",
        -      "modifiedThreadCount",
        -      "capped",
        -      "failed"
        -    ],
        -    "type": "object"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]New value: +[
        +  {
        +    "additionalProperties": false,
        +    "properties": {
        +      "capped": {
        +        "type": "boolean"
        +      },
        +      "error": {
        +        "type": "string"
        +      },
        +      "failed": {
        +        "items": {
        +          "additionalProperties": false,
        +          "properties": {
        +            "error": {
        +              "type": "string"
        +            },
        +            "messageIds": {
        +              "items": {
        +                "type": "string"
        +              },
        +              "type": "array"
        +            }
        +          },
        +          "required": [
        +            "messageIds",
        +            "error"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      },
        +      "matchedMessages": {
        +        "type": "number"
        +      },
        +      "query": {
        +        "type": "string"
        +      },
        +      "submittedMessages": {
        +        "type": "number"
        +      },
        +      "submittedThreadCount": {
        +        "type": "number"
        +      }
        +    },
        +    "required": [
        +      "query",
        +      "matchedMessages",
        +      "submittedMessages",
        +      "submittedThreadCount",
        +      "capped",
        +      "failed"
        +    ],
        +    "type": "object"
        +  },
        +  {
        +    "type": "null"
        +  }
        +]
    • Changedget_thread2 fields changed
      • changedOutput schema / properties / messages / items / required
        Previous value: -[
        -  "id",
        -  "threadId",
        -  "labelIds",
        -  "from",
        -  "to",
        -  "subject",
        -  "date",
        -  "snippet",
        -  "plaintextBody",
        -  "htmlBody",
        -  "attachments"
        -]New value: +[
        +  "id",
        +  "threadId",
        +  "labelIds",
        +  "from",
        +  "to",
        +  "subject",
        +  "date",
        +  "snippet"
        +]
      • addedOutput schema / properties / metadataOnly
        Added value: +{
        +  "const": true,
        +  "type": "boolean"
        +}
  6. 1 tool updatev0.14.0
    • Changedunsubscribe2 fields changed
      • addedInput schema / properties / force
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / duplicateOf
        Added value: +{
        +  "type": "string"
        +}
  7. 5 tool updatesv0.11.0
    • Changedbulk_modify7 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / labelsToCreate
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / matchedThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / matchedThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / unverifiedPredicates
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "matchedMessages",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "capped",
        -  "failed"
        -]New value: +[
        +  "dryRun",
        +  "matchedMessages",
        +  "matchedThreadCount",
        +  "matchedThreads",
        +  "modifiedMessages",
        +  "modifiedThreadCount",
        +  "modifiedThreads",
        +  "capped",
        +  "unverifiedPredicates",
        +  "failed"
        +]
    • Changedbulk_unsubscribe5 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / requests
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / results / items / properties / wouldCall
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "requested",
        -  "attempted",
        -  "unsubscribed",
        -  "skippedDuplicates",
        -  "skippedOutOfTime",
        -  "results"
        -]New value: +[
        +  "dryRun",
        +  "requested",
        +  "attempted",
        +  "unsubscribed",
        +  "skippedDuplicates",
        +  "skippedOutOfTime",
        +  "requests",
        +  "results"
        +]
    • Changedsearch2 fields changed
      • addedOutput schema / properties / threads / items / properties / signals
        Added value: +{
        +  "items": {
        +    "enum": [
        +      "newsletter",
        +      "automated",
        +      "calendar",
        +      "replyToMismatch"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / properties / threads / items / required
        Previous value: -[
        -  "threadId",
        -  "messageCount",
        -  "from",
        -  "subject",
        -  "date",
        -  "labelIds",
        -  "snippet",
        -  "hasAttachments"
        -]New value: +[
        +  "threadId",
        +  "messageCount",
        +  "from",
        +  "subject",
        +  "date",
        +  "labelIds",
        +  "snippet",
        +  "hasAttachments",
        +  "signals"
        +]
    • Changedsweep_snoozed8 fields changed
      • addedInput schema / $schema
        Added value: +"http://json-schema.org/draft-07/schema#"
      • addedInput schema / additionalProperties
        Added value: +false
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dueLabels
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / dueThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / dueThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "date",
        -  "wokenCount",
        -  "woken",
        -  "failedCount",
        -  "errors"
        -]New value: +[
        +  "date",
        +  "dryRun",
        +  "dueLabels",
        +  "dueThreadCount",
        +  "dueThreads",
        +  "wokenCount",
        +  "woken",
        +  "failedCount",
        +  "errors"
        +]
    • Changedtriage_digest4 fields changed
      • addedOutput schema / properties / signals
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "automated": {
        +      "type": "number"
        +    },
        +    "calendar": {
        +      "type": "number"
        +    },
        +    "newsletter": {
        +      "type": "number"
        +    },
        +    "replyToMismatch": {
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "newsletter",
        +    "automated",
        +    "calendar",
        +    "replyToMismatch"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / properties / topSenders / items / properties / signals
        Added value: +{
        +  "items": {
        +    "enum": [
        +      "newsletter",
        +      "automated",
        +      "calendar",
        +      "replyToMismatch"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / properties / topSenders / items / required
        Previous value: -[
        -  "sender",
        -  "name",
        -  "count",
        -  "unread"
        -]New value: +[
        +  "sender",
        +  "name",
        +  "count",
        +  "unread",
        +  "signals"
        +]
      • changedOutput schema / required
        Previous value: -[
        -  "query",
        -  "sampled",
        -  "hasMore",
        -  "unread",
        -  "withAttachments",
        -  "byAge",
        -  "topSenders",
        -  "topLabels"
        -]New value: +[
        +  "query",
        +  "sampled",
        +  "hasMore",
        +  "unread",
        +  "withAttachments",
        +  "byAge",
        +  "signals",
        +  "topSenders",
        +  "topLabels"
        +]
  8. 4 tool updatesv0.9.0
    • Addedbulk_unsubscribe
    • Addedlist_subscriptions
    • Addedlist_unsubscribe
    • Addedunsubscribe
  9. 8 tool updatesv0.6.0
    • Changedbulk_modify1 field changed
      • addedInput schema / properties / query / minLength
        Added value: +1
    • Addedcreate_filter
    • Addedcreate_label
    • Addeddelete_filter
    • Addedget_profile
    • Addedlist_filters
    • Changedsnooze2 fields changed
      • addedInput schema / properties / until / minLength
        Added value: +1
      • removedInput schema / properties / until / pattern
        Removed value: -"^\\d{4}-\\d{2}-\\d{2}$"
    • Addedtriage_digest
  10. 1 tool updatev0.1.9
    • Changedbulk_modify2 fields changed
      • addedOutput schema / properties / capped
        Added value: +{
        +  "type": "boolean"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "matchedMessages",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "failed"
        -]New value: +[
        +  "matchedMessages",
        +  "modifiedMessages",
        +  "modifiedThreadCount",
        +  "modifiedThreads",
        +  "capped",
        +  "failed"
        +]
  11. 15 tool updatesv0.1.8
    • First observedarchive
    • First observedbulk_modify
    • First observeddownload_attachment
    • First observedget_thread
    • First observedlist_labels
    • First observedlist_snoozed
    • First observedmark_read
    • First observedmark_unread
    • First observedmodify_labels
    • First observedsearch
    • First observedsnooze
    • First observedsweep_snoozed
    • First observedtrash
    • First observedunsnooze
    • First observeduntrash

TDQS

A4.1/5.0

Scored across 29 tools

Disambiguation4/5

Most tools have clearly distinct purposes, and descriptions include explicit USE WHEN / DO NOT USE guidance that resolves most boundaries. Redundancy remains at the edges: list_unsubscribe vs unsubscribe vs bulk_unsubscribe and modify_labels vs bulk_modify vs archive/mark_read can be confused despite the notes.

Naming Consistency4/5

Strong, mostly consistent verb_noun convention (list_subscriptions, get_thread, create_label, delete_filter, mark_read). A few names lack verbs (snooze, archive, trash, search, what_changed, follow_ups), which is a minor deviation from the pattern but still readable.

Tool Count3/5

29 tools exceeds the comfortable scoped range; a Gmail server needs many but this surface is heavy, with dedicated single-purpose tools (archive, mark_read, mark_unread, trash, untrash) that overlap with modify_labels and bulk_modify.

Completeness4/5

Coverage is broad across read, search, bulk modify, unsubscribe, snooze, attachments, filters and history, with only send/drafts and a few edge operations missing (by design). Minor gaps an agent can work around.

Maintenance

ActivityActive
ResponsivenessSlow

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that enables Gmail integration, allowing users to manage emails (send, receive, read, trash, mark as read) directly through MCP clients like Claude Desktop.
    1
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Manage your emails effortlessly with a standardized interface for drafting, sending, retrieving, and organizing messages. Streamline your email workflow with complete Gmail API coverage, including label and thread management.
    64
    481 npm
    62
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Gmail MCP server — scope-gated tools (readonly / send / modify), path jails for attachments + downloads, hardened OAuth credentials, Sigstore-signed releases.
    983 npm
    16
    MIT
  • A
    license
    A
    quality
    F
    maintenance
    A Gmail MCP server with native multi-account support, enabling management of multiple Gmail accounts from a single server instance.
    7
    6
    MIT