dsh-plugin-browserskill
Tencent/BrowserSkillBrowserSkill 的 DeepSeek Harness 浏览器自动化桥接插件,通过原生浏览器工具控制可见的 Chrome 和 Edge Agent Window,支持可访问性与 VOM 页面观察、截图、隔离的多会话控制和 Web UI 实时观察浮层。
English | 中文
dsh-webmcp is a DeepSeek Harness plugin that lets an agent discover and invoke the site tools a website exposes through the W3C WebMCP protocol, using a built-in headless Chromium.
WebMCP is a W3C Web Agents Community Group standardization proposal (github.com/webmachinelearning/webmcp). In 2026-08 OpenAI shipped site-tool support in the ChatGPT desktop built-in browser and launched a challenge around it; Google Chrome Labs also open-sourced webmcp-tools. This plugin brings that capability to a DeepSeek Harness agent without relying on an external browser or manual selector maintenance.
| Aspect | Traditional automation | WebMCP site tools |
|---|---|---|
| How a site exposes actions | Hard-coded selectors and scripts you maintain | Protocol-declared surfaces (navigator.modelContext, window.webmcp, form[data-webmcp-tool]) |
| Discovery | Reverse-engineer the page | webmcp_discover returns the normalized tool list |
| Invocation | Replay clicks and typed input | webmcp_invoke runs the site's real tool function in-page |
| Consent | The site has no say in the call | The protocol requires site-side confirmation |
| Drift | Breaks on markup changes | Follows the site's own declared contract |
dsh plugin --profile web add github:T-Markus-Liang/dsh-webmcp
The plugin also ships bin/dsh-webmcp-serve.mjs, a stdio MCP gateway that bridges a site's WebMCP tools to ANY MCP client (Claude Code/Desktop, Codex, …). Point it at a URL and the site's tools become a local MCP server any MCP client can consume natively.
dsh-webmcp-serve <url> [--allow-private-hosts] [--manifest-ttl-ms N] [--no-cache]
Client config example:
{ "mcpServers": { "my-site": { "command": "node", "args": ["/path/to/dsh-webmcp/bin/dsh-webmcp-serve.mjs", "https://example.com/app"] } } }
Protocol: newline-delimited JSON-RPC 2.0 (MCP stdio) implementing initialize / ping / tools/list / tools/call. tools/list comes from page discover (dual-mount / Map / Promise / executeToolByName fully compatible); tools/call reuses the same BrowserSession pipeline. Manifests are disk-cached under ~/.dsh-webmcp/manifests/ (default TTL 300s; --no-cache disables). Diagnostics go to stderr only — stdout is the pure MCP channel. Private-network targets are refused by default (plugin parity).
The gateway also serves an HTTP transport (Streamable-HTTP style): POST /mcp for request/response and GET /sse (Server-Sent Events) for server→client notifications such as tools/list_changed. Enable with:
dsh-webmcp-serve <url> --http --host 0.0.0.0 --port 9000 --token <secret>
POST /mcp carries request/response JSON-RPC; GET /sse is the event stream.
Bearer auth: send Authorization: Bearer <secret> (missing/wrong → 401). Multiple
MCP clients can call it concurrently; tools/list_changed is broadcast to every
live SSE subscriber. A streamable-http MCP client config:
{ "mcpServers": { "my-site": { "type": "http", "url": "http://host:9000/mcp",
"headers": { "Authorization": "Bearer <secret>" } } } }
Sites don't have to wait for a browser to expose actions to agents. The plugin ships a zero-dependency embeddable snippet (sitekit/webmcp-register.js) so a site author can declare a tool directly on the page with one <script src> include, then expose it via WebMCP.register(...).
<script src="https://cdn.example.com/webmcp-register.js"></script>
<script>
WebMCP.register({
name: 'search_products', // required, unique
description: 'Search the catalog', // human-readable, agent-facing
inputSchema: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] },
outputSchema: { type: 'object', properties: { count: { type: 'number' } } },
annotations: { readOnlyHint: true }, // MCP ToolAnnotations
execute: async (args) => ({ count: queryYourDb(args.q) }),
});
</script>
The snippet is designed to be safe and honest:
document.modelContext and navigator.modelContext. The mount position shifted between Chrome builds, so checking only one silently fails (the CloudNSite lesson).destructiveHint defaults to true (and openWorldHint to true) unless the author explicitly opts out, so the v1.1.0 host-side confirm guard always sees a conservative, truthful value.false and changes nothing.window.webmcp fallback — when no modelContext is present the tool is pushed to a window.webmcp registry that the bridge probes; Chrome 151+'s Promise<void> registerTool is awaited, and an idempotent guard prevents double registration.dsh-webmcp-serve --check <url> runs discover + computeReadiness against a site and prints a report: url, title, tool count, schema completeness, annotation coverage, and readOnly / destructive counts (score = 100 × (0.6 × schema + 0.4 × annotations)).
dsh-webmcp-serve --check https://ai-sdk-webmcp.persona-chat.dev
Exit codes: 0 when score >= 60 (agent-ready), 1 below, 2 when discover fails.
The plugin registers two agent tools.
# Discover the site tools a page exposes (agent tool-call; wrapper is illustrative)
agent:
tool: webmcp_discover
input:
url: https://example.com
# Invoke a discovered site tool with arguments (args is optional)
agent:
tool: webmcp_invoke
input:
url: https://example.com
tool: <name-from-discover>
args:
query: "Show the latest posts"
Both tools accept an optional refresh (boolean) parameter: forces re-navigation. Invocations may return an argsWarning field listing required args you omitted (per the tool's inputSchema.required). webmcp_invoke also accepts confirm (boolean): required true when the tool is annotated destructiveHint (host-side guard; re-invoke after reviewing the annotation).
All options are optional and are set under the config block in cordis.patch.yml.
| Option | Default | Description |
|---|---|---|
headless | true | Run Chromium headless. |
navigationTimeoutMs | 30000 | Timeout for navigating to the page, in milliseconds. |
invokeTimeoutMs | 20000 | Timeout for a single tool invocation, in milliseconds. |
chromiumPath | "" | Explicit path to a Chromium executable; overrides automatic resolution. |
allowPrivateHosts | false | When false (default), URLs targeting loopback/private networks (localhost, 127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7, *.local, *.internal) are rejected — protects intranet from prompt-injected scans. Set true for local development fixtures. |
sessionTtlMs | 30000 | If a tool targets the exact URL navigated recently (within TTL), navigation is skipped and the live page is reused. Set 0 to always navigate. A refresh: true per-call override forces navigation. |
maxResultChars | 12000 | Tool results serialized above this size are truncated to a { truncated, totalBytes, preview, hint } envelope — protects the agent's context window from huge outputs. |
maxSessions | 3 | Per-origin browser sessions, bounded pool. LRU eviction beyond capacity (1-8). |
idleTtlMs | 30000 | Idle sessions (their browsers) are closed after this; 0 disables reclamation. |
trace | true | JSONL call tracing to ~/.dsh-webmcp/trace/ (daily rotation, best-effort). Set false to disable. |
Since v0.1.1 tool discovery probes BOTH spec mounts — navigator.modelContext and document.modelContext — alongside window.webmcp and declarative <form data-webmcp-tool> elements. Since v0.2.1 it also understands polyfill/native storage shapes (Map-backed tool stores, promise-returning getTools()) and routes calls through the mount's own executeToolByName when a registered entry carries no inline function. Since v1.1.0 discovery also probes /.well-known/webmcp and /.well-known/mcp.json (surface well-known), and every tool entry carries its MCP annotations.
TypeScript consumers: import type { DiscoverResult, InvokeResult, BridgeConfig } from 'dsh-webmcp/types' — the bridge API is fully typed via types/index.d.ts (no build step).
When choosing a Chromium executable, the plugin tries, in order:
config.chromiumPathDSH_WEBMCP_CHROMIUMms-playwright cache scanchannel: 'chrome' (system Chrome)GET /webmcp/status
{
"plugin": "dsh-webmcp",
"version": "0.1.0",
"browser": { "launched": true },
"config": {}
}
GET /webmcp/dashboard serves a self-refreshing (5s) HTML observability page:
config summary, pool state, aggregate stats (count / success-rate / p50 / p95 / avg),
the last 20 traced calls, and manifest-drift events. /webmcp/status now also embeds
a stats block. Tracing appends one JSONL line per call to
~/.dsh-webmcp/trace/YYYY-MM-DD.jsonl (override with DSH_WEBMCP_TRACE_DIR).
Manifest drift: re-discovering a URL whose tool set changed yields
_meta.drift = { added, removed } (in-memory per-origin baseline) plus a
manifest-drift trace line.
Readiness endpoint: GET /webmcp/readiness returns the Agent-readiness view —
per-origin score, schema completeness, annotation coverage, and readOnly /
destructive counts (score = 100 * (0.6 * schema + 0.4 * annotations)). The same
dashboard hosts the interactive Tool tester (a POST /webmcp/tester form): pick
a discovered tool, enter its args, toggle the confirm checkbox, and read back the
call result JSON (body: { url, tool, args, confirm }). This is the free, open
answer to the commercial readiness scanners (web-mcp.net charges $49/mo).
WebMCP itself is standardizing fast: Chrome 149 and Edge 150 ship it behind an Origin Trial today, ChatGPT Desktop supports site tools natively, and Brave (Leo) ships an experimental integration. Firefox and Safari have only filed standards positions so far.
This plugin drives any locally available Chromium-family binary (see the resolution chain below). It deliberately does not support driving Firefox or Safari.
Honest positioning: the W3C proposal lists headless browsing and fully autonomous agents among its non-goals — its vision is human-in-the-loop use inside an agent-capable browser. This plugin is a pragmatic transition bridge and a site-debugging tool: it lets harnesses without such a built-in agent reach sites that already expose WebMCP tools today. For the full collaborative UX, prefer native implementations once your browser ships them.
Every tool result carries an error code on failure (and argsWarning on
missing required args). Host-side codes come from the bridge itself:
| code | meaning |
|---|---|
bad-url | URL lacks an http(s):// scheme |
private-host-blocked | target is loopback/private/link-local — or resolves to one (DNS-rebinding guard, v0.2.2) |
dns-failed | hostname did not resolve |
network | connection refused/reset/unreachable |
timeout | navigation or evaluation exceeded its budget |
navigate-failed | other navigation failure |
internal | unexpected bridge failure |
confirm-required | tool is annotated destructiveHint:true — re-invoke with confirm:true |
Page-side codes come from the injected agent: unknown-tool (no such tool on
the page), not-callable (tool has no callable implementation), tool-threw
(the tool itself threw — message and truncated stack included).
Condensed; full ladder with acceptance criteria lives in ROADMAP.md.
| Version | Theme | Status |
|---|---|---|
| v0.2.0 | private-network shield + session reuse | ✅ shipped |
| v0.2.1 | polyfill/native runtime compat + argsWarning + result budget | ✅ shipped |
| v0.2.2 | page-agent engineering + DNS guard + error taxonomy | ✅ shipped |
| v0.3.0 | stdio MCP server gateway mode | ✅ shipped |
| v0.4.0 | per-origin session pool: concurrency + LRU + idle reclamation | ✅ shipped |
| v0.5.0 | observability: JSONL trace + dashboard + manifest drift | ✅ shipped |
| v1.1.0 | annotations passthrough + destructive guard + well-known probing | ✅ shipped |
| v1.2.0 | push (tools/list_changed) + outputSchema passthrough + dashboard readiness & tester | ✅ shipped |
| v1.3.0 | TS contract + type/runtime consistency audit | ✅ shipped |
| v1.4.0 | gateway HTTP/SSE transport + bearer auth + multi-client | ✅ shipped |
| later | polyfill auto-injection, diagnostics bundle, dsh-browser interop | exploratory |
MIT