Skip to content

Agent-Callable Layer (WP Abilities + MCP)

New in v4.59.8: Dino Discounts can be driven by an AI agent. The plugin publishes its discount engine as a set of agent-callable abilities — through the WordPress Abilities API (WordPress 6.9) and, via the MCP Adapter, the Model Context Protocol. An AI assistant connected to your store can read your discounts, dry-run a change against a sample cart, publish it, and roll it back — all through the exact same REST routes (and the exact same manage_woocommerce permission gate) your admin team already uses.

Same engine, new driver.

An ability is a thin, capability-gated wrapper over a route the admin app already calls. The agent doesn't get a private back door — it gets a steering wheel for the car you already drive.


If your store runs WordPress 6.9+ and you've installed an MCP host (the small connector layer described below), you can talk to your discounts. For example:

  • "What discounts are live right now, and how much has each one given away this month?"
  • "Build a 'spend £50, get 10% off' cart discount, show me what it would do to a £60 cart, then publish it."
  • "Roll the rules back to before yesterday's flash-sale change."

The assistant turns each request into one or more abilities (the verbs below), runs them against your store, and reports back. Nothing happens that your team couldn't do by hand in the Dino Discounts admin — and write actions are always reversible.

Plain-language control

Describe the discount you want; the agent assembles a valid rule, previews it, and publishes it — no wizard clicks.

Safe by construction

Writes validate before they touch anything, publish atomically snapshots the prior state, and every change is one restore-snapshot away from undone.

One source of truth

Abilities call the same dino-discounts/v1 REST routes as the admin React app. An agent's edit and a human's edit land on identical code paths.

Zero new attack surface

Every ability is gated on manage_woocommerce — twice. There are no public abilities and no separate auth.


The agent layer is inert by default. It wires itself onto two upstream hooks and does nothing at all unless the host that fires them is present — no fatal error, no new dependency, no behaviour change on a store that doesn't want it.

To turn it on you need:

  1. WordPress 6.9 or newer — for the built-in Abilities API (wp_abilities_api_init). With just this, the abilities become callable over the Abilities REST API.
  2. The MCP Adapter (mcp_adapter_init) — the WordPress feature plugin that exposes registered abilities to MCP clients. With this installed, the same abilities are published as MCP tools under an MCP server named dino-discounts, ready for any MCP-capable assistant to discover and call.

Dino Discounts itself needs no configuration: as soon as those hosts are active, the catalogue registers automatically. If either is absent, that part of the layer simply doesn't register.


Ten abilities ship, in two categories. Seven are read-only or compute-only (safe to call freely); three mutate and are clearly marked. Every one is gated on the manage_woocommerce capability.

Category: dino-discounts-rules — read, preview, publish, roll back

Section titled “Category: dino-discounts-rules — read, preview, publish, roll back”

| Ability | Does | Effect | |---|---|---| | dino-discounts/get-rule-schema | Returns the live JSON Schema for a discount rule (the same schema the on-save sanitiser validates against). Read this first to learn the rule shape. | Read-only | | dino-discounts/list-rules | Returns the full active rule set, with each rule's store-wide redemption count. | Read-only | | dino-discounts/get-rule | Returns one active rule by its id (UUID or integer). | Read-only | | dino-discounts/preview-cart | Prices a hypothetical cart against a supplied rule set — the same dry-run the admin Cart Preview uses. Optionally returns a debug trace of which rules matched. | Compute-only (no writes) | | dino-discounts/publish-rule | Safely creates or updates a single rule: validate → upsert → optional preview → publish → report rollback snapshot. See the worked flow. | Mutating | | dino-discounts/list-snapshots | Returns the rule history: every audit snapshot (id, name, comment, timestamp, diff summary) written on publish or reset. | Read-only | | dino-discounts/restore-snapshot | Atomically restores the active rules to a previous snapshot. Writes a fresh snapshot of the current state first, so the restore is itself reversible. | Mutating |

Category: dino-discounts-insights — analytics & performance (read-only)

Section titled “Category: dino-discounts-insights — analytics & performance (read-only)”

| Ability | Does | Effect | |---|---|---| | dino-discounts/read-discount-analytics | Per-rule discount performance from completed orders: how often each rule applied and the total discounted, over an optional date range. | Read-only | | dino-discounts/read-coupon-analytics | Coupon-code redemption analytics over an optional date range. | Read-only | | dino-discounts/read-rule-performance | The discount-engine performance log: recent rule-evaluation timing traces, to spot slow rules. | Read-only |


  • Capability: every ability requires manage_woocommerce — the same capability AbstractController::check_permission() enforces on every admin REST route.
  • Gated twice (defence in depth): the Abilities API / MCP checks the capability in the ability's permission_callback before executing, and the underlying REST controller re-checks it when the call dispatches. A request that slips one gate still meets the other.
  • No public abilities: there is no anonymous or lower-privilege ability. An unauthorised call returns a recoverable dino_discounts_ability_forbidden error (HTTP 401/403), not a silent failure.
  • No new business logic: an ability adds no validation, pricing, or storage of its own — it dispatches an existing dino-discounts/v1 route in-process with rest_do_request(). The route still owns validation, the capability gate, the schema sanitiser, and the snapshot-on-publish audit trail.

When the MCP Adapter is active, an MCP client connecting to the dino-discounts server discovers these ten tools (the description text is what the agent uses to pick the right one):

