Python SDK

Drive the engine from any Python.

The MCP server speaks JSON-RPC. The SDK is a thin client that lets you call any of the 70+ tools from a normal Python script, CI job, or backend service — no MCP host required.

Install

The SDK ships with the engine. After cloning the repo:

install
git clone https://github.com/webloomhq/engine ~/.webloom/engine
cd ~/.webloom/engine
pip install -e .
# OR add to PYTHONPATH:
export PYTHONPATH=$HOME/.webloom/engine:$PYTHONPATH

Quick start

hello world
from webloom_sdk import Session

with Session("slot-1") as s:
    s.launch_session()                        # start Chrome with debug port
    s.new_tab(url="https://example.com")
    s.click(selector="a.cta")
    s.fill(selector="#email", value="me@example.com")
    s.key_press(key="Enter")
    # All 70+ tools are methods on the Session.

The shape

A Session spawns one engine subprocess. JSON-RPC over stdio. Reuse one session for many calls — the handshake is set up once.

PatternUse
s.call("tool_name", **args)Call any tool. Returns the tool's text output.
s.call_json("tool_name", **args)Same but tries to JSON-parse the response. Many engine tools return JSON-encoded text.
s.click(selector=...)Shortcut methods for the common tools. Sugar over s.call().

Vision + click pattern

v = s.vision_check(question="click coords for the post button")
# v == {"ok": True, "answer": "...", "click": {"x": 720, "y": 480}}
if v.get("click"):
    s.click_at_coords(x=v["click"]["x"], y=v["click"]["y"])

Parallel fanout

res = s.run_parallel(calls=[
    {"tool": "scan_tab", "args": {"session": "slot-1", "tab": "t1"}},
    {"tool": "scan_tab", "args": {"session": "slot-1", "tab": "t2"}},
    {"tool": "scan_tab", "args": {"session": "slot-1", "tab": "t3"}},
], max_concurrency=3)
# res = {"ok": True, "count": 3, "results": [...]}

Drift-heal a broken selector

hints = s.drift_heal_suggest(
    old_selector='[data-testid="old-id"]',
    descriptor="post tweet button",
)
# hints = {ok: True, candidates: [{selector: "...", score: 7, reason: "..."}, ...]}
best = hints["candidates"][0]["selector"]
s.click(selector=best)

Talking to a logged-in Chrome from a cron job

The engine attaches to a Chrome instance running with --remote-debugging-port=PORT. The session has your real cookies and sessions. From a cron:

#!/usr/bin/env python3
"""Daily LinkedIn post — runs at 9am via cron."""
import os, datetime
from webloom_sdk import Session

today = datetime.date.today().isoformat()
post_text = f"Build log {today}: shipped 3 commits on WebLoom, added vision_check..."

with Session("slot-1") as s:
    s.launch_session()  # idempotent if already launched
    s.navigate(url="https://www.linkedin.com/feed/?showShareBox=true")
    s.wait_for(selector='[role="textbox"]', timeout_seconds=10)
    s.lexical_set_text(container_selector='[role="textbox"]', text=post_text)
    s.click(selector='button[aria-label*="Post"]')

Environment

VarPurpose
WEBLOOM_ENGINEPath to server.py. Auto-detects ~/.webloom/engine/server.py if unset.
ANTHROPIC_API_KEYRequired for vision_check.
CAPTCHA_PROVIDER + CAPTCHA_API_KEYRequired for solve_captcha (2captcha works today).

Async usage

The SDK is synchronous by default. For async code, wrap in a thread executor:

import asyncio
from webloom_sdk import Session

async def main():
    with Session("slot-1") as s:
        s.launch_session()
        # Run the call on a thread so the event loop isn't blocked
        scan = await asyncio.to_thread(s.scan_tab, tab="t1")
        print(scan)

asyncio.run(main())

Errors

All tool errors raise WebLoomError with the engine's message:

from webloom_sdk import Session, WebLoomError

with Session("slot-1") as s:
    try:
        s.click(selector="#nonexistent")
    except WebLoomError as e:
        print("tool failed:", e)