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 <noreply@anthropic.com>
This commit is contained in:
parent
8056e510c5
commit
4d2e40455d
4 changed files with 65 additions and 6 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
python3Packages.buildPythonApplication {
|
python3Packages.buildPythonApplication {
|
||||||
pname = "hara-gmail-mcp";
|
pname = "hara-gmail-mcp";
|
||||||
version = "0.1.0";
|
version = "0.2.0";
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
nativeBuildInputs = [ python3Packages.setuptools ];
|
nativeBuildInputs = [ python3Packages.setuptools ];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "hara-gmail-mcp"
|
name = "hara-gmail-mcp"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)"
|
description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
||||||
|
|
@ -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:
|
def _fetch_summary(conn: imaplib.IMAP4_SSL, uid: str) -> MessageSummary:
|
||||||
typ, data = conn.uid(
|
typ, data = conn.uid(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
"""Hara Gmail MCP server.
|
"""Hara Gmail MCP server.
|
||||||
|
|
||||||
Exposes a small toolset for reading and (later) replying to mail across
|
Exposes a small toolset for reading and writing mail across the configured
|
||||||
the configured Gmail accounts. v1 ships read-only tools; reply/archive/label
|
Gmail accounts.
|
||||||
follow once Hara is using these reliably.
|
|
||||||
|
|
||||||
Tools:
|
Tools:
|
||||||
list_accounts() list configured accounts
|
list_accounts() list configured accounts
|
||||||
list_inbox(email, limit) recent messages from an account
|
list_inbox(email, limit) recent messages from an account
|
||||||
search(email, query, limit) IMAP SEARCH wrapper
|
search(email, query, limit) IMAP SEARCH wrapper
|
||||||
read_email(email, uid) full body of one message
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ from dataclasses import asdict
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from .accounts import AccountStore
|
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")
|
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)
|
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:
|
def main() -> None:
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=os.environ.get("HARA_GMAIL_LOG_LEVEL", "INFO"),
|
level=os.environ.get("HARA_GMAIL_LOG_LEVEL", "INFO"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue