Skip to main content

Flavor MCP — Architecture

Flavor exposes a curated, read-only set of commerce and ERP capabilities to external AI agents (Claude, ChatGPT, Cursor, …) through a custom Model Context Protocol (MCP) server named flavor-mcp.

This page is the developer/architecture view. For the operator setup flow (installing the AI Pack, turning AI Access on, creating an Application Password, connecting a client), see the customer AI Access guide. To author a new ability, see Creating a Flavor AI Ability.

First-party, beta, opt-in

This is an advanced, first-party extensibility surface documented as it exists today — not a frozen public API with stability guarantees or a marketplace contract. The abilities/MCP layer ships as the AI Pack (a Core Component) and is in beta. It requires Flavor Core 2.9.1+ and WordPress 6.9+ (the WordPress Abilities API lives in core from 6.9), and it is off until an admin turns it on.

The stack at a glance

AI agent (Claude / Cursor / …)
│ HTTPS + WordPress Application Password (HTTP Basic)

POST /wp-json/flavor/mcp ← flavor-mcp custom server (HTTP transport)
│ transport permission: current_user_can('manage_options')

WordPress Abilities API (WP 6.9+) ← abilities registered via wp_register_ability()


Flavor commerce / ERP service layer ← the SAME repositories the REST controllers use

Three Flavor-owned layers, plus a bundled vendor adapter:

  1. The abilities — each a thin, read-only adapter over Flavor's existing repository/service layer, registered with the WordPress Abilities API. See Creating a Flavor AI Ability.
  2. The flavor-mcp server — a custom-named MCP server with an explicit allowlist. The only path by which abilities reach an agent.
  3. The foundation — bootstrap, ability loader, availability gate, and unified audit/error logging. Dormant unless the AI Pack is installed and an admin has switched AI Access on.
  4. The vendor adapter — the WordPress MCP Adapter (WP\MCP\*) + php-mcp-schema, bundled inside the AI Pack. It provides the MCP protocol/transport machinery; Flavor supplies the server definition, allowlist, and logging handlers.

The foundation classes

All under flavor-core/includes/ai/ (the AI Pack installs its abilities/ and vendor/ alongside these):

ClassRole
Flavor_Core_AI_FoundationBootstrap orchestrator. Boots the stack on plugins_loaded — but only when the AI Pack is installed and AI Access is on.
Flavor_Core_AI_Abstract_AbilityThe base class every ability extends. Owns the un-bypassable execute pipeline (rate-limit → run → PII-redact → audit).
Flavor_Core_AI_Ability_LoaderDiscovers ability classes and registers the available ones with the Abilities API.
Flavor_Core_AI_Ability_Availability_GatePolicy layer: which abilities are available on this install (licensed, owning product active, tier).
Flavor_Core_AI_Mcp_ServerCreates the flavor-mcp custom server with the explicit allowlist.
Flavor_Core_AI_Audit_Log_Handler / Flavor_Core_AI_Error_HandlerRoute adapter transport/error events into Flavor's unified logger instead of raw error_log().

Why a custom server with an explicit allowlist

The WordPress Abilities API ships with a default MCP server that auto-exposes any ability flagged meta.mcp.public = true. Flavor deliberately does not use that path:

  • Abilities reach agents only through flavor-mcp, whose toolset is exactly the slug allowlist the ability loader discovered for the active products on this site.

  • Every Flavor ability hardcodes meta.mcp.public = false (in the base class). A static audit check + an edit-time hook enforce that no ability source ever sets it true.

  • The default server is actively suppressed at boot:

    add_filter( 'mcp_adapter_create_default_server', '__return_false' );

This is defense in depth: even if some ability were filtered public, flavor-mcp doesn't read that flag, and the default server is never stood up (so nothing leaks through an implicit path, and errors never route through the vendor's raw error_log-based handler).

The net effect: a plugin-only site exposes only the plugin abilities; a theme-only site only theme abilities; a site with both exposes both.

The transport endpoint

