From 4d2e40455d412a71acae8af0657492042635942d Mon Sep 17 00:00:00 2001 From: Hara Date: Sun, 3 May 2026 07:14:42 +0200 Subject: [PATCH] hara-gmail-mcp: add mark_read and archive tools (v0.2.0) Adds two write-capable IMAP tools: - gmail_mark_read: sets \Seen flag on a message - gmail_archive: copies to [Gmail]/All Mail and removes from INBOX The IMAP connection already used SELECT (read-write mode); this just exposes the mutation surface through MCP. Co-Authored-By: Claude Sonnet 4.6 --- nixos/pkgs/hara-gmail-mcp/default.nix | 2 +- nixos/pkgs/hara-gmail-mcp/pyproject.toml | 2 +- .../src/hara_gmail_mcp/imap_client.py | 28 +++++++++++++ .../src/hara_gmail_mcp/server.py | 39 +++++++++++++++++-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/nixos/pkgs/hara-gmail-mcp/default.nix b/nixos/pkgs/hara-gmail-mcp/default.nix index e82523e..6d62d10 100644 --- a/nixos/pkgs/hara-gmail-mcp/default.nix +++ b/nixos/pkgs/hara-gmail-mcp/default.nix @@ -6,7 +6,7 @@ python3Packages.buildPythonApplication { pname = "hara-gmail-mcp"; - version = "0.1.0"; + version = "0.2.0"; pyproject = true; src = ./.; nativeBuildInputs = [ python3Packages.setuptools ]; diff --git a/nixos/pkgs/hara-gmail-mcp/pyproject.toml b/nixos/pkgs/hara-gmail-mcp/pyproject.toml index b2a985e..fb1db6d 100644 --- a/nixos/pkgs/hara-gmail-mcp/pyproject.toml +++ b/nixos/pkgs/hara-gmail-mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hara-gmail-mcp" -version = "0.1.0" +version = "0.2.0" description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)" requires-python = ">=3.11" dependencies = [ diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py index 34f2e29..de204e6 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py @@ -153,6 +153,34 @@ def read_email( ) +def mark_read( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Mark a message as read by adding the \\Seen flag.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("store", uid, "+FLAGS", r"(\Seen)") + + +def archive( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Archive a message: copy to All Mail then delete from INBOX.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("copy", uid, "[Gmail]/All Mail") + conn.uid("store", uid, "+FLAGS", r"(\Deleted)") + conn.expunge() + + def _fetch_summary(conn: imaplib.IMAP4_SSL, uid: str) -> MessageSummary: typ, data = conn.uid( "fetch", diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py index 797d41c..0310786 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py @@ -1,14 +1,15 @@ """Hara Gmail MCP server. -Exposes a small toolset for reading and (later) replying to mail across -the configured Gmail accounts. v1 ships read-only tools; reply/archive/label -follow once Hara is using these reliably. +Exposes a small toolset for reading and writing mail across the configured +Gmail accounts. Tools: list_accounts() list configured accounts list_inbox(email, limit) recent messages from an account search(email, query, limit) IMAP SEARCH wrapper read_email(email, uid) full body of one message + mark_read(email, uid) mark a message as read + archive(email, uid) archive a message (remove from INBOX) """ from __future__ import annotations @@ -21,7 +22,7 @@ from dataclasses import asdict from mcp.server.fastmcp import FastMCP from .accounts import AccountStore -from .imap_client import list_inbox, read_email, search +from .imap_client import archive, list_inbox, mark_read, read_email, search logger = logging.getLogger("hara_gmail_mcp") @@ -92,6 +93,36 @@ def gmail_read_email(email: str, uid: str) -> str: return json.dumps(asdict(msg), ensure_ascii=False) +@mcp.tool() +def gmail_mark_read(email: str, uid: str) -> str: + """Mark a message as read (sets the \\Seen flag). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + mark_read(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + +@mcp.tool() +def gmail_archive(email: str, uid: str) -> str: + """Archive a message (copies to All Mail, removes from INBOX). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + archive(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + def main() -> None: logging.basicConfig( level=os.environ.get("HARA_GMAIL_LOG_LEVEL", "INFO"),