Blog · 2026-10-06

Website tools (private built WebMCP) for exact agentic results

TL;DR

  • We built SiteActions: typed, WebMCP-shaped tools that we declare for websites that do not ship their own. The readers are the half that ships today, and we call them site tools: 41 tools across 17 sites, checked by 517 tests.
  • On live news and shopping sites, typed readers cut the median input from 16,519 tokens to 6,144 and the median cost from $0.0033 to $0.0014 per task, at the same accuracy (17 of 18 runs per arm).
  • A typed result carries its units (“per night, for the cheapest room that sleeps two”). The accessibility tree cannot, and units are where agents go wrong.
  • Typed tools make reading cheaper, not acting. And a typed answer that is wrong looks authoritative, so we check tools against the page and never let a page write the text on an approval card.

A browser agent has to find out what is on a page before it can do anything with it. Today it has three ways to do that:

  1. Raw DOM. Read the HTML and write scripts against it.
  2. The accessibility tree. Read the roles and names the browser computes for screen readers. Many agents do this, and so does our extension.
  3. Typed tools. Named operations with input schemas and structured results. The agent calls read_hotels and gets back rows like name: Fountain Hotel; price_eur_per_night: 46. It does not have to work out which <span> holds the price, or whether 46 is per night or per stay.

The first two give an agent a page to interpret. The third gives it an answer. That is what we mean by exact results.

What WebMCP is

WebMCP is the web standard proposal for option 3, where the page itself is the provider. A page calls document.modelContext.registerTool(...) with a name, a description, an inputSchema (JSON Schema), an execute function and annotations such as readOnlyHint. An agent running in the browser lists the page’s tools and calls them, inside the tab and with the person’s own session. It is a Draft Community Group Report of the W3C Web Machine Learning Community Group, with editors from Microsoft and Google, and Chrome has it behind a flag (section 6 has the details and sources).1

The catch is that almost no site ships it yet. So we built our own layer in the same shape.

Our layer: SiteActions

We call the layer SiteActions. Its readers — the half described below — are site tools, and they are what ships today. The other half is the actions that change a page rather than read it; that half is still being built, and nothing below measures it.

Site tools are typed tools that we declare for a site, in WebMCP’s shape, until the site declares its own. Each one has a name, a description, an input schema, a read-only flag and a function. A site that later ships WebMCP maps onto the same shape, and the agent does not change.

Today the registry has 41 tools across 17 hosts: live news and shopping sites (BBC, CNN, zap.co.il, one.co.il, sports.walla.co.il) and our simulated CRM, to-do app, ad managers, hotels, listings, prices, parcels, flights and health-fund sites. They are checked by 517 tests against 62 saved pages, all passing at the time of writing.2

How it is built:

  • Configuration over code. A tool is a small reviewed config (a scope, a row selector, named cells, links, page facts) handed to one of six shared extractors: list, article, table, record, tasks and rows. The hotels reader, for example, names its money column price_eur_per_night and reads it from a data-price-eur attribute, because the class names on that site rotate with every build and the data-* attributes do not.
  • Units in the name and in a note. Every result can carry a note, and the hotels note says “per night, for the cheapest room that sleeps two”. A typed tool can state its units; the accessibility tree cannot.
  • Stale is not empty. An extractor returns matched: false with a reason when the page is not the shape it was written for: the markup changed, the list never rendered, or the page is on another site. The agent is told the tool did not match, never that the page was empty.
  • One script, decided in the page. For each site the registry emits a single self-contained script: every extractor that site uses, inlined once, plus its tools. It runs where a model-written page script already runs. The script checks the host and path in the page, not the address the caller believed it was at, so a page that navigated away gets other-site or page-changed, never another site’s selectors.
  • Nothing the page returns is trusted about itself. The tool names a page offers are decided from the address, outside the page. Credentials in the query string (access_token, sig, code and the like) are stripped before the address reaches the model. Dialogs open over the page are named in the result, so a reader cannot report a form as saved while a “Log in” dialog sits on top of it (section 4).
  • One model-facing tool, and page text handled as data. The agent sees a single site-tool call and names the page’s tool as an argument. In a result, an article’s prose goes inside the run’s untrusted-text fence; every other value the page had a hand in is folded to one line and capped, or escaped, so it cannot read as a line of ours.
  • Size-bounded. The executor refuses a script over 20,000 characters, so each extractor is emitted once however many tools use it, and results are capped (40,000 characters, 30 list items, 120 characters a cell).

