Run the store from an AI assistant
This page is for two readers: the merchant who would rather ask an assistant "which products are low on stock?" than click through the admin, and the developer wiring KahunaCart into an AI workflow or an agent of their own.
The Model Context Protocol (MCP) is the standard an AI client uses to call tools on an outside system. grav-mcp is the MCP server for Grav: it connects an assistant to a Grav site over the REST API, and it picks up the 92 KahunaCart tools automatically once your store is reachable.
Before you begin
- Grav 2.0 or later.
- The Grav API plugin 1.0.22 or later, installed and enabled.
GET /mcp/toolsarrived in 1.0.22. - grav-mcp 0.1.2 or later.
- Node.js 18 or later on the machine running the assistant.
- An API key belonging to a user who holds
api.accessplus thekahunacart.*permissions you want the assistant to have. See REST API for generating one, and Permissions and safety for choosing which.
How it works
KahunaCart ships an mcp.yaml manifest in its plugin root that describes its admin routes as tools. The API plugin serves the union of every enabled plugin's manifest at GET /mcp/tools, filtered to the tools your API key is allowed to call. grav-mcp fetches that list at startup and registers one MCP tool per entry, named kahunacart_<name> — kahunacart_list_products, kahunacart_refund_order. When the assistant calls one, grav-mcp calls the matching API route, and the same permissions and the same rules apply as when a person clicks the button in the admin.
| Piece | Where it lives | What it does |
|---|---|---|
mcp.yaml |
KahunaCart's plugin root | Describes each route as a tool: name, description, method, path, permission, input schema |
GET /mcp/tools |
The API plugin | Serves every enabled plugin's manifest, filtered to what the calling key may see |
| grav-mcp | The machine running the assistant | Registers one MCP tool per entry and calls the API on the assistant's behalf |
Note
Nothing in grav-mcp is written per plugin. Every KahunaCart tool comes from the manifest, so a KahunaCart update that adds a route adds a tool with no change to grav-mcp.
Connect Claude Code
- Open your Claude Code MCP config.
- Add a
gravserver:
{
"mcpServers": {
"grav": {
"command": "npx",
"args": ["-y", "grav-mcp"],
"env": {
"GRAV_API_URL": "https://mysite.com/api",
"GRAV_API_KEY": "grav_your_api_key_here"
}
}
}
}
- Restart Claude Code.
The assistant now has the core Grav tools and every KahunaCart tool your key can call.
GRAV_API_URL is the API plugin's base — your site plus /api, without the version prefix. GRAV_API_KEY is the key you generated, starting with grav_.
Warning
The key carries its owner's permissions. Do not commit it, and do not paste it into a shared config file. grav-mcp reads it from the environment and never logs it or returns it.
Other MCP clients
Any MCP client that launches a stdio server takes the same command and the same two environment variables. Point it at npx -y grav-mcp with GRAV_API_URL and GRAV_API_KEY set, and consult that client's own documentation for where its MCP configuration lives — Claude Desktop, Cursor and VS Code each keep it somewhere different.
grav-mcp also runs standalone, which is how you test it outside a client:
GRAV_API_URL=https://mysite.com/api GRAV_API_KEY=grav_abc123 npx grav-mcp
The same settings are available as CLI flags: --url, --key, --environment, --plugin-tools, --transport and --port. For a remote deployment, run it over HTTP instead of stdio:
npx grav-mcp --url https://mysite.com/api --key grav_abc123 --transport http --port 3100
Check the connection
- Ask the assistant to run
discover_plugins. - Read the
mcp_toolsblock in the answer. It lists the loaded tools grouped by plugin, andkahunacartshould be one of them. - If your client can show its tool list, look for names starting with
kahunacart_instead.
When kahunacart is not there, check these in order:
- A warning on grav-mcp's stderr. grav-mcp never fails on a plugin-tools problem; it prints a warning and starts with the core Grav tools only. The warning names the cause.
- The API plugin's version. A 404 from
/mcp/toolsmeans an API plugin older than 1.0.22. - The key's permissions. The key needs
api.accessbefore any KahunaCart permission is consulted, and it only sees tools whose permission it holds. - The plugin. Only enabled plugins are read, so a disabled KahunaCart contributes nothing.
Fix the cause, then ask the assistant to run refresh_plugin_tools. No restart is needed.
What the assistant can do
KahunaCart's manifest publishes 92 tools. Which of them exist in your session depends on your key's permissions.
A few routes the admin uses are not published as tools. File, image and category-picture uploads and the release publish route are multipart, which manifest version 1 does not cover. The CSV and PDF report exports are not JSON. The sidebar badge and the two admin script routes are left out as well. Every other JSON route the admin calls is here.
Products and variants
kahunacart.products.manage. 18 tools.
| Tool | What it does |
|---|---|
list_products |
Catalog products, newest first, with variants, categories, tags and attribute values. Takes q, page, per_page |
get_product |
One product with everything on it, plus its ten most recent completed orders |
create_product |
Create a product and at least one variant |
update_product |
Change fields, variants, categories, tags, attributes and translations |
delete_product |
Delete the product, its variants, options, releases, file rows and images |
delete_variant |
Remove one variant and return the ones that remain |
list_product_files |
The downloadable files attached to a product |
update_product_file |
Edit a downloadable's name, download limit, expiry and position |
delete_product_file |
Detach a downloadable and delete the bytes |
list_product_images |
The product's gallery, in position order |
update_product_image |
Change an image's alt text or its place in the gallery |
delete_product_image |
Detach an image and delete the bytes |
list_releases |
A product's versioned releases with their files, plus its evergreen files |
create_release |
Cut a new version of a product |
update_release |
Edit a release's version, date, changelog or published flag |
delete_release |
Delete a release and the rows of the files on it |
attach_release_file |
Move a file already on the product into a release |
detach_release_file |
Take a file back out of its release, making it evergreen |
Options
kahunacart.products.manage. 9 tools.
| Tool | What it does |
|---|---|
list_product_options |
The product's option axes, their values, and the variants they describe |
create_product_option |
Add an axis such as Size or Colour, optionally with its values |
update_product_option |
Rename an axis, change how it is rendered, or move it |
delete_product_option |
Remove an axis. The variants stay |
create_option_value |
Add one value to an axis |
update_option_value |
Relabel a value, change its swatch colour, or move it |
delete_option_value |
Remove one value from an axis |
generate_variants |
Report which combinations have no variant yet. A dry run unless apply is true |
assign_variant_options |
Point one existing variant at one combination of option values |
Categories
kahunacart.settings. 7 tools.
| Tool | What it does |
|---|---|
list_categories |
The whole tree as a flat list, with counts, pictures and translations |
create_category |
Add a category to the tree |
update_category |
Change a category's fields |
delete_category |
Delete a category and its picture. Its children are adopted by its parent |
move_category |
Give a category a new parent, a new place among its siblings, or both |
reorder_category |
Swap a category with the sibling above or below it |
delete_category_image |
Remove a category's picture, the row and the bytes |
Tags
kahunacart.settings. 4 tools.
| Tool | What it does |
|---|---|
list_tags |
Every tag with its label, slug, translations and product count |
create_tag |
Create a tag |
update_tag |
Rename a tag, change its slug, or replace its translations |
delete_tag |
Delete a tag and every assignment, and report how many products were carrying it |
Attributes
kahunacart.products.manage. 6 tools.
| Tool | What it does |
|---|---|
list_attributes |
Every attribute definition in display order, with the type list |
get_attribute |
One definition with its options, translations and product count |
create_attribute |
Define a store-wide attribute products can answer |
update_attribute |
Change a definition |
reorder_attributes |
Put the whole list in the order the ids are given |
delete_attribute |
Delete a definition and its values. Refused with 409 while products answer it unless force is true |
Tax
kahunacart.settings. 5 tools.
| Tool | What it does |
|---|---|
list_tax_zones |
Every zone with its criteria decoded and the rates hanging off it |
create_tax_zone |
Create a zone and its rates in one call |
update_tax_zone |
Change a zone's fields. A rates key replaces the whole set |
delete_tax_zone |
Delete a zone and its rates |
import_eu_vat_presets |
Create a zone and rate per EU member state at its standard rate |
Shipping
kahunacart.settings. 4 tools.
| Tool | What it does |
|---|---|
list_shipping_zones |
Every zone in the merchant's own order, with the methods it offers |
create_shipping_zone |
Create a zone and the methods it offers |
update_shipping_zone |
Change a zone's fields. A methods key replaces the whole set |
delete_shipping_zone |
Delete a zone and the methods it offered |
Coupons
kahunacart.settings. 4 tools.
| Tool | What it does |
|---|---|
list_coupons |
Discount codes with their type, limits, schedule and redemption counts |
create_coupon |
Create a discount code |
update_coupon |
Change a coupon's fields |
delete_coupon |
Delete a discount code |
Sales
kahunacart.sales.manage. 5 tools.
| Tool | What it does |
|---|---|
list_sales |
Every sale with its header and its rules |
get_sale |
One sale with its rules in order and its label translations |
create_sale |
Create an automatic discount. name and label are both required |
update_sale |
Change a sale's header, its rules, or both |
delete_sale |
Delete a sale, its rules and the translations of its label |
Reports
kahunacart.reports. 6 tools, all read-only.
| Tool | What it does |
|---|---|
report_summary |
Revenue, orders, average order value, refunds, top products and a daily series |
report_products |
What sold by product and variant, sorted by revenue |
report_customers |
Who bought, new against returning |
report_taxes |
Tax collected per label and rate, with the taxable base beside it |
report_refunds |
Every refund raised in the range, whatever became of it |
report_low_stock |
Stock-managed variants at or below a threshold, right now. Default 5 |
All but report_low_stock take from and to as YYYY-MM-DD, and default to the last 30 days.
Orders
kahunacart.orders.view to read, kahunacart.orders.manage to act, kahunacart.orders.refund to refund. 8 tools.
| Tool | What it does |
|---|---|
list_orders |
Every order that got past the cart, newest first. Takes q, page, per_page |
get_order |
One order with its lines, adjustments, transactions and timeline |
create_order |
Record an order taken over the phone, at a stall or by email |
refund_order |
Refund through the provider that took the payment. Omit amount for the rest |
mark_order_paid |
Settle an order whose payment arrived outside KahunaCart |
fulfill_order |
Mark the whole order shipped and queue the shipped email |
cancel_order |
Call off an unpaid sale and reverse stock, coupon and download links |
resend_order_confirmation |
Queue the confirmation email again |
Downloads
kahunacart.orders.view to read, kahunacart.orders.manage to revoke. 2 tools.
| Tool | What it does |
|---|---|
list_order_downloads |
Every download grant on an order, with its file, uses, expiry and revocation |
revoke_download |
Kill one grant. There is no un-revoke |
Customers
kahunacart.customers.view to read, kahunacart.customers.manage to act. 5 tools.
| Tool | What it does |
|---|---|
list_customers |
Everyone who has bought, with order count and lifetime revenue |
get_customer |
One customer with their orders, addresses, account state and add-on panels |
invite_customer |
Open a store account if needed and queue the set-password email |
clear_customer_2fa |
Destroy the authenticator secret on a customer's account |
set_customer_state |
Switch a customer's ability to sign in on or off |
Webhook log
kahunacart.settings. 2 tools, both read-only.
| Tool | What it does |
|---|---|
webhook_summary |
Per-provider counts, the current run of failures, and the retention window |
list_webhook_deliveries |
Recent deliveries, newest first. Takes provider, page, per_page |
Providers
kahunacart.settings. 5 tools.
| Tool | What it does |
|---|---|
list_providers |
Every registered payment method and what it declares it can do |
check_provider |
Ask a provider to prove its configured credentials work |
sync_provider |
Push the catalog to a provider, or pull the provider's into it |
setup_provider_webhook |
Ask a provider to register this store's webhook endpoint |
make_default_provider |
Write checkout.default_provider so checkout starts here |
Config and admin
kahunacart.orders.view. 2 tools, both read-only.
| Tool | What it does |
|---|---|
get_config |
Currency, decimal places, storefront route, languages, providers and job health |
list_admin_sections |
The extra admin screens other plugins have contributed |
Six things you can ask for
| What you type | What the assistant calls |
|---|---|
| "How many orders are unpaid from last week?" | kahunacart_list_orders. Every row carries payment_status and created_at, and the assistant filters and counts. The list has no status filter of its own |
| "Add a Material attribute with cotton and linen, and set it on every tee." | kahunacart_create_attribute with type: select, then kahunacart_list_products with q=tee, then kahunacart_update_product on each. attributes merges, so nothing else on the product is touched |
| "Refund order 482 in full." | kahunacart_list_orders with q=482 to turn the order number into an id, then kahunacart_refund_order with no amount |
| "Which products are low on stock?" | kahunacart_report_low_stock, threshold 5 unless you name another |
| "Create a 20% summer sale on the Apparel category until 31 August." | kahunacart_list_categories for the id, then kahunacart_create_sale with one rule of scope: category, mode: percent, percent: "20" and an expires_at timestamp |
| "Is Stripe still connected?" | kahunacart_check_provider. A failed check is a successful call reporting ok: false with the provider's own message |
Permissions and safety
Three things stand between an assistant and your store.
The key decides which tools exist. GET /mcp/tools leaves out every tool whose permission the calling key does not hold, so a key with only kahunacart.reports produces a session where the refund tool is not present at all. A super admin sees everything.
Every call is checked again. grav-mcp checks the permission before it calls, and the API plugin checks it again on the route. A tool that slipped through would still be refused with 403.
Annotations tell the client what a tool is. Every GET is marked read-only. Deletes are marked destructive, and so are refund_order, cancel_order and revoke_download. An MCP client that asks before running destructive tools has what it needs to ask.
A store with demo.readonly on refuses every write with 403 before the handler runs, whatever the key's permissions say. Reads are never blocked.
Tip
Give the assistant its own API key rather than reusing yours, with only the permissions the job needs. Revoking one key then costs you nothing else.
| Goal | Permissions on the key's account |
|---|---|
| Answer questions about the numbers | api.access, kahunacart.reports |
| Answer questions about orders and customers | api.access, kahunacart.orders.view, kahunacart.customers.view |
| Run the catalog | api.access, kahunacart.products.manage |
| Run promotions | api.access, kahunacart.sales.manage, kahunacart.settings |
| Process orders, without refunds | api.access, kahunacart.orders.view, kahunacart.orders.manage |
| Process orders, refunds included | the row above plus kahunacart.orders.refund |
| Everything the admin can do | api.access and every kahunacart.* permission |
To limit which plugins publish tools at all, set --plugin-tools or GRAV_MCP_PLUGIN_TOOLS. all is the default, none turns plugin tools off, and a comma-separated list of plugin slugs loads only those:
npx grav-mcp --url https://mysite.com/api --key grav_abc123 --plugin-tools kahunacart
Two conventions matter when you read what the assistant reports back. Money goes in as decimal strings in the store currency ("19.99") and comes back as minor-unit integers (price_minor, total_minor, refunded_minor); read currency_exponent from get_config to convert. And attributes on a product update is a merge keyed by attribute slug: a slug you do not send keeps its value, and a slug sent as null removes it. Every other list-valued field replaces its whole set. See Money conventions.
Add-ons
The Licenses and Subscriptions add-ons ship their own mcp.yaml manifests, so installing either one adds its tools to the same session. Both appear the moment the plugin is enabled and the tools are refreshed.
Licenses
Licenses publishes 20 tools under the kahunacart_licenses_ prefix. The admin tools need the kahunacart.licenses permission. The last three call the public validation routes your software already uses, so an assistant can check a customer's key without a permission of its own.
| Tool | What it does | Permission |
|---|---|---|
kahunacart_licenses_list_licenses |
List licenses | kahunacart.licenses |
kahunacart_licenses_get_license |
Get a license | kahunacart.licenses |
kahunacart_licenses_issue_license |
Issue a license by hand | kahunacart.licenses |
kahunacart_licenses_update_license |
Edit a license | kahunacart.licenses |
kahunacart_licenses_suspend_license |
Suspend a license | kahunacart.licenses |
kahunacart_licenses_reinstate_license |
Reinstate a license | kahunacart.licenses |
kahunacart_licenses_revoke_license |
Revoke a license | kahunacart.licenses |
kahunacart_licenses_regenerate_license |
Regenerate a license key | kahunacart.licenses |
kahunacart_licenses_free_license_seat |
Free an activation seat | kahunacart.licenses |
kahunacart_licenses_list_license_rules |
List licensing rules | kahunacart.licenses |
kahunacart_licenses_save_product_rule |
Write a product's licensing rule | kahunacart.licenses |
kahunacart_licenses_save_variant_rule |
Write a variant's licensing rule | kahunacart.licenses |
kahunacart_licenses_delete_product_rule |
Delete a product's licensing rule | kahunacart.licenses |
kahunacart_licenses_delete_variant_rule |
Delete a variant's licensing rule | kahunacart.licenses |
kahunacart_licenses_get_license_settings |
Read the licensing settings | kahunacart.licenses |
kahunacart_licenses_update_license_settings |
Save the licensing settings | kahunacart.licenses |
kahunacart_licenses_rotate_signing_secret |
Rotate the response signing secret | kahunacart.licenses |
kahunacart_licenses_validate_key |
Validate a license key | none (public route) |
kahunacart_licenses_check_updates |
Check an updates entitlement | none (public route) |
kahunacart_licenses_latest_release |
Get the newest release a key may have | none (public route) |
Subscriptions
Subscriptions publishes 17 tools under the kahunacart_subscriptions_ prefix. Reads need kahunacart.subscriptions.view; writes need kahunacart.subscriptions.manage. A renewal is an ordinary order, so marking one paid is kahunacart_mark_order_paid from the base plugin.
| Tool | What it does | Permission |
|---|---|---|
kahunacart_subscriptions_list_plans |
List subscription plans | kahunacart.subscriptions.view |
kahunacart_subscriptions_get_plan |
Get a subscription plan | kahunacart.subscriptions.view |
kahunacart_subscriptions_list_plan_variants |
List variants a plan can sell | kahunacart.subscriptions.view |
kahunacart_subscriptions_create_plan |
Create a subscription plan | kahunacart.subscriptions.manage |
kahunacart_subscriptions_update_plan |
Update a subscription plan | kahunacart.subscriptions.manage |
kahunacart_subscriptions_delete_plan |
Delete a subscription plan | kahunacart.subscriptions.manage |
kahunacart_subscriptions_set_plan_provider_refs |
Set a plan's provider references | kahunacart.subscriptions.manage |
kahunacart_subscriptions_list_subscriptions |
List subscriptions | kahunacart.subscriptions.view |
kahunacart_subscriptions_get_subscription |
Get a subscription | kahunacart.subscriptions.view |
kahunacart_subscriptions_get_metrics |
Subscription metrics | kahunacart.subscriptions.view |
kahunacart_subscriptions_cancel_subscription |
Cancel a subscription | kahunacart.subscriptions.manage |
kahunacart_subscriptions_uncancel_subscription |
Undo a scheduled cancellation | kahunacart.subscriptions.manage |
kahunacart_subscriptions_pause_subscription |
Pause billing | kahunacart.subscriptions.manage |
kahunacart_subscriptions_resume_subscription |
Resume billing | kahunacart.subscriptions.manage |
kahunacart_subscriptions_extend_subscription |
Extend the current period | kahunacart.subscriptions.manage |
kahunacart_subscriptions_add_subscription_note |
Add a note to a subscription | kahunacart.subscriptions.manage |
kahunacart_subscriptions_resend_pay_link |
Resend a renewal pay link | kahunacart.subscriptions.manage |
Refresh after installing a plugin
Install or enable a plugin while the assistant is connected, then ask it to run refresh_plugin_tools. The tool re-reads every manifest and reconciles the tool list in place: new tools are added, changed ones are updated, and tools whose plugin was disabled or removed are dropped. It reports added, updated, removed and any manifest warnings, and the MCP client is told the tool list changed.
No restart of grav-mcp or your editor is involved. refresh_plugin_tools needs api.access and nothing more.
Note
refresh_plugin_tools still works when plugin tools were started with --plugin-tools none. It reports that they are disabled rather than loading them.
Write tools for your own plugin
Any Grav plugin can publish its own routes the same way. Drop an mcp.yaml in the plugin root, beside blueprints.yaml and permissions.yaml, and describe each route: a name, a description written for the model, the method and path, the permission the route enforces, and a JSON Schema for the arguments. The API plugin serves it and grav-mcp registers it, with no code written on either side.
version: 1
prefix: kahunacart # optional; defaults to the plugin slug. Tool name = "{prefix}_{name}"
tools:
- name: list_products
title: List products
description: >
List catalog products with paging, search and status filters. Returns product rows with their
variants and attributes. Use get_product for one product with everything on it.
method: GET
path: /kahunacart/products
permission: kahunacart.products.manage
annotations:
readOnly: true
input:
type: object
properties:
q: { type: string, description: "Search title, slug or SKU" }
status: { type: string, enum: [draft, published, archived] }
page: { type: integer, minimum: 1, default: 1 }
per_page: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: update_product
title: Update a product
description: Change one or more fields of a product. Only the fields sent are changed.
method: PATCH
path: /kahunacart/products/{id}
permission: kahunacart.products.manage
annotations:
idempotent: true
input:
type: object
required: [id]
properties:
id: { type: integer, description: "Product id" }
title: { type: string }
status: { type: string, enum: [draft, published, archived] }
Annotations default by method: GET is read-only and idempotent, DELETE is destructive and idempotent, PUT and PATCH are idempotent, and POST is none of them. Setting a key overrides that one default. Only JSON bodies are supported; leave multipart routes out.
If your tool list depends on data rather than a file, add entries in code instead. The API plugin fires onApiMcpTools with an McpToolCollector under $event['tools'], and $event['tools']->add('<your-slug>', [...]) takes the same fields a manifest entry does. Manifest files are read first, so a name your own mcp.yaml already claimed wins.
The full format, the JSON Schema subset a manifest may use, and what GET /mcp/tools returns are documented in the API plugin README. A typo costs you that one tool, never the rest of the file: the endpoint reports it under warnings.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No kahunacart_ tools in the assistant's list |
grav-mcp could not load plugin tools at startup, or they are switched off | Read grav-mcp's stderr for the warning. Check that --plugin-tools is not none, and that a slug list includes kahunacart. Then run refresh_plugin_tools |
Warning: Failed to load plugin tools: … Plugin tools need the 'api.access' permission. |
The key's account has no api.access, so GET /mcp/tools answered 401 or 403 |
Grant api.access to that account, or use a key from an account that has it. A key created with a scope list is capped to those scopes whatever the account may do |
Warning: Failed to load plugin tools: this site's API plugin has no /mcp/tools endpoint… |
The API plugin predates 1.0.22 | Update the API plugin, then run refresh_plugin_tools |
Some kahunacart_ tools are there and others are not |
The key holds some kahunacart.* permissions and not others |
Grant the missing permission. The tool list is filtered per caller |
A tool answers 403 |
The key lacks that permission, or the store has demo.readonly on and the tool writes |
Grant the permission, or turn demo.readonly off on a store that is not a demo |
delete_attribute answers 409 |
Products still carry a value for that attribute. The message names the count | Send force: true to delete the values too, or clear them first |
| No tools from a plugin you just enabled | grav-mcp registered its tools at startup, before the plugin existed | Run refresh_plugin_tools. Only enabled plugins are read |