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.
- 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
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
| Approach | Use it when | What the agent gets |
|---|---|---|
create_agent with MCP tools | You want a working tool-calling agent with the least code | Browserless MCP tools on LangChain's prebuilt agent loop, which runs on LangGraph |
Custom StateGraph | You need your own nodes, routing, or memory around the tool calls | The same MCP tools inside a graph you define with ToolNode and tools_condition |
| Playwright tool | You need specific page interactions, or control over exactly what comes back | A 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.
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
Install the packages
- pip
- uv
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install "langchain[mcp]>=1.4.0" langgraph langchain-anthropicuv init browserless-langgraph && cd browserless-langgraph
uv add "langchain[mcp]>=1.4.0" langgraph langchain-anthropicThe
mcpextra installs FastMCP, whichlangchain.mcp.MCPAdapteruses to connect to MCP servers.MCPAdapterneedslangchain1.4.0 or later and is in beta, so importing it logs aLangChainBetaWarning.Set your keys
export BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE
export ANTHROPIC_API_KEY=your-anthropic-api-keyCreate 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 tocreate_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())Run the agent
python agent.pyResponse:
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.comEach tool call shows up in
result["messages"]as aToolMessage, 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
| Deployment | Endpoint |
|---|---|
| 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 Docker | ws://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.