From the model’s side, a site tool and a WebMCP tool look the same: a name, a schema, structured rows. The difference is who wrote them and whether they can be trusted. That difference is the second half of this post.

What we measured, and what we have not

Before any table, here is what we have measured and what we have not.

  • We have not run an agent against a page that registers WebMCP tools itself. We have written the contract for that: our test sites will register tools, with a mode switch for off, on, partial, lying and slow. Neither the in-page provider nor the consumer is built yet.
  • What we have measured is the same idea, written by us: the site tools above, which live in a library, not in the page.

Every paid number below comes from cheap models (glm-5.3-flash, deepseek-v4.1-flash), and every run’s cost is recorded. Each table says whether it ran on simulated sites, on saved pages, or on live public sites.


1. One page, three representations

We start with the cost of the first read. For every saved page that our mocked-browser test harness serves, we measured three things in o200k tokens, with no model involved 3:

  • the saved HTML;
  • the accessibility snapshot, exactly as our agent renders it into the prompt;
  • the site reader’s arrival read, exactly as the agent receives it, including its untrusted-text fence and preamble.

Of the 42 pages, 35 have a reader.

Representation (35 pages with a reader) Median tokens Total tokens
Raw saved HTML (scripts and styles already stripped) 9,981 469,338
Accessibility snapshot (default, not full) 459 61,513
Typed reader result 615 25,722

Measured with no model calls. Fixture pages are captures of real pages and of our simulators. They are not live fetches. 3

The medians make the snapshot look cheapest. But the snapshot is small because it leaves out what the question needs. Here is the default snapshot of a simulated hotel city page, cut short:

Page: "hotels.example — Lisbon p1" .../city/lisbon/
[e1] link "hotels.example"
[e2] link "Home"
[e3] link "Details"
[e4] link "Details"
... (12 "Details" links)
[e15] link "Next"

The page has twelve hotels and the snapshot names none of them and prices none. The reader result for the same page starts like this:

Site tool read_hotels read this page: 12 rows (result r1). Showing 10.
Note: price_eur_per_night is per night, for the cheapest room that sleeps two;
  a hotel's own page gives totals for the whole stay; page: 1; of pages: 11; ...
1. name: Fountain Hotel; price_eur_per_night: 46; rating: 3.0; reviews: 4628; ...

To measure this across pages, we took every field value from every list or record reader (25 pages, 1,056 values). Then we checked whether each value appears verbatim in the other two representations 4:

Where the reader’s values appear Share
Raw saved HTML 976 / 1,056 (92%)
Default accessibility snapshot 561 / 1,056 (53%)
Hotels, prices and listings simulators: accessibility snapshot 0% on every page

Some HTML values miss because the reader normalises them, for example a separator in a price. Some snapshot values miss because the default snapshot lists interactive elements rather than all the text. The agent then spends extra calls on text reads or on its own DOM scripts. That is where the per-task costs in section 2 come from.

The note line matters as much as the rows. “per night, for the cheapest room that sleeps two” is a unit, and units are where agents go wrong (section 4). A typed tool can state its units. The accessibility tree cannot.

Caveat: this snapshot comes from our in-process ARIA renderer over saved markup. It is not Chrome’s Accessibility.getFullAXTree on a live page. Chrome’s tree carries more static text.


2. The same task with and without typed tools

Three test lanes run each task twice: in the off arm the agent has the accessibility snapshot, text reads and its own page scripts, and in the on arm the same agent also gets the reader. The two arms run in alternating order, so neither always goes first. All of these lanes run our real agent loop.

2a. Live public sites (BBC, CNN, zap.co.il), glm-5.3-flash

Six read-and-answer cases, three rounds after the grader fixes, 18 runs per arm. The traffic went through a proxy system, not our own addresses.

Arm Passed Median input tokens Median cost Median wall time Median tool calls DOM scripts written (sum)
accessibility tree + scripts (off) 17/18 16,519 $0.0033 20.7 s 3 33
typed reader (on) 17/18 6,144 $0.0014 14.2 s 2 6

Live sites, glm-5.3-flash, 18 runs per arm over three rounds, medians across all runs of each arm. 5

