@mcptrail/webmcp-highlight
v0.1.0
Published
Zero-config highlighting of the DOM changes an agent's WebMCP tool calls make — overlay rings, a change timeline, and off-screen reveal. Framework-agnostic core + React binding.
Maintainers
Readme
@mcptrail/webmcp-highlight
Zero-config highlighting of the DOM changes an agent's WebMCP tool calls make. Wrap each tool call as a transaction, diff the DOM, and draw an overlay ring on what changed — plus a change timeline and off-screen reveal. Framework-agnostic core with an optional React binding.
It ships hooks, not looks: the accent and surface colors are inferred from the host page at runtime, so highlights match any design system with no configuration.
Why
When an AI agent drives a page through WebMCP, its edits happen without a cursor or a click — things just change. webmcp-highlight makes those changes legible to the human watching: every tool call flashes exactly what it touched, names the tool, and (if the change is below the fold or in another tab) scrolls it into view.
Install
npm install @mcptrail/webmcp-highlightreact is an optional peer dependency — only needed for the /react entry.
Vanilla
import { createWebMcpHighlight } from "@mcptrail/webmcp-highlight";
const wmh = createWebMcpHighlight(); // infers theme, mounts the timeline panel
wmh.autoWrapRegisterTool(); // patch navigator.modelContext.registerTool
// ...or wrap your own dispatcher:
function invoke(name, args) {
return wmh.track(name, args, () => runTool(name, args));
}React
import { WebMcpHighlightProvider } from "@mcptrail/webmcp-highlight/react";
<WebMcpHighlightProvider>
<App />
</WebMcpHighlightProvider>;Register imperatively instead of auto-wrapping:
import { useTrackedRegisterTool } from "@mcptrail/webmcp-highlight/react";
const register = useTrackedRegisterTool();
register({ name: "book_hotel", execute: async (a) => { /* ... */ } });How it works
Every tracked call runs run() inside a per-transaction MutationObserver window, then classifies the result:
| State | Behavior |
|-------|----------|
| changes on screen | draw the ring immediately |
| changes rendered but below the fold | scroll into view, then draw (revealed) |
| changes only in a display:none container | flag offscreen (nothing to show without a tab switch) |
Zero-config theming samples the page once: the accent is the dominant chromatic button background (by OKLCH chroma, with consensus across buttons), falling back to the CSS AccentColor system color; the surface is a representative card, falling back to Canvas/CanvasText. Call refresh() after a light/dark toggle.
Headless
Ignore the built-in UI and render your own from the events:
const wmh = createWebMcpHighlight({ panel: false });
wmh.onChange((e) => {
// e.tool, e.args, e.result, e.changes[], e.regions[], e.offscreen, e.revealed
});Options
| Option | Default | Notes |
|--------|---------|-------|
| root | document.body | subtree to observe |
| reveal | true | scroll off-screen changes into view before drawing |
| panel | true | render the built-in timeline panel |
| ringLifeMs | 1000 | how long a ring stays before fading |
| announce | true | announce changes to screen readers via an aria-live region |
| onChange | — | called after each tracked call settles |
Accessibility
A silent DOM mutation is invisible to a screen reader — and with an agent driving the page, every change is silent. With announce on (default), each settled tool call posts a concise message to a visually-hidden aria-live="polite" region, so a non-sighted user hears what the agent did:
"book_hotel changed 3 elements, scrolled into view"
Rings also honor prefers-reduced-motion (the flash becomes a static ring), and the injected overlays are pointer-events: none and never mutate the host node, so they don't disturb focus or layout.
API
const wmh = createWebMcpHighlight(options);
wmh.track(tool, args, run); // wrap a tool call; returns run()'s result
wmh.autoWrapRegisterTool(); // patch navigator.modelContext.registerTool
wmh.onChange(cb); // subscribe; returns an unsubscribe fn
wmh.refresh(); // re-run theme inference (after a theme toggle)
wmh.theme(); // the resolved { accent, surface }
wmh.log(); // recent ToolCallEvents, newest first
wmh.destroy(); // remove all injected chrome + observersDevelopment
npm install
npm run typecheck
npm test # Vitest + jsdom
npm run build # tsup → dist (ESM + CJS + d.ts)Roadmap
- A full contrast-floor loop on the ring color (guarantee visibility on any surface).
attributeFiltertuning + a configurable coalescing radius.- First-class off-screen "reveal" strategies (tab-badge, custom scroll).
License
MIT © Zied Guetari
