For AI agents: a documentation index is available at /llms.txt
Skip to main content

Building LangGraph agents with Browserless

LangGraph is LangChain's runtime for stateful, looping agents. This guide gives a LangGraph agent web access through Browserless: first with the hosted Browserless MCP server loaded through LangChain's MCPAdapter, then inside a custom StateGraph, and finally with a Playwright tool for full page control.

Prerequisites
  • A Browserless API token from your account dashboard
  • Python 3.10 or higher
  • An API key for a chat model provider. The examples use Anthropic, so set ANTHROPIC_API_KEY
  • Familiarity with LangGraph's Graph API
note

The LangChain page covers BrowserlessLoader for loading pages into documents. Use this page when you want an agent that decides which pages to visit and what to do on them.

Which approach to use​

ApproachUse it whenWhat the agent gets
create_agent with MCP toolsYou want a working tool-calling agent with the least codeBrowserless MCP tools on LangChain's prebuilt agent loop, which runs on LangGraph
Custom StateGraphYou need your own nodes, routing, or memory around the tool callsThe same MCP tools inside a graph you define with ToolNode and tools_condition
Playwright toolYou need specific page interactions, or control over exactly what comes backA Python tool you write, running on a Browserless session via connect_over_cdp

Start with the prebuilt agent. Move to a custom graph when you need extra steps around the tool loop, and add a Playwright tool when the MCP tools don't expose an interaction you need.

info

LangGraph's create_react_agent is deprecated in LangGraph v1 in favor of LangChain's create_agent, which builds the same tool-calling loop as a LangGraph graph. The examples use create_agent.