On deepseek-v4.1-flash, with one run of each case per arm, the median input went from 8,505 tokens to 1,239, and the cost from $0.0034 to $0.0012. The arms passed 6/6 and 5/6. The one failure was the grader’s fault, not the model’s.

2b. Ads-manager simulators (X and Meta), both models

These are our simulated ad managers, run once each after the day’s fixes: fourteen tasks, both arms, 56 runs, and 56/56 passed 6.

glm off glm on deepseek off deepseek on
X, 6 tasks $0.0049, 9 calls, 17.2 s $0.0025, 3 calls, 6.3 s $0.0046, 9 calls, 6.3 s $0.0022, 3 calls, 2.6 s
Meta, 8 tasks $0.0062, 6.5 calls, 14.6 s $0.0040, 3 calls, 6.5 s $0.0059, 7 calls, 7.1 s $0.0049, 3.5 calls, 4.6 s

Simulated sites, medians per task. 6

2c. Mocked browser over saved pages, both models

No Chrome and no network: the saved pages are served at their real addresses. That makes the pages identical from run to run 7.

Run Arm Passed Median cost Median input tokens
2026-10-04, 13 questions, 104 runs off 49/52 $0.0010 2,236
on 52/52 $0.0007 901
2026-10-05, 14 questions, 112 runs off 53/56 $0.0017 6,327
on 56/56 $0.0011 4,988

Saved pages (fixtures). 7

All three reds in the off arm come from one cause. The snapshot names a headline link by its headline and teaser joined together, and the model gave both as the title. A typed title field has nothing to join.

2d. Where the gain disappears

The gain is not universal, and the cases where it shrinks say something too.

  • Acting. Two long write tasks on the to-do simulator took 74 calls without the reader and 68 with it (reopening a task), and 85 and 39 (the other task). About a third of those calls are a snapshot after each click. A reader shortens the reading. It does not shorten the acting. WebMCP action tools (submit*, set*) target exactly this cost, and we have not measured them.
  • Walking many pages. On the prices simulator the costs were $0.015 without the reader and $0.014 with it, because the task visits many product pages and the arrival read is only one of them.
  • Pages that are already clean. Article pages saved little, because our article-reading tool already returns the article.

2e. Accessibility tree against an authored descriptor, earlier stack

On an earlier version of our browser stack, one lane ran three journeys over two simulated sites. One arm had only Accessibility.getFullAXTree. The other had our WebMCP-shaped descriptor: named collections and named tools 8.

Journey Descriptor arm ARIA-only arm
Prices: one product 15 calls, 6/6 facts 13 calls, 6/6 facts
Listings: cars 6 calls, 5/5 facts 5 calls, 5/5 facts
Listings: property 7 calls, 4/4 facts 4 calls, 4/4 facts

Simulated sites. These counts are browser calls in a scripted lane, not model turns. 8

The tree alone reached 18 of the 19 facts. This result goes against our thesis, and we report it as measured. What the descriptor added was one fact that the tree does not carry: which paid tier a promoted row bought (held in a data-* attribute). It also removed guesses that happened to be right that day. The ARIA arm had to assume that the largest list of listitems was the results and not the navigation. It also had to rebuild “4187 listings” from four separate text nodes. On one fact the tree did better: the descriptor’s title reader returned Hyundai i30 2019Promoted, with the badge glued on, while the tree had the title as a clean node.

On pages that are already accessible and that we designed ourselves, the tree is a strong baseline. A typed tool wins where the tree loses meaning: units, tiers, “N of M”, loading against empty, and virtualised tables.

The core table

Raw DOM Accessibility tree Typed tools
First-read tokens, median of 35 pages 9,981 3 459 3 615 3
Reader values present in that read 92% 4 53% 4 100% by construction
Live sites: median input tokens per task not run as an arm 16,519 5 6,144 5
Live sites: median tool calls not run as an arm 3 5 2 5
Live sites: median wall time not run as an arm 20.7 s 5 14.2 s 5
Live sites: median cost (glm) not run as an arm $0.0033 5 $0.0014 5
Live sites: accuracy not run as an arm 17/18 5 17/18 5
Saved pages: accuracy (latest run) not run as an arm 53/56 7 56/56 7

The “off” arm is not pure ARIA. The agent also wrote 33 DOM scripts across those 18 runs. So the middle column is “the accessibility tree plus scripts the agent writes for itself”, which is what agents actually do. We have not yet run a raw-DOM-only arm or an arm with page-declared WebMCP tools.


