Skip to main content
Glama
enihcam
by enihcam

opencode-webmcp

MCP bridge connecting OpenCode to Chromium's WebMCP API.

Exposes WebMCP tools registered in a Chromium page as MCP tools consumable by any MCP client (OpenCode, Claude Code, etc.).

How It Works

OpenCode (MCP client)
    ↕  MCP stdio protocol
webmcp-bridge (MCP server)
    ↕  Puppeteer + CDP
Chromium (WebMCP enabled)
    ↕  monkey-patched document.modelContext
Web page with registered WebMCP tools

The bridge launches Chromium with --enable-features=WebMCPTesting, navigates to a target page, discovers tools via the WebMCP API, and makes them available to MCP clients.

Monkey-patch: Chrome 150+ has a bug where page.webmcp.invokeTool() returns {} instead of the actual tool result. The bridge works around this by intercepting registerTool to capture execute functions and replacing executeTool to call the captured functions directly.

Related MCP server: openclaw-tools-mcp

Prerequisites

  • Node.js 18+

  • Chromium/Chrome with WebMCP support (Chrome 150+)

  • An MCP client (OpenCode, Claude Code, etc.)

Setup

git clone <repo-url> webmcp-bridge
cd webmcp-bridge
npm install

Usage

Standalone

CHROME_PATH=/usr/bin/chromium \
WEBMCP_TARGET_URL="https://www.google.com" \
WEBMCP_HEADLESS=false \
node server.js

With a config file

node server.js --config ./webmcp.yaml

If --config is omitted, the bridge looks for ./webmcp.yaml, ./webmcp.yml, or ./webmcp.json in the current working directory before falling back to environment variables.

Layered precedence (highest wins): CLI args → config file → environment variables → built-in defaults.

For example, node server.js --no-headless overrides headless: true in a config file, which overrides WEBMCP_HEADLESS=true in the environment.

Supported keys (camelCase): chromePath, targetUrl, headless, historyMax, logHistory.

Example webmcp.yaml:

chromePath: /usr/bin/chromium
targetUrl: https://www.google.com
headless: false
historyMax: 500
logHistory: false

Or webmcp.json:

{
  "chromePath": "/usr/bin/chromium",
  "targetUrl": "https://www.google.com",
  "headless": false
}

OpenCode (opencode.json)

{
  "mcp": {
    "webmcp-bridge": {
      "command": ["node", "/path/to/webmcp-bridge/server.js"],
      "enabled": true,
      "type": "local",
      "environment": {
        "CHROME_PATH": "/usr/bin/chromium",
        "WEBMCP_TARGET_URL": "about:blank",
        "WEBMCP_HEADLESS": "false"
      }
    }
  }
}

Quick Start (OpenCode)

After adding the config above, restart OpenCode. The bridge tools are now available — just ask:

"Navigate to a WebMCP-enabled page and tell me what tools are available"

OpenCode calls webmcp_navigate → bridge opens the page → webmcp_status to show discovered tools. From there you can invoke any page tool, take screenshots, evaluate JS, or manage tabs conversationally.

For a full 8-step walkthrough (status → navigate → discover → invoke → screenshot → evaluate → history), see examples/GETTING_STARTED.md.

Environment Variables

Variable

Default

Description

CHROME_PATH

/usr/bin/chromium

Path to Chromium/Chrome executable

WEBMCP_TARGET_URL

https://www.google.com

Initial page to navigate to

WEBMCP_HEADLESS

true

Run headless (true) or with a visible window (false)

WEBMCP_HISTORY_MAX

1000

Maximum number of tool invocations to keep in the in-memory history ring buffer

WEBMCP_LOG_HISTORY

false

When true, write each history entry as one JSON line to stderr

WEBMCP_DECLARATIVE_SCAN

true

Scan the page DOM for declarative WebMCP tools (<form toolname="…">); set to false to disable

Bridge-Native Tools

These tools are always available, regardless of what the page exposes:

Tool

Description

webmcp_navigate

Navigate Chrome to a URL and refresh discovered WebMCP tools

webmcp_status

Report connection status, current URL, and available tools

webmcp_evaluate

Execute arbitrary JavaScript in the page context

webmcp_invoke_tool

Call any WebMCP tool by name with arguments

webmcp_register_test_tools

Register sample tools (test_greet, test_calculator, test_fetch_title) for validation

webmcp_screenshot

Capture a screenshot of the current page (PNG/JPEG, viewport/full-page/clip) and return it as an MCP image

webmcp_history

Return recent tool invocations from the in-memory history buffer (most recent first; optional limit, optional toolName filter)

webmcp_clear_history

Empty the in-memory history buffer

webmcp_open_tab

Open a new tab, optionally navigating to a URL; returns the new tabId

webmcp_switch_tab

Make a specific tab the active tab

webmcp_list_tabs