Run a prebuilt agent with Browserless MCP tools​

  1. Install the packages

    python -m venv .venv
    source .venv/bin/activate # On Windows: .venv\Scripts\activate
    pip install "langchain[mcp]>=1.4.0" langgraph langchain-anthropic

    The mcp extra installs FastMCP, which langchain.mcp.MCPAdapter uses to connect to MCP servers. MCPAdapter needs langchain 1.4.0 or later and is in beta, so importing it logs a LangChainBetaWarning.

  2. Set your keys

    export BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE
    export ANTHROPIC_API_KEY=your-anthropic-api-key
  3. Create the agent

    Create agent.py. It connects to the hosted Browserless MCP server with your token as a bearer credential, keeps three of the server's tools, and hands them to create_agent:

    import asyncio
    import os

    from fastmcp.client import Client
    from langchain.agents import create_agent
    from langchain.mcp import MCPAdapter

    TOKEN = os.environ["BROWSERLESS_TOKEN"]
    BROWSERLESS_MCP_URL = "https://mcp.browserless.io/mcp"

    # Keep the tool list short so the model picks the right tool more often.
    ALLOWED_TOOLS = {"browserless_smartscraper", "browserless_search", "browserless_map"}


    async def main():
    # A string `auth` is sent as an `Authorization: Bearer` header.
    # Scrapes can outlast FastMCP's default request timeout, so allow 5 minutes.
    client = Client(BROWSERLESS_MCP_URL, auth=TOKEN, timeout=300)

    async with MCPAdapter(client) as adapter:
    tools = [t for t in await adapter.list_tools() if t.name in ALLOWED_TOOLS]

    agent = create_agent(
    "claude-sonnet-5-5",
    tools,
    system_prompt=(
    "You answer questions about the web. Use the Browserless tools to "
    "search and scrape pages, and cite the URLs you used."
    ),
    )
    result = await agent.ainvoke(
    {
    "messages": [
    {
    "role": "user",
    "content": "Scrape https://news.ycombinator.com and list the top 3 stories.",
    }
    ]
    }
    )
    print(result["messages"][-1].content)


    if __name__ == "__main__":
    asyncio.run(main())
  4. Run the agent

    python agent.py

    Response:

    Here are the top 3 stories on Hacker News right now:

    1. <story title> (https://...)
    2. <story title> (https://...)
    3. <story title> (https://...)

    Source: https://news.ycombinator.com

    Each tool call shows up in result["messages"] as a ToolMessage, so you can print the whole list to see what the agent scraped.

The server also exposes the stateful browserless_agent tool, account tools, and more. Add names to ALLOWED_TOOLS to give the agent more of them, or drop the filter to load every tool the server advertises.

Build a custom LangGraph graph​

create_agent is a fixed loop: call the model, run any tools it asked for, repeat. When you need your own nodes, such as a validation step, a human approval, or routing between subagents, build the graph yourself. This graph does the same loop with LangGraph primitives and adds a checkpointer so the agent remembers earlier turns in a thread.

import asyncio
import os

from fastmcp.client import Client
from langchain.chat_models import init_chat_model
from langchain.mcp import MCPAdapter
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

TOKEN = os.environ["BROWSERLESS_TOKEN"]
ALLOWED_TOOLS = {"browserless_smartscraper", "browserless_search", "browserless_map"}


def build_graph(tools):
model = init_chat_model("claude-sonnet-5-5").bind_tools(tools)

# MCP tools are async, so the model node is async too.
async def call_model(state: MessagesState):
return {"messages": [await model.ainvoke(state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
# Route to "tools" when the model requested a tool call, otherwise end the run.
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
return builder.compile(checkpointer=InMemorySaver())


async def main():
client = Client("https://mcp.browserless.io/mcp", auth=TOKEN, timeout=300)
async with MCPAdapter(client) as adapter:
tools = [t for t in await adapter.list_tools() if t.name in ALLOWED_TOOLS]
graph = build_graph(tools)

# The thread_id scopes the checkpointer's memory to one conversation.
config = {"configurable": {"thread_id": "research-1"}}
for question in [
"Map https://docs.browserless.io and tell me how many URLs you found.",
"Scrape the first URL from that list and summarize it in two sentences.",
]:
result = await graph.ainvoke(
{"messages": [{"role": "user", "content": question}]}, config
)
print(result["messages"][-1].content, "\n")


if __name__ == "__main__":
asyncio.run(main())

Response:

I found <N> URLs on docs.browserless.io, including https://docs.browserless.io/... and ...

The page at https://docs.browserless.io/... covers <summary sentence 1>. <summary sentence 2>.

The second question works because the checkpointer keeps the first turn's messages, including the browserless_map result, under thread_id. Swap InMemorySaver for a database-backed checkpointer to keep threads across restarts.

Write a Playwright tool​

The MCP tools return content in one call. When the agent needs to click, fill forms, or read something the MCP tools don't return, give it a tool that drives a Browserless session with Playwright. This tool opens a session, reads the page, and closes the session before returning.

pip install playwright

You don't need playwright install, since the browser runs on Browserless.

import os

from langchain.tools import tool
from playwright.async_api import async_playwright

TOKEN = os.environ["BROWSERLESS_TOKEN"]
BROWSERLESS_URL = f"wss://production-sfo.browserless.io?token={TOKEN}"


@tool
async def read_page(url: str) -> dict:
"""Load a web page in a real browser and return its title and visible text.

Args:
url: The full URL to load, including https://.
"""
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(BROWSERLESS_URL)
try:
# Use the default context so connection settings such as proxies apply.
page = await browser.contexts[0].new_page()
await page.goto(url, wait_until="domcontentloaded")
text = await page.inner_text("body")
# Truncate so one large page doesn't flood the model's context.
return {"title": await page.title(), "url": page.url, "text": text[:8000]}
finally:
# Close in finally so a failed page load doesn't leave the session running.
await browser.close()

Pass it to either agent like any other tool. With create_agent, use create_agent("claude-sonnet-5-5", [read_page]). In the custom graph, call build_graph(tools + [read_page]) so the model gets both the MCP tools and the Playwright tool.

Response, from asking "What's on https://example.com?":

The page is titled "Example Domain". It says the domain is for use in documentation
examples and links to more information from IANA.

Configure the browser session​

For the Playwright tool, set every Browserless session option on the connect_over_cdp URL. Routes such as /stealth go in the path, and options such as proxies and timeouts go in the query string.

Stealth​

Use the stealth route for sites with bot detection:

BROWSERLESS_URL = f"wss://production-sfo.browserless.io/stealth?token={TOKEN}"

Residential proxies​

Route traffic through residential IPs for geo-targeted pages or sites that rate-limit datacenter IPs:

BROWSERLESS_URL = f"wss://production-sfo.browserless.io?token={TOKEN}&proxy=residential&proxyCountry=us"

Regional and self-hosted endpoints​

DeploymentEndpoint
US West (San Francisco)wss://production-sfo.browserless.io?token={TOKEN}
Europe (London)wss://production-lon.browserless.io?token={TOKEN}
Europe (Amsterdam)wss://production-ams.browserless.io?token={TOKEN}
Self-hosted Dockerws://localhost:3000?token={TOKEN}

For the MCP tools, pick the region on the MCP server instead. See regional endpoints.

FAQ & Troubleshooting​

Tool calls return Server error 401: Invalid API key

The token isn't reaching Browserless. Listing tools doesn't need a valid token, so the agent starts fine and only fails when it calls a tool. Check that BROWSERLESS_TOKEN is set in the shell that runs the script and that you pass it as auth=TOKEN on the FastMCP Client, which sends it as Authorization: Bearer <token>.

ModuleNotFoundError: No module named 'langchain.mcp'

langchain.mcp ships in langchain 1.4.0 and later. Upgrade with pip install -U "langchain[mcp]>=1.4.0". If you're on an older project that uses the separate langchain-mcp-adapters package, follow LangChain's migration guide to move to MCPAdapter.

MCP tool calls time out on large scrapes or crawls

Raise timeout on the FastMCP Client (in seconds), or ask the agent to work on fewer pages. browserless_crawl is in beta and only available on Cloud plans, so keep crawls small while you test.

Can I use LangGraph.js instead of Python?

Yes. In JavaScript, MCPAdapter comes from the @langchain/mcp-adapters package and takes a map of named servers. Point one at https://mcp.browserless.io/mcp with the same Authorization: Bearer header. The JavaScript adapter prefixes tool names with the server name, so adjust any tool filter to match. For a custom tool, use playwright-core with chromium.connectOverCDP and the same Browserless URL.

The page loads but the content is empty or blocked

Switch the Playwright tool to the stealth route and add a residential proxy. If the site still blocks you, use the browserless_smartscraper MCP tool, which cascades through HTTP fetch, proxies, a headless browser, and CAPTCHA solving on its own.

Next steps​

Was this page helpful?