Skip to content

Remote (HTTP transport)

By default Ghostvault speaks stdio — the MCP client (Claude Desktop, Cursor, ZCode, Cline) spawns it as a subprocess and communicates over stdin/stdout. That's the right choice when the agent and the browser run on the same machine.

But sometimes you want to run Ghostvault on a different machine than the agent: a cloud VM with residential IPs, a dedicated scraping box, a headless Linux server while you drive it from your laptop. That's what the HTTP transport is for.

When to use it

Scenario Use HTTP?
Agent and browser on your laptop No — use stdio (the default)
Browser must run on a server (residential IPs, always-on, more RAM) Yes
Multiple agents / scripts sharing one Ghostvault instance Yes
Driving Ghostvault from a non-MCP script via the SDK over the network Yes

How to start the server

Set GHOSTVAULT_TRANSPORT=http. The server then listens on http://$GHOSTVAULT_HOST:$GHOSTVAULT_PORT/mcp (streamable HTTP).

GHOSTVAULT_TRANSPORT=http \
GHOSTVAULT_HOST=127.0.0.1 \
GHOSTVAULT_PORT=8765 \
/absolute/path/to/python -m ghostvault

Relevant environment variables (see Environment variables):

Variable Default Purpose
GHOSTVAULT_TRANSPORT stdio Set to http to enable HTTP mode.
GHOSTVAULT_HOST 127.0.0.1 Bind address. Defaults to loopback.
GHOSTVAULT_PORT 8765 Port to listen on.

The MCP endpoint is at /mcp — so the full URL is http://127.0.0.1:8765/mcp.

Pointing clients at the URL

From the SDK

from ghostvault_sdk import GhostvaultClient

async with GhostvaultClient(url="http://127.0.0.1:8765/mcp") as gv:
    accounts = await gv.list_accounts()

No subprocess is spawned — the client connects to the running server over HTTP.

From an MCP client (Cursor, Cline, ZCode, …)

Most MCP clients support an url / transport field for HTTP servers. Check your client's docs — the shape is usually:

{
  "mcpServers": {
    "ghostvault": {
      "url": "http://10.0.0.5:8765/mcp",
      "transport": "http"
    }
  }
}

For specific clients, see the example configs in examples/.

Security considerations

HTTP transport is NOT authenticated by default

Anyone who can reach the port can call any tool — list your accounts, read your Gmail, sign in to new accounts. Treat the endpoint the way you'd treat an unlocked session.

Bind to loopback by default

GHOSTVAULT_HOST defaults to 127.0.0.1, so the server is only reachable from the same machine. Do not change this to 0.0.0.0 (all interfaces) unless you've put a reverse proxy with authentication in front of it.

Use a reverse proxy with TLS for remote access

For real remote access, run Ghostvault bound to loopback and put a TLS-terminating reverse proxy (nginx, Caddy, Traefik) in front of it. The proxy handles:

  • TLS — so credentials and session cookies aren't sent in cleartext.
  • Authentication — HTTP Basic auth, client certificates, or an OAuth proxy.
  • Rate limiting — brute-force protection on the endpoint.

Minimal Caddy example (auto-TLS + Basic Auth):

ghostvault.example.com {
    basicauth /* {
        your-user $2a$14$...bcrypt-hash...
    }
    reverse_proxy 127.0.0.1:8765
}

Then point the client at https://ghostvault.example.com/mcp.

Other hardening tips

  • Firewall the port (ufw allow from <trusted-ip> to any port 8765) as a second layer.
  • Run as a dedicated user with no sudo — the server holds real Google sessions.
  • Enable encryption at rest (GHOSTVAULT_ENCRYPTION_ENABLED=true) so a stolen disk image doesn't expose session cookies. See Encryption at rest.
  • Enable auto-close (GHOSTVAULT_AUTO_CLOSE_AFTER_TASK=true) so decrypted profiles don't sit on disk between calls.

Combining with the SDK

The HTTP transport + SDK is the cleanest pattern for a remote scraping pipeline:

# On the server:
#   GHOSTVAULT_TRANSPORT=http GHOSTVAULT_PORT=8765 python -m ghostvault

# On your laptop:
import asyncio
from ghostvault_sdk import GhostvaultClient

async def scrape():
    async with GhostvaultClient(url="https://ghostvault.example.com/mcp") as gv:
        session = await gv.open_ephemeral("https://example.com")
        content = await gv.get_page_content()
        print(content)
        await gv.close_ephemeral()

asyncio.run(scrape())