← iamidentity.ai/blog
IAMIDENTITY.AI
Agentic AI MCP IBM Verify DPoP Local LLM

The agent that cleans my inbox is small. The idea behind it is not.

I had 60,000+ emails in Gmail and no quick, safe way to clean up what I actually wanted to delete in bulk. So I built a local, free cleanup agent around Qwen 3, a custom MCP server, and an approval flow that keeps risky actions in my hands.

Robert Graham 8 July 2026 · updated 11 July 2026 ~13 min read
Architecture
How the inbox agent is wired

The full request path: you chat with a local model, the agent proposes, the MCP server holds all real authority, IBM Verify decides and approves on your phone, and Gmail only ever sees a trash-only credential. A kill switch revokes the grant and Verify sessions on a suspicious tap.

Personal Gmail Agent local-first · the model proposes, the human commits, identity decides chat reasoning tool call + DPoP proof OIDC login · PKCE · session introspect · cnf.jkt push · Tier 3 gmail.modify only suspicious tap · kill switch You browser · 127.0.0.1 Agent SvelteKit UI + server loop Verify login · httpOnly session preview cards · activity log never calls execute itself LLM brain Ollama · qwen3:14b · local Claude / Gemini optional assumed wrong or lying MCP server THE SECURITY PERIMETER bearer / DPoP gate (RFC 9449) deterministic sensitivity tiers two-phase commit · one-time token trash-only · no delete scope confirm-secret on execute IBM Verify OIDC login + PKCE token introspection DPoP-bound tokens push MFA factors Your phone approve · deny · suspicious Gmail API scope: gmail.modify Trash only · 30-day recovery Kill switch revokes the Google grant deletes Verify sessions tenant-wide SSF/CAEP TRANSMITTER: ROADMAP live request path fires on a suspicious tap or 3 denies in 5 min holds all real authority
Animated diagram
The system at a glance

This gives readers the mental model immediately: Qwen 3 stays on the desktop, the MCP server narrows what the model can do, IBM Verify decides when the action is sensitive, and Gmail only ever sees the constrained credential.

Local desktop

Qwen 3

Reasoning stays on the machine. The mailbox does not leave the desktop for a hosted model.

Control plane

Custom MCP server

Broad intent becomes narrow tools: preview first, commit second, and no blind bulk action.

Identity layer

IBM Verify + DPoP

Credentials are sender constrained and sensitive operations can require explicit approval.

Mailbox

Gmail with gmail.modify

Bulk cleanup stays recoverable. Permanent delete is outside the credential's reach.

Privacy
All local
Cost
Free on desktop
Safety
Preview → commit

I had more than 60,000 emails in Gmail and no quick, safe way to get rid of the clutter I actually wanted to delete in bulk. Newsletters, security codes, promotions, old receipts, and one-off purchase confirmations just kept piling up. The obvious move is to hand the whole thing to a cloud assistant and let it rip. 

The problem was not just volume. It was trust. I wanted a fast way to visualize what was about to happen, see how many messages matched, understand which senders were involved, and avoid waking up the next morning to find that an outside agent had confidently deleted something I needed.

So I built my own. It runs entirely on my laptop, uses Qwen 3 locally, and never sends mailbox content off to a hosted AI service. That matters for privacy, but it also matters for cost: once the machine is sitting on my desk, the intelligence is effectively free. Around that, I wrapped the same identity fabric this series has been building all along: a custom MCP server is the perimeter, IBM Verify decides, the credential is sender constrained, and my phone approves anything sensitive.

Do not ask the model to behave. Make misbehavior impossible.

That line became the design principle for the whole project. The local model was sometimes wrong, occasionally overconfident, and exactly the sort of thing you should never trust with permanent destructive power. That turned out to be fine, because the system was built so the model never needed to be perfect.

01The one decision that makes it safe

Before any model, before any identity layer, the first design choice was the most important one. The agent cannot permanently delete anything. Not because I told it not to. Because it literally cannot.

Gmail gives you two very different ways to remove a message. One moves it to Trash, where it sits for thirty days and can be pulled back. The other bypasses Trash and destroys the message immediately. That second path requires the broad https://mail.google.com/ scope. The narrower gmail.modify scope can still search, label, archive, and move messages to Trash, but the permanent delete call gets a hard refusal.

