Anthropic
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 they hit different paths on it (see Troubleshooting). This page walks through the same toy task — GET /hn returning the top 10 Hacker News stories as JSON — at both levels of ceremony.
- A Browserless API token (available in your account dashboard)
- An Anthropic API key
- Node.js 20.6 or later (for the built-in
--env-fileflag)
Setting environment variables
- .env file
- Command line
ANTHROPIC_API_KEY=your-anthropic-key
BROWSERLESS_API_KEY=your-browserless-token
PORT=3000
export ANTHROPIC_API_KEY=your-anthropic-key
export BROWSERLESS_API_KEY=your-browserless-token
export PORT=3000
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-4-7",
max_tokens: 4096,
system: INSTRUCTIONS,
mcp_servers: [
{
type: "url",
name: "browserless",
url: "https://mcp.browserless.io/mcp",
authorization_token: BROWSERLESS_API_KEY,
},
],
messages: [
{
role: "user",
content: "Fetch the top 10 stories from https://news.ycombinator.com",
},
],
betas: ["mcp-client-2025-04-04"],
});
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. - 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:
const turns = [
{ role: "user", content: "Fetch the top 10 stories from https://news.ycombinator.com" },
];
const first = await client.beta.messages.create({ /* …, messages: turns */ });
turns.push({ role: "assistant", content: first.content });
turns.push({ role: "user", content: "Now sort them by points." });
const second = await client.beta.messages.create({ /* …, 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 the troubleshooting note below). See the MCP setup page for the full list of configuration options.
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-04-04"] on the request. Without that opt-in, the parameter is rejected.
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 the model's final output on the Messages API. If you ask for 10 stories with rich fields and hit a truncated array, raise it. 4096 is usually plenty 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 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.
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