# AgenticSchema · [](https://github.com/searchstefano/agenticschema/blob/main/LICENSE) [](https://www.npmjs.com/package/@agenticschema/core) [](https://github.com/searchstefano/agenticschema/blob/main/CONTRIBUTING.md)
Turn the Schema.org markup a page already has into MCP tools an AI agent can call.
Most pages already publish structured data. Agents still scrape them. This library closes that
gap: it reads the `JSON-LD`, microdata and RDFa already in the page and emits Model Context
Protocol tools. You write no new API and you run no backend.
```
Website
│
│ the Schema.org markup it already publishes:
│ JSON-LD · microdata · RDFa
▼
AgenticSchema
│
│ one callable tool per thing the page describes
│
┌─────┴─────┐
▼ ▼
WebMCP MCP
(browser) (Node)
└─────┬─────┘
▼
Agent
```
Here is what comes out of a page that exists today:
```
world.openfoodfacts.org/product/3017620422003
read get_web_site
read get_organization
read get_search_action
action search_web_site(search_term_string)
```
`search_web_site` is executable. An agent holding it queries Open Food Facts directly instead of
guessing a URL or going through a search engine. Nobody published anything new to make that
happen: the page has carried a `SearchAction` all along, and even the parameter name is the one
the page itself declares in `query-input`.
Run that page yourself. No browser, and no transport to configure:
```bash
npx @agenticschema/server https://world.openfoodfacts.org/product/3017620422003
```
(`get_search_action` in that list is noise, a read tool over the action's own definition. It is a
[known rough edge](#known-rough-edges), left visible rather than trimmed out of the example.)
On your own site it is one file. The script-tag build is a plain classic script with the WebMCP
polyfill already inside, roughly 27 KB gzipped, so it goes wherever a `
```
You install no package and you configure no bundler. That tag reads the page and registers the
tools, which is the whole of step one. Something still has to carry them to an agent: Chrome has
run an origin trial for the native path since version 149, and there is a local relay for
development. [The next section](#read-this-first-registration-is-not-transport) is about that
choice, and it is the part people get wrong.
## Contents
The six sections worth reading before anything else. GitHub's own outline menu has the rest.
- [Read this first: registration is not transport](#read-this-first-registration-is-not-transport)
- [Quick start](#quick-start)
- [Why this is worth doing at all](#why-this-is-worth-doing-at-all)
- [The core pipeline](#the-core-pipeline)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
---
## Read this first: registration is not transport
This is the single thing that trips people up, so it comes before everything else.
Getting a page's data to an agent takes **two** steps, and this library only does the first one:
```
① REGISTRATION ② TRANSPORT
@agenticschema/browser a WebMCP-capable browser,
reads the page's Schema.org an extension, or a local relay
markup and registers tools carries those tools to the agent
on document.modelContext
│ │
└──────────► document.modelContext ◄─────┘
(the meeting point)
```
`@agenticschema/browser` writes tools into `document.modelContext`. That is the whole job. It
does **not** open a connection to anything, because a browser tab cannot listen on a port. See
[Three constraints](#three-constraints-that-shaped-the-design).
So after adding the script tag you have a page whose tools are correctly registered and that
**no agent can reach yet**. Nothing is broken; the second half is simply not there. You pick the
transport separately, and the choice depends on who is meant to call the tools. See
[Choosing a transport](#choosing-a-transport).
The symptom of forgetting step ② is very specific and worth recognising: **the tools show up in
Chrome DevTools (Application panel) but your MCP client reports zero sources.** DevTools reads
`document.modelContext` in-process; your MCP client is a separate program that cannot. Everything
is working, and nothing is connected.
---
## Quick start
Two tags. The first registers the tools, the second carries them to a local MCP client such as
Claude Desktop, Cursor or Claude Code.
```html
```
Then run the relay and point your MCP client at it:
```json
{
"mcpServers": {
"webmcp-local-relay": {
"command": "npx",
"args": ["-y", "@mcp-b/webmcp-local-relay@latest"]
}
}
}
```
Open the page, and the tools appear in your client. Verify with `webmcp_list_sources`: your tab
should be listed with a tool count above zero.
Four things worth knowing before you paste that in:
- Order matters. The relay embed reads whatever is already registered and subscribes to changes,
so put it after the registration tag.
- Leave off `type="module"`. The script-tag build is an IIFE, so it runs as an ordinary script,
which is what makes it work through a tag manager. Adding `type="module"` still works, but it
costs you `document.currentScript` and with it the simplest way to read options. See
[How the adapter finds its own tag](#how-the-adapter-finds-its-own-tag).
- Tag ② is for development. Shipping it to real visitors makes every one of their browsers probe
`127.0.0.1`. See [Keep the relay out of production](#keep-the-relay-out-of-production).
- Pin your versions. Unversioned jsDelivr URLs are cached at the edge for days, long enough to
keep serving a build you have already replaced. `@latest` and `@4` above are pins.
If you only want the browser's own built-in agent to use the tools, you need tag ① alone.
---
## Why this is worth doing at all
That first sentence is measurable rather than a pitch. Schema.org publishes the usage statistics
from Google's crawl, counting how many domains use each term:
| Term | Domains |
| ----------------- | ------- |
| `potentialAction` | 10M+ |
| `SearchAction` | 10M+ |
| `EntryPoint` | 10M+ |
| `urlTemplate` | 10M+ |
| `query-input` | 10M+ |
Over ten million domains already declare how to search them, machine-readably, today. That is a
capability this library hands to an agent as a callable tool, and nobody had to publish anything
new for it to work. The vocabulary's tail is shorter than it looks, too: of 958 types, 16 appear
on 10M+ domains, 50 on 1M+ and 95 on 100K+, so a hand-written profile registry can cover the part
of the web that exists in practice.
One caveat worth stating plainly: those counts are what sites **declare**, not what is well formed
enough to map. That is a different number, and the table does not claim it. Source:
[schemaorg/schemaorg](https://github.com/schemaorg/schemaorg/tree/main/data/public_stats/google) `data/public_stats/google`,
2026-07.
---
## Try it without committing to anything
Three ways in, in rising order of commitment.
### 1. In the browser, nothing installed
**[Open the playground →](https://searchstefano.github.io/agenticschema/)**
Paste any JSON-LD and watch the tools appear. Try the hostile sample: it is the fastest way to
see what the library *refuses* and why. Alongside it,
[a live page carrying the script tag](https://searchstefano.github.io/agenticschema/demo.html)
for the WebMCP path end to end.
Both pages load the packages from jsDelivr at exact versions, so what you are trying is what
you would ship, not a local build.
### 2. Read a real page from the terminal
No browser, and no transport question. The Node adapter fetches the page itself and speaks plain
MCP over stdio:
```bash
npx @agenticschema/server https://en.wikipedia.org/wiki/Backpack
```
Wire it into Claude Desktop:
```json
{
"mcpServers": {
"page": {
"command": "npx",
"args": ["-y", "@agenticschema/server", "https://en.wikipedia.org/wiki/Backpack"]
}
}
}
```
Every entity also becomes a readable MCP resource, which the browser adapter cannot do.
This is the shortest path to seeing real output, and the one with the fewest moving parts. If
you are evaluating the library, start here.
### 3. On your own site
See [Quick start](#quick-start) above, then [The script tag, in full](#the-script-tag-in-full).
---
## The script tag, in full
### Where the tag goes
The build is a single classic script with no bundler and no package install behind it, so
anywhere you can paste a `
```
**Adding** `type="module"` **gives up rule 1.** The tag then has to be identifiable some other way:
the URL above still matches rule 3, but a self-hosted copy under an unrelated filename matches
nothing, and its options are ignored in silence.
```html
```
> **Using 0.1.2 or earlier?** That build was ESM, so the tag needed `type="module"`, rule 1
> never applied and rule 3 did not exist. `data-agenticschema` was mandatory for *any* option
> to have an effect, and its absence was silent. Measured on one page with three JSON-LD blocks,
> `data-max-tools="2"` without the marker produced 5 tools instead of 2.
When an option is ignored nothing warns you: the page keeps working and quietly uses defaults,
which is indistinguishable from success until you go counting tools.
### Through a tag manager
Google Tag Manager and Cloudflare Zaraz inject a plain `
```
Anything not on this list, such as profiles, payload caps, custom tools and timeouts, is
reachable only from [the JavaScript API](#the-javascript-api). The attribute surface is
deliberately the small, safe subset that makes sense to set from markup.
### Content-Security-Policy
A page with a CSP has to allow `cdn.jsdelivr.net` in `script-src`, or the tag never executes:
```
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net;
```
If you would rather not open the CDN, self-host `dist/cdn/auto.js`. It is a single
self-contained file, roughly 27 KB gzipped, with the WebMCP polyfill already inside.
The relay embed from tag ② is a **second** origin to allow, and it also creates a `blob:` iframe
and opens a WebSocket, so its CSP needs are wider:
```
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net;
frame-src blob:;
connect-src ws://127.0.0.1:9333;
```
That is one more reason to keep the relay tag out of your production CSP entirely.
### Version pinning
Use an exact version in production. The unpinned specifier always serves the latest release, and
jsDelivr caches unversioned URLs at the edge for days.
**0.1.1 and earlier register no tools at all** on a browser without native WebMCP: the polyfill
was left out of the bundle, and because the failure was silent the page looked healthy. Use
`@0.1.2` or later.
---
## Choosing a transport
The tools are registered. Something has to carry them to an agent. There are three real options
and one non-option:
| Transport | Who calls the tools | Setup | Good for |
| ---------------------------- | ------------------------------------------------------------ | -------------------------------------------------- | ----------------------------------------------------- |
| **Native browser WebMCP** | the browser's own agent | none: the browser provides `document.modelContext` | the end state, once it is on by default |
| **Chrome origin trial** | the browser's own agent, on your visitors' own Chrome | register a token for your origin and serve it | trying the native path on real traffic today |
| **Local relay** (`embed.js`) | your desktop MCP client: Claude Desktop, Cursor, Claude Code | one script tag + `npx @mcp-b/webmcp-local-relay` | development, testing, personal automation |
| **Browser extension** | whatever the extension is wired to | install the extension | using tools across sites you do not control |
| **Nothing** | nobody | none | registering tools and wondering why no one calls them |
Only the local relay needs anything from your page's HTML. That is the one this section covers,
because it is the one people reach for first and the one whose failure mode is confusing.
Native WebMCP is not on by default in any browser yet, but it is no longer flag-only. Chrome
exposes it for local development via `chrome://flags/#enable-webmcp-testing` (restart required);
some builds also need `--enable-experimental-web-platform-features`. Since Chrome 149 there is
also an [origin trial](https://developer.chrome.com/blog/ai-webmcp-origin-trial): register a token
for your origin, serve it, and the native path works for ordinary visitors on production traffic
with no flag on their side.
Because it is still off by default for nearly everyone, `@mcp-b/webmcp-polyfill` stays a hard
dependency of the browser adapter rather than an optional one. The polyfill is the normal case
here.
### What `embed.js` actually does
Worth understanding before you put it on a page, because it does more than load a script:
```
┌──────────────────────────────────────┐
│ Host page │
│ document.modelContext + your tools │ ← @agenticschema/browser put them here
└──────────────────┬───────────────────┘
│ postMessage
┌──────────────────▼───────────────────┐
│ Hidden iframe (blob: URL) │ ← embed.js injects this
│ injected by embed.js │
└──────────────────┬───────────────────┘
│ WebSocket ws://127.0.0.1:9333
┌──────────────────▼───────────────────┐
│ webmcp-local-relay (Node process) │ ← npx @mcp-b/webmcp-local-relay
└──────────────────┬───────────────────┘
│ stdio / JSON-RPC
┌──────────────────▼───────────────────┐
│ Claude Desktop / Cursor / any client│
└──────────────────────────────────────┘
```
Concretely, on every page load it:
1. injects a hidden `