Add create_draft tool and allow_send toggle per account
- create_draft: saves email as draft in IMAP Drafts folder - allow_send: global config toggle (default true), overridable per account - When sending disabled, send_email returns error suggesting create_draft
This commit is contained in:
@@ -4,7 +4,8 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com).
|
||||
|
||||
## Features
|
||||
|
||||
- **11 MCP tools**: search, read, send, archive, move, mark, list/create/rename/delete folders, server info
|
||||
- **12 MCP tools**: search, read, send, draft, archive, move, mark, list/create/rename/delete folders, server info
|
||||
- **Send toggle**: Disable `send_email` globally or per account — agents use `create_draft` instead
|
||||
- **IMAP IDLE watcher**: Real-time monitoring of new emails, forwarded to Poke automatically
|
||||
- **Multi-account support**: Configure multiple email accounts in a single config file
|
||||
- **Bearer token auth**: Secure the server with `MCP_API_KEY` so only you can use it
|
||||
@@ -96,7 +97,8 @@ docker run -d \
|
||||
|------|-------------|
|
||||
| `search_emails` | Search by from, to, subject, date range |
|
||||
| `read_email` | Read full email content by UID |
|
||||
| `send_email` | Send email with text/HTML, CC/BCC, reply threading |
|
||||
| `send_email` | Send email with text/HTML, CC/BCC, reply threading (can be disabled) |
|
||||
| `create_draft` | Save email as draft for review before sending |
|
||||
| `archive_email` | Move email to Archive folder |
|
||||
| `move_email` | Move email between folders |
|
||||
| `mark_email` | Set read/unread/flagged/unflagged |
|
||||
|
||||
@@ -7,6 +7,10 @@ poke_api_key: "your-api-key-here"
|
||||
# MCP_API_KEY is set via environment variable (not in this file)
|
||||
# It secures the MCP server so only you can use it
|
||||
|
||||
# Global send toggle — set to false to disable send_email for all accounts
|
||||
# Agents can still use create_draft. Override per account with allow_send.
|
||||
allow_send: true
|
||||
|
||||
accounts:
|
||||
# Minimal — SMTP falls back to IMAP host/credentials
|
||||
- id: personal
|
||||
|
||||
@@ -85,6 +85,7 @@ def parse_accounts(config: dict) -> list[dict]:
|
||||
}
|
||||
]
|
||||
|
||||
global_allow_send = config.get("allow_send", True)
|
||||
required = ("imap_host", "imap_username", "imap_password")
|
||||
for i, acc in enumerate(accounts):
|
||||
acc.setdefault("id", f"account-{i}")
|
||||
@@ -95,6 +96,7 @@ def parse_accounts(config: dict) -> list[dict]:
|
||||
acc.setdefault("smtp_port", 587)
|
||||
acc.setdefault("smtp_username", acc.get("imap_username"))
|
||||
acc.setdefault("smtp_password", acc.get("imap_password"))
|
||||
acc.setdefault("allow_send", global_allow_send)
|
||||
for field in required:
|
||||
if field not in acc:
|
||||
raise RuntimeError(
|
||||
@@ -229,6 +231,18 @@ def detect_archive_folder(client: IMAPClient) -> str:
|
||||
return "Archive"
|
||||
|
||||
|
||||
def detect_drafts_folder(client: IMAPClient) -> str:
|
||||
import imapclient as imc
|
||||
|
||||
result = client.find_special_folder(imc.DRAFTS)
|
||||
if result:
|
||||
return result
|
||||
for name in ("Drafts", "[Gmail]/Drafts", "INBOX.Drafts"):
|
||||
if client.folder_exists(name):
|
||||
return name
|
||||
return "Drafts"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Poke webhook
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -568,6 +582,11 @@ async def send_email(
|
||||
accounts = ctx.lifespan_context["accounts"]
|
||||
acc = resolve_account(accounts, account_id)
|
||||
|
||||
if not acc.get("allow_send", True):
|
||||
return {
|
||||
"error": f"Sending is disabled for account '{acc['id']}'. Use create_draft instead."
|
||||
}
|
||||
|
||||
def _send():
|
||||
msg = MIMEMultipart("alternative") if html else MIMEText(body)
|
||||
if html:
|
||||
@@ -627,6 +646,49 @@ async def send_email(
|
||||
return await asyncio.to_thread(_send)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Save an email as a draft for review before sending. The draft appears in the account's Drafts folder."
|
||||
)
|
||||
async def create_draft(
|
||||
ctx: Context,
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
account_id: Optional[str] = None,
|
||||
cc: Optional[str] = None,
|
||||
bcc: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
) -> dict:
|
||||
accounts = ctx.lifespan_context["accounts"]
|
||||
acc = resolve_account(accounts, account_id)
|
||||
|
||||
def _draft():
|
||||
msg = MIMEMultipart("alternative") if html else MIMEText(body)
|
||||
if html:
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
msg.attach(MIMEText(html, "html"))
|
||||
|
||||
msg["From"] = acc["smtp_username"]
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
if cc:
|
||||
msg["Cc"] = cc
|
||||
if bcc:
|
||||
msg["Bcc"] = bcc
|
||||
|
||||
client = get_imap_client(acc)
|
||||
try:
|
||||
drafts_folder = detect_drafts_folder(client)
|
||||
if not client.folder_exists(drafts_folder):
|
||||
client.create_folder(drafts_folder)
|
||||
client.append(drafts_folder, msg.as_bytes(), flags=[b"\\Draft", b"\\Seen"])
|
||||
return {"success": True, "folder": drafts_folder}
|
||||
finally:
|
||||
client.logout()
|
||||
|
||||
return await asyncio.to_thread(_draft)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Archive an email by moving it to the Archive folder instead of deleting."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user