3. Who writes the tools

There are three possible authors, and each one changes what can go wrong.

The site (WebMCP). The tool cannot go out of date, because the site ships it together with its markup. It is also written by the party that benefits from what the agent does. Our planner design puts page-declared tools first, then our own descriptors, then ARIA. It records the asymmetry this way: “falling back is safe, preferring is not. A missing tool costs a turn; a lying tool costs a wrong answer delivered confidently.”

Us (descriptors and site tools). We review these tools, and they rot when the site redesigns. Handwritten readers take about half a day per site including tests. Writing the ad-manager descriptors showed how much a live page withholds from a reader:

Reddit X Meta
test hooks automation-*, data-testid data-slot none at all
campaign id yes, via an attribute yes, as text none anywhere
readable columns name, status, status detail 3 of 13 1 of 13
empty-state marker yes yes none

On Meta, [role="row"] matched 1 element, the header, on a page with eight campaigns. A selector derived correctly from the ARIA table contract would have matched nothing, forever, without an error. A first-party tool would not have this problem. That is the strongest argument for WebMCP we have.

A model (the site-tool generator). A cheap model writes configuration for extractors we have reviewed. It never writes code. Every draft goes through review, and nothing it writes runs unreviewed. The benchmark grades values against our handwritten readers, which serve as the answer key, and includes held-out pages and negative pages 9:

Set (2026-10-05, one attempt each) glm-5.3-flash deepseek-v4.1-flash
10 simulators, positive pairs 29/40 26/40
Held-out pages (generator never saw them) 2/7 2/7
Negative pages (“not matched” or “empty” is right) 4/5 4/5
6 real sites (saved pages) 6/11 4/11
Cost, 10 simulators $0.023 $0.061

Simulated and saved pages, no live fetches. 9

The model understood every site: all 64 stage-1 answers were correct. What it failed at was generalising. It fitted URL paths to the one example page it saw, so a second page of the same kind got no tool. It also cannot learn what “still loading” looks like from a single rendered capture. Our verdict: generation is a good first draft at under $0.01 per site, and it is not yet an unattended pipeline. Our go/no-go bar is 60% held-out on real sites, and the generator does not meet it.


4. The failure that matters: a declared tool that answers wrongly

A typed answer reads as authoritative, even when it is wrong. We have seen this happen with our own, honest readers.

The reader that confirmed an unsaved change. On a CRM simulator task that changes records conditionally in bulk, with the reader on, the model pressed Save. The CRM opened its Log in dialog and saved nothing. The model then re-read the record through the reader, saw its own unsaved choice in the form, and reported success. The result was 0 of 3 before the fix and 4 of 4 after it. The reader was not lying. It reported what the page showed. But the page showed state that had not been saved. The fix has two parts:

  • a reader’s result now names any dialog open over the page and carries the page’s status line;
  • the guidance the agent receives says plainly: “A site tool reads what the page shows now, including a value you typed or chose and have not saved. It does not confirm that a change was saved.”

Zero rows is not an answer. During a live check on the X ads manager, the page was mid-load: thirteen skeleton rows, no status badges, no empty state. Both selectors described the page correctly, and the tool would still have reported “no campaigns” to an account that had three. The rule now:

  1. If a read returns zero rows, read the page’s own empty marker in the same call.
  2. If the page did not say “empty”, read the loading marker.
  3. If neither marker settles it, refuse and store nothing. Meta has no empty marker, so it always refuses.

The contract for page-declared tools makes this rule general: [] means “there are none”, and anything else is a named error such as NOT_LOADED or NEEDS_SESSION.

Deliberate lies. Each of our test sites declares one defect for its “lying” mode. Each defect is a trap that the site’s DOM already sets for agents:

Site The lie Real fields, wrong meaning
hotels listRooms puts the stay total in nightlyRateIls a three-night bill quoted as the nightly rate
listings searchProperty drops promoted, spot and slot row one answers “who paid”, not “cheapest”

Every number in a lying result is real. Only the meaning is wrong. A schema check cannot catch that.