MCP server: dino-discounts
dino-discounts/get-rule-schema Read the discount rule schema (read-only)
dino-discounts/list-rules List discount rules (read-only)
dino-discounts/get-rule Get a single discount rule by id (read-only)
dino-discounts/preview-cart Dry-run a cart against rules (compute-only)
dino-discounts/publish-rule Publish (create/update) a rule (mutating)
dino-discounts/list-snapshots List rule history snapshots (read-only)
dino-discounts/restore-snapshot Roll back rules to a snapshot (mutating)
dino-discounts/read-discount-analytics Per-rule discount analytics (read-only)
dino-discounts/read-coupon-analytics Coupon redemption analytics (read-only)
dino-discounts/read-rule-performance Engine performance log (read-only)

How an assistant typically maps a request to one or more abilities:

| You ask | Abilities the agent calls | |---|---| | "Which discounts are pulling their weight this month?" | read-discount-analytics | | "Show me rule r-summer-10." | get-rule | | "What would a £60 cart of two T-shirts get right now?" | list-rulespreview-cart | | "Add 10% off orders over £50 and publish it." | get-rule-schemapublish-rule | | "Undo the last rule change." | list-snapshotsrestore-snapshot | | "Why did checkout feel slow after that last change?" | read-rule-performance |

Worked example: preview, then publish a rule

Section titled “Worked example: preview, then publish a rule”

The publish-rule ability is the flagship — and it is deliberately not a blind POST /rules (which would replace the entire rule set, so a careless "add one rule" could wipe the rest). Instead it runs a safety chain, every step of which is an existing REST route:

  1. Validate — dry-runs your rule through /rules/diff-summary. If it's incomplete, the call aborts before any write and returns a recoverable rule.schema_invalid error (code + message + HTTP 400) so the agent knows the rule wasn't publishable and can fix it.
  2. Upsert — reads the current rules and merges yours by id: a matching id is replaced, a new id is appended. Omit id and one is generated and returned, so the agent can re-publish the same rule idempotently later.
  3. Preview (optional) — if you pass a preview_cart, the merged set is dry-run priced against it as an extra check; a preview error aborts the publish.
  4. Publish — writes the merged set, atomically saving a restore snapshot of the prior rules first. (Internally it publishes the canonical node tree, not a flat list, so promotion grouping and names are preserved exactly — the same write path a human operator's edit takes.)
  5. Report — returns the rollback_snapshot_id, so undo is a single restore-snapshot away.

A publish-rule call (the rule object is in the shape get-rule-schema returns — who / when / where / trigger / rewards / messaging):

{
"rule": {
"name": "Spend £50, get 10% off",
"is_active": true,
"who": { "roles": null, "is_logged_in": null },
"when": { "schedule": null },
"where": { "scope": "entire-cart" },
"trigger": { "type": "cart-subtotal", "min_subtotal": 50 },
"rewards": { "type": "percentage", "amount": 10 },
"messaging": { "cart_label": "Spend & save: 10% off", "cart_label_visible": true }
},
"preview_cart": [
{ "isCustom": true, "name": "Sample item", "price": 60, "quantity": 1 }
],
"snapshot_name": "Add spend-and-save 10%",
"comment": "Created via AI assistant"
}

A successful response — note the rollback_snapshot_id for one-call undo:

{
"published": true,
"rule_id": "f1c2…-uuid",
"created": true,
"updated": false,
"rule_count": 7,
"rollback_snapshot_id": 412,
"archived_coupons": [],
"preview": {
"success": true,
"cart": [ /* priced cart line items */ ],
"discounts": [ /* the discounts that applied, per rule — the £6 off lives here */ ],
"cart_total": 60.0,
"cart_tax": 0,
"effective_currency": "GBP"
}
}

The preview body is exactly what the preview-cart ability returns — see its shape in the REST API Reference.

If the rule were incomplete, step 1 would short-circuit instead — the agent gets back a recoverable error (and nothing is written):

{
"code": "rule.schema_invalid",
"message": "Every discount has errors — fix at least one before publishing.",
"data": { "status": 400 }
}

The ability layer normalises every failure to this envelope — a stable code, a human-readable message, and the HTTP status — so the agent can branch on why a call failed (forbidden, validation, conflict) and adjust.

To undo, the agent reads list-snapshots (or reuses the rollback_snapshot_id) and calls restore-snapshot with that id — which itself snapshots the current state first, so even the rollback is reversible.


The layer is intentionally thin. Three small classes under includes/Abilities/ do all of it:

  • AbilityCatalog — declares every ability and its two categories as pure data: name, label, description (this drives the agent's tool selection), capability, REST verb + route, and the input/output JSON Schemas. Routes are built from AbstractController::REST_NAMESPACE (dino-discounts/v1), so a catalogue target can't drift from the registered REST surface without a test failing.
  • RestGateway — the execution seam. It turns an ability call into a WP_REST_Request, dispatches it with rest_do_request() (the same dispatcher WordPress uses for an HTTP REST call, so the controller runs its full validation + capability gate + handler), and normalises the result: a 2xx response becomes the ability's data; a >= 400 response or WP_Error becomes a stable, agent-recoverable error envelope (code + message + HTTP status) the agent can read and adjust to.
  • RuleComposer — the only place that does more than mirror one route. It chains several routes through the gateway for the two abilities that need it: get-rule (GET /rules then pick by id, since the store has no single-rule read route) and the publish-rule safety chain above.

AbilitiesModule wires the catalogue onto wp_abilities_api_init (registering each category then each ability via wp_register_ability()) and mcp_adapter_init (registering the dino-discounts MCP server with the same tools). Both callbacks guard on the host being present, which is why the layer is inert when it isn't.

Because every ability bottoms out in a dino-discounts/v1 route, the REST API Reference is the authoritative description of what each one ultimately does.