LangChain

A browser tool that starts a fresh Chromium is fine for reading a public page and useless for anything behind a login: the session dies with the process. These four tools drive an AntiBrow profile instead, so the cookies, the storage, the fingerprint and the exit IP are the same ones the agent used last time.

browser_goto url Navigates the profile and returns the HTTP status and page title.
browser_read selector? Visible text, from one element or the whole page, truncated to keep it out of the context window.
browser_click selector Clicks, then reports the URL it landed on.
browser_fill selector, text Types into an input.

There is no AntiBrow-specific LangChain package to install: the SDK returns standard Playwright objects, so the toolkit below is the whole integration and you can change it. If you would rather not write code at all, the same profiles are reachable over the MCP server.

Install

pip install antibrow langchain-core
python -m antibrow login --key ab_live_...

The engine downloads on first launch and is cached. Unlimited local profiles and one concurrent browser are free; see the SDK reference for every launch() option.

The session object

One browser, one page, shared by every tool - which is what makes the tools composable: browser_read reads whatever browser_goto last opened.

from typing import Optional

from antibrow import launch


class AntiBrowSession:
    """One persistent browser profile, shared by every tool in the toolkit."""

    def __init__(self, profile: str = "agent", **launch_kwargs):
        self._browser = launch(profile, focus_window=False, **launch_kwargs)
        self._page = self._browser.new_page()

    def goto(self, url: str) -> str:
        response = self._page.goto(url, wait_until="load")
        return f"{response.status if response else 'no-response'} {self._page.title()}"

    def read_text(self, selector: Optional[str] = None, limit: int = 4000) -> str:
        target = self._page.locator(selector) if selector else self._page.locator("body")
        return target.first.inner_text()[:limit]

    def click(self, selector: str) -> str:
        self._page.locator(selector).first.click()
        return f"clicked {selector}; now at {self._page.url}"

    def fill(self, selector: str, text: str) -> str:
        self._page.locator(selector).first.fill(text)
        return f"filled {selector}"

    def close(self) -> None:
        self._browser.close()

Building the tools

Each method becomes a StructuredTool with an explicit schema, so the model is told what a selector is instead of guessing:

from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field


class GotoInput(BaseModel):
    url: str = Field(description="Absolute URL to open.")


class ReadInput(BaseModel):
    selector: Optional[str] = Field(default=None, description="CSS selector; omit for the whole page.")


class ClickInput(BaseModel):
    selector: str = Field(description="CSS selector of the element to click.")


class FillInput(BaseModel):
    selector: str = Field(description="CSS selector of the input to fill.")
    text: str = Field(description="Text to type into it.")


def antibrow_tools(session: AntiBrowSession) -> list[StructuredTool]:
    return [
        StructuredTool.from_function(
            session.goto, name="browser_goto", args_schema=GotoInput,
            description="Open a URL in the agent's persistent browser profile."),
        StructuredTool.from_function(
            session.read_text, name="browser_read", args_schema=ReadInput,
            description="Read visible text from the current page."),
        StructuredTool.from_function(
            session.click, name="browser_click", args_schema=ClickInput,
            description="Click an element on the current page."),
        StructuredTool.from_function(
            session.fill, name="browser_fill", args_schema=FillInput,
            description="Type text into an input on the current page."),
    ]

Run it once without a model to check the wiring:

session = AntiBrowSession(profile="langchain-demo", temporary=True)
try:
    tools = antibrow_tools(session)
    print([t.name for t in tools])
    print(tools[0].invoke({"url": "https://example.com"}))
    print(tools[1].invoke({"selector": "h1"}))
finally:
    session.close()
['browser_goto', 'browser_read', 'browser_click', 'browser_fill']
200 Example Domain
Example Domain

Binding them to a model

model_with_tools = model.bind_tools(antibrow_tools(session))

# Or hand the same list to whatever agent constructor you already use -
# these are ordinary StructuredTool objects, with nothing AntiBrow-specific
# about how they are called.

Keep one session per agent run and close it in a finally. A closed browser leaves nothing behind; an abandoned one is a whole Chromium still running.

Profiles, proxies, concurrency

# A named profile keeps cookies, storage and its fingerprint between runs,
# so an agent that signed in yesterday is still signed in today.
session = AntiBrowSession(profile="research-01")

# Its own exit IP, answered inside the engine - no extension, no local proxy:
session = AntiBrowSession(profile="research-01", proxy="http://user:pass@host:5001")

# Nothing to clean up afterwards:
session = AntiBrowSession(profile="scratch", temporary=True)

Two agents that must not share an identity need two profile names, not two tabs. How many may run at once is your plan's concurrency limit, and launch() raises ConcurrencyLimitError rather than silently queueing.

What it does not do

It does not promise that a given site will accept an automated session. A persistent identity removes the tells that come from starting over every run - a fresh profile, a stock automation fingerprint, your own IP - and that is all it removes. Our dated measurements, including the checks that fail, are published under reports.