Connecting LlamaIndex agents to Browserless
LlamaIndex is a Python framework for building agents and retrieval pipelines over your data. This guide gives a LlamaIndex FunctionAgent web access through Browserless, first with the hosted Browserless MCP server and then with LlamaIndex's Playwright tool spec for multi-step page interaction.
- A Browserless API token from your account dashboard, set as
BROWSERLESS_TOKEN - Python 3.10 or higher
- An OpenAI API key, set as
OPENAI_API_KEY. Any LlamaIndex LLM that supports function calling works - Familiarity with LlamaIndex agents
Which approach to use
| Approach | Use it when | What the agent gets |
|---|---|---|
| Browserless MCP server | The agent needs page content, search, site maps, screenshots, or PDFs | Browserless MCP tools through McpToolSpec, with no browser code to write |
| Playwright tool spec | The agent needs to click, fill forms, and move between pages in one session | LlamaIndex's PlaywrightToolSpec tools, running on a Browserless session via connect_over_cdp |
Start with MCP. Switch to the Playwright tool spec when the agent has to interact with a page rather than just read it.
Connect LlamaIndex to the Browserless MCP server
Install the packages
- pip
- uv
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install llama-index-core llama-index-tools-mcp llama-index-llms-openaiuv init browserless-llamaindex && cd browserless-llamaindex
uv add llama-index-core llama-index-tools-mcp llama-index-llms-openaiSet your keys
export BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE
export OPENAI_API_KEY=your-openai-api-keyCreate the agent
Create
agent.py.BasicMCPClientconnects to the hosted server over Streamable HTTP with your token in anAuthorizationheader, andMcpToolSpecturns the server's tools into LlamaIndex tools:import asyncio
import os
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
TOKEN = os.environ["BROWSERLESS_TOKEN"]
class BrowserlessToolSpec(McpToolSpec):
"""McpToolSpec that skips underscore-prefixed schema fields.
Browserless tools accept an optional `_prompt` field. Pydantic rejects field
names with a leading underscore, so drop them before building each tool's schema.
"""
def create_model_from_json_schema(self, schema, model_name="DynamicModel"):
properties = {
name: spec
for name, spec in schema.get("properties", {}).items()
if not name.startswith("_")
}
return super().create_model_from_json_schema(
{**schema, "properties": properties}, model_name
)
async def main():
client = BasicMCPClient(
"https://mcp.browserless.io/mcp",
headers={"Authorization": f"Bearer {TOKEN}"},
# Scrapes can outlast the 30-second default, so allow 5 minutes.
timeout=300,
)
tool_spec = BrowserlessToolSpec(
client=client,
# Keep the tool list short so the model picks the right tool more often.
allowed_tools=["browserless_smartscraper", "browserless_search", "browserless_map"],
)
tools = await tool_spec.to_tool_list_async()
agent = FunctionAgent(
tools=tools,
llm=OpenAI(model="gpt-5-mini"),
system_prompt=(
"You answer questions about the web. Use the Browserless tools to "
"search and scrape pages, and cite the URLs you used."
),
)
response = await agent.run(
"Scrape https://news.ycombinator.com and list the top 3 stories."
)
print(str(response))
if __name__ == "__main__":
asyncio.run(main())Without the
BrowserlessToolSpecsubclass,to_tool_list_async()fails withFields must not use names with leading underscores, because every Browserless MCP tool declares the optional_promptfield. The subclass only removes that field from the schema the model sees. Required fields such asurlare unchanged.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://...)
BasicMCPClient opens a new MCP session for each tool call. That's a good fit for the stateless REST API tools. For multi-step work in one browser, use the stateful browserless_agent tool, which keeps a session alive across calls when the model passes back its sessionId, or use the Playwright tool spec below.
Drive a Browserless session with PlaywrightToolSpec
PlaywrightToolSpec from llama-index-tools-playwright gives the agent eight tools that act on one shared browser: navigate_to, navigate_back, click, fill, get_current_page, extract_text, extract_hyperlinks, and get_elements. Pass it a browser connected to Browserless instead of a local one, and the whole agent run happens in a single cloud session.
pip install llama-index-core llama-index-tools-playwright llama-index-llms-openai
You don't need playwright install, since the browser runs on Browserless.
import asyncio
import os
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
from llama_index.tools.playwright import PlaywrightToolSpec
from playwright.async_api import async_playwright
TOKEN = os.environ["BROWSERLESS_TOKEN"]
BROWSERLESS_URL = f"wss://production-sfo.browserless.io?token={TOKEN}"
async def main():
async with async_playwright() as p:
# connect_over_cdp attaches to the remote browser instead of launching a local one.
browser = await p.chromium.connect_over_cdp(BROWSERLESS_URL)
try:
tools = PlaywrightToolSpec(async_browser=browser).to_tool_list()
agent = FunctionAgent(
tools=tools,
llm=OpenAI(model="gpt-5-mini"),
system_prompt="Use the browser tools to navigate pages and read their content.",
)
response = await agent.run(
"Go to https://news.ycombinator.com, open the comments for the top story, "
"and summarize the discussion in three bullet points."
)
print(str(response))
finally:
# Close in finally so a failed run doesn't leave the session running.
await browser.close()
if __name__ == "__main__":
asyncio.run(main())
Response:
The top story is "<story title>". The discussion covers:
- <point 1>
- <point 2>
- <point 3>
The tools reuse the browser's current page between calls, so navigate_to, click, and extract_text all operate on the same tab. The Browserless session stays open until browser.close() runs or the session timeout is reached.
Configure the browser session
For the Playwright tool spec, 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
to_tool_list_async() fails with Fields must not use names with leading underscores
You're using McpToolSpec directly. Browserless MCP tools declare an optional _prompt field, and Pydantic can't build a model with that field name. Use the BrowserlessToolSpec subclass from the MCP example, which drops underscore-prefixed fields before building each schema.
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 the header value reads Bearer <token> with a space.
MCP tool calls time out
BasicMCPClient waits 30 seconds per request by default. Raise timeout (in seconds) as in the example, 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.
The click tool times out on elements that exist
PlaywrightToolSpec waits 1 second for clicks by default. Pass a longer playwright_timeout in milliseconds, for example PlaywrightToolSpec(async_browser=browser, playwright_timeout=10_000), for pages that render slowly.
The page loads but the content is empty or blocked
Switch 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.