Expose a Steel browser to any MCP client
Build a Model Context Protocol server in Go with the official SDK and chromedp that hands any MCP client a Steel cloud browser through explicit session-handle tools.
Scaffolds a starter project locally. Requires the Steel CLI.
This is a Model Context Protocol server that hands any MCP client a Steel cloud browser to drive. It uses the official TypeScript SDK and drives the browser with Playwright over CDP through connectOverCDP, so there is no local Chrome to launch. The server carries no model key of its own: the client supplies the model, this process owns the cloud session.
Five tools make up the surface. create_session opens a Steel session and returns its id; navigate, extract, and screenshot take that id and act on the browser; release_session closes it.
Each tool declares its shape, the id ties them together
Every tool is a server.registerTool call: a name, a description, an inputSchema written as a Zod shape, and the handler. The shape is the contract the client sees and the type of the handler's argument in one place:
server.registerTool("navigate",{description: "Open a URL in the session's browser tab and wait for it to load. Returns the resolved title and URL.",inputSchema: {session_id: z.string().describe("Handle returned by create_session."),url: z.string().describe("Absolute URL to open, e.g. https://news.ycombinator.com."),},},async ({ session_id, url }) => {const page = getPage(session_id);await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });return { content: [{ type: "text", text: JSON.stringify({ url: page.url(), title: await page.title() }) }] };},);
Every tool except create_session takes a session_id and resolves it through getPage, which reads a Map keyed by the Steel session id. That id is the handle the model threads back on each call, and it is what keeps browsers apart: the server holds no hidden "current page," so two clients with two ids never touch each other's sessions. The Go recipe covers why the explicit handle, rather than one session hidden in server state, is the shape the MCP spec now recommends. screenshot returns an image content block so the client renders the PNG, and release_session plus the releaseAll signal handlers make sure a session stops billing when the client goes away.
Run it
cd examples/mcp-tscp .env.example .env # set STEEL_API_KEY for local runsnpm install
Get a Steel key at app.steel.dev/settings/api-keys. The server runs straight from TypeScript with ts-node, so an MCP client launches it through npx and gets the key from the client's env block. For Claude Desktop, add this to claude_desktop_config.json:
{"mcpServers": {"steel": {"command": "npx","args": ["ts-node", "/absolute/path/to/examples/mcp-ts/index.ts"],"env": { "STEEL_API_KEY": "your-steel-api-key" }}}}
Restart the client and ask it to open a page and read it back. It calls create_session, then navigate and extract against the returned id, and release_session at the end. Open the live_view_url from create_session to watch the browser. Stdio uses stdout for the JSON-RPC stream, so the server logs only to stderr.
Make it yours
- Add a tool. Another
server.registerToolwith asession_idfield, resolve the page withgetPage, and act on it. Aclicktool isawait page.click(selector). - Start authenticated. Pass options to
steel.sessions.createto attach a profile or credentials so a session opens already logged in. - Return structured output. Add an
outputSchemato a tool and returnstructuredContentso the client gets typed fields instead of a JSON string.
Related
Steel + MCP server (Go) and Steel + MCP server (Rust) are the same five tools as single static binaries; read the Go one for the handle-versus-hidden-state rationale. puppeteer-ts and playwright-ts are the bare browser recipes, and vercel-ai-sdk-ts drives Steel from an in-process agent. The TypeScript SDK docs cover transports, resources, and prompts beyond the tools shown here.
Scaffolds a starter project locally. Requires the Steel CLI.
This is a Model Context Protocol server that hands any MCP client a Steel cloud browser to drive. It uses FastMCP, the decorator API in the official Python SDK, and drives the browser with Playwright over CDP. Because it connects to a remote browser with connect_over_cdp, there are no local browser binaries to install: the server is a thin process that owns the cloud session and nothing else, and it has no model key of its own.
Five tools make up the surface. create_session opens a Steel session and returns its id; navigate, extract, and screenshot act on a session by id; release_session closes it.
The decorator is the schema, the id is the handle
Each tool is a plain async function under @mcp.tool(). FastMCP reads the type hints and the docstring to build the JSON Schema the client sees, so the signature is the whole contract:
@mcp.tool()async def navigate(session_id: str, url: str) -> dict:"""Open a URL in the session's browser tab and wait for it to load.Args:session_id: Handle returned by create_session.url: Absolute URL to open, e.g. https://news.ycombinator.com."""page = _page(session_id)await page.goto(url, wait_until="domcontentloaded", timeout=45_000)return {"url": page.url, "title": await page.title()}
Every tool except create_session takes a session_id and looks it up in _sessions, a plain dict keyed by the Steel session id. That id is the handle the model threads back on each call, which is what keeps browsers apart: the server holds no hidden "current page," so two clients with two ids never touch each other's sessions. The Go recipe covers why the explicit handle, rather than one session hidden in server state, is the shape the MCP spec now recommends. screenshot returns FastMCP's Image, so the client renders the PNG instead of a base64 string, and release_session plus the _release_all cleanup make sure a session does not keep billing after the client goes away.
Run it
cd examples/mcp-pycp .env.example .env # set STEEL_API_KEY for local runsuv sync
Get a Steel key at app.steel.dev/settings/api-keys. Point an MCP client at the script through uv run and pass the key in the client's env block. For Claude Desktop, add this to claude_desktop_config.json:
{"mcpServers": {"steel": {"command": "uv","args": ["run", "--directory", "/absolute/path/to/examples/mcp-py", "python", "main.py"],"env": { "STEEL_API_KEY": "your-steel-api-key" }}}}
Restart the client and ask it to open a page and read it back. It calls create_session, then navigate and extract against the returned id, and release_session at the end. Watch the run at the live_view_url that create_session returns. One stdio rule: stdout carries the JSON-RPC stream, so the server prints nothing there. Log to stderr if you add diagnostics.
Make it yours
- Add a tool. Write one more
async defunder@mcp.tool()that takessession_id, look the page up with_page, and act on it. Aclicktool isawait page.click(selector). - Start authenticated. Pass arguments to
steel.sessions.createto attach a profile or credentials so a session opens already logged in. - Return typed data. Tools here return dicts, strings, and an
Image. Return a Pydantic model from a tool and FastMCP emits a structured-content schema the client can validate against.
Related
Steel + MCP server (Go) and Steel + MCP server (Rust) are the same five tools as single static binaries; read the Go one for the handle-versus-hidden-state rationale. stagehand-py and google-adk-py are the in-process agent recipes in Python. The Python SDK docs cover transports, resources, and prompts beyond the tools shown here.
Scaffolds a starter project locally. Requires the Steel CLI.
This is a Model Context Protocol server that lends a Steel cloud browser to any MCP client. It is built on rmcp, the official Rust SDK, and drives the browser over CDP with chromiumoxide. It compiles to a single binary with no interpreter and no model key of its own: the client supplies the model, this process owns the browser and nothing else.
Five tools make up the surface. create_session opens a Steel session and returns its id; navigate, extract, and screenshot take that id and act on the browser; release_session closes it. Each tool is one #[tool]-annotated method on SteelMcp, and #[tool_router] plus #[tool_handler] turn those methods into the served schema.
Holding the browser open between calls
The hard part of a browser MCP server is not any single tool, it is keeping one browser alive and reachable across separate calls. create_session does three things that have to outlive the call that made them:
let (browser, mut handler) = Browser::connect(cdp_url).await?;let handler_task = tokio::spawn(async move { while handler.next().await.is_some() {} });let page = browser.new_page("about:blank").await?;
Browser::connect returns a command handle plus a handler stream that pumps the CDP websocket. Nothing polls it on its own, so the spawned task that drives it to exhaustion is mandatory: drop it and the next goto hangs with no error. All three, the Browser, the join handle, and the Page, go into a SessionEntry stored in Arc<Mutex<HashMap<String, SessionEntry>>>, keyed by the Steel session id. Because Page and the Arcs are cheap to clone, get copies a whole entry out and releases the map lock before any browser work, so two sessions never block each other.
That id is the handle the model threads back on every later call, which is what keeps sessions apart. The Go recipe covers why the explicit handle, rather than one browser hidden in server state, is what the MCP spec now asks for. The short version: each Steel session is its own isolated cloud browser, and naming it on every call means two clients holding two ids can never read each other's pages. release_session removes the entry, aborts the handler task, and releases the Steel session so it stops billing.
Run it
cd examples/mcp-rscp .env.example .env # set STEEL_API_KEY for local `cargo run`cargo build --release
Get a Steel key at app.steel.dev/settings/api-keys. Point an MCP client at the compiled binary and pass the key through the client's env block. For Claude Desktop, add this to claude_desktop_config.json:
{"mcpServers": {"steel": {"command": "/absolute/path/to/examples/mcp-rs/target/release/mcp-rs","env": { "STEEL_API_KEY": "your-steel-api-key" }}}}
Restart the client and ask it to open a page and read it back. It calls create_session, then navigate and extract against the returned id, and release_session at the end. Open the live_view_url from create_session to watch the browser work. Note that stdio uses stdout for the JSON-RPC stream, so the server keeps it clean and writes nothing there itself.
Make it yours
- Add a tool. Write one more
async fnwith a#[tool]attribute and aParameters<T>argument carryingsession_id. Aclicktool isentry.page.find_element(sel).await?.click().await?. - Start authenticated. Pass a populated
SessionCreateParamstosessions().createto attach a profile or credentials so the session opens already logged in. - Return richer output. Tools here return
Content::textandContent::image. Swap in structured JSON content when a client wants typed fields instead of a string.
Related
Steel + MCP server (Go) is the same five tools on the official Go SDK and chromedp; read it for the handle-versus-hidden-state rationale. chromiumoxide is the bare CDP browser, rig drives it from an in-process agent, and swiftide reads pages through Steel's scrape API instead. The rmcp docs cover transports, resources, and prompts past the tools shown here.
Scaffolds a starter project locally. Requires the Steel CLI.
This is a Model Context Protocol server that hands any MCP client (Claude Desktop, an IDE, your own agent) a Steel cloud browser to drive. It is built on the official MCP Go SDK and talks to the browser over CDP with chromedp. The whole server is one statically linked binary with no runtime, no node_modules, and no model key of its own: the client brings the model, this process only owns the browser.
The server exposes five tools. create_session starts a Steel session and returns its id; navigate, extract, and screenshot act on a session; release_session tears it down. The id Steel returns is the only thing tying the calls together.
The session id is the handle
A browser MCP server has to answer one question: when two tools run against "the browser," which browser do they mean? Hiding a single session in a global is the trap the MCP spec calls out, because a second client on the same process would inherit the first one's cookies and page. The 2026 spec removed the transport-level session and says state should ride on an explicit handle "a browser_id minted from a tool and passed back as an ordinary argument." That is exactly what create_session does:
func (s *server) createSession(ctx context.Context, _ *mcp.CallToolRequest, _ createInput) (*mcp.CallToolResult, createOutput, error) {steelSession, err := s.steel.Sessions.Create(ctx, steel.SessionCreateParams{ ... })// ... connect chromedp to steelSession.WebsocketURL ...s.sessions[steelSession.ID] = sessreturn nil, createOutput{SessionID: steelSession.ID, LiveViewURL: sess.viewerURL}, nil}
Every other tool takes a session_id and looks it up in server.sessions (a plain map behind a mutex), so the model names the browser it means on each call. Two clients hold two ids and never collide, and because the handle is a normal tool argument it works the same whether the client connected over stdio or HTTP. The Steel session itself is the isolation boundary: each one is its own cloud browser with its own cookies, so the server's only job is to never share a single id across callers.
The allocator in createSession runs on context.Background(), not the request context. The request is cancelled the moment the tool call returns, but the browser has to outlive that call to serve the next one. releaseAll, deferred in main, releases whatever is still open when the client disconnects, so a forgotten session does not bill against your account until its idle timeout.
Run it
cd examples/mcp-gocp .env.example .env # set STEEL_API_KEY for local `go run`go build -o steel-mcp .
Get a Steel key at app.steel.dev/settings/api-keys. Point an MCP client at the binary and pass the key through the client's env block. For Claude Desktop, add this to claude_desktop_config.json:
{"mcpServers": {"steel": {"command": "/absolute/path/to/examples/mcp-go/steel-mcp","env": { "STEEL_API_KEY": "your-steel-api-key" }}}}
Restart the client and ask it to "open news.ycombinator.com and tell me the top story." It will call create_session, navigate, extract, then release_session on its own. Watch the run live at the live_view_url that create_session returns.
One stdio rule: the JSON-RPC stream owns stdout, so the server logs only to stderr (log writes there by default). A stray fmt.Println corrupts the protocol and the client drops the connection.
Make it yours
- Add a tool. A
clicktool is a fewchromedp.Clicklines and one moremcp.AddToolcall. Take asession_id, look it up withs.get, act on the tab. - Start authenticated. Swap the
SessionCreateParamsincreateSessionto attach a profile or credentials so a session opens already logged in. - Go remote. Replace
mcp.StdioTransportwith the SDK's streamable-HTTP handler to serve many clients from one process. The handle pattern already carries the state, so nothing else changes.
Related
Steel + MCP server (Rust) is the same server built on rmcp and chromiumoxide; compare the two for how each language holds the session map. chromedp, genkit, and google-adk-go are the other Go recipes, covering the raw browser, a tool-calling agent, and Google's ADK. The MCP Go SDK docs cover transports, resources, and prompts beyond the tools used here.
Related recipes
Run a durable browser agent with Restate
Build a Restate Virtual Object in TypeScript that uses durable OpenAI planning steps and Steel scraping to answer browser research questions.
Build a browser agent with Genkit
Use Steel with Genkit Go to build a tool-calling agent that navigates and extracts from a chromedp-backed browser and completes a web task.
Build a browser agent with Eino
Use Steel with the ByteDance Eino framework to build a ReAct agent that calls Steel's scrape API as a tool to research and answer a web question.