Anthropic SDK
Anthropic exposes two paths for putting Claude behind a Browserless-powered agent loop:
- Direct integration with the Messages API MCP connector. One
messages.createcall, MCP server as a singlemcp_serversentry. Lightest possible shape. - Managed integration with Managed Agents (beta). A persistent
Environment, a namedAgent, andSession+eventsstreams. Better when you need long-lived sessions or server-stored history.
Both routes call the same hosted MCP server, though the Messages API uses /mcp while Managed Agents requires the legacy /sse endpoint. Start with the quickstart below, then build a fuller example with both integration paths: a GET /hn endpoint that returns the top 10 Hacker News stories as JSON.
- Browserless API token (available in your account dashboard)
- An Anthropic API key
- Node.js 20.6 or later (for the built-in
--env-fileflag)
Quickstart
Get your API keys
Go to your Browserless account dashboard and copy your API token.
Then set your environment variables:
- .env file
- Command line
ANTHROPIC_API_KEY=your-anthropic-key
BROWSERLESS_API_KEY=your-browserless-tokenexport ANTHROPIC_API_KEY=your-anthropic-key
export BROWSERLESS_API_KEY=your-browserless-tokenInstall the Anthropic SDK
npm install @anthropic-ai/sdkConnect to Browserless
Pass the hosted Browserless MCP server in
mcp_serversand expose its tools with anmcp_toolsetentry. Anthropic calls the Browserless tools server-side, so there's no browser code to write. Save this asquickstart.mjs:import Anthropic from "@anthropic-ai/sdk";
// Reads ANTHROPIC_API_KEY from the environment.
const client = new Anthropic();
const message = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
// The MCP connector is in beta, so it needs the beta client and this opt-in.
betas: ["mcp-client-2025-11-20"],
mcp_servers: [
{
type: "url",
name: "browserless",
url: "https://mcp.browserless.io/mcp",
authorization_token: process.env.BROWSERLESS_API_KEY,
},
],
// Without a toolset, Claude can't see the server's tools. The name must match the server entry.
tools: [{ type: "mcp_toolset", mcp_server_name: "browserless" }],
messages: [
{
role: "user",
content: "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
},
],
});
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}Run it
node --env-file=.env quickstart.mjsIf you exported the variables instead of using a
.envfile, runnode quickstart.mjs. Node exits with.env: not foundwhen the file is missing.Claude loads the page through Browserless and replies with the titles:
1. Show HN: …
2. …
3. …
Direct integration: Messages API MCP connector
The Messages API accepts hosted MCP servers via the mcp_servers parameter (beta). Anthropic's API server makes the MCP calls on Claude's behalf and returns the final answer in one request.
Install
npm init -y
npm install @anthropic-ai/sdk express
server.js
import express from "express";
import Anthropic from "@anthropic-ai/sdk";
const PORT = process.env.PORT ?? 3000;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY || "";
const INSTRUCTIONS =
"You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";
const app = express();
const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
app.get("/hn", async (_req, res) => {
const message = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
system: INSTRUCTIONS,
mcp_servers: [
{
type: "url",
name: "browserless",
url: "https://mcp.browserless.io/mcp",
authorization_token: BROWSERLESS_API_KEY,
},
],
tools: [{ type: "mcp_toolset", mcp_server_name: "browserless" }],
messages: [
{
role: "user",
content: "Fetch the top 10 stories from https://news.ycombinator.com",
},
],
betas: ["mcp-client-2025-11-20"],
});
const text = message.content
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("");
res.type("application/json").send(text);
});
app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});
Add "type": "module" to your package.json so the import statements work.
Run it
node --env-file=.env server.js
curl http://localhost:3000/hn
# → [
# { "rank": 1, "title": "Show HN: …", "url": "https://…", "points": 412 },
# …
# ]
How it works
- Express receives
GET /hn. - The server calls
client.beta.messages.createwith themcp_serversarray pointing athttps://mcp.browserless.io/mcp, authenticated viaauthorization_token. The matchingmcp_toolsetentry intoolsexposes that server's tools to Claude. - Anthropic's API fetches the MCP tool list, Claude picks the right Browserless tool (typically
browserless_smartscraper), and Anthropic executes the call server-side againsthttps://news.ycombinator.com. - The scraped page comes back to Claude, which extracts the top 10 stories into the JSON shape requested by
system. - The model's final response is returned as content blocks. We concatenate the
textblocks and pipe them straight to the HTTP response.
Multi-turn state
The Messages API is stateless — to maintain a conversation, append the assistant turn back into messages and send the next user turn:
// Sent with every call. The API doesn't store mcp_servers or tools between requests.
const config = {
model: "claude-opus-5-5",
max_tokens: 16000,
betas: ["mcp-client-2025-11-20"],
mcp_servers: [
{
type: "url",
name: "browserless",
url: "https://mcp.browserless.io/mcp",
authorization_token: BROWSERLESS_API_KEY,
},
],
tools: [{ type: "mcp_toolset", mcp_server_name: "browserless" }],
};
const turns = [
{ role: "user", content: "Fetch the top 10 stories from https://news.ycombinator.com" },
];
const first = await client.beta.messages.create({ ...config, messages: turns });
// Append the full content array, not just the text. It carries the MCP tool calls and results.
turns.push({ role: "assistant", content: first.content });
turns.push({ role: "user", content: "Now sort them by points." });
const second = await client.beta.messages.create({ ...config, messages: turns });
Persist turns in your own store (Redis, Postgres) keyed by user or session. Unlike OpenAI's previous_response_id, Anthropic does not retain conversation state server-side on this path — you own it.
Managed integration: Managed Agents
Managed Agents move the entire agent — system prompt, tools, and MCP server bindings — onto Anthropic's infrastructure. You bootstrap a named Agent once, then open Sessions against it and stream events. Server-side state and a richer event stream are the payoff.
Reach for this when you need long-lived sessions across many turns, server-stored history, or a centrally-versioned agent definition.
Bootstrap (run once)
This script creates the Environment and the Agent. Save the resulting IDs into .env.
// bootstrap-agent.js
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY;
const MCP_URL = `https://mcp.browserless.io/sse?token=${BROWSERLESS_API_KEY}`;
const MA_BETA = "managed-agents-2026-04-01";
const INSTRUCTIONS =
"You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";
const environment = await client.beta.environments.create({
name: "browserless-hn-env",
config: { type: "cloud", networking: { type: "unrestricted" } },
}, { betas: [MA_BETA] });
const agent = await client.beta.agents.create({
name: "browserless-hn",
model: "claude-opus-4-7",
system: INSTRUCTIONS,
mcp_servers: [{ name: "browserless", type: "url", url: MCP_URL }],
tools: [
{
type: "mcp_toolset",
mcp_server_name: "browserless",
default_config: {
enabled: true,
permission_policy: { type: "always_allow" },
},
},
],
}, { betas: [MA_BETA] });
console.log(`MANAGED_AGENTS_ENVIRONMENT_ID=${environment.id}`);
console.log(`MANAGED_AGENTS_AGENT_ID=${agent.id}`);
console.log(`MANAGED_AGENTS_AGENT_VERSION=1`);
Run once and paste the output into your .env:
node --env-file=.env bootstrap-agent.js
The agent definition stores the token as part of mcp_servers[0].url. To rotate the token, create a new agent version via client.beta.agents.update(...) with the new URL. For production deployments where the token must never appear in any stored config, Anthropic's Vault credentials are the right tool — but get this recipe working first.
server.js
The per-request handler opens a session against the bootstrapped agent, sends a user-message event, and consumes the event stream until the agent stops.
import express from "express";
import Anthropic from "@anthropic-ai/sdk";
const PORT = process.env.PORT ?? 3000;
const MA_BETA = "managed-agents-2026-04-01";
const app = express();
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
app.get("/hn", async (_req, res) => {
const session = await client.beta.sessions.create({
agent: {
type: "agent",
id: process.env.MANAGED_AGENTS_AGENT_ID,
version: Number(process.env.MANAGED_AGENTS_AGENT_VERSION),
},
environment_id: process.env.MANAGED_AGENTS_ENVIRONMENT_ID,
betas: [MA_BETA],
});
// Open the stream BEFORE sending the message — avoids a race per MA docs.
const events = await client.beta.sessions.events.stream(session.id, { betas: [MA_BETA] });
await client.beta.sessions.events.send(session.id, {
events: [{
type: "user.message",
content: [{ type: "text", text: "Fetch the top 10 stories from https://news.ycombinator.com" }],
}],
betas: [MA_BETA],
});
let finalText = "";
for await (const event of events) {
if (event.type === "agent.message") {
for (const block of event.content) {
if (block.type === "text") finalText += block.text;
}
}
if (event.type === "session.status_idle") break;
if (event.type === "session.status_terminated") {
res.status(500).type("text/plain").send("session terminated");
return;
}
}
res.type("application/json").send(finalText);
});
app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});
What you get over the Messages API path
- Server-side session state. Conversation history, tool-call history, and intermediate state live on Anthropic's side. You only hold
session.id. - Versioned agents. The system prompt, tools, and MCP server bindings are part of a named
Agentyou can roll back or A/B byversion. - Richer event stream. You see tool calls, results, and partial deltas as discrete events instead of one final blob.
Trade-off: more moving pieces, a bootstrap step, and the MA-specific /sse workaround called out below.
Stealth, CAPTCHAs, and regional endpoints
Both paths route through the same Browserless infrastructure, so stealth mode, automatic CAPTCHA solving, and residential proxies all apply.
The Messages API mcp_servers entry and the Managed Agents mcp_servers entry both accept the regional pin via URL query parameter, since neither lets you set arbitrary headers on the MCP request:
https://mcp.browserless.io/mcp?browserlessUrl=https://production-lon.browserless.io
For Managed Agents, swap /mcp for /sse (see FAQ & Troubleshooting below). See the MCP setup page for the full list of configuration options.
Resources
- Anthropic Messages API — MCP connector
- Anthropic Managed Agents overview
- Claude Agent SDK page – a third option using Playwright over CDP instead of the hosted MCP server
- Browserless MCP setup
- REST API tools
- Browser agent
- Connection URLs
FAQ & Troubleshooting
Messages API: "Unknown parameter: mcp_servers" or 400
The MCP connector is still in beta. Use client.beta.messages.create (not client.messages.create) and include betas: ["mcp-client-2025-11-20"] on the request. Without that opt-in, the parameter is rejected.
The current beta also requires a tools entry of { type: "mcp_toolset", mcp_server_name: "browserless" } whose name matches the mcp_servers entry. Sending mcp_servers without it returns a validation error. If you're upgrading from the older mcp-client-2025-04-04 beta, this toolset entry is the change to make.
Managed Agents: MCP init fails with "SSE data line is not valid JSON"
Managed Agents' MCP client does not currently complete the Streamable HTTP handshake at https://mcp.browserless.io/mcp. Use the legacy SSE endpoint instead (https://mcp.browserless.io/sse). This is the opposite of the Messages API path, which must use /mcp. If you copy snippets between the two integrations, double-check the URL.
Empty response text
Both paths return content blocks rather than a single string field. For the Messages API, filter message.content for block.type === "text". For Managed Agents, accumulate text from agent.message events (each carries a content array of { text } blocks) and stop the loop on session.status_idle or session.status_terminated.
max_tokens truncates the JSON
max_tokens caps everything Claude generates on the Messages API, including its thinking, which is always on for claude-opus-5-5. If you hit a truncated array or stop_reason: "max_tokens", raise it. 16000 leaves plenty of room for this recipe. Managed Agents handles this via the agent definition rather than per-request.
mcp_servers "disappear" between calls (Messages API)
The Messages API does not persist MCP server configuration server-side. Re-send the full mcp_servers array and its mcp_toolset entry in tools on every messages.create call, even in a multi-turn conversation. The Managed Agents path stores the binding on the Agent resource, so you set it once at bootstrap.