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.
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:
- 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.
- The
flavor-mcpserver — a custom-named MCP server with an explicit allowlist. The only path by which abilities reach an agent. - 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.
- 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):
| Class | Role |
|---|---|
Flavor_Core_AI_Foundation | Bootstrap orchestrator. Boots the stack on plugins_loaded — but only when the AI Pack is installed and AI Access is on. |
Flavor_Core_AI_Abstract_Ability | The base class every ability extends. Owns the un-bypassable execute pipeline (rate-limit → run → PII-redact → audit). |
Flavor_Core_AI_Ability_Loader | Discovers ability classes and registers the available ones with the Abilities API. |
Flavor_Core_AI_Ability_Availability_Gate | Policy layer: which abilities are available on this install (licensed, owning product active, tier). |
Flavor_Core_AI_Mcp_Server | Creates the flavor-mcp custom server with the explicit allowlist. |
Flavor_Core_AI_Audit_Log_Handler / Flavor_Core_AI_Error_Handler | Route 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 ittrue. -
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 ID | flavor-mcp |
| REST namespace | flavor |
| Route | mcp |
| Full endpoint | POST {site}/wp-json/flavor/mcp |
| Server version | v1.0.0 |
| Transport | HTTP (MCP protocol 2025-06-18) |
| Auth | WordPress Application Passwords (HTTP Basic) |
| Transport permission | current_user_can('manage_options') |
The endpoint URL is also returned by the helper flavor_ai_mcp_endpoint() (it resolves to rest_url( 'flavor/mcp' )).
readThe 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.)
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:
- Dormant unless installed. Bail immediately if the AI Pack's bundled vendor autoloader is absent — a Core-only customer pays zero cost.
- Respect the activation toggle. Bail if
flavor_ai_is_mcp_active()returns false (admin hasn't turned AI Access on). - Re-enable Application Passwords (above), register a minimal PSR-4 autoloader for the bundled vendor packages, and load the foundation classes.
- Hook the loader's category + ability registration onto the Abilities API init hooks.
- If the adapter class is present, suppress the default server, initialize the adapter, and hook the
flavor-mcpserver ontomcp_adapter_init.
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 (nowp_register_abilitycall) to build the server allowlist. Read onmcp_adapter_init.register_all()— onwp_abilities_api_init, callsregister()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).
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:
- The AI Pack is installed —
flavor_ai_is_mcp_installed()(bundled vendor autoloader present). Otherwise the foundation is dormant. - AI Access is enabled —
flavor_ai_is_mcp_active()(admin toggleFlavor → Flavor AI → AI Access, stored as theflavor_core_ai_mcp_enabledoption). Default off. Installing the Pack alone exposes nothing. - The site is licensed — a stale or missing license exposes zero abilities (enforced at the availability layer and per-request in
check_permission()). Checked viaflavor_core_is_licensed(). - 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).
- 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.
| Slug | Returns | PII |
|---|---|---|
flavor/products-query | Catalog search; price + stock per row. Internal cost/margin omitted. | None |
flavor/orders-query | Order list/search summaries; customer name + email per row. | full_name, email (masked) |
flavor/order-get | Single order detail incl. nested billing/shipping blocks. | first/last/full name, company, email, phone, address, VAT, formatted address, customer note (masked) |
flavor/stock-query | Per-warehouse stock breakdown + total for a product/variant. | None |
flavor/customers-query | CRM contact search. | first/last/full name, email, phone, company, job title, notes, customer_name (masked) |
flavor/invoices-query | Fiscal invoices + myDATA transmission status. | counterpart_name, notes (masked) |
flavor/erp-dashboard-stats | Aggregate 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.
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:
| Channel | What it records |
|---|---|
core.mcp.execute | One audit entry per ability execution: user id, slug, outcome (success / rate_limited / error), latency. JSON-serializable. |
core.mcp.pii_unmask | A privileged caller received un-masked PII (warning level; separate line so the unmask event is alertable independently). |
core.mcp.transport | Adapter transport-level events (request dispatch, registration). |
core.mcp.error | Adapter/transport errors (routed away from the vendor's raw error_log handler). |
core.mcp.register | Server/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-mcpserver registers tools only; MCP resources/prompts arrays are empty in Phase 1.