Skip to content

refactor(browser): serve the in-app browser's tools from the desktop over client MCP - #59477

Draft
Hona wants to merge 1 commit into
devfrom
browser-client-mcp
Draft

Hona wants to merge 1 commit into
devfrom
browser-client-mcp

Conversation

@Hona

@Hona Hona commented Oct 27, 2026

Copy link
Copy Markdown
Member

Draft to look at the shape of the code. The in-app browser now registers its tools the way James's client MCP does (#57502–#57506): the desktop hosts an MCP server called browser over /api/experimental/client-mcp, and the server has no browser code at all. @opencode/plugin-browser is deleted.

Before / after

flowchart LR
  subgraph Before
    direction LR
    A1[Agent] --> T1[plugin-browser tools<br/>server]
    T1 --> C1[connection.ts<br/>pending map, grants, state copy]
    C1 -- "event: control {requestID}" --> D1[Desktop pane]
    D1 -- "rpc.command(requestID)" --> C1
    D1 -- "rpc.result(outcome)" --> C1
    D1 -- "rpc.state(inventory)" --> C1
    D1 <-- "rpc.tunnel.*" --> C1
  end
  subgraph After
    direction LR
    A2[Agent] --> M2[core MCP<br/>attached client 'browser']
    M2 -- "tools/call + _meta.sessionID" --> D2[Desktop pane]
    D2 -- "result + structuredContent" --> M2
  end
Loading
Before After
Server code packages/plugin-browser (connection, tools, skill, tunnel, proxy, files) none
Wire plugin RPC: attach, state, command, result, tunnel.* + control events one WebSocket, MCP JSON-RPC
Tool schemas Effect Schema on the server same Browser.Operations, exported as JSON Schema by the desktop
Guidance browser skill MCP instructions
Code Mode path tools.browser.tabs.open tools.browser.tabs_open
Model tool name browser_tabs_open browser_tabs_open (same)

The whole desktop side

packages/gui-plugins/src/browser/pane.ts serves one connection per server and directory and routes each call by session:

connectClientMcp({
  url: entry.server.url,
  headers: entry.server.headers,
  name: "browser",
  directory,
  instructions,
  tools: browserTools((sessionID) => {
    const pane = Array.from(served).find((item) => item.sessionID === sessionID)
    return pane && {
      execute: (command, signal) => run(pane, command, signal),
      read: (path, signal) => client.file.read({ path, location: { directory } }, { signal }),
    }
  }),
})

packages/gui-plugins/src/browser/mcp.ts turns Browser.Operations into tools: it decodes the input, runs the page, checks the output against the operation's schema, and returns structuredContent. Screenshots go back as image parts. Other captures go back as resource blobs, which the server saves to a file.

What the SDK and core needed

  • connectClientMcp (packages/client/src/mcp.ts):
    • instructions option.
    • Per-tool outputSchema.
    • structuredContent and resource parts in results.
    • ctx.signal, which aborts on notifications/cancelled or when the socket closes.
  • Mcp.instructions() includes the instructions of the attached client that serves each name, so the browser skill becomes server instructions.
  • McpTool content: textual resources over 32 KiB are saved to a file instead of inlined, so a Lighthouse JSON report or a .cpuprofile does not flood the context.

Behavior changes to decide on

  • Network: pages load on the desktop's network. localhost is the user's machine, not the server. The tunnel/proxy is gone, so a remote server's dev ports are no longer reachable.
  • Tool visibility: tools are scoped to the folder, not the session. Every session in a directory that a desktop pane shows sees browser_*. A session with no pane gets [browser.disconnected]. Before, a deny rule hid the tools until a pane attached to that exact session.
  • Permissions: browser tools use the default MCP action names (browser_<tool>), not the browser action.
  • Screenshots: returned as an image only, with no server-local path.
  • Uploads: paths are relative to the workspace and read through GET /api/fs/read. Absolute server paths are no longer accepted.
  • Error messages: many still say browser.tabs.list({}); Code Mode now uses browser.tabs_list.

@ryanmeador

Copy link
Copy Markdown

Thanks for working on this! I’m exploring a related browser use case and would appreciate some guidance.

I run OpenCode on a Linux machine and connect through opencode web from several devices, including an Android phone. I’d like the user and agent to share a browser running on that Linux machine, with an interactive viewer inside the web UI. The browser would keep running when I disconnect, and I could reconnect from another device.

Two questions before exploring an implementation:

  • General direction: Does a server-hosted browser with a web viewer fit OpenCode’s intended direction, whether built-in or provided through plugins?
  • This PR: How would this refactor affect that approach? Could a server-hosted implementation reuse the browser tools being developed here, or would a separate MCP integration be the better fit?

I’d be interested in contributing and want to understand how this could complement the desktop browser work.

@colthreepv

colthreepv commented Nov 7, 2026 •

Copy link
Copy Markdown

Just posting as encouragement. This direction seems fine and avoids proxying, and hopefully keeps everything on desktop app (to be decided, from what I see).

We need a top level browser in OpenCode Desktop.

EDIT:
@ryanmeador
regarding server-hosted browser, https://github.com/anomalyco/browse this came out this week.

Maybe that project was spun up from this PR. Not sure. Plausible?

@ryanmeador

Copy link
Copy Markdown

I hadn't seen that, thank you for the tip. That looks like it will do everything I want, just not quite with the UX I'm looking for: that is a tab in a regular browser, and I was hoping for a tab right in OpenCode itself next to the session transcript, so it's all visually together. I wonder if this browse tool and a remote window viewer (VNC?) embedded in OpenCode might work well together.

@colthreepv

Copy link
Copy Markdown

Uhm. I don't think VNC is the way (unless you have specific needs?), just rework the embedding as stated in this PR should come out as more polished and persistent Browser.

As a general rule of thumb, I spent my saturday to look at OpenCode UX / polish, and I think it needs a month of intense hammering to become as smooth as more famous competitors
It's all feasible, keep it up!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants