SMS Bridge — self-hosted BETA how-to
Snapshot: 1.1.0-beta (see VERSION). Not 1.2.0.
Status: experimental personal/self-host beta packaging.
Not a public product launch. Not security-reviewed. Not available on Google Play / app stores.
Licensing: decision pending — this tree does not grant open-source, commercial, or redistribution rights; third-party Android/Python/OS components keep their own licenses. See LICENSE-DECISION-PENDING.txt.
This guide packages what was recoverable without touching a live phone bridge, live reverse proxy, or personal message data.
What this is
A single-owner, self-hosted relay:
1. Android companion app (sideloaded) on one phone you control
2. Your Linux Python bridge + SQLite on a host you control
3. HTTPS front door (e.g. Caddy) on a dedicated hostname you control
4. Local stdio MCP adapter on the assistant box, holding an access key in a protected file
5. Assistant connector registration (account-local; may apply to multiple bots under the same owner — confirm in UI)
There is no multi-tenant hosted service in this design. Do not share one MCP access key or one device pairing across unrelated people.
Verified vs not (honest)
| Capability | Evidence in this **1.1.0** snapshot |
|---|---|
| Individual SMS send through phone | Verified once on operator's Samsung (self-send test) |
| SMS + MMS **text** read, including **group MMS** participants / `is_group` / protocols | Verified on **one** Samsung after 1.1.0 (not claimed universal) |
| Feature scope | SMS + MMS text only; single-recipient SMS send; attachment **types** may list, bytes not retrieved; other chat transports out of scope |
| MMS image/attachment retrieval | **Not in 1.1.0** (tracked separately as 1.2.0 work) |
| Auto-start after boot / update / app-open | **Best-effort only** in 1.1.0 (`BootReceiver`); after APK updates reopen + **Start bridge**. Improved auto-start is 1.2.0 work — not claimed here |
| HTTP→HTTPS redirect quirks | Seen on one personal deployment; **not** a generic claim |
| Security review | Informal threat model only — **not** a formal audit |
Unit tests in android-companion/ cover pure MsgLogic merge/address helpers only.
Architecture
[Assistant / Grok Bot]
| stdio MCP (local adapter + token file)
v
[Your HTTPS hostname] --Caddy/TLS--> [127.0.0.1 Python bridge + SQLite]
^ |
| long-poll /device/poll + /device/result
+---------------- [Android companion app on phone] ------------------+
- Transport encryption: TLS to your hostname. Not end-to-end encryption of SMS bodies between phone and assistant.
- Device token: stored on phone encrypted with Android Keystore AES-GCM.
- Send path: assistant must obtain explicit user approval before
send_smswithconfirm=true(product policy + app check). - Dedup: phone journals command IDs before radio send; never blind-retries in-flight sends.
Prerequisites
- Android phone with SMS (tested path: Samsung sideload). minSdk 26 / target 35 in this tree.
- Linux host with Python 3, ability to run a local service + SQLite.
- Public DNS name dedicated to this bridge + reverse proxy that terminates TLS.
- Ability to sideload APKs ("Install unknown apps") and, on Samsung/One UI when SMS is blocked, Allow restricted settings.
- Assistant environment that can run a custom MCP command (python3 + adapter script + env).
Bundle layout
HOW_TO_SELF_HOST_BETA.md ← this file
LICENSE-DECISION-PENDING.txt
THIRD_PARTY_NOTICES.md
MANIFEST.sha256
android-companion/ ← sanitized source (package org.smsbridge.companion)
binaries/SMSBridge-Companion-1.1.0-beta.apk ← optional prebuilt companion
server/ ← sanitized Python bridge, pair/revoke, tests, unit + Caddy templates
mcp-adapter/sms-bridge-mcp-stdio.py
docs/PHONE_API_CONTRACT.md
docs/THREAT_MODEL.md
docs/ROLLBACK.md
scripts/bootstrap-mcp-token.sh
scripts/run-isolated-server-tests.sh
scripts/security-scan.sh
EXCLUSIONS.md
Excluded on purpose: personal APK signed for a private operator package id, keystores, token files, SQLite DBs, message logs, contacts, live Caddy with real host/IP, build caches.
1) Build the companion APK (recommended)
Personal prebuilt APKs defaulted to a real hostname and a personal package/signature — do not redistribute those.
From android-companion/:
./run-unit-tests.sh
./build.sh
# → out/SMSBridge-Companion-1.1.0-beta.apk
build.sh creates a new keystore under ~/.secrets/smsbridge-beta-companion/ by default (outside the ZIP). Keep the .jks / pass private.
Default server URL in the companion is empty — the operator types https://their-host before pairing.
Alternative: source-only
If you cannot build here, ship source + these instructions; recipients build with Android build-tools 35 + JDK 17.
2) Server + HTTPS
Sources are in server/ (Python 3 stdlib only).
mkdir -p ~/sms-bridge && cp server/server.py server/pair.sh server/revoke.sh ~/sms-bridge/
chmod 755 ~/sms-bridge/*.sh
export SMSB_HOME=~/sms-bridge
# Generate MCP token + hash (writes mode-0600 files; do not paste token into chat)
./scripts/bootstrap-mcp-token.sh
# Optional: copy hash only to the bridge host if token file lives elsewhere
mkdir -p ~/.config/systemd/user
cp server/systemd/sms-bridge.service ~/.config/systemd/user/
# Edit the unit: set SMSB_PROXY to your reverse-proxy peer IP when proxied (often 127.0.0.1)
systemctl --user daemon-reload
systemctl --user enable --now sms-bridge.service
Reverse proxy: start from server/caddy/Caddyfile.example with a dedicated hostname.
SMSB_PROXY defaults to empty (fail-closed) — X-Forwarded-For is ignored until you set it.
If you omit SMSB_PROXY behind a proxy on a private IP, LAN test routes (/api/health, /) can leak on the public hostname — set it.
Public paths only: /status, /device/* (device bearer), /mcp (MCP bearer).
Localhost admin: pair.sh / revoke.sh → /admin/*.
Phone API: docs/PHONE_API_CONTRACT.md.
Isolated synthetic tests (no phone): ./scripts/run-isolated-server-tests.sh.
3) Install on Android (sideload)
1. Transfer the companion APK to the phone; allow install from that source.
2. Open App info for SMS Bridge.
3. Grant SMS / Phone / Notifications as prompted.
4. If allowing SMS says the app was denied and suggests restricted access:
- Leave Permissions, return to the main App info screen
- Use the ⋮ / three-dot menu → Allow restricted settings
- Authenticate, then grant SMS again
5. Battery → Unrestricted (OEM names vary).
6. Enter your HTTPS base URL (no path) and the one-time pairing code from the server.
7. Tap Pair this phone, then Start bridge.
8. Confirm the persistent foreground notification ("SMS Bridge running").
After every APK update: reopen the app, confirm version, tap Start bridge again (service may not auto-resume).
4) Register the MCP connector
1. Place mcp-adapter/sms-bridge-mcp-stdio.py on the assistant machine.
2. Write the MCP token to a protected file (e.g. chmod 600).
3. Register a custom MCP command approximately like:
command: python3
args: ["/absolute/path/sms-bridge-mcp-stdio.py"]
env:
SMSB_MCP_URL=https://smsbridge.example/mcp
SMSB_MCP_TOKEN_FILE=/absolute/path/mcp-access.token
4. Confirm registration in the product UI. Connector config may be account-local (shared across that owner's bots) — verify before enabling broadly.
5. Wait until tools finish loading; then call health/status first.
Never paste device tokens, pairing codes, or MCP keys into chat.
5) Smoke test (with consent)
1. Health / status — phone online, no send.
2. List threads — expect SMS and (when present) MMS; group threads show is_group / multiple participants.
3. Read one thread the owner named — use completion/status polling if a command was queued; do not blindly re-issue duplicates.
4. Send only after explicit owner approval of recipient + exact body.
Verification codes and OTPs: keep out of chat and durable logs whenever possible.
6) Operations notes
- One phone account per self-hosted bridge is the intended model.
- Rotate MCP keys and revoke devices on loss/theft.
- Offline phone / airplane mode / process death → assistant commands wait or fail; sends left mid-flight become
unknown, not retried. - Reboot/update: see 1.1.0 auto-start row above (manual Start still expected).
7) Rollback / uninstall
See docs/ROLLBACK.md.
Threat model (summary)
See docs/THREAT_MODEL.md. Short version: anyone who can call /mcp with your key, or control the phone app / server disk, can read SMS/MMS text the phone can see and can queue sends the product policy allows. TLS protects the path; it is not E2EE. Sideloaded SMS apps are sensitive — treat the APK signing key as critical.
Remaining gaps (honest)
1. Operator license choice still pending (LICENSE-DECISION-PENDING.txt).
2. Broader device testing beyond one Samsung.
3. Formal security review — not done.
4. Distribution decision (direct APK only vs any store attempt — Play SMS policy is hostile to this use case).
5. Not a multi-tenant hosted product; each owner self-hosts one phone account.