By James Aspinwall
Alfred can send push notifications to your phone through two completely different interfaces — the Claude CLI (via MCP) and WhatsApp. Both ring the same bell, but the wiring behind the walls is different. Here’s how.
The Shared Core
Both paths converge on the same Elixir module: Pushover. This GenServer wraps the Pushover HTTP API and exposes three entry points:
-
Pushover.send_sync/1— send immediately, wait for confirmation -
Pushover.set/2— schedule for later using natural language (“in 5 minutes”) -
Pushover.set/3— same, but with a title
No matter how the request arrives, it ends up calling one of these functions. The difference is everything that happens before the call.
Path 1: Claude CLI → MCP Server
Claude CLI/Desktop
→ MCP protocol (JSON-RPC over stdio)
→ MyMcpServer.handle_tool_call("pushover_send", args, state)
→ Permissions.Platform.pushover_send(perms, args)
→ permission check (key 60_001 or 11111)
→ Pushover.send_sync(payload)
When you use Claude Code or Claude Desktop, the tool call travels over the Model Context Protocol — a standardized JSON-RPC protocol. The MCP server (MyMcpServer) receives it, looks up the authenticated user’s permission map from the session state, and delegates to Permissions.Platform, which gates access behind permission keys before calling Pushover.
Key characteristics:
- Permission model: AccessControl permission keys (integer-based). Each user has a set of granted keys checked at the business logic layer.
- Full payload: Supports all Pushover fields — message, title, priority, sound, url, url_title.
-
Time context injection: For
pushover_schedule, the tool description is dynamically appended with the user’s timezone and current local time, so the LLM can correctly interpret relative times like “tomorrow at 9am”. - Multi-user: Any authenticated MCP client with the right permission keys can use it.
Path 2: WhatsApp → Anthropic API → Tool Use
WhatsApp message
→ WhatsAppClaude GenServer (debounce buffer)
→ Anthropic Messages API (HTTP POST with tool definitions)
→ Claude returns tool_use response
→ WhatsAppClaude.execute_tool("pushover_send", input)
→ Pushover.send_sync(payload)
The WhatsApp path is fundamentally different. WhatsAppClaude is a GenServer that subscribes to incoming WhatsApp messages via Notifier. When a message arrives from an allowed JID, it buffers it (3-second debounce for multi-message bursts), then sends the conversation history to the Anthropic Messages API directly — not through MCP.
Claude’s response may include a tool_use block requesting to call pushover_send. WhatsAppClaude executes this locally via execute_tool/2, which calls Pushover.send_sync/1 directly — no permission layer, no MCP protocol.
Key characteristics:
-
Access control: JID allowlist (
@allowed_jids). If your WhatsApp number isn’t in the list, Alfred ignores you entirely. No permission keys needed — it’s binary. - Tool loop: If Claude requests a tool call, the result is fed back into the conversation and Claude continues. Multiple tool calls can chain in sequence.
- Reduced payload: Supports message, title, priority, and sound — but not url/url_title (WhatsApp context doesn’t need them).
-
No time injection: The
current_timetool is available separately for Alfred to check the time when needed, rather than embedding it in tool descriptions.
Side by Side
| Aspect | MCP (Claude CLI) | |
|---|---|---|
| Transport | JSON-RPC over stdio | Anthropic HTTP API |
| Auth | Permission keys (AccessControl) | JID allowlist |
| Permission check |
Business logic layer (Permissions.Platform) |
None — implicit trust |
| Tool definitions |
Registered in MyMcpServer |
Module attributes in WhatsAppClaude |
| Tool execution | MCP handler → permission gate → Pushover |
execute_tool/2 → Pushover directly |
| Time awareness | Injected into tool description |
Separate current_time tool |
| Who decides to call it | The external Claude model (CLI/Desktop) | A Claude model called by WhatsAppClaude |
Why Two Paths?
MCP is a protocol for AI tool integration — it’s how Claude Code and Claude Desktop discover and call server-side tools. It’s designed for multi-user systems with proper authentication and authorization.
WhatsApp Claude is a self-contained orchestrator. It receives natural language from WhatsApp, forwards it to the Anthropic API, and when Claude responds with a tool call like pushover_send, WhatsApp Claude executes it locally and feeds the result back. The LLM never touches Pushover directly — it only expresses intent. WhatsApp Claude sits in the middle, serving the LLM the tools it needs and carrying out its decisions.
The Brittleness Problem
The direct-call approach has a cost: WhatsApp Claude is coupled to internal function signatures. Every tool it supports requires a hand-written execute_tool/2 clause that knows the exact function name, argument shape, and return format. If Pushover.set/3 changes its argument order, or Pushover moves to a remote node, WhatsApp Claude breaks — silently — while the MCP tools keep working because they’re maintained in one place.
Every new capability means duplicating work: define the tool schema in WhatsAppClaude, write the execute_tool handler, keep it in sync with the canonical MCP definition. Two tool lists, two execution paths, two places to update when the underlying module evolves.
The MCP server, by contrast, is the single source of truth. Change a tool definition once in MyMcpServer, and every MCP client — Claude CLI, Claude Desktop, or any future client — picks it up automatically. The LLM adapts to new schemas on its own since it reads tool descriptions at call time.
The MCP Client Path Forward
The alternative is for WhatsApp Claude to become an MCP client. Instead of defining its own tools and calling Elixir functions directly, it would connect to MyMcpServer, discover tools through the protocol, and proxy tool-use requests from the Anthropic API through MCP:
WhatsApp message
→ WhatsAppClaude (orchestrator)
→ Anthropic Messages API (tools from MCP discovery)
→ Claude returns tool_use
→ WhatsAppClaude proxies call to MyMcpServer (MCP client)
→ MyMcpServer handles permission check + execution
→ result flows back to Claude
This eliminates duplication entirely. One tool definition, one permission model, one execution path. WhatsApp Claude becomes a thin transport — it handles WhatsApp message buffering, conversation history, and the Anthropic API call, but delegates all tool knowledge to the MCP server. Add a new tool to MyMcpServer and WhatsApp Claude gets it for free.
This aligns with the project’s own design principle: permissions in the logic layer, not the transport. Today, WhatsApp Claude is a transport layer that also does its own tool wiring and access control. Making it an MCP client pushes all of that back to the unified logic layer where it belongs.
The current direct-call approach works and is simpler for a small tool set. But as capabilities grow, the MCP client architecture is the clear direction — one contract, every client benefits.