@machfivetechchicago/machvive-webmcp-ai
v6.10.1
Published
I don't want to use your product's agent; I want my agent to be able to use your product.
Maintainers
Readme
WebMCP empowers agents by giving them access to your core business functionality within a safe, semantic sandbox.
A Business Accelerator is a targeted, authoritative, conversion-optimized landing page with automated onboarding that facilitates a non-linear, multi-touch sales cycle. Exposing your Business Accelerators to agents makes perfect sense: it gives agents access to interactive experiences that deliver real business value—not just content.
But if agents are becoming part of your customer journey, you need to understand how they engage with your site. We give you the tools to measure that engagement. Record agent sessions and play them back to see exactly how they navigate, interact, and use your Business Accelerators. This gives you detailed insight into agent behavior—and into the broader ecosystem of agent-driven engagement.
No framework, no build step, no runtime dependencies — just standards-based custom
elements with Shadow DOM that work anywhere customElements does.
| Component | Tag | What it does |
| --- | --- | --- |
| WebMCP polyfill | <machvive-webmcp-polyfill> | Shims navigator.modelContext so a page can expose tools to AI agents |
| WebMCP inspector | <machvive-webmcp-inspect> | Lists registered tools, builds a form from each schema, runs them |
| WebMCP analytics | <machvive-webmcp-analytics> | Captures every tool call for listing, editing, export, replay, and dataLayer |
| Products | <machvive-webmcp-products> | Turns the page's schema.org JSON-LD into searchable, read-only tools |
| M5T Magnet | <machvive-m5t-magnet> | Exposes a MachFive Magnet's lead capture to agents |
| Lorum Ipsum | <machvive-lorum-ipsum> | Generates placeholder copy, and publishes it as a WebMCP tool |
📖 Read the full guide on the Wiki — what WebMCP is and why it beats scripted clicking, how the components compose, and the constraints worth knowing before you adopt. For the commercial case rather than the technical one, start with WebMCP as a Business Accelerator.
🎮 Try the live playground — a running WebMCP endpoint. Point Browser Use or your own agent at it and watch it discover and call the page's tools without touching the DOM. Source: webmcp-playground.
⚡ Integration with Vite (Vanilla JS)
This package is optimized for modern ESM environments like Vite. You can import individual components or the entire library at once.
1. Installation
Install the package via NPM into your Vite project:
npm install @machfivetechchicago/machvive-webmcp-ai2. Import Components
Inside your main entry script (e.g., main.js or index.js), import the components you need to register them with the browser's customElements registry.
Option A: Cherry-Pick (Recommended for smaller production bundles)
// Import only the specific tools your app requires
import '@machfivetechchicago/machvive-webmcp-ai/webmcp-polyfill';
import '@machfivetechchicago/machvive-webmcp-ai/lorum-ipsum';Option B: Bulk Import (Registers all components at once)
// Quick setup to make all components available globally
import '@machfivetechchicago/machvive-webmcp-ai';3. Use in HTML
Once imported, use the tags directly in your markup. Because these are vanilla Web Components utilizing the Shadow DOM, they are fully isolated and work out of the box in index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Vite App</title>
</head>
<body>
<!-- Use your custom elements anywhere -->
<machvive-lorum-ipsum></machvive-lorum-ipsum>
<script type="module" src="/main.js"></script>
</body>
</html>🤖 WebMCP Polyfill
machvive-webmcp-polyfill installs a navigator.modelContext shim following the
W3C WebMCP proposal,
so a page can declare agent-callable tools before browsers ship the API natively.
It is a no-op when the browser already implements WebMCP, so your page always talks
to the real implementation where one exists. Because the native API is
[SecureContext], the polyfill installs only on a secure origin — which includes
localhost, 127.0.0.1, and file:// URLs in most browsers.
Offline and intranet bundles
That default mirrors the native API so you cannot build against a surface the
browser will never provide. It does not fit every case: an offline bundle, or a
page served to a LAN address like 192.168.1.20, has no native implementation
coming, and the check only blocks. Opt in there:
<machvive-webmcp-polyfill allow-insecure></machvive-webmcp-polyfill>import { installWebmcpPolyfill } from '@machfivetechchicago/machvive-webmcp-ai/webmcp-polyfill';
installWebmcpPolyfill({ allowInsecureContext: true });The element retries the install, so the attribute works even though the module installs on import. The refusal warning names the current origin and both escape hatches, so the cause is visible from the console.
<machvive-webmcp-polyfill></machvive-webmcp-polyfill>Importing the module installs the shim immediately — you can register tools without waiting for the element to upgrade:
import '@machfivetechchicago/machvive-webmcp-ai/webmcp-polyfill';
navigator.modelContext.registerTool({
name: 'add_to_cart',
description: 'Add a product to the shopping cart',
inputSchema: {
type: 'object',
properties: { sku: { type: 'string', description: 'Product SKU' } },
required: ['sku']
},
execute: async ({ sku }) => {
await cart.add(sku);
return { content: [{ type: 'text', text: `Added ${sku} to cart.` }] };
}
});Use unregisterTool(name) to drop a single tool, or provideContext({ tools }) to
replace the whole toolset when app state changes which tools make sense.
Bridging to an agent
The polyfill is a registry — it has no transport. Bridge code (an extension, a devtools panel, a test harness) discovers tools and invokes them:
import { TOOLS_CHANGED_EVENT } from '@machfivetechchicago/machvive-webmcp-ai/webmcp-polyfill';
window.addEventListener(TOOLS_CHANGED_EVENT, (e) => console.log(e.detail.tools));
const result = await navigator.modelContext.callTool('add_to_cart', { sku: 'M5T-001' });navigator.modelContext.tools returns the descriptors minus their handlers.
callTool never throws: a handler that rejects comes back as
{ content: [...], isError: true }.
🔎 Inspector
machvive-webmcp-inspect lists every registered tool, renders a form from its
inputSchema, and executes it with what you type. Values are coerced to the types
the schema declares — an integer field sends 3, not "3" — required fields are
enforced before anything runs, and object/array fields accept JSON.
<!-- inline: renders where you place it -->
<machvive-webmcp-inspect></machvive-webmcp-inspect>
<!-- floating: docks as an overlay panel with a toggle, no layout impact -->
<machvive-webmcp-inspect floating></machvive-webmcp-inspect>
<!-- floating and expanded on load -->
<machvive-webmcp-inspect floating open></machvive-webmcp-inspect>
<!-- bottom-right is crowded: chat widgets and support launchers live there too -->
<machvive-webmcp-inspect floating position="bottom-left"></machvive-webmcp-inspect>
<!-- no launcher drawn; opens on the key combo instead -->
<machvive-webmcp-inspect floating hidden-fab hotkey="ctrl+shift+k"></machvive-webmcp-inspect>position takes bottom-right (default), bottom-left, top-right or
top-left, and --mv-fab-offset-inline / --mv-fab-offset-block nudge it from
the edge. hotkey is opt-in — a component that claimed a key combination on
every page embedding it would be a poor guest, so no listener binds without it.
show() and hide() drive the panel from script. The list refreshes automatically
as tools are registered or removed.
📊 Analytics
machvive-webmcp-analytics captures every WebMCP invocation — params, result,
duration, and errors — then lets you list, edit, export, replay, or forward them.
import '@machfivetechchicago/machvive-webmcp-ai/webmcp-analytics';<machvive-webmcp-analytics></machvive-webmcp-analytics>Import it before you register tools. Capture works by wrapping each tool's
handler at registration time, so anything registered earlier is invisible to it —
the component warns in the console when it detects this. Wrapping the handler rather
than the caller is deliberate: it records invocations from any caller, including a
native navigator.modelContext and real agents, neither of which route through this
library.
Captured calls persist to IndexedDB, so a log survives reloads and is not bound by the ~5 MB localStorage ceiling. The store degrades to memory-only where IndexedDB is unavailable. The default cap is 500 entries, oldest evicted first.
Working with the log
import { callLog } from '@machfivetechchicago/machvive-webmcp-ai/webmcp-analytics';
await callLog.ready; // restore from IndexedDB is async
callLog.entries; // captured calls, oldest first
callLog.toJSON(); // export as JSON
callLog.import(json); // merge a previously exported log
callLog.update(id, { params }); // edit before replaying
await callLog.replay(id); // re-run as captured
await callLog.replay(id, { sku: 'OTHER' }); // re-run with edited paramsRecording is strictly best-effort: a failure inside the log — a throwing subscriber, an unwritable store — can never change a tool's result or make a passing call look like it errored.
Google Tag Manager
Pushing to window.dataLayer is off unless you opt in, so importing the
component never emits tracking traffic on its own:
<machvive-webmcp-analytics datalayer></machvive-webmcp-analytics>Each call then pushes { event: 'webmcp_tool_call', webmcp_tool, webmcp_status,
webmcp_duration_ms, webmcp_params }. Without the attribute, push individual entries
on demand with callLog.pushToDataLayer(id) or the per-entry button in the UI.
🧲 M5T Magnet
machvive-m5t-magnet bridges a MachFive Magnet —
an interactive widget that automates conversational lead generation — to WebMCP,
so an agent can use it the way a visitor would.
<!-- the page already carries your magnet snippet: nothing else needed -->
<script src="https://your-magnet-host/m5t/v5/coreSnippet?appguid=..." async></script>
<machvive-m5t-magnet></machvive-m5t-magnet>
<!-- or have the element load it, naming the origin your magnet admin gave you -->
<machvive-m5t-magnet app-guid="your-app-guid" src="https://your-magnet-host/m5t/v5/coreSnippet">
</machvive-m5t-magnet>The package embeds no host. Where your magnet is served from is runtime configuration, so this package ships no external URLs and a change of hosting costs you no upgrade.
import '@machfivetechchicago/machvive-webmcp-ai/m5t-magnet';It loads the magnet snippet, waits for the runtime, and registers tools. It renders nothing itself — the magnet draws its own launcher.
The tools come from your magnet, not from this package
A magnet already declares what it collects: each step's field, type, prompt, and options. That declaration is an agent interface, so the tool surface is derived from it. Configure a new step in your magnet admin and it appears to agents with no code change here.
Against a magnet asking product interest, name, and email, you get:
| Tool | |
| --- | --- |
| magnet_describe | what the form asks and whether it can schedule |
| magnet_options | allowed choices for a multiple-choice field |
| magnet_start | open the form with answers filled in |
| magnet_status | has the visitor submitted yet |
magnet_options only appears if something has choices. Add a booking step and
magnet_describe starts reporting that the magnet can schedule a meeting.
The agent fills in; the visitor submits
magnet_start prefills and opens the widget. It does not submit. An agent
posting someone's email address without them seeing it is a consent problem, and
lead capture is exactly where that matters.
Some steps can't be pre-answered at all. A scheduler step is the important one:
if it were prefilled the step would be skipped, so a remote booking would never
register and the lead would look booked without being booked. Step types this
package doesn't recognise degrade the same way — described, not guessed at.
Keys beyond the declared fields are passed through rather than rejected — that's how a campaign or source id reaches the lead record.
On a magnet that opens with a welcome card and buttons, the widget clears answers
when the visitor taps one, so prefill doesn't survive. magnet_describe says so.
Invalid input comes back as data so the agent can correct itself:
"Nope" is not a choice for product_interest.
Options: Evaluating MachVive | Building with it and need help | ...Events
magnet-ready when tools register, magnet-capture when a visitor submits, and
magnet-error if the runtime never loads. The element also exposes .config
(the magnet definition it bound to) and .captures.
🛒 Products from JSON-LD
<machvive-webmcp-products></machvive-webmcp-products>That is the whole integration. The component reads the schema.org Product
markup already on your page — the JSON-LD you publish so search engines
understand your catalogue — and exposes it as three read-only tools.
The work is already done. Sites carry this markup because search rewards it, and somebody is already keeping it current. No backend, no new API, nothing to keep in sync: the catalogue an agent wants is the one your SEO team maintains.
| Tool | |
| --- | --- |
| search_products | Free text plus category, brand, availability, minPrice, maxPrice, limit, offset |
| get_product | One product in full by SKU, MPN or GTIN, including its original JSON-LD |
| list_product_facets | The categories, brands, stock states and price range present on the page |
Filter values come from your data. category, brand and availability are
declared as enums built from the products actually on the page, so an agent never
guesses a category name that does not exist — and because the inspector renders an
enum as a <select>, you get working dropdowns with no configuration. Change the
catalogue and call load(); the enums follow.
Read-only by construction. It searches and reads. It does not add to a cart, place an order or submit anything, which is why it is safe to publish automatically and why it needs no credentials. A test asserts no registered tool name contains a mutating verb.
Attributes
| Attribute | |
| --- | --- |
| src | Optional JSON-LD URL. Omit to read the page's own markup |
| no-tool | Read the data but publish nothing |
| theme | light or dark |
const el = document.querySelector('machvive-webmcp-products');
el.products; // the normalized catalogue
el.facets; // categories, brands, availability, price range
await el.load(); // re-read and republish after the page's JSON-LD changesIt handles the markup you actually have
The normalizer is the substance here, and it ships as its own entry point — importable from Node with no DOM, because the component subpath pulls in the polyfill, which needs a browser:
// Works in a build step, a CI check, or a plain Node script.
import { collectProducts, facetsOf } from '@machfivetechchicago/machvive-webmcp-ai/jsonld';
const products = collectProducts(JSON.parse(someJsonLdString));
console.log(facetsOf(products)); // what an agent could filter onIt is useful well beyond the element. Real JSON-LD is correct-but-inconsistent, so it
copes with @graph, ItemList/itemListElement, bare arrays, a lone Product,
@type as an array or a full URL, brand as a string or a node, offers as an
object or an array, AggregateOffer price ranges, every gtin flavour, prices as
decorated strings, and availability arriving as https://schema.org/InStock
rather than a word. A malformed block is skipped rather than taking the page's
other blocks down with it.
Variants
Shopify and most storefronts describe a product with options as a
ProductGroup carrying hasVariant, where the variants hold only what differs
— often just a URL. Everything describing the product lives on the group.
The group is treated as the product and its variants are summarised:
{ "name": "Men's Tree Runner", "sku": "TR-MENS", "price": 100,
"availability": "InStock", "variants": 49, "variesBy": ["size", "color"] }That is one useful row instead of 49 nameless ones, and a fraction of the
context. get_product resolves a variant SKU to its group, because the
identifier on the box is the variant's. A group with no price of its own takes
the lowest variant price and reports the range; it counts as in stock if any
variant is, since "out of stock" because one size has gone is the wrong answer
for an agent to act on.
One tool set per page however many elements you place; the first owns it and hands
over if removed. Results are capped at 50 with a default of 10 and a truncated
flag, because a tool result lands in an agent's context window.
Every bound is declared on the schema as well as enforced — limit carries
minimum/maximum/default, prices carry minimum, and free text carries
maxLength. A constraint that lives only in a description is invisible to a
validator and to an agent planning a call. Over-long text is refused with a
reason rather than truncated, because shortening a query answers a question the
agent did not ask.
Choosing which magnet tools to publish
magnet_start opens and prefills the widget on a real person's screen. The other
three only read. A deployment that wants agents to explain a form without
touching it asks for the read tier:
<machvive-m5t-magnet tools="read"></machvive-m5t-magnet>| Value | Publishes |
| --- | --- |
| all (default) | all four |
| read | magnet_describe, magnet_options, magnet_status |
| none | nothing — config is still readable from el.config |
Without the attribute, the magnet's own mag_agent_tools decides, so a merchant
setting travels with the magnet rather than the markup (true/false map to
all/none). Changing the attribute re-publishes immediately.
One element owns the magnet_* names per page. They are a page-wide
namespace, so a second element registering them would silently replace the first
— which happens the moment a Magnet runtime mounts one while the page already
carries its own tag. The first to claim wins, later ones warn and register
nothing, and removing the owner hands the namespace to a survivor rather than
stripping the page. publishTools() re-claims after provideContext.
🌓 Theming
The UI components follow the viewer's OS preference automatically — no
configuration, no flash of the wrong palette. Set theme to override:
<machvive-webmcp-inspect></machvive-webmcp-inspect> <!-- follows the OS -->
<machvive-webmcp-analytics theme="dark"></machvive-webmcp-analytics>
<machvive-webmcp-inspect theme="light"></machvive-webmcp-inspect>theme is also a reflected property, so el.theme = 'dark' and
el.theme = null (back to following the OS) work from script.
Overriding the palette
Colors are CSS custom properties on the host. Custom properties inherit through
shadow boundaries where ordinary styles do not, so you can restyle the components
from your own stylesheet without ::part or !important:
machvive-webmcp-inspect,
machvive-webmcp-analytics {
--mv-accent: #7c3aed;
--mv-bg: #ffffff;
--mv-fg: #111827;
--mv-border: #e5e7eb;
}Every token has both a light and a dark value; overriding one replaces it in both themes unless you scope your override inside your own media query.
| Token | Role |
| --- | --- |
| --mv-fg / --mv-muted / --mv-faint | Text: primary, secondary, hints |
| --mv-bg / --mv-surface / --mv-input-bg | Panel, raised areas, form fields |
| --mv-border / --mv-border-soft / --mv-control-border | Outlines, dividers, controls |
| --mv-hover / --mv-selected | Interactive states |
| --mv-accent / --mv-accent-fg | Primary button |
| --mv-ok-bg / --mv-ok-fg / --mv-err-bg / --mv-err-fg | Status badges |
| --mv-danger / --mv-danger-border | Destructive actions |
Every combination of OS preference and theme is verified to meet WCAG AA
(≥ 4.5:1) across the full UI. If you override tokens, re-check your own contrast.
🤖 Claude Code skill
If you build with Claude Code, install the companion skill so Claude knows how to use these components — the API, and the constraints that quietly break integrations:
/plugin marketplace add Mach-Five-Group/machvive-webmcp-ai
/plugin install machvive-webmcp@machviveClaude then knows to import analytics before registering tools, that the polyfill
needs a secure context, that a top-level import breaks under SSR, and that
sideEffects: false silently drops the component registrations — the four things
that fail in ways that look like something else.
Placeholder copy
<machvive-lorum-ipsum> generates its own text. Slotted content still wins, so
anything you put inside it is projected as before — the generated copy is the
fallback.
<machvive-lorum-ipsum sentences="3"></machvive-lorum-ipsum>
<machvive-lorum-ipsum lang="english" paragraphs="2" seed="7"></machvive-lorum-ipsum>| Attribute | Default | Meaning |
| --- | --- | --- |
| lang | latin | latin or english. Anything else falls back to Latin |
| sentences | 5 | Sentences per paragraph. -1 picks 1–5 at random |
| paragraphs | 1 | Joined by a blank line |
| seed | — | Omit for fresh copy; set for repeatable output |
| theme | OS | light or dark |
Properties mirror the attributes, plus text (what it rendered) and
regenerate() (fresh copy; a seeded element rerolls identically).
lang="english" is the one worth reaching for. Latin is the convention, but its
word lengths and letter frequencies are not English's — a column that survives Cicero
can still break on business-speak, which is the register your real copy is written in.
The generator is pure and DOM-free, so it is useful well beyond the element — seeding a mock chat, filling a fixture, stress-testing a column:
import { loremIpsum, loremSentence, WORD_BANKS } from '@machfivetechchicago/machvive-webmcp-ai';
loremIpsum({ sentences: 2, lang: 'english' });
loremIpsum({ paragraphs: 3, seed: 42 }); // identical every call
loremSentence({ lang: 'english' });Seeds matter more than they look. Without one, a page regenerates different copy on every render, which turns a visual diff into noise and makes a screenshot worthless as a regression check.
It publishes a tool
A connected <machvive-lorum-ipsum> registers generate_placeholder_text on
navigator.modelContext, so a page has something real for an agent — or the
inspector — to call before its author has written a tool of their own.
Open the inspector, pick the tool, fill in the form, run it, and the copy on the page
changes. That is the whole WebMCP loop, verifiable on day one.
It is a deliberately safe tool to hand out: pure text generation, no network, no storage, no state beyond the element itself.
| Parameter | Type | |
| --- | --- | --- |
| lang | enum | latin or english — renders as a select |
| sentences | integer | per paragraph; -1 randomises 1–5 |
| paragraphs | integer | |
| seed | integer | omit for fresh copy |
One tool per page however many blocks you place — tool names are a page-wide
namespace, so the first connected element owns it and hands it on if it is removed.
Add no-tool to opt a block out, or to every block to publish nothing.
<machvive-lorum-ipsum no-tool></machvive-lorum-ipsum>If your page calls provideContext, publish it again afterwards. That API
replaces the entire toolset by design, so it drops anything an element registered
when it connected. The component does not fight that — a page calling
provideContext is asserting ownership of the toolset — so ask for it back:
navigator.modelContext.provideContext({ tools: myTools });
document.querySelector('machvive-lorum-ipsum')?.publishTool();publishTool() also transfers ownership to the block you call it on, and
withdrawTool() removes the tool without setting the attribute.
Because of this, importing @machfivetechchicago/machvive-webmcp-ai/lorum-ipsum
installs the polyfill too. That is deliberate — a component that registers tools
against a registry that may not exist yet is how the analytics component once shipped
capturing nothing.
Ported from the Lorem Ipsum Generator.
TypeScript
Declarations ship with the package — no @types/* needed. Importing a component
augments HTMLElementTagNameMap, so querySelector returns the real element type,
and the polyfill declares navigator.modelContext plus the change event:
import '@machfivetechchicago/machvive-webmcp-ai/webmcp-polyfill';
import type { WebmcpToolResult } from '@machfivetechchicago/machvive-webmcp-ai';
const el = document.querySelector('machvive-lorum-ipsum'); // MachviveLorumIpsum | null
const res: WebmcpToolResult = await navigator.modelContext.callTool('add_to_cart', { sku: 'A' });Verified against moduleResolution: "bundler" and "node16" under --strict.
Server-Side Rendering (SSR)
These are browser components. Both classes extend HTMLElement at module load — not
when an element is created — so importing the package on a server, where no DOM
exists, throws immediately:
ReferenceError: HTMLElement is not definedThis never happens in a browser-only setup like Vite, where HTMLElement is always
present. It comes up only in frameworks that render on the server first — Next.js,
Nuxt, Astro, SvelteKit, Remix. Import the package from a client-only path instead:
// Next.js (App Router) — mark the component "use client", then import on mount
'use client';
import { useEffect } from 'react';
export default function Page() {
useEffect(() => { import('@machfivetechchicago/machvive-webmcp-ai'); }, []);
return <machvive-lorum-ipsum>Hello</machvive-lorum-ipsum>;
}// Nuxt — onMounted only runs in the browser
onMounted(() => import('@machfivetechchicago/machvive-webmcp-ai'));// SvelteKit — onMount only runs in the browser
import { onMount } from 'svelte';
onMount(() => import('@machfivetechchicago/machvive-webmcp-ai'));In Astro, put the import in a <script> tag — those are client-side by default —
rather than in the component's frontmatter.
A static top-level import will not work in any of these: the module is evaluated
during the server render, before any browser-only lifecycle hook gets a chance to run.
Use the dynamic import() form shown above.
The rest of the ecosystem
| | | | --- | --- | | webmcp-playground · source | A live page built with these components, publishing callable tools over HTTPS | | webmcp-browser-use | Drive that page with a real AI agent — discovers the tools and calls them, never touching the DOM | | WebMCP Directory listing | What an independent crawler finds on machvive.com — all seven tools it exposes, with their descriptions |
Documentation
The project Wiki carries the long-form material:
| | | | --- | --- | | WebMCP as a Business Accelerator | Why an agent is not just another visitor, and what that changes for a business | | WebMCP Canonical Use Cases | The two ways an agent reaches your page, and why the examples only show one | | Machvive WebMCP Polyfill Web Component Lib | Full guide: the components, how they compose, adoption constraints | | Magnet Integration | Exposing a MachFive Magnet's lead capture to agents |
License
Apache-2.0 © MachFiveTech Chicago