So I am using gmail.modify and nothing more. In this system, “delete” really means “move to Trash.” It is recoverable for thirty days, reversible with a single call, and enforced by the credential itself rather than by polite prompting.

Trash-only, never delete, stated up front

The two guarantees live on the landing screen, not in a prompt: bulk cleanup only ever moves mail to Trash, and every run goes plan → confirm → execute.

The inbox agent landing screen: 'Clean your inbox, safely.' with two badges, 'Trash-only, never delete' and 'Plan then confirm then execute', plus an inbox summary and agent activity log.
Structural, not behavioral

There is a unit test in the repo whose only job is to fail the build if any source file ever references the permanent delete call or the broad scope. The safety is structural. The test just guards the structure.

This is the whole philosophy in one move. The rest of the build is just the same idea repeated at different layers.

02The model proposes. I commit.

A local model is not magic. Give it a list of hundreds of message IDs and ask it to decide what should go, and eventually it will lose the thread. That is a normal failure mode for a smaller local model, and pretending otherwise is how you end up bulk deleting the wrong thing.

So the model never touches raw message IDs. It works in two phases.

In the first phase it composes a Gmail search, something like “promotions older than ninety days from this sender.” It calls a preview_cleanup tool. The MCP server runs the search, counts the matches, groups them by sender, pulls a few representative samples, and mints a single-use token. Nothing is deleted. Nothing can be deleted yet.

Two-step safety, in the product

The built-in explainer says it plainly: it previews what it would trash first  count, senders, and sample messages and nothing moves until you confirm. Sensitive mail needs an extra step, and the session is protected by IBM Verify.

A 'How this works' panel explaining: what it does (bulk Gmail cleanup to Trash, never permanent delete), two-step safety (preview then confirm), sensitive mail (financial/personal flagged, needs phone approval), and sign-in with IBM Verify.

Only after that do I decide whether the preview looks right. If it does, the system trades that single-use token for the scoped action that moves matching messages to Trash. The model proposes. I commit. That separation is what makes the workflow usable instead of reckless.

Two-phase commit

The model gets to be creative about what to propose. It does not get to be the thing that pulls the trigger.

03Some email is different

Trashing a promotion is one thing. Trashing a bank statement or a message from family is something else entirely. I did not want the model making that distinction on its own.

So the sensitivity decision is deterministic and lives in the MCP server, not in the prompt. Before anything gets trashed, the server inspects senders and subjects. If the sender is a bank, or the subject looks like a statement or payment notice, the message is flagged financial. If the sender is me or someone on my family list, it is flagged personal. Any flagged message in the set pushes the whole operation up a tier.

The model does not get a vote on that. It cannot talk its way into deciding that a financial email is harmless clutter. The rule looks at the raw data and decides.

Why this matters

The risk line should be deterministic. A model can be clever. The policy layer should be boring.

04A login, a session, and a key that stays local

This app runs on localhost, on my own laptop, for exactly one user. It still got a real login. “It’s just local” is how personal tools become the softest target in the house.

So the app is gated behind IBM Verify with authorization code plus PKCE, an httpOnly session, and a server-side session guard. Signing out actually destroys the session, not just the cookie. The token behind that session is DPoP-bound, which means the token is tied to a private key held on the laptop.

If someone steals the token, it is not enough. Presenting it without the matching private key fails. A bearer token works for whoever holds it. A sender-constrained token works only in the hand it was issued for.

A real login, even on localhost

The local app still sits behind IBM Verify authorization code + PKCE, an httpOnly session, and a DPoP-bound token tied to a private key on the laptop.

Sign-in screen for the inbox agent: 'Sign in to your inbox agent' with a 'Sign in with IBM Verify' button and 'Secured by IBM Verify.'
Screenshot
When the session is revoked, the app stops

Signing out or a revoke  actually destroys the session, not just the cookie. The server-side guard refuses to act, even mid-task on sensitive senders like a bank.

The inbox agent showing a session_revoked state in the inbox summary, with recent Tier 3 previews and a bank sender (chase.com) in the agent activity log.

05My phone gets the last word

Reading is quiet. The agent can search and summarize all day. Trashing ordinary clutter is a confirmation step. But the moment an operation touches something flagged financial or personal, the agent has to ask my phone.

