@gr3p/universal-webmcp
v0.3.2
Published
A local, progressive-enhancement runtime for exposing existing web UIs to WebMCP-capable agents.
Maintainers
Readme
Universal WebMCP Runtime
Stop feeding the DOM to your AI agent. Give it the tool it needs.
Universal WebMCP Runtime turns an existing web UI into a compact, inspectable tool surface. Instead of repeatedly serializing page HTML or accessibility trees, an agent can select one capability descriptor, invoke it with structured input, and receive structured JSON from the visible UI.
In the current benchmark, the selected-tool path used 27.2%–86.9% fewer tokens than targeted ARIA snapshots and 68.3%–99.5% fewer than relevant HTML. The measured results also document the important counterexample: an unfiltered cold catalog can be larger than ARIA, so selection and caching are part of the operating model—not optional optimizations.
@gr3p/universal-webmcp addresses both sides of a browser-agent integration:
| If you are… | Use the project to… | Entry point | |---|---|---| | A web developer | Expose existing forms, actions, navigation, and visible data as policy-governed tools | Website integration | | An agent user or automation author | Let Codex, Claude, and compatible agents detect or install the runtime, select a compact tool, and avoid repeated page-wide extraction | Agent integration |
Both paths use the same local runtime: listTools() returns non-invocable JSON descriptors, while invokeTool() operates the visible UI through its existing session, validation, and application feedback. Agent-side injection is used only when the browser host explicitly provides a writable Playwright, Puppeteer, or approved CDP bridge.
Existing page UI → discover capabilities → apply policy → agent invokes visible UI
Design goals
Most websites are built for people. Agents need the same experience in a form they can reliably inspect and invoke.
Universal WebMCP Runtime adds that layer on top of the page you already have:
- Progressive enhancement: the page stays fully usable when the runtime is absent.
- Local and session-preserving: actions use the visible UI, current authentication state, validation, and application feedback.
- Less context overhead: agents select a small structured tool instead of repeatedly ingesting the whole DOM or accessibility tree.
- Safe by default: risk, confidence, source admission, and user confirmation share one policy path.
- Works with dynamic apps: reconciliation covers DOM changes, SPA navigation, open shadow roots, same-origin frames, and explicit app invalidation events.
- No mandatory infrastructure: no API key, backend, remote DOM upload, or telemetry is required.
Typical use cases include e-commerce catalogs, search and filtering, dashboards, internal tools, CRM workflows, browser QA, crawling, and automation.
Measured token footprint
The repository benchmark collects the same complete repeated-record set through five representations and tokenizes each payload with o200k_base. WebMCP objects use minified JSON; task prompts and model responses are excluded equally from every path.
Results measured on 2026-07-31:
| Complete collection payload | Controlled fixture, 24/24 records | Public live page, 25/25 records | |---|---:|---:| | Relevant region HTML | 6,662 | 245,375 | | Relevant region ARIA snapshot | 2,901 | 10,187 | | WebMCP result with cached catalog | 1,829 | 1,041 | | Selected tool descriptor + input + result | 2,113 | 1,332 | | Full catalog + input + result | 3,879 | 17,034 |
For the selected-tool path, that is 68.3% fewer tokens than HTML and 27.2% fewer than ARIA in the controlled fixture; on the live page it is 99.5% fewer than HTML and 86.9% fewer than ARIA.
This is not a blanket first-turn claim. The richer complete catalog, including deterministic identities and compiled input schemas, was 33.7% larger than targeted ARIA in the fixture and 67.2% larger on the live page. The measured advantage depends on selecting the relevant descriptor or caching the catalog; cumulative WebMCP usage became smaller than repeated ARIA snapshots at the second task in both runs.
Reproduce the controlled benchmark locally, or run the explicitly separate live variant:
npm run benchmark:tokens
npm run benchmark:tokens:liveBoth commands fail unless the collection reports complete: true and matching expected and collected counts. Live results vary with public page content, locale, delivery location, and experiments. Generated JSON and SVG files are written to the ignored .benchmark/ directory.
Website integration
npm install @gr3p/universal-webmcpimport { createWebMCPRuntime } from '@gr3p/universal-webmcp';
// Run after the search-products form shown below has mounted.
const runtime = createWebMCPRuntime({
mode: 'hybrid',
confirmationPolicy: 'risk-based',
});
runtime.start();
const tools = runtime.listTools();
const result = await runtime.invokeTool('search-products', {
fields: { query: 'running shoes' },
});
runtime.stop(); // pause observation and native registrations
runtime.destroy(); // permanently release the runtimeConfirmation is application-owned rather than part of the WebMCP ModelContext
API. In a browser, confirmation-required actions use the visible native
window.confirm() dialog by default. Applications with their own confirmation UI
can provide it explicitly:
const runtime = createWebMCPRuntime({
confirmationProvider: {
async requestConfirmation(request) {
return { confirmed: await showConfirmationDialog(request) };
},
},
});Set confirmationProvider: null to disable the browser fallback and fail closed
whenever policy requires confirmation.
The native fallback is for trusted, application-owned integrations. Code injected
into a third-party page must use confirmationProvider: null or a genuinely
host-owned confirmation channel; page globals are not a trust boundary.
The root import is ESM and has no import-time browser side effect. For a script-tag integration, load the IIFE hosted on GitHub Pages and opt into auto-start explicitly:
<script
src="https://gr3p1p3.github.io/universal_webmcp/webmcp-runtime.iife.js"
data-webmcp-auto
></script>
<script>
const tools = AgentReadyWebMCP.autoRuntime?.listTools();
</script>For a version-pinned URL backed by the npm release, use jsDelivr:
<script
src="https://cdn.jsdelivr.net/npm/@gr3p/[email protected]/dist/browser.iife.js"
data-webmcp-auto
></script>An ESM browser entry is also available:
import { createWebMCPRuntime } from '@gr3p/universal-webmcp/browser';The GitHub Pages URL follows the current main build. Prefer the version-pinned npm URL when reproducibility matters.
What the runtime exposes
| Capability | What an agent can do | How it is created |
|---|---|---|
| Query | Read supported repeated lists and structured result collections | Deterministic discovery |
| Form | Fill and submit safe, visible text fields | Discovery or explicit mapping |
| Action | Click, select, toggle, navigate, or operate a repeated item | Discovery or explicit mapping |
| Native tool registration | Share approved tools with a browser model-context implementation | Optional adapter |
| Synchronization | Wait for a tool or for the UI to become idle | refresh(), waitForTool(), waitForIdle() |
Automatic queries require at least two visible, non-ignored records. Supported
patterns include ul/ol lists and repeated children in semantic or marked
containers (for example role="list" or id="results"). Generic text regions,
ordinary HTML tables, and collections with zero or one record are not
automatically exposed as queries. Use an explicit application query tool when
those states must remain addressable.
Deterministic semantic compilation
Automatic discovery is compiled before registration:
rendered DOM → Semantic UI Graph → ranked capabilities → compact WebMCP catalogThe compiler applies the same precedence on every scan:
- explicit WebMCP metadata;
- complete semantic forms;
- repeated structures and grouped item actions;
- ARIA and native HTML controls;
- conservative text heuristics.
A complete form dominates text fields it can address unambiguously and its
single automatic submitter. The form submits with that exact submitter through
the native submission algorithm, preserving its overrides and validation while
avoiding page-owned click handlers after approval.
Likewise, inferred links and popover invokers execute the exact native-default
target captured at discovery without running source click handlers, so page
code cannot substitute a different target mid-event. Declarative commandfor
invokers remain unexposed because their trusted native CommandEvent contract
cannot be replayed safely without reintroducing that retargeting window.
Popover actions that would hide an open target also fail closed because the
public hide API cannot preserve the approved invoker source.
Approval binds the selected form, submitter, editable fields, and other
successful controls up to native submit dispatch. The application's own
submit and formdata handlers remain authoritative after dispatch and may
validate, normalize, or route the request; the runtime neither replaces the
native algorithm nor claims to freeze the eventual network request. If the
browser cannot submit the approved exact submitter through requestSubmit(),
the action fails closed instead of falling back to a synthetic click.
Forms whose only enabled submitter is input[type="image"] are not exposed:
the runtime never fabricates unapproved image coordinates such as (0, 0).
Explicit submitter tools and ambiguous multiple-submit actions remain visible.
Ambiguous fields and multiple submit actions remain separate tools. Repeated actions are grouped, declared
equivalents are deduplicated, and only the highest-ranked automatic candidates
enter the catalog. Explicit application contracts are never removed by the
catalog budget.
Distinct controls are never merged merely because their labels match. A site
can deliberately collapse interchangeable controls by assigning the same
data-webmcp-equivalent value within one semantic scope.
Application-internal UI can be removed from automatic discovery by adding
data-webmcp-ignore to an element; the marked element and its entire subtree
are excluded, including open shadow roots and same-origin frames on that host.
Ignored descendants are also excluded from query output and editable form
parameters. This does not change native HTML submission: existing successful
controls still participate in the application's form submission. Use an explicit
application contract when the submitted payload itself must be restricted.
Names prefer stable application anchors such as data-webmcp-tool, id, and
unique form-control name. Each descriptor also carries a
metadata.semanticId; positional selectors are used only as the identity
fallback when the page supplies no stable anchor. Supported HTML constraints are
compiled into JSON Schema, including supplied-field constraints, compatible
email formats, patterns, select enums, and toolparamdescription. HTML
minlength/maxlength are intentionally omitted because HTML counts UTF-16 code
units while JSON Schema counts Unicode code points. Schemas guide callers;
they are not a general runtime JSON Schema validator. Native form validation
still governs submission.
const runtime = createWebMCPRuntime({
discovery: {
catalog: {
maxTools: 32,
minimumConfidence: 0.85,
dominance: true,
},
},
});
runtime.start();
const graph = runtime.getSemanticGraph();
const excluded = graph?.nodes.filter((node) => !node.selected);getSemanticGraph() exposes the latest decision graph without handlers or DOM
nodes. Excluded candidates report dominated, equivalent,
below-confidence, or catalog-budget, making automatic registration
inspectable rather than opaque. analyzeUI(root, options) provides the same
read-only report without starting a runtime.
Native registration follows the current WebMCP draft: tools are registered with
document.modelContext.registerTool(), internal handlers are exposed as
execute callbacks, and each registration is removed by aborting the
AbortSignal supplied at registration time. The deprecated
navigator.modelContext and non-standard unregisterTool(name) API are not
used.
The runtime supports four modes:
hybrid— explicit and adapter-provided tools plus local discovery; the default.auto— discover capabilities from the current UI.explicit— use only application-registered tools.adapter— admit only tools registered withprovenance.source: 'adapter'; the browser adapter publishes registrations and does not import a native catalog.
Make important workflows stable
Discovery is useful for general UI capabilities. For business-critical or fragile workflows, give the application a stable, owned contract with a manual mapping:
import {
createManualMappingTool,
createWebMCPRuntime,
} from '@gr3p/universal-webmcp';
const runtime = createWebMCPRuntime({ mode: 'hybrid' });
runtime.registerTool(createManualMappingTool({
name: 'search.products',
selector: 'form[aria-label="Product search"]',
action: 'submit',
kind: 'form',
description: 'Search the visible product catalog by query.',
inputSchema: {
type: 'object',
properties: { fields: { type: 'object' } },
},
risk: { level: 'low' },
}));
runtime.start();Manual mappings resolve their selector at invocation time, so ordinary re-rendering does not require rebuilding the runtime. Prefer stable, application-owned selectors and accessible names.
You can also improve discovery with semantic HTML and optional WebMCP metadata:
<form
role="search"
aria-label="Product search"
data-webmcp-tool="search-products"
data-webmcp-description="Search the visible product catalog by query."
>
<label for="query">Search products</label>
<input id="query" name="query" type="search" />
<button type="submit">Search</button>
</form>Built for safe browser automation
invokeTool() and native registration use the same policy decision path. Mutating or low-confidence actions can require user confirmation; critical-risk actions can be denied automatically.
Important defaults and boundaries:
- Automatically discovered form filling skips hidden, password, file, checkbox, radio, disabled,
aria-hidden,aria-disabled, andinertcontrols. - Action results never return field values.
- Repeated controls are collapsed into indexed tools instead of creating one tool per row or product.
- Scroll-backed lists can load incrementally and return a
completenessobject. Checkcomplete, counts, andsource: without a declared total,scroll-exhausteddescribes observed UI exhaustion, not proof of a complete virtualized, paginated, or backend dataset. Use an application-owned query for that guarantee. - The runtime never reads cookies or tokens, executes arbitrary code, crosses origin boundaries, bypasses CAPTCHA/authentication, or reverse-engineers private APIs.
For an agent-facing operating contract, use the provider-neutral agent skill. It teaches an agent to detect an existing runtime, install and inject the npm bundle only through a supported browser bridge, cache a compact catalog, invoke structured tools, and fall back safely when injection is unavailable.
Agent integration
The skill follows the open Agent Skills folder format and can be used by Codex, Claude Code, and compatible agents.
- Codex user installation: ask
$skill-installerto installhttps://github.com/gr3p1p3/universal_webmcp/tree/main/skills/universal-webmcp-runtime. - Codex repository installation: copy the
skills/universal-webmcp-runtimefolder to.agents/skills/universal-webmcp-runtime. - Claude Code user installation: copy the folder to
~/.claude/skills/universal-webmcp-runtime. - Claude Code repository installation: copy the folder to
.claude/skills/universal-webmcp-runtime.
Then ask the agent to browse normally. The skill activates for interactive
browsing, crawling, scraping, and browser QA. It prefers native WebMCP or an
already integrated runtime; when the host exposes approved Playwright,
Puppeteer, or CDP injection, it can install
@gr3p/universal-webmcp and inject the compiled IIFE. A read-only browser
surface is never treated as permission or capability to inject code.
The supplied injection recipe disables page-owned confirmation, so inferred mutations are blocked until a trusted host/application confirmation channel is available. Reading supported collections works locally; use the host's normal browser interaction path when a mutation cannot be authorized through the runtime.
Keep agents synchronized with the UI
runtime.refresh();
await runtime.waitForTool('query.results', { timeoutMs: 2_000 });
const sync = await runtime.waitForIdle({
settleMs: 100,
timeoutMs: 2_000,
});Form and action invocations wait for the UI to settle by default. Elements marked with aria-busy="true" or data-webmcp-busy="true" keep the runtime busy until the application clears them.
For state changes that do not produce useful DOM mutations, provide a framework-neutral invalidation source:
import {
createEventInvalidationSource,
createWebMCPRuntime,
} from '@gr3p/universal-webmcp';
const runtime = createWebMCPRuntime({
invalidationSources: [
createEventInvalidationSource(window, ['app:state-ready']),
],
});
runtime.start();
window.dispatchEvent(new Event('app:state-ready'));Creating an open shadow root on an already-mounted host with attachShadow()
does not itself emit an observable DOM mutation. After mounting that component,
call runtime.refresh() or dispatch the configured invalidation event once.
The runtime then discovers the root and observes subsequent mutations inside it.
Busy checks include the configured root itself, open shadow roots, and same-origin
frames enabled by discovery options. Idle means the observed UI is quiet, not
that arbitrary network activity has finished; applications should mark pending
work as busy. Automatic post-action waits are bounded by the synchronization
timeout and may end when the invoked capability is removed or replaced. Call
waitForIdle() explicitly when a workflow must wait beyond that transition.
For agent builders
The runtime gives browser agents a compact protocol:
- List the catalog once.
- Select the smallest relevant tool by semantic contract.
- Validate JSON input against its schema.
- Invoke by stable name.
- Verify the requested outcome and refresh only after a relevant UI change.
This replaces repeated page-wide DOM dumps with named tools, structured inputs, structured results, and explicit policy outcomes. The bundled skill works with Codex, Anthropic/Claude, and other browser-agent hosts.
Development and verification
Development requires Node.js 20.19+ or 22.12+ (including newer supported releases). The published runtime package supports Node.js 20+ for ESM import.
npm install
npm run check
npm run build
npm run test:e2e:install
npm run test:e2e
npm run dev
npm pack --dry-runThe dev command starts the Vite playground. Its Live WebMCP proof runs the real
browser registration adapter against an instrumented modelContext bridge and
shows the exact agent-facing payload. The demo catalog is intentionally bounded to
the application controls (search, filters, catalog views, pagination, product
details, and cart actions). The manual-mapping editor and the proof, runtime, and
QA controls remain visible and usable by people, but their containing regions carry
data-webmcp-ignore, the explicit contract for excluding a subtree from discovery.
Click Run live registration proof to see:
runtime stopped → 0 registered tools
runtime active → the application-only tools with names, descriptions, schemas, and annotations
runtime stopped → 0 registered toolsThis is also enforced by test/e2e/playground.spec.ts. Unit tests use jsdom and
do not require a live WebMCP implementation. The Playwright suite opens the
playground in Chromium, Firefox, and WebKit and verifies discovery, native bridge
registration and revocation, policy, lifecycle, DOM synchronization, and safe
invocation.
Live smoke tests are intentionally separate from CI because external-site content and availability are not controlled by this repository:
npm run test:e2e:live
WEBMCP_LIVE_URL=https://example.com/ npm run test:e2e:liveThe default live target is https://www.wikipedia.org/. Live tests inspect and invoke read-only capabilities only.
What this is not
Universal WebMCP Runtime is not a remote MCP server, backend API replacement, CAPTCHA/authentication bypass, payment automation system, private-API reverse-engineering tool, arbitrary-code runner, cookie/token reader, or cross-origin iframe bridge. The host page must remain usable without the runtime.
License
GNU Affero General Public License v3.0 only (AGPL-3.0-only).
