# Opencode Webmcp

> MCP server bridge — exposes Chromium WebMCP tools (document.modelContext) as MCP tools. Works with OpenCode, Claude Code, and any MCP client. Enables AI agents to discover and invoke browser-registered tools.

- **Type:** MCP server
- **Install:** `agentstack add mcp-enihcam-opencode-webmcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [enihcam](https://agentstack.voostack.com/s/enihcam)
- **Installs:** 0
- **Category:** [Web & Browser](https://agentstack.voostack.com/c/web-and-browser)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [enihcam](https://github.com/enihcam)
- **Source:** https://github.com/enihcam/opencode-webmcp

## Install

```sh
agentstack add mcp-enihcam-opencode-webmcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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.

## Prerequisites

- Node.js 18+
- Chromium/Chrome with WebMCP support (Chrome 150+)
- An MCP client (OpenCode, Claude Code, etc.)

## Setup

```bash
git clone  webmcp-bridge
cd webmcp-bridge
npm install
```

## Usage

### Standalone

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

### With a config file

```bash
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`:
```yaml
chromePath: /usr/bin/chromium
targetUrl: https://www.google.com
headless: false
historyMax: 500
logHistory: false
```

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

### OpenCode (opencode.json)

```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 (``); 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):
```json
{ "name": "webmcp_screenshot", "arguments": {} }
```

Full-page JPEG at quality 75:
```json
{ "name": "webmcp_screenshot", "arguments": { "format": "jpeg", "quality": 75, "fullPage": true } }
```

Region clip (200×100 at top-left):
```json
{ "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.

```json
{ "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-patch** — `page.evaluateOnNewDocument` injects a script that intercepts `document.modelContext.registerTool` and `executeTool` to capture actual `execute` functions
- **Declarative API scanner** — also scans the DOM for `` 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:

   ```html
   
     
     
     Search
   
   ```

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 tools** — `test_fetch_title` uses `fetch()` which is subject to page CSP. Test on a permissive origin.

## License

MIT

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [enihcam](https://github.com/enihcam)
- **Source:** [enihcam/opencode-webmcp](https://github.com/enihcam/opencode-webmcp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-enihcam-opencode-webmcp
- Seller: https://agentstack.voostack.com/s/enihcam
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
