When official ad-platform MCPs ship — Google Ads MCP, Meta Ads MCP, GA4 MCP — the obvious question is what is mureo for. If the API connection is no longer the differentiator, why install another layer between the agent and the platform?
The 0.9.x series is the answer in code. Across ten releases mureo stopped being a self-contained framework with bundled adapters and became a pluggable control plane: a small, stable ABI; an entry-points discovery mechanism; a versioned promise about which changes will and will not break installed plugins; a UI extension layer; and a safety wrapper that audits and throttles every plugin-supplied tool call.
This post walks the architecture. Five concentric layers, each shipped as its own PR series in 0.9.0–0.9.9.
1. Capabilities — the contract vocabulary
mureo.core.providers.capabilities defines a StrEnum of 13 stable
identifiers: read_campaigns, write_budget, read_keywords,
write_audience, and so on. Skills declare what they need;
providers declare what they offer. The matcher in
mureo.core.skills.matcher resolves the two sets.
The Capability enum is the smallest possible vocabulary for “what does this provider do”. It is also the load-bearing ABI: adding a new member is non-breaking, removing or renaming one is a major version bump. Phase 1 fixes 13 values; the ABI stability promise treats those as frozen.
2. Protocols — the method shapes
On top of Capabilities sit four domain Protocols —
CampaignProvider, KeywordProvider, AudienceProvider,
ExtensionProvider — plus the umbrella BaseProvider. Each fixes
a small set of synchronous method signatures using a shared
vocabulary of frozen=True dataclasses (Campaign, Ad,
Keyword, Anomaly, etc.).
The split matters. BaseProvider is three attributes (name,
display_name, capabilities) — the minimum a plugin must declare
to be discoverable. The four domain Protocols are opt-in: a
read-only provider implements only the read_* methods of one
Protocol; a full provider implements all four. The MCP exposure is
a separate, opt-in MCPToolProvider Protocol on top — a plugin can
ship analytics without ever exposing raw tools to the agent.
This is what lets a Bing Ads or Apple Search Ads or in-house platform plugin slot into the same skills that mureo’s bundled adapters use, without the plugin having to know anything about how those skills are wired.
3. Registry — entry-points discovery
The registry (mureo.core.providers.registry) uses Python’s
standard importlib.metadata.entry_points API. A plugin declares
its provider class under the mureo.providers group in its
pyproject.toml:
[project.entry-points."mureo.providers"]bing_ads = "my_plugin:BingAdsProvider"mureo discovers and validates the class at startup, defers instantiation, isolates per-plugin faults (a malformed plugin records a warning and continues; it does not crash the framework), and applies a first-wins policy on duplicate provider names.
There is no plugin manifest format, no JSON schema, no out-of-tree
configuration directory. The provider class object itself is the
manifest. The same group convention is reused for mureo.skills,
mureo.web_extensions, and mureo.runtime_context_factory.
4. ABI stability promise — the social contract
A pluggable framework is useless if upgrading the framework breaks
the plugins. mureo’s docs/ABI-stability.md
enumerates exactly what is part of the ABI and what is an
implementation detail:
Capabilityenum values: stable- Domain
Protocolmethod signatures: stable - Frozen dataclass shapes: stable, additive evolution allowed
- Entry-point group names: stable
- Provider name regex, SKILL.md frontmatter keys: stable
- Module-level functions like
discover_providers,match_skills,parse_skill_md: stable signatures and semantics
Semver mapping is explicit: MAJOR breaks the ABI, MINOR only
extends it (new Capability members, new optional fields, new
Protocols), PATCH is bugfixes only. The pre-1.0 caveat is
documented too — Phase 1 stability is held across MINOR bumps in
the 0.x series, with breaks deferred to 0.x → 0.(x+1) or
1.0. Plugin authors are told to pin mureo>=0.8,<1.
The promise is not “we’ll be careful” — it is a typed surface with explicit boundaries. Anything not on the list “is an implementation detail and may change without notice.”
5. Web extensions — UI surface for plugins
Plugins don’t just add Capabilities and tools — they often need an
operator-facing surface to configure themselves. The 0.9.5 web
extension layer (mureo.web_extensions entry-point group) lets a
plugin register additional tabs and API routes inside the
mureo configure browser UI without each surface having to know
about the plugin.
The mechanism mirrors the providers / skills pattern: discovery
runs once at startup, isolates per-plugin faults, and exposes
survivors as frozen WebExtensionEntry records. The front-end
fetches /api/extensions once when the dashboard opens, renders
one nav tab per extension, and lazy-loads each extension’s HTML /
scripts / styles on first activation. Operators who never visit a
given tab pay zero added page weight. With zero plugins installed,
the configure UI is byte-identical to v0.9.4 — backward
compatibility falls out of the discovery semantics.
0.9.6 added optional per-locale labels for these tabs via
display_name_i18n, so a plugin can ship an English label and a
Japanese label without touching mureo’s i18n catalogue.
6. Safety layer — wrapping third-party plugin tools
The fifth and most consequential piece is the safety layer added in 0.9.1 (PR #114). A plugin can expose MCP tools to the agent, but mureo refuses to dispatch those tools blindly. Every plugin tool call goes through the same wrapper:
- Audit log — every call records one secret-masked JSON line
to
~/.mureo/plugin_audit.jsonl(created mode 0600, strict 512- byte truncation, list/depth caps). Reads and writes both. Never raises. - Throttle — a shared conservative
Throttler.acquire()runs on every call, with a dedicatedPLUGIN_THROTTLEpreset distinct from mureo’s bundled platform throttles. - Standard MCP annotations honoured — mureo reads the standard
Tool.annotations.readOnlyHint. A tool that does not declare itself read-only is treated as mutating, conservatively, and receives structural strategy parity: confirm + STRATEGY-gate (skill-mediated),action_logpromotion, observation window, rollback-intent (mechanical). - Fault isolation — a plugin exception surfaces as a clean tool error to the agent; it does not crash the MCP server. The exception is re-raised unchanged after the audit record is written.
The optional Tool._meta["mureo"] keys (reversal, throttle,
observation_days) let plugin authors opt into finer-grained
semantics, but the safe defaults apply with no declaration at
all. A plugin author cannot accidentally skip the safety layer,
and a malicious plugin cannot ship a mutating tool that bypasses
the audit log.
This is what makes the control-plane framing concrete. mureo’s contribution is no longer “the API connection” — that lives in the plugin (or, increasingly, in the official MCP the plugin wraps). mureo’s contribution is the audit, the throttle, the strategy gate, and the rollback path applied uniformly across every provider, official or third-party, that the agent calls.
Why this matters once the official MCPs ship
Restate the original question. Google Ads MCP ships. Meta Ads MCP ships. The agent can now call those directly without mureo in the path.
What an agent calling official MCPs directly does not have:
- a shared
STRATEGY.mdconsulted before each decision - an append-only
action_logof every mutation, replayable as a rollback - a single
mureo configuresurface to register OAuth, switch native↔official tool source per platform, and toggle credentials - safety semantics applied uniformly across heterogeneous platform MCPs (a Google Ads change and a Meta Ads change going through the same audit and throttle pipeline)
- a place for a third-party plugin — Bing Ads, Apple Search Ads, an in-house ad system — to slot into the same skills the agent already uses
That is the control-plane shape. The plugin architecture in 0.9.x
is the load-bearing infrastructure that makes the claim hold up:
adding a new platform is a pyproject.toml entry-point and a class
that implements one or more Protocols, and from that point on the
agent treats it identically to a bundled platform — including the
safety layer.
The bundled google_ads and meta_ads adapters were rewritten in
0.9.x to go through exactly this Protocol surface
(mureo/adapters/google_ads, mureo/adapters/meta_ads). There is
no inside track; the “official” platforms are the first two
plugins.
The architecture described here was shipped across mureo 0.9.0
through 0.9.9 (PRs #87, #89, #92–#100, #114, #116, #125, #127,
#129). The ABI stability surface is enumerated in
docs/ABI-stability.md;
the plugin authoring walkthrough lives in
docs/plugin-authoring.md.
Both ship in-tree and are versioned with the OSS release.