← All posts

  • architecture
  • plugin-architecture
  • positioning

mureo as a control plane: the plugin architecture in 0.9.x

mureo 0.9 ships in ten releases that together rebuild the framework as a pluggable control plane: a stable Capability+Protocol ABI, entry-points discovery, an ABI stability promise, configure-UI web extensions, and a safety layer that wraps every third-party plugin tool. This post walks the architecture and explains why this shape matters once the official ad-platform MCPs ship.

mureo as a control plane: the plugin architecture in 0.9.x

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:

  • Capability enum values: stable
  • Domain Protocol method 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.x0.(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 dedicated PLUGIN_THROTTLE preset 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_log promotion, 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.md consulted before each decision
  • an append-only action_log of every mutation, replayable as a rollback
  • a single mureo configure surface 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.