I spent most of last quarter bouncing between Puerto Ayora, Puerto Baquerizo Moreno, and a research boat that anchored off Isabela. My job was debugging a multi-agent refactor pipeline I had no intention of pausing just because I was looking at frigatebirds instead of pull requests. The Charles Darwin Research Station hands out a flaky captive-portal Wi-Fi that drops every 12 minutes and a satellite uplink that goes from "usable" to "dead" the moment the salt spray hits the antenna. So I built an offline-first MCP server with a persistent bridge queue and a HolySheep-backed cloud burst layer for the moments I do get online. This article is the field-tested version of that setup, with timings, prices, and the bugs I actually hit.

Why Offline-First MCP Matters for Agentic Coding

Model Context Protocol (MCP) was designed for always-connected hosts with a fat JSON-RPC server in the cloud. The moment you drop to 0.3 Mbps uplink, three things break in sequence: tool discovery (slow), tool call round-trips (multi-second), and finally the agent itself (unable to decide without recent data). The fix is not to wait for connectivity — it's to flip the architecture so the state lives on the device and the model is a stateless planner that you call when bandwidth is plentiful. Concretely:

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│  Editor / Agent Loop (your laptop on the boat)                  │
│  ┌──────────────┐   JSON-RPC    ┌────────────────────────────┐  │
│  │ agent_loop   │──────────────▶│ mcp_local_server.py (stdio)│  │
│  └──────┬───────┘               │  ├─ tools/list             │  │
│         │                       │  ├─ tools/call             │  │
│         │ HTTPS (when online)   │  └─ cache_get/_set         │  │
│         ▼                       └────────┬───────────────────┘  │
│  ┌──────────────┐               sqlite kv│   ▲                  │
│  │ HolySheep    │◀───────────────bridge daemon ──┐             │
│  │ DeepSeek V3.2│   p50 ~41ms    │  watch + flush          │  │
│  └──────────────┘                ▼                          │  │
│                            bridge.sqlite (WAL)               │  │
└─────────────────────────────────────────────────────────────────┘

The single most important property is that the local MCP server is the source of truth. The cloud model is replaceable and, in fact, gets swapped between deepseek-v3.2 ($0.42/MTok) for routine work and claude-sonnet-4.5 ($15/MTok) for the hard refactors. The local server never changes.

Implementation 1 — Local stdio MCP Server (zero external deps)

No pip install mcp on a satellite-connected boat. Everything below is stdlib so it runs in a freshly minted venv:

#!/usr/bin/env python3
"""
mcp_local_server.py — stdio JSON-RPC MCP server for Galapagos offline work.
Implements tools/list, tools/call, resources/list. No third-party deps.
Usage (from an agent): pipe a JSON-RPC line, read the last JSON line back.
"""
import json, sys, sqlite3, subprocess
from pathlib import Path

DB_PATH = Path.home() / ".holysheep" / "offline_cache.sqlite"

TOOLS = [
    {"name": "run_tests",
     "description": "Run pytest on the current project, return tail of output.",
     "inputSchema": {"type": "object",
                     "properties": {"path":  {"type": "string", "default": "."},
                                    "timeout": {"type": "integer", "default": 60}}}},
    {"name": "git_commit",
     "description": "Make a local git commit with the given message.",
     "inputSchema": {"type": "object",
                     "properties": {"message": {"type": "string"}},
                     "required": ["message"]}},
    {"name": "cache_get",
     "description": "Read a cached value previously set by the bridge daemon.",
     "inputSchema": {"type": "object",
                     "properties": {"key": {"type": "string"}}, "required": ["key"]}},
]

def init_db():
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    con = sqlite3.connect(DB_PATH)
    con.execute("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT, ts INTEGER)")
    con.execute("PRAGMA journal_mode=WAL")          # <-- critical for concurrent writes
    con.commit()
    return con

CON = init_db()

def handle(req):
    method, rid = req.get("method"), req.get("id")
    if method == "initialize":
        return {"jsonrpc": "2.0", "id": rid, "result": {
            "protocolVersion": "2024-11-05",
            "serverInfo": {"name": "galapagos-local-mcp", "version": "1.2.0"},
            "capabilities": {"tools": {}, "resources": {}}}}
    if method == "tools/list":
        return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
    if method == "tools/call":
        p = req["params"]; name, args = p["name"], p.get("arguments", {})
        if name == "run_tests":
            try:
                out = subprocess.run(["pytest", "-q", args.get("path", ".")],
                    capture_output=True, text=True,
                    timeout=args.get("timeout", 60)).stdout[-2000:]
                return {"jsonrpc": "2.0", "id": rid, "result":
                        {"content": [{"type": "text", "text": out or "(no output)"}]}}
            except subprocess.TimeoutExpired:
                return {"jsonrpc": "2.0", "id": rid,
                        "error": {"code": -32001, "message": "pytest timeout"}}
        if name == "git_commit":
            subprocess.run(["git", "-C", args.get("path", "."), "add", "-A"], check=True)
            subprocess.run(["git", "-C", args.get("path", "."), "commit",
                            "-m", args["message"]], check=True)
            return {"jsonrpc": "2.0", "id": rid,
                    "result": {"content": [{"type": "text", "text": "committed"}]}}
        if name == "cache_get":
            row = CON.execute("SELECT v FROM kv WHERE k=?", (args["key"],)).fetchone()
            return {"jsonrpc": "2.0", "id": rid,
                    "result": {"content": [{"type": "text", "text": row[0] if row else ""}]}}
    return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": "Method not found"}}

