Two Paths to the Same Bell: How Alfred Sends Pushover Notifications

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:

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:

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:

Side by Side

Aspect MCP (Claude CLI) WhatsApp
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.