# AgenticSchema · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/searchstefano/agenticschema/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/@agenticschema/core.svg?style=flat)](https://www.npmjs.com/package/@agenticschema/core) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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 `