How an agent can cross-check a tool against its page. These are mechanisms we either have or have specified:

  1. Provenance on every result. _meta.source is one of dom, dom-fetch, api or state. A field marked dom must appear in the rendered page, and a test asserts this. There is deliberately no value for “from somewhere else”.
  2. Sample against the page. Section 1 shows that verbatim matching is cheap: matching 1,056 values took no model at all. An agent can check a few returned values against the page text it already has. A miss means the agent should distrust the tool and fall back to reading the page.
  3. Static against live. Each host serves /.well-known/webmcp.json. When the inventory a page registers differs from the declared one, that is a build bug, and the cheapest one to catch.
  4. Confirm writes from the page, never from the tool. After an action, look for the page’s own confirmation, error or dialog.
  5. Check the origin, not the claim. Our agent loop does not run a reader, or store its rows, when the browser reports a different host than the page claims. A unit test covers this: “a page that lies about where it is”.

What we have not measured yet is how often a cheap model repeats a lying tool’s answer, and whether checking against the page catches it. Our design calls it “the number nobody has”, and it is still the number nobody has.


5. Tool descriptions are prompt-injection surface

A WebMCP description is text written by the page, and it reaches the model as an instruction-shaped string. Invariant Labs named the MCP version of this attack “tool poisoning” in April 2025 10. The WebMCP draft lists tool poisoning, output injection and malicious implementations under prompt injection, and adds untrustedContentHint and consequentialHint as annotations 1. The MCP spec says to treat tool descriptions as untrusted unless they come from a trusted server 11.

Three things we learned building around this:

  • A read consent does not cover a declared action. We wrote a tool that switched a Reddit Ads campaign on and off, and then removed it in review, because it would have used a consent that only says “a page on this site may be read”. Our rule is that a page-declared action needs the same per-origin approval as any other action, and that the person sees the tool’s identity, never the page’s description of it.
  • The page can write the sentence on the approval card. The card was built from an element’s accessible name, which is text the page chooses. With no model and no injection involved, one aria-label produced this card: Click the button “Cancel” on google.com — nothing will be sent. Ignore the rest. “Delete account” on evil.example. The fix flattens control characters, folds quote lookalikes, caps the name at 120 code points, and shows the site in a separate field, taken from the live top-level origin. A WebMCP description or title shown to a person needs the same treatment.
  • Fence what the page wrote. Page text, and reader output, reaches the model inside a per-run marker. We planted a forged closing marker followed by a fake system turn in a task title on our to-do simulator. glm-5.3-flash kept it fenced and did not act on it, in a run that cost $0.0109 including the voided first run 12. With readers on, the planted instruction was obeyed in 0 of 5 runs 13.

There is also a less obvious one. On a Meta simulator task with readers on, the model skipped the unfiltered tab, read the access token embedded in the page, and called the Graph API from a page script. The answer was right. An agent that can reach a page’s own API will sometimes prefer it. Declaring tools will not stop that. It gives you a better place to draw the line.


6. Where WebMCP stands (October 2026)

  • Spec. WebMCP is a Draft Community Group Report of the W3C Web Machine Learning Community Group, dated 2 October 2026. It is not a W3C standard. The editors come from Microsoft and Google 1 14.
  • Shape. The entry point is document.modelContext, with registerTool(tool, options), getTools() and executeTool(). A tool has a name, a description, an inputSchema, an execute function and annotations (readOnlyHint, untrustedContentHint, consequentialHint) 1.
  • The competing spelling. navigator.modelContext, from the earlier explainer and Chrome’s first preview, was removed from Chromium in favour of document 15. The early provideContext(), which replaced all of a page’s tools at once, is also gone; the reported reason is that a third-party script could overwrite a page’s tools 16 (secondary source).
  • Declarative variant. HTML forms take toolname and tooldescription. Without toolautosubmit, the browser focuses the submit button and the person submits 17.
  • Shipping. Chrome started an early preview for Chrome 146 in February 2026, behind chrome://flags/#enable-webmcp-testing 18 19. An origin trial starts in Chrome 149 (announced 9 June 2026) 20. We could not confirm an Edge flag or version.
  • Our position. Our test-site contract registers tools on document.modelContext, on navigator.modelContext, and on a namespace of our own. This lets us test in Node today without waiting for the spellings to settle.

Prior art, and how it differs

