{"slug": "my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send", "title": "My AI Drafts My Follow-Up Emails Every Morning. I Still Press Send.", "summary": "A developer extended a custom MCP server so that Claude drafts follow-up sales emails to leads every weekday morning, but deliberately gave the agent no send capability — drafts are written to files and the user reviews and presses send manually. The build required one new tool, save_draft, plus a schema fix adding an email column to the existing SQLite leads table via an ALTER TABLE statement, since the original lead tracker stored names, companies, notes and follow-up dates but no email addresses. The author framed the missing send tool as a hard safety boundary: \"A prompt is a request. A missing tool is a wall.", "body_md": "You know the feeling. A lead goes quiet, you open a blank email, stare at it for a minute, and push it to tomorrow.\n\nHere’s the thing. Knowing *who* to email is rarely the problem. *Actually writing the email* is.\n\nIn part one, I built a tiny MCP server that lets Claude read my leads. It can tell me who’s due for a follow-up. But it can’t help with the bit everyone avoids.\n\nSo this time I gave it one more job. Every weekday morning, it drafts the follow-ups. I read them, fix a line, and press send myself.\n\nYou don’t need part one to follow along. I’ll show every change. It took one new tool and a short script. Plus one slightly embarrassing fix to my own code.\n\nLet’s be honest. The tempting version is a bot that emails your leads on its own while you sleep.\n\nI didn’t build that. On purpose.\n\nA follow-up goes to someone who already gave you their time. Maybe they asked about pricing. Maybe they said “ping me after Diwali.” *That context is the whole email.* If the model gets it wrong, you don’t get a second first impression.\n\nAnd models do get it wrong. Sometimes they invent a detail that sounds right, like a meeting you never had. In a draft, that’s a two-second fix. In a sent email, it’s an apology.\n\nSo the system has one hard line:\n\nThe agent can write as many drafts as it likes. It cannot send a single email.\n\nThat isn’t a rule in the prompt. It’s a rule in the code. There is no send tool, so there’s nothing to misuse.\n\nHere’s why it matters. A prompt is a request. A missing tool is a wall.\n\nI’ll own this one. The lead tracker from part one stores names, companies, notes and follow-up dates.\n\n*It doesn’t store email addresses.*\n\nYes, really. I built a follow-up system with no way to follow up.\n\nHere’s the fix. Add three lines to the end of db() in leads_server.py, just before return conn:\n\n```\ncols = [row[1] for row in conn.execute(\"PRAGMA table_info(leads)\")]    if \"email\" not in cols:        conn.execute(\"ALTER TABLE leads ADD COLUMN email TEXT\")\n```\n\nIt checks the table and adds the column only if it’s missing. Your existing leads stay put. New and old databases both just work.\n\nThen let add_lead take an email:\n\n``` python\n@mcp.tool()def add_lead(name: str, email: str = \"\", company: str = \"\", follow_up: str = \"\", notes: str = \"\") -> str:    \"\"\"Add a new lead. follow_up is a date like 2026-10-08.\"\"\"    with db() as conn:        cur = conn.execute(            \"INSERT INTO leads (name, email, company, follow_up, notes) VALUES (?, ?, ?, ?, ?)\",            (name, email or None, company, follow_up or None, notes),        )    return f\"Added lead #{cur.lastrowid}: {name} ({company})\"\n```\n\nYour old leads still have no address, though. So let update_lead set one too. Replace it with this version:\n\n``` python\n@mcp.tool()def update_lead(lead_id: int, status: str = \"\", follow_up: str = \"\", note: str = \"\", email: str = \"\") -> str:    \"\"\"Update a lead's status, follow-up date or email address, or append a note.\"\"\"    with db() as conn:        lead = conn.execute(\"SELECT * FROM leads WHERE id = ?\", (lead_id,)).fetchone()        if not lead:            return f\"No lead with id {lead_id}\"        conn.execute(            \"UPDATE leads SET status = ?, follow_up = ?, notes = ?, email = ? WHERE id = ?\",            (                status or lead[\"status\"],                follow_up or lead[\"follow_up\"],                (lead[\"notes\"] + \"\\n\" + note).strip() if note else lead[\"notes\"],                email or lead[\"email\"],                lead_id,            ),        )    return f\"Updated lead #{lead_id}\"\n```\n\nNow you can just tell Claude *“Dr. Patel’s email is patel@smiledental.com”* and it fills the gap.\n\nThis is the only new tool. It writes a draft to a file. That’s it.\n\nAdd from email.message import EmailMessage to your imports, plus this line under DB:\n\n```\nDRAFTS = Path(__file__).parent / \"drafts\"\n```\n\nThen add the tool, just above the if __name__ == \"__main__\": line at the bottom:\n\n``` php\n@mcp.tool()def save_draft(lead_id: int, subject: str, body: str) -> str:    \"\"\"Save a follow-up email for this lead as a draft file for a human to review.    This never sends anything. Plain text, under 120 words, no links.\"\"\"    with db() as conn:        lead = conn.execute(\"SELECT * FROM leads WHERE id = ?\", (lead_id,)).fetchone()    if not lead:        return f\"No lead with id {lead_id}\"    if not lead[\"email\"]:        return f\"Lead {lead_id} has no email address. Skipped.\"\nmsg = EmailMessage()    msg[\"To\"] = lead[\"email\"]    msg[\"Subject\"] = subject    msg[\"X-Unsent\"] = \"1\"  # Outlook opens this as an editable draft    msg.set_content(body)\nDRAFTS.mkdir(exist_ok=True)    path = DRAFTS / f\"{date.today().isoformat()}-lead-{lead_id}.eml\"    path.write_bytes(bytes(msg))    return f\"Draft saved: {path.name}\"\n```\n\nTwo small choices here do a lot of work.\n\n**The model never types the email address.** It passes a lead_id, and the tool looks up the address itself. So the agent can't invent one, or send Dr. Patel's email to Dr. Mehta.\n\n**The** **X-Unsent: 1 header.** Double-click the .eml file in Outlook and it opens as a draft you can edit, not a received message. In Apple Mail, open it and use *Message > Send Again*, which gives you an editable copy. Either way, you tweak a line and press send yourself.\n\nCreate morning.py next to your server:\n\n``` python\nimport asyncioimport sysfrom pathlib import Path\npython\nfrom dotenv import load_dotenvfrom langchain.agents import create_agentfrom langchain_mcp_adapters.client import MultiServerMCPClient\nHERE = Path(__file__).parentload_dotenv(HERE / \".env\")  # cron doesn't see your shell's environment\nMODEL = \"anthropic:claude-sonnet-5-5\"  # any tool-calling model works\nRULES = \"\"\"You prepare my follow-up emails each morning.\n1. Call due_follow_ups.2. For each lead, read their notes and write ONE short follow-up.3. Save it with save_draft. Never claim you sent anything.\nStyle: plain English, under 120 words, one clear question at the end.Mention something specific from the notes. No \"just checking in\".If a lead has no notes, skip them and say so.\"\"\"\npython\nasync def main():    client = MultiServerMCPClient({        \"leads\": {            \"command\": sys.executable,            \"args\": [str(HERE / \"leads_server.py\")],            \"transport\": \"stdio\",        }    })    tools = await client.get_tools()\nagent = create_agent(MODEL, tools, system_prompt=RULES)    result = await agent.ainvoke(        {\"messages\": [{\"role\": \"user\", \"content\": \"Prepare today's follow-ups.\"}]}    )    print(result[\"messages\"][-1].text)\nif __name__ == \"__main__\":    asyncio.run(main())\n```\n\nInstall the extras with pip install langchain langchain-mcp-adapters langchain-anthropic python-dotenv. Then put your key in a .env file in the same folder:\n\n```\nANTHROPIC_API_KEY=your-key-here\n```\n\nRun python morning.py once by hand. You should see a short summary, plus fresh .eml files in a drafts folder.\n\nNotice sys.executable. It means \"the same Python that's running this script.\" Remember the full-path headache from part one? This quietly solves it. Whatever Python runs morning.py also runs the server.\n\nOn a Mac or Linux machine, open your crontab with crontab -e and add one line:\n\n```\n0 9 * * 1-5 /full/path/leads-mcp/.venv/bin/python /full/path/leads-mcp/morning.py >> /full/path/leads-mcp/morning.log 2>&1\n```\n\nThat runs it at 9am, Monday to Friday, and keeps a log so you can see what happened.\n\nOn a Mac, if your folder lives in Documents, Desktop or Downloads, give cron Full Disk Access in *System Settings > Privacy & Security*. Otherwise macOS quietly blocks it from reading your files.\n\nOn Windows, Task Scheduler does the same job. Point it at the same Python and script.\n\n**Cron runs in an empty room.** It doesn’t load your shell, so your API key isn’t there. The script works perfectly when you run it by hand, then fails silently at 9am. That’s why morning.py loads .env itself. If something breaks, check morning.log first.\n\n**A sleeping laptop misses the job.** If your Mac is closed at 9am, cron simply skips that day. If that bothers you, use launchd instead. It runs a missed job when the machine wakes up. Or put the whole folder on a tiny cloud server and forget about it.\n\n**Generic notes give you generic emails.** This one isn’t a code problem. If a lead’s note just says “interested,” the model has nothing to work with, and you get the dreaded *“just checking in.”* That’s why the prompt tells it to skip leads with no notes. The fix is upstream: write one specific line after every call. *“Wants it live before Diwali”* turns into a far better email than *“follow up.”*\n\nThe agent didn’t save me from writing emails. It saved me from the *blank page*.\n\nOpening a draft that’s 80% right and fixing one sentence is a different job from starting cold. It’s the difference between editing and writing. Most mornings, editing wins.\n\nAnd because nothing sends without me, I trust it. That trust is the part most automation skips. You can’t scale something you’re nervous about.\n\nHere’s what actually works: let the AI do the part you procrastinate on, and keep the part where your name is on the line.\n\n*Where do you draw the line? Would you let an agent send emails to your leads without checking them first? I’m curious where people land on this. Tell me in the responses.*\n\n*If you missed part one, it’s* *here**. It’s where the lead tracker comes from.*\n\n*Follow me if you want part three. I’m thinking of giving this agent a memory of past emails, so it never repeats itself.*\n\n[My AI Drafts My Follow-Up Emails Every Morning. I Still Press Send.](https://pub.towardsai.net/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send-049cf4bdb06a) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send", "canonical_source": "https://pub.towardsai.net/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send-049cf4bdb06a?source=rss----98111c9905da---4", "published_at": "2026-10-17 13:01:02+00:00", "updated_at": "2026-10-17 13:34:10.223619+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "ai-safety"], "entities": ["Claude", "Anthropic", "MCP", "save_draft", "leads_server.py", "SQLite"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send", "markdown": "https://wpnews.pro/news/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send.md", "text": "https://wpnews.pro/news/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send.txt", "jsonld": "https://wpnews.pro/news/my-ai-drafts-my-follow-up-emails-every-morning-i-still-press-send.jsonld"}}