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

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.create call, MCP server as a single mcp_servers entry. Lightest possible shape.
  • Managed integration with Managed Agents (beta). A persistent Environment, a named Agent, and Session + events streams. 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.

Prerequisites

Quickstart​

  1. Get your API keys

    Go to your Browserless account dashboard and copy your API token.

    Then set your environment variables:

    ANTHROPIC_API_KEY=your-anthropic-key
    BROWSERLESS_API_KEY=your-browserless-token
  2. Install the Anthropic SDK

    npm install @anthropic-ai/sdk
  3. Connect to Browserless

    Pass the hosted Browserless MCP server in mcp_servers and expose its tools with an mcp_toolset entry. Anthropic calls the Browserless tools server-side, so there's no browser code to write. Save this as quickstart.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);
    }
  4. Run it

    node --env-file=.env quickstart.mjs

    If you exported the variables instead of using a .env file, run node quickstart.mjs. Node exits with .env: not found when 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​

  1. Express receives GET /hn.
  2. The server calls client.beta.messages.create with the mcp_servers array pointing at https://mcp.browserless.io/mcp, authenticated via authorization_token. The matching mcp_toolset entry in tools exposes that server's tools to Claude.
  3. Anthropic's API fetches the MCP tool list, Claude picks the right Browserless tool (typically browserless_smartscraper), and Anthropic executes the call server-side against https://news.ycombinator.com.
  4. The scraped page comes back to Claude, which extracts the top 10 stories into the JSON shape requested by system.
  5. The model's final response is returned as content blocks. We concatenate the text blocks 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 Browserless token is baked into the Agent's MCP URL

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 Agent you can roll back or A/B by version.
  • 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​

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.

Next steps​

Was this page helpful?