Skip to main content
Glama
kevincolten

spark-mcp-remote

by kevincolten

spark-mcp-remote

Reach your Spark mailbox from any Claude client on your tailnet.

Spark's official Spark for Claude is a stdio MCP server that shells out to the Spark CLI, which only exists where Spark Desktop is running (macOS/Windows). That's fine on that one machine, useless from anywhere else.

This is a small bridge that spawns Readdle's server as a child process and exposes it over MCP Streamable HTTP, authenticated with OAuth 2.1. Run it on the Mac next to Spark, bind it to that Mac's Tailscale address, and every other device on your tailnet can add the URL as an MCP server. No tunnel, no public hostname, no TLS certificate.

Claude Code / Claude Desktop, any tailnet device
        │  HTTP over WireGuard (Tailscale)
        ▼
spark-mcp-remote  (this)          ← OAuth 2.1 + DCR, send/read-only gates
        │  stdio
        ▼
readdle/spark-claude-extension    ← official, vendored, unmodified
        │  execFile
        ▼
spark CLI  →  Spark Desktop  →  your accounts

It proxies at the MCP level (tools/list, tools/call), so upstream tool changes flow through without touching this repo. It does not run on Linux/Coolify — there is no headless Spark. It has to live on the machine that runs Spark Desktop.

The bridge also adds an email_pdf tool. Give it a message_id or Spark deep link from a search result; it calls Spark's thread tool and returns the complete conversation as a readable PDF embedded in the MCP result. The PDF includes the thread text and attachment listing, while the original attachments remain separate. Chat clients that support MCP binary resources can expose the PDF as a file. When connecting through Austin Devs MCP, its gateway must pass the embedded resource through rather than wrap it in JSON.

Requirements

  • macOS or Windows with Spark Desktop signed in

  • Spark CLI enabled: Spark → Settings → AI Agents → Spark CLI Setup

  • Node ≥ 20.6 (for --env-file)

  • Tailscale on this Mac and on whatever you connect from

Related MCP server: iCloud Email MCP Server

Install

git clone https://github.com/kevincolten/spark-mcp-remote.git
cd spark-mcp-remote
npm install
npm run setup     # clones readdle/spark-claude-extension into vendor/, writes .env with a random token
npm start

Check it:

curl -s localhost:8787/healthz
# /mcp is OAuth-protected: unauthenticated requests must 401 and point at the
# metadata that starts the handshake.
curl -si -X POST localhost:8787/mcp | grep -i 'HTTP/\|www-authenticate'

There is no way to hand-craft a bearer for /mcp — a token has to be issued through the OAuth flow. npm test drives that flow end to end if you want to see it work locally.

Expose it (Tailscale)

There is no tunnel and no ingress to configure. Bind the listener to this Mac's Tailscale address and the tailnet is the network boundary: WireGuard does the encryption, and only devices you have authorised can route to it at all.

tailscale ip -4                 # e.g. 100.87.225.42
tailscale status --json | python3 -c 'import sys,json;print(json.load(sys.stdin)["Self"]["DNSName"])'

Put the address in .env and restart:

HOST=100.87.225.42              # or the MagicDNS name; never 0.0.0.0

From another device on the tailnet:

curl -s http://your-mac.your-tailnet.ts.net:8787/healthz

HOST is the only knob — anything the machine can bind works, so the same server runs loopback-only during development and tailnet-wide in production without a second process in front of it.

Two things worth knowing about this trade:

  • The hosted Claude surfaces can't reach it. claude.ai and the Claude mobile app fetch custom connectors from Anthropic's servers, not from your device, so a tailnet address is unreachable to them. What works is a client running on a tailnet device: Claude Code, Claude Desktop.

  • It is plain HTTP. That is fine here — the transport is already encrypted and authenticated by WireGuard, and OAuth still gates /mcp. If a client insists on TLS, tailscale serve --bg --https=443 http://127.0.0.1:8787 puts a real ts.net certificate in front (tailnet-only), and tailscale funnel does the same thing publicly if you ever do want claude.ai back.

Add to Claude

From any device on the tailnet — no header, no token argument:

claude mcp add --transport http spark http://your-mac.your-tailnet.ts.net:8787/mcp
# then `/mcp` in a session to run the OAuth flow

Claude discovers the bridge's own authorization server, registers itself, and opens a consent screen; paste the SPARK_MCP_TOKEN from .env there and it connects. The callback lands on http://localhost:<port>/callback in the browser on that same device, so nothing has to route back to the Mac.

Claude Desktop's custom connector dialog takes the same URL. If it refuses a plain-http one, front the bridge with tailscale serve (above) and give it the https:// form instead.

Install Spark.skill from the Readdle releases page for the full workflow guidance.

Why OAuth

Nothing on a tailnet requires it — so this is deliberate. /mcp can hand a model your entire mailbox, and the tailnet is a flat network: every device on it, and every process and user on this Mac, can reach the port. A shared secret in a client config is the kind of thing that ends up in a dotfile repo; what travels here instead is a short-lived, audience-bound token, and the long-lived secret never goes on the wire at all.