Who declares Where it runs Typed calls?
llms.txt (2024) the site a markdown file at /llms.txt no, it is context and links 21
ChatGPT plugins / OpenAPI (2023), Arazzo, agents.json the site server-side API yes, without the user’s session in the page 22 23 24
Anthropic MCP (Nov 2024; spec 2026-07-28) a server out of page, JSON-RPC yes 11
OpenAI Apps SDK (Oct 2025) an app MCP server plus UI in ChatGPT yes 25
Chrome built-in AI (Prompt API, Summarizer, …) the browser on-device model for the page’s own use not tools for an outside agent 26
WebMCP the page in the tab, with the user’s session yes 1

WebMCP is the only entry that is both typed and running inside the page the person is already logged into. That is why it can beat the accessibility tree, and also why a lying or injected tool is more dangerous there than anywhere else in this table.


What we take from this

  1. On read tasks, typed tools cut cost by roughly half to two thirds, and cut tool calls and wall time, on live sites, on simulators and on saved pages. Accuracy was the same or slightly better.
  2. On pages that are already accessible, the accessibility tree is a strong baseline (18 of 19 facts). Typed tools win on meaning: units, tiers, totals, loading against empty.
  3. Typed tools do not make acting cheaper. Actions are where WebMCP has to prove itself next, and they are also where consent matters most.
  4. Trust has to be checked, not assumed. Provenance, sampling against the page, confirming writes from the page, and an approval card the page cannot write.

Footnotes

  1. WebMCP Draft CG Report, 2 Oct 2026 — https://webmachinelearning.github.io/webmcp/ (source: https://raw.githubusercontent.com/webmachinelearning/webmcp/main/index.bs) ↩ ↩2 ↩3 ↩4 ↩5

  2. Counted on 2026-10-08: 517 tests in 10 test files, over 62 saved pages. ↩

  3. No model was called. Token counts use the o200k_base tokenizer, which is not glm’s own tokenizer, so the ratios between rows matter more than the absolute counts. ↩ ↩2 ↩3 ↩4 ↩5

  4. Field values from list and record readers, links excluded. A value counts as present if it appears verbatim in the snapshot, or in the HTML after whitespace normalisation and entity decoding. ↩ ↩2 ↩3

  5. glm-5.3-flash, six cases, three rounds on 2026-10-02, 18 runs per arm. Medians are taken across all runs of each arm. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11

  6. One run per task per arm per model, 56 runs in all. Total spend: $0.16 on glm-5.3-flash and $0.14 on deepseek-v4.1-flash. ↩ ↩2

  7. Both models, both arms. The 2026-10-04 run cost $0.12 in total and the 2026-10-05 run $0.21. ↩ ↩2 ↩3 ↩4

  8. Run in August 2026 on the previous version of our browser stack, with a scripted lane rather than a model. ↩ ↩2

  9. One attempt per page, 2026-10-05. Values are graded against our handwritten readers. ↩ ↩2

  10. https://invariantlabs.ai/blog/mcp-security-notification ↩

  11. https://modelcontextprotocol.io/specification/latest ↩ ↩2

  12. glm-5.3-flash, 2026-10-02, two runs (the first voided), $0.0109 in total. ↩

  13. glm-5.3-flash, five runs with readers on. ↩

  14. https://github.com/webmachinelearning/webmcp ↩

  15. Chromium CL “WebMCP: Remove navigator.modelContext in favor of document.modelContext”, referenced from https://bugzilla.mozilla.org/show_bug.cgi?id=2053854 ↩

  16. https://dev.to/jangwook_kim_e31e7291ad98/webmcps-origin-trial-providecontext-is-already-gone-54j8 (secondary) ↩

  17. https://raw.githubusercontent.com/webmachinelearning/webmcp/main/declarative-api-explainer.md ↩

  18. https://developer.chrome.com/blog/webmcp-epp ↩

  19. https://www.datacamp.com/tutorial/webmcp-tutorial ; https://ppc.land/chrome-149-origin-trial-puts-webmcp-in-developers-hands-at-last/ ↩

  20. https://developer.chrome.com/blog/ai-webmcp-origin-trial ↩

  21. https://llmstxt.org/ ↩

  22. https://supabase.com/blog/building-chatgpt-plugins-template (ai-plugin.json + OpenAPI) ↩

  23. https://github.com/OAI/Arazzo-Specification ↩

  24. https://americanbazaaronline.com/2025/02/21/newly-launched-startup-wildcard-translates-apis-for-ai-agents459854/ ↩

  25. https://openai.com/index/introducing-apps-in-chatgpt/ ↩

  26. https://developer.chrome.com/docs/ai/built-in-apis ↩