My AI assistant runs on a remote server. My Apple Mail, Calendar, and Messages live on my Mac. Getting them to talk to each other took an MCP server, some AppleScript, and an SSH reverse tunnel — and it works surprisingly well.
The Problem
I run an AI assistant on a remote EC2 instance. It's persistent, always-on, and handles scheduled tasks, cron jobs, and multi-step automations. But macOS services — Mail, Calendar, Messages — are locked to the local machine by design. AppleScript won't work over SSH, and there's no official API for Apple Messages.
The naive solution is to move everything to the cloud. The practical solution is to bring the cloud to the Mac via an MCP server.
What I Built
clawMCP is a TypeScript MCP server that exposes 12 tools across three Apple services:
- Mail — list mailboxes, list/read/search messages
- Calendar — list calendars, query events, create events, find free slots
- Messages — list chats, read message history, send iMessages
It runs locally on my Mac as a launchd service, listening on localhost:3100. The remote AI instance connects to it through an SSH reverse tunnel that maps the remote localhost:3100 back to the Mac's port.
macOS (local machine) EC2 (remote AI host)
+---------------------+ +---------------------+
| clawMCP server | <--SSH-- | AI assistant |
| port 3100 | tunnel | MCP client |
| AppleScript -> Mail | | SSE at :3100 |
| AppleScript -> Cal | +---------------------+
| sqlite3 -> chat.db |
+---------------------+
Implementation Notes
Apple Mail and Calendar are handled via osascript — compiled AppleScript executed from Node.js. It's not elegant, but it's reliable and requires no third-party dependencies.
Apple Messages is different. iMessage doesn't expose an AppleScript API for reading messages (only sending). Instead, I read directly from ~/Library/Messages/chat.db — a SQLite database that stores the full local message history. A query joining message, chat, and handle tables gets you everything you need.
const messages = db.prepare(`
SELECT m.text, m.is_from_me, h.id as sender, m.date
FROM message m
JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
JOIN chat c ON c.ROWID = cmj.chat_id
JOIN handle h ON h.ROWID = m.handle_id
WHERE c.chat_identifier = ?
ORDER BY m.date DESC
LIMIT ?
`).all(chatId, limit);
The tunnel is a simple persistent SSH connection with RemoteForward 3100 localhost:3100. A startup script launches the server and the tunnel together; launchd restarts both if either crashes.
What It Enables
With clawMCP connected, my AI assistant can:
- Check my calendar before scheduling anything
- Read and search my email without me copy-pasting threads
- Look up recent iMessage history for context
- Send me iMessages as a notification channel when long-running tasks complete
The last one is genuinely useful. When a 20-minute build finishes or a data pipeline completes, it pings me via iMessage. No email, no Slack — just a message on my phone from my AI.
Lessons Learned
MCP is the right abstraction here. It gives you tool discovery, type-safe parameters, and a standard transport layer. Building this as a raw HTTP API would have worked but required more glue code on the AI side.
SQLite is surprisingly powerful for this use case. Direct database reads are faster and more flexible than AppleScript for Messages. Just be careful with Full Disk Access permissions — macOS will silently fail without them.
The SSH tunnel is simpler than it sounds. One line in ~/.ssh/config, one RemoteForward directive, and it just works. No VPN, no port forwarding on the router, no cloud relay service.
If your AI lives somewhere other than your local machine, an MCP server + SSH reverse tunnel is a clean pattern for bridging local services. The code is all TypeScript, the surface area is small, and the result is an assistant that actually knows what's on your calendar.
Top comments (0)