This is the tiered part. Tier one is read, no friction. Tier two is a routine trash, with a confirmation card. Tier three is a trash operation that swept up something sensitive, and that triggers an approval through IBM Verify on my phone. I approve on my phone or nothing happens.

That approval gate is not a suggestion the model can route around. The server refuses to execute a tier-three operation until it has a real approval in hand.

The phone gets the last word

When a cleanup sweeps up messages flagged financial or personal, the preview says so and confirming asks for approval on the phone before anything is trashed. Here, four flagged messages were phone-approved, then moved to Trash (recoverable for 30 days).

The inbox agent preview showing sample messages, a '4 messages look financial or personal' warning with Financial tags, a note that confirming will ask for phone approval, and a result: 'Trashed 4 messages · recoverable from Trash for 30 days,' with a PHONE-APPROVED marker in the activity log.

06The model lied to me. It did not matter.

During testing, the local model occasionally acted exactly like a local model. It was overconfident. It invented outcomes. It sometimes described actions that had not happened. That is annoying, but it is only dangerous if the model has real authority.

In this system, it does not. The perimeter does not run on trust. The model cannot permanently delete. It cannot bypass preview. It cannot mint the approval state. And it cannot force the execute path without the exact chain of controls lining up behind it.

The practical lesson

Qwen 3 is smart enough to make this useful on a desktop. The important part is that the walls were designed for the moments when any model is wrong.

07The features kept coming. The walls never moved.

UPDATE
Since I first wrote this, the agent has grown a lot. It archives and labels as well as trashes, I added automation/scheduling. It can clear a sender's backlog while keeping the newest message. Every action has one-click Undo for thirty days. Opening the app now lands on a dashboard: one card per built-in cleanup, each with a count of what is ready and a toggle to run it automatically, daily or weekly.

Here is the part I care about. Not one of those features touched the execute path. The dashboard's Preview button submits the exact same preview request I would type in chat. The routines only read counts; a human still confirms every cleanup. Adding a dozen features to this system meant adding zero new ways to move mail.

Screenshot
The dashboard tells the same truth the perimeter enforces

Each card shows what a cleanup would actually clear. Starred mail and protected senders are already excluded from the number, because an early version once said "2 ready" for two messages the perimeter then correctly refused to touch. The count and the action now share one definition of cleanable.

The inbox agent dashboard: cards for collaboration clutter, space-reclaim pile, orphaned drafts, stale security alerts, and old unread promotions, each with a ready count, a Preview cleanup button, and a Run automatically toggle.

The newest wall is my favorite, because it started as a question rather than a feature request: what about the emails I star? I star mail to keep up front in my inbox and quick access to important email. Nothing in the system knew that.

Now it does, the same way it knows everything else that matters: structurally. A starred message is never bound into a cleanup token at preview, and at execute the server re-checks Gmail's live starred list and drops anything I starred while the confirmation was sitting on screen. If that check fails, the cleanup refuses to run rather than run without it. There is no setting for this and no way to talk the model out of it. A star means keep.

356 proposed. 88 protected. 4 starred. My phone still decides.

One confirmation card carrying the whole model: the proposed set, the mail withheld because senders are protected, the mail withheld because I starred it, the financial flags that push the operation to Tier 3, and the phone approval that follows.

A Tier 3 confirmation card proposing 356 messages for trash, showing 88 kept as protected and 4 kept starred, five messages flagged financial, and the note that trashing will ask for phone approval.
Where this goes

The direction of travel is always the same: move authority further from the model. Next on the list is fronting sensitive actions with an IBM Verify token exchange, so the step-up decision becomes an auditable access policy the agent cannot see or influence at all.

08The point is not Gmail

I built this to clean my inbox. What I actually built was a template for how to let an agent touch something real without pretending the model deserves unconditional trust.

Strip out the Gmail specifics and what is left is a shape: a local model reasons, an MCP server holds the perimeter, identity decides the action, credentials are sender-constrained, and sensitive moves go to a human’s phone. That shape works because it assumes the model will eventually be wrong.

Project note
A practical agent, not a trust exercise
Local Qwen 3 + MCP + Verify + Gmail
The whole point of this build is simple: bulk cleanup should be fast, visual, recoverable, local, and cheap enough to run whenever the inbox gets out of control.

The agent that cleans my inbox is small. The idea behind it is not.