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.
| Pattern | Use |
|---|---|
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
| Var | Purpose |
|---|---|
WEBLOOM_ENGINE | Path to server.py. Auto-detects ~/.webloom/engine/server.py if unset. |
ANTHROPIC_API_KEY | Required for vision_check. |
CAPTCHA_PROVIDER + CAPTCHA_API_KEY | Required 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)