Which to reach for
| Tool | Use when |
|---|---|
web_search | You need a synthesized answer plus source URLs and don’t know which page holds the fact. |
read on a URL | You already have the URL and want clean text without JavaScript. |
browser | The page needs JS, auth, form fills, or interactive clicks. Tabs persist across calls. |
web_search
One query, dispatched through the first available provider in a configured chain: Anthropic, Brave, Codex, Exa, Gemini, Jina, Kagi, Kimi, Parallel, Perplexity, SearXNG, Synthetic, Tavily, Z.AI. The result is a unified SearchResponse; provider order is set once in config, not per call. Optional recency (day, week, month, year) is honored by the providers that support it. Gated by web_search.enabled.
web_search query="bun workspaces hoisting behaviour" recency="month"read against a URL
Pass any http:// or https:// URL to read. Reader-mode is the default: articles, GitHub issues and PRs, Stack Overflow, Wikipedia, NPM pages, arXiv, RSS/Atom feeds, JSON endpoints, and PDFs come back as clean text or markdown. Append :raw to get untouched HTML; append a line selector (:50-100, :50+150) to paginate through the cached output. Bare host:port URLs collide with the selector grammar; add a trailing slash before the selector.
# reader-mode markdown
read https://example.com/docs/api
# raw HTML, then a line range over the cached fetch
read https://example.com/page:raw
read https://example.com/page:200-400Reach for read over browser whenever the content is static. It’s faster, cheaper, and the output is already prose-shaped for the model.
browser
A real Chromium tab driven through Puppeteer. Three actions:
open
Acquires (or reuses) a named tab. name defaults to "main".
Optional url navigates after the tab is ready; viewport sets dimensions;
dialogs auto-accepts or auto-dismisses alert/confirm/
beforeunload.
run
Executes async JS against an existing tab. code is the body of an async function with
page, browser, tab, display, assert,
and wait in scope. The return value is JSON-stringified into the tool result.
close
Releases a tab by name, or every tab with all: true. For spawned-app
browsers, kill: true terminates the process tree.
Tabs survive across run calls and across in-process subagents. Open once, reuse many times. The tab helper exposes observe() for an accessibility snapshot with stable element ids, plus click, fill, type, press, select, uploadFile, waitForUrl, waitForResponse, screenshot, and extract.
Default to tab.observe() over tab.screenshot() for understanding page state — the snapshot returns structured data with element ids you can act on. Screenshot only when visual appearance matters.
browser open name=docs url=https://example.com/login
browser run name=docs code=`
const obs = await tab.observe();
const link;
`
``` = obs.elements.find(e => e.role === "link" && e.name === "Sign in");
await (await tab.id(link.id)).click();
await tab.fill('input[name=email]', 'me@example.com');
await tab.click('text/Continue');
`
browser close name=docsInstead of launching headless, attach to a running Chromium app via CDP or by spawning an Electron binary:
browser open name=cursor app={path: "/Applications/Cursor.app/Contents/MacOS/Cursor"}
browser open name=devtools app={cdp_url: "http://127.0.0.1:9222"}For PRs and issues specifically, prefer the URL schemes documented on GitHub — they cache automatically and read like local files.\n