Warning
Experimental. This extension is experimental and not yet ready for production use. It is built on top of WebMCP, which is itself an experimental, early-stage proposal. Both the underlying specification and this extension's API may change or break at any time without notice. Use at your own risk.
Architecture
The extension has two independent flows: emitting tools at page render time and ingesting usage events at call time. They share only the tool registry.
Emitting tools (frontend render)
| Class | Responsibility |
|---|---|
\Neoblack\ | The contract each tool implements. Tagged webmcp.
(autoconfigured). |
\Neoblack\ /
\Neoblack\ | The serialisable tool description and its behaviour selector. |
\Neoblack\ | Collects manifests for the current request and exposes
tool for the analytics whitelist. |
\Neoblack\ | Serialises the manifests into the page's JSON block. |
\Neoblack\ | Checks each manifest against the spec's name pattern and Chrome's size recommendations; the processor logs findings as warnings. |
Resources/ | The generic runtime holding all four primitive interpreters, the escape-hatch loader and the registration lifecycle. |
Registration in the browser
The runtime in webmcp. runs these steps once per page load:
- Top-level check. Inside an iframe it stops: agents only discover tools of the top-level document, and an embedding third-party page must not receive the tools.
- Read the config block. Missing, unparsable or tool-less → stop.
- Feature detection.
document.is used when it offersmodel Context register. Otherwise — only if legacyNavigatorFallback is on — the deprecatedTool () navigator.. Neither → stop silently.model Context - Build descriptors. Each manifest entry becomes a tool descriptor
(name, title, description, input schema, annotations,
execute). Entries without a name, with an unknown primitive and nomodule, or reusing a name already used in the manifest are dropped. An entry whose data makes the primitive fail is reported once and skipped.Url - Register each tool individually. Every tool gets its own
Abort; the runtime callsController registerand accepts any return value (Tool (descriptor, { signal }) undefined, a Promise or an object). If a call throws or its Promise rejects — for example because another script on the page already took the name — that one tool is skipped and a singleconsole.names it. All other tools are unaffected.warn - Teardown. On
pagehideall controllers are aborted, which unregisters the tools; if the page is restored from the back/forward cache (pageshowwithpersisted) they are registered again.
Every execute call is wrapped so that a synchronous throw becomes a
rejected Promise and text output is capped at outputLimit characters.
Note
Up to version 0.3 the runtime preferred provide for an
"atomic" registration. The specification removed provide, so
tools now appear one after another — this is intended by the specification.
See Security considerations for the background.
Ingesting usage events (call time)
| Class | Responsibility |
|---|---|
\Neoblack\ | The public ingest endpoint. Inert when analytics is disabled; passes unknown tools down the stack so it can coexist with other handlers. |
\Neoblack\ | Fixed-window limiter keyed on a hashed IP + window number (no plaintext IP stored). |
\Neoblack\ | The only class that writes/reads the event table. |
\Neoblack\ | Aggregates rows into the DTOs the backend module renders. |
\Neoblack\ | Thin backend controller; reads the filter, delegates, renders. |
Why the two flows are decoupled
The middleware runs early, before the frontend page is resolved, so it cannot
rely on a rendered manifest. It therefore validates incoming events against
\Neoblack\ — the context-free
provider names — rather than against the per-page manifest.
Important
This is why
\Neoblack\ must be
stable and must equal the
Manifest
name. A mismatch means valid tool
calls are dropped by the ingest middleware.
See also
- Writing tools – the provider interface and manifest in detail.
- Analytics – the event table and how the endpoint is hardened.