Server IDflavor-mcp
REST namespaceflavor
Routemcp
Full endpointPOST {site}/wp-json/flavor/mcp
Server versionv1.0.0
TransportHTTP (MCP protocol 2025-06-18)
AuthWordPress Application Passwords (HTTP Basic)
Transport permissioncurrent_user_can('manage_options')

The endpoint URL is also returned by the helper flavor_ai_mcp_endpoint() (it resolves to rest_url( 'flavor/mcp' )).

caution
The transport is admin-only, not just read

The bundled HTTP transport defaults its REST permission callback to a read capability. Left at the default, any authenticated subscriber could reach the endpoint and enumerate the ability catalog via initialize + tools/list (metadata disclosure). The flavor-mcp server therefore passes an explicit transport permission callback requiring manage_options. Individual abilities also re-check their required capability in check_permission() (defense in depth) — but the transport gate is the first door, and it is admin-only.

Authentication

Agents authenticate with WordPress Application Passwords (HTTP Basic). Because some hardening setups disable Application Passwords site-wide, the foundation re-enables them at the highest filter priority while AI Access is active — the feature owns its own auth dependency:

add_filter( 'wp_is_application_passwords_available', '__return_true', PHP_INT_MAX );

This runs on plugins_loaded, before REST auth (determine_current_user) is evaluated, and only when AI Access is active. Sites without AI Access keep whatever hardening they had. (The Flavor Starter theme's Optimize → Security & API hardening had silently disabled Application Passwords and broken MCP auth — this filter is the fix.)

Application Passwords use the fast-hash scheme — diagnostics gotcha

On WordPress 6.8+, Application Passwords are stored with wp_fast_hash() ($generic$…) and must be verified with wp_verify_fast_hash(), not wp_check_password() (which returns false for $generic$ hashes by design). If you write an auth diagnostic and use the wrong verifier you'll chase a false "no match". [VERIFY] this against your exact WordPress version — it is WP-core behavior, not Flavor code.

The Streamable-HTTP session handshake

When probing the endpoint by hand (e.g. curl), a tools/list call returns HTTP 400 unless you first call initialize and echo the Mcp-Session-Id header it returns on every subsequent request. Real MCP clients (and the mcp-remote proxy the customer guide uses) do this automatically — it only bites manual probes.

initialize → response header: Mcp-Session-Id: <id>
tools/list (send header Mcp-Session-Id: <id>) → 200, list of tools
tools/call (send header Mcp-Session-Id: <id>) → 200, result

[VERIFY] the exact 400-without-session behavior against the bundled adapter version — it is vendor-transport behavior, observed during a connect smoke test, not a Flavor-owned contract.

Boot sequence and hook ordering

Flavor_Core_AI_Foundation::init() runs once on plugins_loaded and is deliberately conservative:

  1. Dormant unless installed. Bail immediately if the AI Pack's bundled vendor autoloader is absent — a Core-only customer pays zero cost.
  2. Respect the activation toggle. Bail if flavor_ai_is_mcp_active() returns false (admin hasn't turned AI Access on).
  3. Re-enable Application Passwords (above), register a minimal PSR-4 autoloader for the bundled vendor packages, and load the foundation classes.
  4. Hook the loader's category + ability registration onto the Abilities API init hooks.
  5. If the adapter class is present, suppress the default server, initialize the adapter, and hook the flavor-mcp server onto mcp_adapter_init.
Discovery and registration are decoupled (ordering)

The adapter fires mcp_adapter_init before wp_abilities_api_init (it adds the abilities hook and fires the adapter hook in the same call). So the flavor-mcp server is created before the abilities are legally registered. The loader resolves this by separating:

  • available_slugs() — discovers + gates abilities on demand (no wp_register_ability call) to build the server allowlist. Read on mcp_adapter_init.
  • register_all() — on wp_abilities_api_init, calls register() on the SAME instances discovery already built (reuse, not a re-scan — the Abilities API binds each ability's permission/execute callbacks to a live instance).

The ability category is registered separately, on the earlier wp_abilities_api_categories_init hook (registering it on the later hook makes wp_register_ability_category() warn and leaves the category missing when abilities reference it).

Bundled vendor is NOT loaded via Composer's autoloader

The AI Pack ships a Composer vendor/, but the foundation registers its own minimal PSR-4 mapper instead of require-ing vendor/autoload.php. Loading Composer's bootstrap would fatal with "Cannot redeclare class Composer\Autoload\ClassLoader" whenever another active plugin (e.g. Query Monitor) bundles its own Composer vendor. The bundle is pure PSR-4, so a namespace→dir mapper is a complete, collision-free substitute.

Availability and gating

An ability is available on a given install only when all of these hold:

  1. The AI Pack is installedflavor_ai_is_mcp_installed() (bundled vendor autoloader present). Otherwise the foundation is dormant.
  2. AI Access is enabledflavor_ai_is_mcp_active() (admin toggle Flavor → Flavor AI → AI Access, stored as the flavor_core_ai_mcp_enabled option). Default off. Installing the Pack alone exposes nothing.
  3. The site is licensed — a stale or missing license exposes zero abilities (enforced at the availability layer and per-request in check_permission()). Checked via flavor_core_is_licensed().
  4. The owning product is active — plugin abilities need WP eCommerce Core loaded, theme abilities need the Flavor Starter theme loaded. Detection reads the product's version constant through the Products Registry, so it confirms the code actually loaded (not just that a row exists).
  5. The license tier allows the ability — today all tiers map to "all abilities"; the per-tier allowlist hook (Availability_Gate::allowed_slugs()) exists so a future ability can be tier-gated without touching any caller.

The shipped abilities (Phase 1)

Seven plugin-side, read-only abilities ship in Phase 1. (No theme-side abilities ship yet — the loader supports an abilities/theme/ directory, but it is empty.) All seven require the manage_options capability and advertise readOnlyHint.

SlugReturnsPII
flavor/products-queryCatalog search; price + stock per row. Internal cost/margin omitted.None
flavor/orders-queryOrder list/search summaries; customer name + email per row.full_name, email (masked)
flavor/order-getSingle order detail incl. nested billing/shipping blocks.first/last/full name, company, email, phone, address, VAT, formatted address, customer note (masked)
flavor/stock-queryPer-warehouse stock breakdown + total for a product/variant.None
flavor/customers-queryCRM contact search.first/last/full name, email, phone, company, job title, notes, customer_name (masked)
flavor/invoices-queryFiscal invoices + myDATA transmission status.counterpart_name, notes (masked)
flavor/erp-dashboard-statsAggregate order/revenue stats + low-stock alerts.None

PII is masked unless the connecting account holds the PII-unmask capability — see Creating a Flavor AI Ability.

The myDATA caveat

flavor/invoices-query is read-only over fiscal documents: it surfaces myDATA transmission status (transmitted / pending / error / cancelled) so an agent can reason about it. It never transmits, cancels, or mutates a myDATA submission — that stays in the human-operated Invoicing UI.

Observability

Every MCP event routes through Flavor's unified logger (flavor_core_log()), never error_log. All channels live under the reserved core.mcp.* namespace:

ChannelWhat it records
core.mcp.executeOne audit entry per ability execution: user id, slug, outcome (success / rate_limited / error), latency. JSON-serializable.
core.mcp.pii_unmaskA privileged caller received un-masked PII (warning level; separate line so the unmask event is alertable independently).
core.mcp.transportAdapter transport-level events (request dispatch, registration).
core.mcp.errorAdapter/transport errors (routed away from the vendor's raw error_log handler).
core.mcp.registerServer/ability registration diagnostics (e.g. Abilities API unavailable on WP < 6.9).

View them in Flavor → Debug, filtered to the core.mcp.* channels. Because the audit context is JSON-serializable, an opt-in site→Hub audit forward is a trivial future extension.

What is NOT in Phase 1

  • Write actions. Every ability is read-only. A write layer (per-capability opt-in + per-action approval) is a future phase — do not document it as available.
  • A Hub-native multi-site MCP server. Phase 1 is entirely site-local, with no dependency on Flavor Hub; it works even if the Hub is offline.
  • Theme-side abilities. The abilities/theme/ directory is supported but empty.
  • Resources and prompts. The flavor-mcp server registers tools only; MCP resources/prompts arrays are empty in Phase 1.