It also means the exposure decision stays reversible. Put a tailscale funnel in front and the bridge is a working claude.ai custom connector with no code change, because OAuth is the only authentication the hosted Claude surfaces accept — their connector dialog has no static-token field (static_headers is an organization-admin beta).

So the bridge is also its own OAuth 2.1 authorization server (src/oauth.js): RFC 9728 protected-resource metadata, RFC 8414 server metadata, RFC 7591 dynamic client registration, S256 PKCE, audience-bound access tokens and rotating refresh tokens. It adds /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server, /oauth/register, /oauth/authorize and /oauth/token.

There is one user and no user database. SPARK_MCP_TOKEN is the credential the consent screen checks, and it doubles as the HMAC key signing every client_id, authorization code and token — so the server holds no session state (supervisord restarts it on every deploy without logging Claude out) and rotating the secret revokes everything at once.

SPARK_MCP_TOKEN is not accepted as a bearer token on /mcp. Only tokens the server issued are, which keeps the long-lived secret off the wire: what travels on each request is a short-lived, audience-bound token that expires in an hour.

Keep it running (macOS)

Two options. Both must run as your user, never as root — the Spark CLI talks to the Spark Desktop GUI session.

Gives you a local start/stop/restart/tail-logs dashboard for this and any other bare-metal services on the Mac. No containers, no cloud.

brew install supervisor
mkdir -p $(brew --prefix)/etc/supervisor.d
sed "s#/Users/YOU#$HOME#g; s#^user=YOU\$#user=$(whoami)#" supervisor/spark-mcp-remote.ini \
  > "$(brew --prefix)/etc/supervisor.d/spark-mcp-remote.ini"
# merge supervisor/supervisord.conf into $(brew --prefix)/etc/supervisord.conf (inet_http_server + include)
brew services start supervisor       # user-level launchd agent, survives reboots
supervisorctl status

UI is at http://127.0.0.1:9001. Keep it on loopback, or bind it to the Tailscale IP to reach it from your phone — never 0.0.0.0. OAuth protects /mcp, but the supervisor UI can restart things and read logs.

At login this can start before tailscaled has the interface up, so a HOST that is a Tailscale IP is not bindable yet. The server retries the bind instead of exiting, so there is nothing to order here.

Intel Macs: replace /opt/homebrew with /usr/local in the ini files.

Option B: launchd directly

cp launchd/com.kevincolten.spark-mcp-remote.plist ~/Library/LaunchAgents/
# edit WorkingDirectory + node path in the plist
launchctl load ~/Library/LaunchAgents/com.kevincolten.spark-mcp-remote.plist

The bridge respawns the upstream process if it dies (e.g. Spark restarted).

Safety switches

Env

Default

Effect

HOST

127.0.0.1

Bind address. This Mac's Tailscale IP serves the tailnet; loopback serves only this machine. Never 0.0.0.0.

PORT

8787

Listening port

SPARK_MCP_TOKEN

required

Consent-screen credential and token signing key, ≥ 24 chars. Not a bearer token — rotating it revokes every issued token.

SPARK_MCP_READ_ONLY=1

off

Hides and blocks draft, comment, action, contact-action, event

SPARK_MCP_ALLOW_SEND=1

off

Enables the event tool and action send/unschedule. Off by default so a leaked token can't send mail.

SPARK_MCP_ENTRY

vendor/…/server/index.js

Point at a different upstream (e.g. the extracted Spark.mcpb)

SPARK_PATH

upstream default

Passed through to Readdle's server

SPARK_MCP_PUBLIC_URL

derived per request from Host / X-Forwarded-*

Pin the external origin used in OAuth metadata when clients reach the bridge by a name this server never sees

Spark's own per-account access levels (read-only / triage / send in Settings → AI Agents) still apply underneath. This bridge only ever narrows, never widens.

Calls are tagged AI_AGENT=claude-remote in Spark's audit log so you can tell remote from desktop.

Test

npm test   # boots the bridge against a fake stdio upstream; checks auth, tool
           # listing, send gate, and the full OAuth flow (discovery, DCR, PKCE,
           # code replay, audience binding, refresh rotation)

License

MIT. The vendored upstream is MIT © Spark Mail Limited.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server that enables Claude Desktop to manage emails via SMTP and IMAP. Send emails, fetch unread messages, and create draft replies directly from conversations.
    3
    13 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol (MCP) server that enables Claude Desktop to interact with iCloud email accounts. This server provides full email functionality including reading, sending, and managing emails through your iCloud account.
    1
    -
  • A
    license
    A
    quality
    C
    maintenance
    MCP server that connects Claude to Microsoft 365 mail for inbox triage, search, classification, and idempotent rule provisioning via Microsoft Graph, with device-code auth and read-only default.
    6
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Local IMAP/SMTP MCP server that lets Claude read, search, draft, send, flag, and move mail across multiple IMAP mailboxes. Credentials stay on your machine.
    -