spark-mcp-remote
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@spark-mcp-remotesearch my inbox for invoices from Acme"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 accountsIt 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 startCheck 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.0From another device on the tailnet:
curl -s http://your-mac.your-tailnet.ts.net:8787/healthzHOST 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:8787puts a realts.netcertificate in front (tailnet-only), andtailscale funneldoes 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 flowClaude 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.
Option A: supervisord (web UI, recommended)
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 statusUI 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.plistThe bridge respawns the upstream process if it dies (e.g. Spark restarted).
Safety switches
Env | Default | Effect |
|
| Bind address. This Mac's Tailscale IP serves the tailnet; loopback serves only this machine. Never |
|
| Listening port |
| required | Consent-screen credential and token signing key, ≥ 24 chars. Not a bearer token — rotating it revokes every issued token. |
| off | Hides and blocks |
| off | Enables the |
|
| Point at a different upstream (e.g. the extracted |
| upstream default | Passed through to Readdle's server |
| derived per request from | 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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Your mailboxes in ChatGPT and Claude: Gmail, iCloud, Fastmail, any IMAP. Passwords stay yours.
Connect any mailbox to Claude, ChatGPT & AI: read, send, reply, schedule & search emails.
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
- QuallaaOAuthcom.quallaa
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Related MCP Servers
- AlicenseAqualityDmaintenanceA 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.313 npmMIT
- FlicenseNot gradedqualityDmaintenanceA 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-
- AlicenseAqualityCmaintenanceMCP 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.61MIT
- FlicenseNot gradedqualityCmaintenanceLocal IMAP/SMTP MCP server that lets Claude read, search, draft, send, flag, and move mail across multiple IMAP mailboxes. Credentials stay on your machine.-