List all open tabs with tabId, url, title, and isActive flag

webmcp_close_tab

Close a tab by tabId (cannot close the last one)

webmcp_screenshot example

Default (viewport PNG):

{ "name": "webmcp_screenshot", "arguments": {} }

Full-page JPEG at quality 75:

{ "name": "webmcp_screenshot", "arguments": { "format": "jpeg", "quality": 75, "fullPage": true } }

Region clip (200×100 at top-left):

{ "name": "webmcp_screenshot", "arguments": { "clip": { "x": 0, "y": 0, "width": 200, "height": 100 } } }

Multi-Tab Sessions

The bridge can manage multiple Puppeteer tabs sharing a single browser context. Each tab has a UUID tabId; one tab is active at a time. Tools without tabId act on the active tab; pass tabId to target a specific tab without switching.

{ "name": "webmcp_open_tab", "arguments": { "url": "https://example.com" } }
// → { "tabId": "a1b2c3d4-…", "url": "https://example.com" }

{ "name": "webmcp_list_tabs", "arguments": {} }
// → { "count": 2, "tabs": [{ "tabId": "…", "url": "…", "title": "…", "isActive": true }, …] }

{ "name": "webmcp_switch_tab", "arguments": { "tabId": "a1b2c3d4-…" } }
{ "name": "webmcp_invoke_tool", "arguments": { "name": "search", "args": { "q": "weather" }, "tabId": "a1b2c3d4-…" } }
{ "name": "webmcp_close_tab", "arguments": { "tabId": "a1b2c3d4-…" } }

Closing the last remaining tab returns an error. When the active tab is closed (with other tabs remaining), the next tab in the map becomes active.

Architecture

  • server.js — single-file MCP server using @modelcontextprotocol/sdk

  • Puppeteer (puppeteer-core) — drives Chromium via Chrome DevTools Protocol

  • Monkey-patchpage.evaluateOnNewDocument injects a script that intercepts document.modelContext.registerTool and executeTool to capture actual execute functions

  • Declarative API scanner — also scans the DOM for <form toolname="…"> and synthesizes MCP tools from [toolname-target] attributes. Wired through a 100ms-debounced MutationObserver that flips a dirty flag whenever [toolname] attributes change. Imperative tools take precedence on name collisions.

Tool Annotations

MCP tool annotations (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are surfaced to MCP clients. Defaults for bridge-native tools:

Tool

readOnly

destructive

openWorld

webmcp_navigate

false

false

true

webmcp_status

true

false

false

webmcp_evaluate

false

true

true

webmcp_invoke_tool

(page-determined)

(page-determined)

(page-determined)

webmcp_screenshot

true

false

false

webmcp_register_test_tools

false

false

false

webmcp_history / webmcp_list_tabs

true

false

false

webmcp_clear_history / webmcp_close_tab

false

true

false

webmcp_open_tab / webmcp_switch_tab

false

false

false

Page-discovered tools forward their annotations to MCP clients as-is. The bridge never overrides page-supplied annotations. If a page calls registerTool("delete_account", { annotations: { destructiveHint: true } }), that annotation flows through to tools/list. Annotation changes on the page trigger notifications/tools/list_changed.

Clients that ignore the annotations field see no behavior change.

Declarative WebMCP API

Chrome's WebMCP API has two surfaces. The bridge handles both:

  1. Imperative — pages call document.modelContext.registerTool({ name, inputSchema, execute }).

  2. Declarative — pages annotate HTML with toolname / toolname-target attributes:

    <form toolname="search">
      <input name="q" toolname-target="search.query" required>
      <input name="limit" toolname-target="search.limit" type="number">
      <button>Search</button>
    </form>

The bridge synthesizes an MCP tool descriptor from this form. Invoking the tool fills the targeted fields with the provided arguments and dispatches a submit event on the form.

Name collisions: if the page registers an imperative tool with the same name, the imperative registration wins.

Opt-out: set WEBMCP_DECLARATIVE_SCAN=false to disable DOM scanning and the MutationObserver entirely.

Troubleshooting

  • "Failed to parse input arguments" — caused by document.modelContext.executeTool() failing on the page side. The monkey-patch bypasses this by calling the captured executor directly.

  • "detached Frame" — the page was navigated away or closed. The bridge auto-recovers by creating a new page. Re-run the tool call.

  • No tools discovered — ensure the page has called document.modelContext.registerTool() with tool definitions. Use webmcp_evaluate to call document.modelContext.getTools() manually.

  • CORS errors in test toolstest_fetch_title uses fetch() which is subject to page CSP. Test on a permissive origin.

License

MIT

A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
3Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Access Kernel's cloud-based browsers and app actions via MCP (remote HTTP + OAuth).

  • Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.

  • Live browser debugging for AI assistants — DOM, console, network via MCP.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/enihcam/opencode-webmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server