Inspiration
We already sell streaming-market research: 17,470 sourced datapoints on micro-drama, African OTT, piracy, the creator economy and sports rights, behind a paid MCP server with 194 tools. So the WebMCP question was never "what demo could we build" — it was sharper and more uncomfortable: what does a paywalled B2B product do when the visitor is an agent?
Today, agents scrape sites like ours. They read the marketing pages, miss the data entirely, and invent the numbers. Blocking them is not an answer either: an agent that leaves with nothing never comes back, and never buys. Giving the data away ends the business.
WebMCP allows a fourth option, and that is the whole submission: describe the asset precisely, withhold its substance, and always say where the substance lives.
What it does
Every page of lens.streaming-radar.com — 49 of them — registers tools on document.modelContext. Seven, all free, no key, no account, and no flag for the visitor to enable: we serve a first-party Chrome origin-trial token in <head>.
ask_site— returns the page sections that answer a question, with canonical URLs.search_intelligence_archive— searches the published newsletter editions and analyses.check_data_coverage— the one that carries the idea. Ask about any company, country or topic and it returns how many sourced datapoints we hold, which report silos they sit in, which countries and years they cover, the confidence mix, when the most recent one was updated, and one sample datapoint — metric name, unit, source, confidence — with its value withheld. Asked about ReelShort: 408 datapoints, across five silos, 2022 to 2028, the most recent updated this week.get_market_signals— the live monitoring feed; it also re-renders the visible feed on the page.estimate_streaming_pnl— drives the on-page P&L calculator and returns the model output. Registered on/calculatoronly: a tool with no valid target is a broken contract.request_report_teaser— the conversion step. Requires an explicitconsentargument and fills the page's own email gate, so the human sees what the agent did.browse_reports— the catalogue.
Then the boundary itself. Until this week, a paid tool called without entitlement answered Authentication required — three words, a dead end. Now the JSON-RPC error carries error.data: what the silo holds (5,306 datapoints, 51 countries, 2004 to 2033 on our vertical-drama silo), one sample with its value withheld, the URLs to buy or authenticate, and the exact instruction to replay the call with a key. The refusal proves possession instead of ending the conversation.
The same computation answers the free tool and the refusal, because they answer the same question and must answer identically.
How we built it
The site is static HTML with no bundler and no package.json, so the SDK is vendored as a pinned ESM asset and the module graph is injected by the site's page producer, never edited page by page. Tools are side-effect-free ES modules, registered once per document, unregistered on pagehide.
The tools never reimplement the page. request_report_teaser calls the site's own email-gate API through a small public seam; estimate_streaming_pnl moves the calculator's real controls and lets the page compute; get_market_signals reads the same endpoint the visible feed reads. One behaviour, one implementation, whether a human or an agent triggers it.
Agent traffic is measured server-side with a cookieless beacon — no cookie, no identifier, nothing to consent to — because GA4 only loads after a cookie banner and an agent never clicks Accept. Without it, agent traffic would be strictly invisible.
Challenges we ran into
A cache header that silently ate a release. Two new tools worked on the origin and reached no browser. Cloudflare Pages does not let a more specific _headers rule override a broader one — it concatenates them: max-age=31536000, immutable, public, max-age=300, must-revalidate. Browsers keep the first directive, so the modules stayed immutable for a year. The fix was to move the whole graph out of the /js/ prefix; there is no negation and no override.
Short acronyms matching inside words. Our coverage counter used ILIKE '%ott%' and returned 109 rows; word-bounded, 78. Twenty-eight percent phantom matches in a number we use as a commercial argument. The same bug in our signal matcher had been filing Ridley Scott stories into the sports silo — ott inside Scott, mma inside summary. Both are bounded now, with regression tests.
Bilingual pages. Our HTML carries French and English in the same DOM, hidden by CSS. In a detached document no CSS applies, so the first version of ask_site handed agents both languages glued together: "En brefTL;DR".
A surface that switches off in silence. Without a valid origin-trial token, document.modelContext simply does not exist and the SDK no-ops — no error anywhere. Our pipeline health check now reads the token off the deployed page, decodes its expiry, and goes red fourteen days before.
Accomplishments that we're proud of
It is not a demo. It is a production B2B product, the surface is measured, and the independent webmcp.com directory grades the implementation A. 1,404 tests green, including three whose only job is to prove no datapoint value can escape through the free surface.
What we learned
An agent-native paywall is not a wall with an API. It is a boundary that explains itself. Publish the shape, withhold the substance, always say where the substance lives — everything else follows.
What's next for Streaming Lens
An agent-reachable checkout, so the handoff completes without a human copying a key. And per-call payment at the edge: our MCP already runs on Cloudflare, and a 402 with a machine-readable price is the same envelope we just built, with an amount attached.
Built With
- cloudflare-d1
- cloudflare-pages
- cloudflare-workers
- document.modelcontext
- durable-objects
- es-modules
- hono
- javascript
- json-rpc
- mcp
- oauth2
- postgresql
- postgrest
- python
- resend
- stripe
- supabase
- typescript
- vitest
- webmcp
- workers-kv
Log in or sign up for Devpost to join the conversation.