Programmatic SDK¶
For most users Ghostvault is driven by an AI agent (Claude Desktop, Cursor, ZCode, Cline).
But sometimes you want to call its tools from a plain Python script — a cron job that
checks Gmail, a scraping pipeline, an end-to-end test. The ghostvault-sdk package is
a thin async client that wraps the MCP tool calls so you don't have to spell out tool
names or JSON-encode arguments yourself.
Install¶
The SDK is an optional extra:
This installs mcp (the client library) and exposes ghostvault_sdk.GhostvaultClient.
The GhostvaultClient class¶
A single async context manager that connects to a Ghostvault MCP server. Source:
sdk/ghostvault_sdk/client.py.
Constructor¶
GhostvaultClient(
*,
command: str | list[str] | None = None, # stdio: command to launch the server
url: str | None = None, # http: URL of a running server
env: dict[str, str] | None = None, # stdio: env vars for the subprocess
)
Pass either command (stdio) or url (HTTP), not both:
- stdio (default) — launches the server as a subprocess. The default command is
["python", "-m", "ghostvault"]. Passenv={...}to setGHOSTVAULT_*variables on the subprocess. - HTTP — connects to an already-running server. See Remote (HTTP transport).
The client must be used as an async context manager — that's what opens the MCP transport and runs the initialize handshake.
call(tool, arguments=None)¶
Low-level escape hatch: call any MCP tool by name with a dict of arguments. Returns the raw tool result (a string, or a list of content blocks for screenshot tools).
You usually don't need call — the convenience wrappers below cover every tool.
Wrapper methods¶
All wrappers are coroutines. They map 1:1 to the gv_* MCP tools in the
Tool reference. The returned values are the raw tool results
(strings or content blocks) — for tools that return JSON text, parse with json.loads.
Accounts & lifecycle¶
| Method | Maps to | Notes |
|---|---|---|
await gv.list_accounts() |
gv_list_accounts |
|
await gv.create_account(name=..., os=..., preset=..., locale=..., timezone=..., humanize=..., block_webrtc=..., block_webgl=..., webgl_config=..., fonts=..., addons=..., disable_coop=..., block_images=..., device_profile=..., device_config=..., proxy=..., provider=...) |
gv_create_account |
Only name is required; everything else is optional and None means "use the default". |
await gv.update_account_config(account_id, ...) |
gv_update_account_config |
Same advanced/device knobs as create_account, plus reset_keys and reset_device_keys. |
await gv.sign_in(account_id) |
gv_sign_in |
Blocks until login completes or times out. |
await gv.open_account(account_id) |
gv_open_account |
Reuses saved session (no login window). |
await gv.switch_account(account_id) |
gv_switch_account |
|
await gv.lock_account(account_id) |
gv_lock_account |
Close + re-encrypt. |
await gv.sign_out(account_id) |
gv_sign_out |
Invalidates the Google session server-side. |
await gv.delete_account(account_id) |
gv_delete_account |
Removes profile + DB row. |
Anonymous scraping (ephemeral)¶
| Method | Maps to | Notes |
|---|---|---|
await gv.open_ephemeral(url=None, *, headless=None, proxy=None, block_images=None) |
gv_open_ephemeral |
Returns a session id; all browser tools work on it unchanged. |
await gv.close_ephemeral(session_id=None) |
gv_close_ephemeral |
Defaults to the active session. |
Browser interaction¶
| Method | Maps to | Notes |
|---|---|---|
await gv.open_url(url, *, wait_until="domcontentloaded") |
gv_open_url |
|
await gv.get_page_content(*, include_screenshot=False, max_chars=20000) |
gv_get_page_content |
Returns text + optional PNG. |
await gv.screenshot(*, full_page=False) |
gv_screenshot |
Returns an MCP Image block. |
await gv.click_element(target, *, timeout_ms=10000, humanize=None, delay_after_ms=0) |
gv_click_element |
humanize resolves under the 3-layer policy. |
await gv.fill_input(target, value, *, timeout_ms=10000, humanize=None, typing_speed_wpm=None, typing_variance=0.35, mistake_rate=None, delay_after_ms=0) |
gv_fill_input |
Set humanize=True for login forms. |
await gv.press_key(key, *, modifiers=None, hold_ms=0, delay_after_ms=0) |
gv_press_key |
e.g. key="Enter", modifiers=["Control"]. |
await gv.scroll(*, direction="down", amount=3, humanize=True, delay_after_ms=0) |
gv_scroll |
Good for reCAPTCHA v3 warm-up. |
await gv.read_inbox(*, limit=20) |
gv_read_gmail |
Debug¶
| Method | Maps to | Notes |
|---|---|---|
await gv.get_logs(*, n=50) |
gv_get_logs |
Recent entries from the action log. |
Private/public gate¶
| Method | Maps to | Notes |
|---|---|---|
await gv.get_auth_status() |
gv_get_auth_status |
|
await gv.setup_password() |
gv_setup_password |
Opens a browser window — needs a display. |
await gv.unlock(password) |
gv_unlock |
|
await gv.logout() |
gv_logout |
Lock the gate. |
await gv.make_private(account_id) |
gv_make_private |
|
await gv.make_public(account_id) |
gv_make_public |
How it launches the server (stdio)¶
When you don't pass url=, the client spawns the server as a subprocess:
The default command is ["python", "-m", "ghostvault"]. Two gotchas:
pythonmust be on PATH for the subprocess. The client checks withshutil.whichand raisesFileNotFoundErrorwith a helpful message if not. Pass an absolute path viacommand=to be safe:
- Env vars (like
GHOSTVAULT_HEADLESS) don't auto-propagate from your shell — pass them explicitly:
Connecting to a remote HTTP server¶
If Ghostvault is already running on a server (e.g. GHOSTVAULT_TRANSPORT=http), skip the
subprocess and point the client at the URL:
async with GhostvaultClient(url="http://10.0.0.5:8765/mcp") as gv:
accounts = await gv.list_accounts()
See Remote (HTTP transport) for how to start the server in HTTP mode.
A complete example script¶
This is a minimal end-to-end: create an account, sign in, open a URL, read the page,
clean up. Save as my_script.py and run with python my_script.py.
import asyncio
import json
from ghostvault_sdk import GhostvaultClient
def _text(result) -> str:
"""Extract text from an MCP tool result (handles str or content blocks)."""
if isinstance(result, str):
return result
# SDK returns a list of content blocks for some tools — join text ones.
parts = []
for block in result:
if hasattr(block, "text"):
parts.append(block.text)
elif isinstance(block, dict) and "text" in block:
parts.append(block["text"])
return "".join(parts)
async def main() -> None:
# Launch the server as a subprocess in headful mode so we can do first-time login.
async with GhostvaultClient(
command=["/abs/path/to/.venv/bin/python", "-m", "ghostvault"],
env={"GHOSTVAULT_HEADLESS": "false"},
) as gv:
# 1. Create an account slot with a realistic hardware profile.
created = await gv.create_account(
name="Work",
device_profile="macbook-pro-14-m2",
locale="en-US",
timezone="America/New_York",
)
data = json.loads(_text(created))
account_id = data["id"]
print(f"Created account {account_id}")
if data.get("consistency_warnings"):
print(" warnings:", data["consistency_warnings"])
# 2. Sign in (opens a browser window — you complete login + 2FA).
await gv.sign_in(account_id)
print("Signed in.")
# 3. Open a URL in the account's session.
await gv.open_url("https://mail.google.com")
# 4. Read the page text + take a screenshot.
content = await gv.get_page_content(include_screenshot=True)
print(_text(content)[:500])
# 5. Read Gmail.
inbox = await gv.read_inbox(limit=5)
print(_text(inbox))
# 6. Lock the account (close browser, keep session).
await gv.lock_account(account_id)
print("Locked.")
if __name__ == "__main__":
asyncio.run(main())
More examples
The repo ships a full set of runnable scripts under
examples/scripts/ —
see the Examples index for a categorized list.
Related pages¶
- Tool reference — the underlying
gv_*tools - Remote (HTTP transport) — connecting to a remote server
- Examples index — runnable scripts
- Humanize policy — how
humanizeargs are resolved