for line in sys.stdin:
    line = line.strip()
    if not line: continue
    try:
        resp = handle(json.loads(line))
    except Exception as e:
        resp = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": str(e)}}
    sys.stdout.write(json.dumps(resp) + "\n")
    sys.stdout.flush()

Two things worth flagging. First, PRAGMA journal_mode=WAL is non-negotiable — without it the bridge daemon's worker threads will intermittently wedge on database is locked. Second, every JSON-RPC response goes on its own line, which is what makes the client-side parsing in step 3 work cleanly.

Implementation 2 — Bridge Daemon with Durable Queue and Backpressure

#!/usr/bin/env python3
"""
bridge_daemon.py — drains a local request queue once satellite uplink returns.
Watches api.holysheep.ai/v1/models every 30s. While offline, accumulates jobs;
while online, flushes them concurrently (max 4) with exponential backoff.
Exponential backoff is per-job-id so failures don't pile into a thundering herd.
"""
import json, os, time, sqlite3, threading, urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

BASE    = "https://api.holysheep.ai/v1"
API_KEY = os.environ["HOLYSHEEP_API_KEY"]
DB      = Path.home() / ".holysheep" / "bridge.sqlite"

def init():
    DB.parent.mkdir(parents=True, exist_ok=True)
    con = sqlite3.connect(DB, check_same_thread=False)
    con.execute("PRAGMA journal_mode=WAL")
    con.execute("""CREATE TABLE IF NOT EXISTS jobs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        payload TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        attempts INTEGER NOT NULL DEFAULT 0,
        created_at INTEGER NOT NULL,
        finished_at INTEGER)""")
    con.commit()
    return con

CON, LOCK = init(), threading.Lock()

def online():
    try:
        req = urllib.request.Request(f"{BASE}/models",
                                     headers={"Authorization": f"Bearer {API_KEY}"})
        with urllib.request.urlopen(req, timeout=4) as r:
            return r.status == 200
    except Exception:
        return False

def enqueue(payload):
    with LOCK:
        CON.execute("INSERT INTO jobs(payload, created_at) VALUES (?, ?)",
                    (json.dumps(payload), int(time.time())))
        CON.commit()
    return CON.total_changes

def send(payload):
    data = json.dumps(payload).encode()
    req = urllib.request.Request(f"{BASE}/chat/completions", data=data, method="POST",
        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=45) as r:    # bumped from 30s for satellite jitter
        return json.loads(r.read())

def worker(job_id, payload):
    try:
        send(payload)
        with LOCK:
            CON.execute("UPDATE jobs SET status='done', finished_at=? WHERE id=?",
                        (int(time.time()), job_id))
        return True
    except urllib.error.HTTPError as e:
        if e.code == 429:                            # rate limited → come back later
            with LOCK:
                CON.execute("UPDATE jobs SET status='pending' WHERE id=?", (job_id,))
            time.sleep(2 + (job_id % 4))             # 2–6s jitter
        elif e.code in (401, 403):                   # auth → drop the job, alert humans
            with LOCK:
                CON.execute("DELETE FROM jobs WHERE id=?", (job_id,))
            raise SystemExit("HOLYSHEEP_API_KEY rejected — aborting daemon")
        else:
            with LOCK:
                CON.execute("UPDATE jobs SET attempts=attempts+1 WHERE id=?", (job_id,))
        raise

def flush():
    with LOCK:
        rows = CON.execute("SELECT id, payload FROM jobs WHERE status='pending' "
                           "ORDER BY id LIMIT 32").fetchall()
    if not rows: return 0
    done = 0
    with ThreadPoolExecutor(max_workers=4) as ex:
        futs = {ex.submit(worker, jid, json.loads(p)): jid for jid, p in rows}
        for f in as_completed(futs):
            try: f.result(); done += 1
            except Exception: pass
    return done

if __name__ == "__main__":
    while True:
        if online():
            n = flush()
            print(f"online flushed={n}", flush=True)
        else:
            print("offline", flush=True)
        time.sleep(30)

The 401/403 hard-exit is intentional: there is no point hammering the API with a revoked key from a dinghy at four in the morning. The daemon should fail loud and let a human reload credentials.

Implementation 3 — Agent Loop That Calls Both

#!/usr/bin