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) 

Tool providerToolRegistryToolManifestProcessorPage HTMLwebmcp.jsModelContextTool providerToolRegistryToolManifestProcessorPage HTMLwebmcp.jsModelContextTool providerToolRegistryToolManifestProcessorPage HTMLwebmcp.jsModelContextmanifest($cObj, $processedData)providers returningnull are droppedlist of ManifestJSON block(JSON_HEX_* escaped)read #webmcp-configper tool: primitiveinterpreter or moduleUrlloop[each tool]registerTool(descriptor, { signal })registered — or rejected(e.g. duplicate name: warn once, skip)tools appear one by one;there is no atomic registration
Emitting tools — from PHP provider to registered agent tool
Class Responsibility
\Neoblack\Webmcp\Tool\ToolProviderInterface The contract each tool implements. Tagged webmcp.tool (autoconfigured).
\Neoblack\Webmcp\Tool\Manifest / \Neoblack\Webmcp\Tool\Primitive The serialisable tool description and its behaviour selector.
\Neoblack\Webmcp\Registry\ToolRegistry Collects manifests for the current request and exposes toolNames() for the analytics whitelist.
\Neoblack\Webmcp\DataProcessing\ToolManifestProcessor Serialises the manifests into the page's JSON block.
\Neoblack\Webmcp\Tool\ManifestValidator Checks each manifest against the spec's name pattern and Chrome's size recommendations; the processor logs findings as warnings.
Resources/Public/JavaScript/webmcp.js The generic runtime holding all four primitive interpreters, the escape-hatch loader and the registration lifecycle.

Registration in the browser 

The runtime in webmcp.js runs these steps once per page load:

  1. 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.
  2. Read the config block. Missing, unparsable or tool-less → stop.
  3. Feature detection. document.modelContext is used when it offers registerTool() . Otherwise — only if legacyNavigatorFallback is on — the deprecated navigator.modelContext . Neither → stop silently.
  4. 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 no moduleUrl, or reusing a name already used in the manifest are dropped. An entry whose data makes the primitive fail is reported once and skipped.
  5. Register each tool individually. Every tool gets its own AbortController ; the runtime calls registerTool(descriptor, { signal }) and accepts any return value (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 single console.warn names it. All other tools are unaffected.
  6. Teardown. On pagehide all controllers are aborted, which unregisters the tools; if the page is restored from the back/forward cache (pageshow with persisted) 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.

Ingesting usage events (call time) 

Agent on pagewebmcp.jsEventMiddlewareRateLimiterToolRegistrytx_neoblackwebmcp_eventEditorDashboardControllerStatisticsServiceEventRepositoryAgent on pagewebmcp.jsEventMiddlewareRateLimiterToolRegistrytx_neoblackwebmcp_eventEditorDashboardControllerStatisticsServiceEventRepositoryAgent on pagewebmcp.jsEventMiddlewareRateLimiterToolRegistrytx_neoblackwebmcp_eventEditorDashboardControllerStatisticsServiceEventRepositorytool callPOST /webmcp-eventsendBeacon { tool, client }same-origin guard(Sec-Fetch-Site)allow(ip, limit)?tool in toolNames()?log(tool, client, ts)Backend moduleopen System > WebMCPcollect(filter)aggregate by tool / client / daySELECT
Ingesting usage events — from tool call to backend dashboard
Class Responsibility
\Neoblack\Webmcp\Middleware\EventMiddleware The public ingest endpoint. Inert when analytics is disabled; passes unknown tools down the stack so it can coexist with other handlers.
\Neoblack\Webmcp\Security\RateLimiter Fixed-window limiter keyed on a hashed IP + window number (no plaintext IP stored).
\Neoblack\Webmcp\Domain\Repository\EventRepository The only class that writes/reads the event table.
\Neoblack\Webmcp\Service\StatisticsService Aggregates rows into the DTOs the backend module renders.
\Neoblack\Webmcp\Controller\DashboardController 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\Webmcp\Registry\ToolRegistry::toolNames() — the context-free provider names — rather than against the per-page manifest.