Language

Get in touch

WooCommerce plugin · since 2018 · aka WOOBE

BEAR

A spreadsheet-style bulk editor for WooCommerce products, with an MCP server that lets AI agents edit the shop safely.

First release
Active installs
30,000+
Rating
4.7 / 5 (227)Read reviews
  • PHP
  • WordPress
  • WooCommerce CRUD (WC_Product)
  • WP_Query
  • $wpdb / MySQL
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • jQuery
  • DataTables
View on wordpress.org wordpress.org

About

BEAR shows the WooCommerce catalog as one editable table and changes thousands of products at once: prices, stock, SKUs, taxonomies, attributes, variations and custom meta. Every change goes through one field-write path that uses WooCommerce's product objects for product properties, is recorded with its previous value and can be rolled back from the History tab.

The hard part is not the table. It is making a bulk write correct on a real shop: values stored in five different places, variable products whose prices and stock live on their variations, WooCommerce side effects such as stock status derived from stock quantity, and shared hosting that kills a PHP request after 30 seconds.

Since 2026 the plugin also runs an MCP server. An AI agent can find products, preview a change, apply it in portions and roll it back, through the same filter, bulk and history engines the screen uses, behind keys, a confirmed session token (optional for the shop-wide key, always on for personal keys) and a per-sector permission map.

The free and the premium builds are one codebase switched by a single flag; the free build limits page size, meta fields, history depth and the MCP write rate.

BEAR · bulk editor

what was solved

LIVEsince 2018

Products grid and field model

shipped · 2018

What it givesStaff can see and change any product or variation detail in one spreadsheet-like screen, even on catalogs too large to open at once.

Problem
One screen has to show and edit any value of a catalog of any size: post fields, WooCommerce props, taxonomies, attributes, downloads and arbitrary meta, for products and their variations.
Solution

A declarative field registry drives every column and every write, and the table is paged on the server, one WP_Query page per AJAX request.

Stack
  • WP_Query
  • DataTables server-side mode
  • WC_Product setters
  • wp_options
Tasks · 6
Large catalogs cannot be loaded into the browser at onceServer-side paging: each table page is a WP_Query with offset, order and filters

woobe_get_products() takes start, length and order from DataTables and passes them to WOOBE_PRODUCTS::gets(), which builds the WP_Query. Sorting by a meta column switches to meta_value_num for number, timestamp and unix types, meta_value otherwise.

Page size is capped at 100 rows in the settings model, with the comment that larger pages caused HTTP 500 on weak servers; the free build is fixed at 10. When the variations toggle is on, the variations of each variable product are queried and inserted as rows right after their parent.

Each column type is stored differently inside WooCommerceOne registry entry per column: storage type, value type, editor, sanitizer, rights

woobe_get_fields() describes every column: field_type (field, meta, prop, taxonomy, attribute, downloads, gallery, upsells and others), value type, edit view, sanitizer, product types it does not apply to, and visibility for shop managers. Global attributes and all product taxonomies are added at runtime from wc_get_attribute_taxonomies() and get_object_taxonomies().

update_page_field() dispatches on field_type: update_post_meta for meta, WC_Product setters plus save() for props, wp_set_post_terms for taxonomies, rebuilt WC_Product_Attribute lists for attributes. Grid cells, History reverts and most bulk writes end in this function. Bulk taxonomy and attribute edits write terms directly and fire the same History hook before writing.

Custom meta keys must be told apart from WooCommerce internalsA subclass of WC_Product_Data_Store_CPT exposes its protected internal key list

woobe_meta_get_keys() reads all meta keys of a sample product and subtracts WooCommerce's internal keys. That list is a protected property of the product data store, so WOOBE_PDS_CPT extends the store only to return it, adding _button_text and _product_url.

Meta keys are trimmed but not lowercased or sanitized, because real keys written by other plugins, such as _woocs_sale_price_USD, are case sensitive.

A typed meta field must work in columns, filters and bulk editOne meta definition is mapped by hooks into the registry, filter panel and bulk panel

woobe_extend_fields() turns each saved meta definition into a column; a number type gets the floatval sanitizer and numeric sort. The same definition feeds woobe_filter_numbers (from/to range), woobe_filter_text (LIKE, =, !=, NOT LIKE, empty, not empty), switcher filters, and woobe_bulk_number (set, increase or decrease by value or by percent).

A switcher column can store values other than 1/0 (for example yes/no) through a per-key override rule read by over_switcher_swicher_to_val(). The free build keeps the first 2 meta fields.

Serialized array meta has no flat cell to editPopup editor sends keys and values as parallel arrays, rebuilt into nested PHP arrays

process_jsoned_meta_data() rebuilds a meta value from keys/values pairs for arrays and keys2/values2 for objects, decoding nested JSON values, and falls back to the raw value for plain strings and numbers. History stores such values as JSON with JSON_HEX_QUOT and JSON_HEX_TAG so they survive the round trip.

Per-user editor state must survive object cache flushesOne non-autoloaded option row per user with a TTL per key, instead of transients

Filters and bulk configs are kept between requests by WOOBE_STORAGE. Under Redis or Memcached a transient lives only in the object cache and is lost on a flush, so the default storage is an option row woobe_memory_<user id>: every key carries its own expiry, expired keys are dropped on write, autoload is off, and the row is deleted with the user. Session, transient and cookie modes remain behind the woobe_storage_type filter.

Filtering

shipped · 2018

What it givesStaff can pick exactly the products they need, including by variation price or stock, and the same selection is used for editing and export.

Problem
Filters on prices, stock, dimensions, text and taxonomies must select exactly the right products, including variable products whose values live on variations, and the same set must be reused by paging, bulk edit and export.
Solution

The filter is stored on the server under a key and compiled on every query into tax_query, meta_query and posts_where clauses through one WP_Query hook.

Stack
  • WP_Query
  • tax_query
  • meta_query
  • posts_where
  • $wpdb->prepare
  • MySQL REGEXP
Tasks · 5
Paging, bulk and export requests must see the same filtered setFilter saved under a random key per run; one WP_Query hook rebuilds it per request

woobe_filter_products() stores the parsed filter form under woobe_filter_<key>, where the key is a random string generated on each filtering, so bulk by filter works in several browser tabs at once. Every WOOBE_PRODUCTS::gets() call passes its args through woobe_apply_query_filter_data(), which reads the stored filter and returns tax_query, meta_query, post__in and post_status, and attaches posts_where hooks for the rest.

Grid paging, the bulk count, the export count and the variation tools all call gets(), so they cannot disagree about which products a filter means.

Price and stock live on variations, but the grid lists parentsRange query over products and variations, then variation hits mapped to their parents

regular_price_where(), sale_price_where(), stock_quantity_where() and dimensions_where() query postmeta for both product and product_variation, then read post_parent of the matched variations and restrict the main query to parents plus variations with ID IN (...).

When nothing matches, the clause becomes ID = -1, so an empty match returns no products instead of the whole catalog, which matters one click before a bulk edit.

Numbers stored as meta strings compare wrongly in MySQLCAST to DECIMAL with explicit scale; empty strings excluded from every range

Custom number ranges use meta_query type DECIMAL(30,20): a bare DECIMAL has scale 0, so a search from 100 to 150 would also find 99.xx. Price SQL casts to DECIMAL(30,10).

An empty weight is stored as '' and MySQL reads '' as 0, so price and dimension ranges require meta_value <> '' and an empty box never counts as 0. A missing end of a range is open (substituted by -999999999 or 999999999), and from equal to to means exactly that value.

Users need AND, OR and NOT inside one text search boxSmall search syntax (^ OR, ^~ AND, ^! NOT) compiled to MySQL REGEXP clauses

posts_txt_where() splits the text on ^; a ~ prefix joins the part with AND, a ! prefix turns it into NOT REGEXP inside a separate AND group. The behaviors exact, begin, end, not and empty map to = and anchored REGEXP; regex brackets and braces are escaped. It applies to title, content, excerpt and slug.

The SKU filter takes a comma-separated list and builds prepared LIKE conditions with esc_like, OR for match modes and AND for 'not'.

Matching all of several terms needs one tax_query clause per termAND split into one clause per term; unknown operators fall back to IN

For the AND operator each term becomes its own tax_query clause under relation AND, because a single clause with operator AND can return no products. Operators are whitelisted to IN, NOT IN, AND, EXISTS and NOT EXISTS; WordPress drops a clause with an unknown operator and would match every product.

Catalog visibility is translated into combinations of the product_visibility terms exclude-from-catalog and exclude-from-search, and a stock status filter also limits product_type to types that have a stock status.

Bulk edit engine

shipped · 2018

What it givesChanges to prices, stock and variations across thousands of products finish reliably on ordinary hosting and follow the shop's own pricing and stock rules.

Problem
A bulk change can touch thousands of products on hosting with short PHP time limits, and every write must respect WooCommerce rules for prices, stock and variations.
Solution

Target IDs are resolved once, then written in small AJAX batches through the same single-field write path the grid uses, which carries the WooCommerce rules.

Stack
  • AJAX batching
  • WC_Product
  • WC_Product_Variation
  • WC_Product_Attribute
Tasks · 6
A bulk edit of thousands of products hits PHP time limitsIDs fetched once, then posted in batches of N per AJAX request; N is a setting

woobe_bulk_products_count() stores the bulk config under a random 16-character bulk_key and returns every filtered ID (fields=ids, no paging). __woobe_bulk_products() in bulk.js slices that list and posts one batch after another, updates the progress bar, and calls woobe_bulk_finish at the end; the user can abort the current request.

The batch size is number_posts_processed, 10 to 100 per request, 30 by default, so a weak server can trade speed for stability.

Edited products drop out of the filter while the run is goingFrozen ID list: each batch carries IDs from the snapshot, not a re-run query

Paging the filtered query between batches would skip products: a price edit moves a product out of a price filter and every later offset shifts. The ID list taken at the start is held by the client and each batch request sends its IDs explicitly, without the filter key.

After the run, checked rows that no longer appear in the redrawn table are removed from the selection, so the next action does not target products the user can no longer see.

Number behaviors must combine with formulas and price roundingPipeline per product: behavior, formula with another meta, random range, rounding

_process_number_data() applies set, increase or decrease by value or percent, and cross-field rules such as sale price = regular minus a percent. Then an optional formula with another meta field (+, -, *, / with division by zero guarded), then an optional random amount in a range with set decimals.

update_page_field() rounds in the shop's price decimals: to whole units, up to 0.05, up to 0.1, or to a fixed cents ending such as .99. Text fields get append, prepend, replace (case sensitive or not) and placeholders such as {TITLE}, {SKU}, {meta:key} and {attribute:pa_size}.

Sale price must stay below regular price and dates must take effectSale >= regular skipped, sale <= 0 removes it; _price recomputed when dates change

In a bulk run a computed sale price that reaches the regular price is skipped and a value of zero or less deletes the sale. A single write of a sale price at or above regular is clamped to regular minus the smallest price unit.

When sale dates are written, _price is set at once to the price that applies now (sale not started, running, ended) and product transients are cleared, instead of waiting for WooCommerce's scheduled sale job.

Stock and price writes must not damage variable products or shared stockVariable parents and parent-stock variations left alone; stock kept across toggles

WooCommerce reads the prices of a variable product from its variations, so a price written to the parent is skipped and a clearing write removes any leftover parent price. A stock of 0 or less normally switches stock management on; this is not done on a variable parent that does not track stock, where it would send every variation without its own stock out of stock, nor on a variation that sells from its parent's stock, which it would detach.

WooCommerce clears quantity, backorders and low stock amount when stock management is switched off. keep_stock_on_switch() saves them in its own meta and restores them when management is switched on again.

Variations must be created and edited by attribute combinationCartesian product of chosen terms, deduped; edits matched by combination with wildcard

generate_combinations() builds the cartesian product of the selected attribute terms. Applying it attaches the attributes as variation attributes, skips combinations a product already has, copies parent prices, gives each new variation the SKU parent-SKU-<id> to avoid duplicates, and re-sorts variations through the data store.

Editing, deleting and exporting a subset of variations compares each variation's attributes with the chosen combinations: keys and values sorted, '-1' entries treated as any value, and both sides URL-decoded because non-Latin attribute slugs are stored encoded.

History and rollback

in the product

What it givesA wrong change, by hand, in bulk or by an AI assistant, can be undone instead of fixed product by product.

Problem
Every change, by hand, in bulk or from an AI agent, has to be undoable, including side effects WooCommerce applies on its own, and the rollback of a large run hits the same time limits as the run.
Solution

Previous values are recorded per field in two custom tables by a hook on the single write path, and reverted in portions through the same path.

Stack
  • Custom MySQL tables
  • $wpdb
  • WordPress actions
Tasks · 6
Record before-values for any field type without per-feature codeOne hook before every field write; previous value stored per field in a custom table

update_page_field() fires woobe_before_update_page_field and WOOBE_HISTORY::write() reads the current value and encodes it by type: term IDs, attributes, gallery and relations as JSON, downloads as name/file/hash, stock status as 1/0, sale dates as ISO dates with their time zone offset, so a revert restores the exact schedule.

Rows hold field_key, product_id, prev_val, mod_date, bulk_key, user_id and via_mcp, indexed by product, bulk key and user. A second table keeps one head per bulk run: state (completed or terminated), start, finish, product count and the field keys.

WooCommerce changes stock status as a side effect of stock editsSide effects written as extra rows of one change and reverted together, in order

Switching stock management, or a stock of 0 on an untracked product, makes WooCommerce derive the stock status; switching management off also drops the quantity. update_page_field() records stock_status, and stock_quantity when needed, as extra rows right after the edited field.

stock_status_pair() and stock_row_at() find these rows by adjacent IDs (+1, +2) and require the same product, author, channel (by hand or through an agent) and bulk key, and a close write time. revert() then restores the quantity before the status, because WooCommerce derives the status from the quantity while stock is managed. An empty previous stock means 'not tracked', so untrack_stock() switches management off instead of writing 0.

Rolling back a bulk run of thousands of rows times out tooRollback in portions by bulk_key; each reverted row deleted; head removed at the end

In the History tab, woobe_history_revert_bulk_portion() reverts 10 rows per AJAX request with a progress bar, and the head row is deleted when the counter reaches the total. Each revert deletes its row, so the next portion simply takes what is left.

The MCP rollback tool is a separate path: revert_bulk_portion() reverts up to 200 rows per call and reports the rows actually removed, because the two rows of a stock change are reverted together and a portion can take one row more than its limit.

revert() removes all woobe_before_update_page_field listeners first, so the rollback's own writes are not recorded as new history.

Several users and AI agents share one historyOne scope() rule: admins see all rows, others their own; enforced on the server

Each row carries user_id and via_mcp. The shop-wide MCP key writes as user -777, which can never collide with a real user or with 0 (not logged in). scope() returns one SQL condition used by every list, revert, delete and clear, so no query can drift from the rule.

Handlers check solo_in_scope() or bulk_in_scope() and answer 403 to a request built by hand for a row the user cannot see. Author names for the list are read in one query.

Existing installs needed a new column without breaking old rowsVersioned schema option, ALTER guarded by a column check, backfill of agent rows

CREATE TABLE IF NOT EXISTS never alters a table that already exists. maybe_upgrade_schema() runs once per schema version (now 2), checks SHOW COLUMNS before each ALTER so a concurrent request does not fail on it, retries on the next load if ALTER fails, and marks old rows of user -777 as written through an agent.

Free build keeps a short history without touching other usersPer-author pruning on shutdown: last 2 single and 2 bulk changes, stock rows kept

In the free build prune_author() keeps the two newest single changes and the two newest bulk runs of one author. The rows that belong to a kept stock change are kept with it through stock_change_rows(). Pruning runs on shutdown after a write, and opening the tab never deletes another user's rows.

Export

in the product

What it givesFiltered product lists export to files the shop can import back, built in parts on weak servers and never left open on the web.

Problem
An export of a filtered catalog with variations must run in parts on weak servers, produce files WooCommerce can import back, and must not leave product data open on the web.
Solution

Export runs as a sequence of AJAX requests appending to one file whose name is derived per export, kept in a closed folder and streamed through an authorized handler.

Stack
  • fputcsv
  • DOMDocument (WXR)
  • hash_hmac
  • .htaccess
  • WP REST API
Tasks · 4
A large export cannot be built in one requestHeader written first, then rows appended per AJAX batch to the same file

woobe_export_products_count() clears earlier files, writes the CSV header or the XML skeleton and returns the filtered IDs. woobe_export_products() appends one batch per request with fopen(a+) and fputcsv, since WP_Filesystem cannot stream line by line; for XML it loads the WXR document and appends item nodes.

Variations are added after their parent, optionally without out-of-stock ones, and can be limited to or excluded by attribute combinations.

The CSV must load into WooCommerce's own product importerAttributes as four columns each, downloads as name/URL pairs, any value flattened

get_csv_titles() writes 'Attribute N value(s)', 'name', 'visible' and 'global' per attribute and 'Download N name' and 'URL' per file slot, the columns the WooCommerce importer reads; meta columns are named 'Meta: key'.

flatten_export_value() reduces arrays and objects (terms, attributes, WC data objects) to scalars joined with '|', so a column type the plugin has never seen, including a third-party one, does not write the word 'Array'.

Export files hold prices and file URLs and must not be publicFolder closed by .htaccess, file name with HMAC tail, download via checked handler

The file name postfix must match a minute timestamp and gets a 12-character tail from hash_hmac('sha256', postfix, wp_salt('auth')): identical for every request of one export, so all batches land in one file, yet not guessable from outside. The tail matters on nginx, which ignores the .htaccess deny rule.

woobe_export_download() checks nonce and capability, accepts only names the extension generates, clears all output buffers and streams with readfile() without Content-Length. Files older than 8 hours are deleted, checked at most once an hour.

An AI agent needs a download link without exposing its keyREST route keyed by a 32-hex single-file token that expires after 15 minutes

A browser click cannot send an Authorization header, and the MCP key is kept out of URLs. The export tool stores the file name under a random token in a transient with a 900-second lifetime, and /woobe/v1/mcp/export/<token> serves only that file.

MCP server for AI agents

shipped · 2026

What it givesAn AI assistant can edit the shop for the owner, but only on a confirmed selection, with a preview first and every change reversible.

Problem
An AI agent works through a network API with no screen, can misread a filter or a count, and can issue a write against the whole catalog; it still has to be safe, reversible and fit PHP time limits.
Solution

Tools reuse the editor's own filter, bulk and history engines; writes take only frozen selections confirmed by their count, can be checked first with a dry-run preview tool, and run in resumable portions.

Stack
  • WP REST API
  • JSON-RPC 2.0
  • Model Context Protocol
  • wp_options
Tasks · 6
A misread filter must not widen a write to the whole catalogWrites take only a frozen selection_id plus confirm_count equal to its size

woobe_find_products runs the filter once and stores the IDs in a non-autoloaded option (up to 200,000 IDs, valid 24 hours). The answer has selection_id, count, a sample and whole_catalogue, measured against an unfiltered query, so a filter that narrows nothing is reported as such.

woobe_apply_bulk accepts no filter at all. It refuses unless confirm_count equals the selection size, so an agent that did not read the number cannot guess it. Products created after the find are not in the selection.

An agent's filter must be refused, not ignored, when the engine cannot apply itStrict validator refuses any key or shape the engine would not apply, and says why

The filter engine reads the fields of the editor's own form and ignores any other key. An agent can send a misspelt key or a date as {from, to}, and an ignored condition would widen the selection. WOOBE_MCP_FILTER checks every key against the field list the engine reads, refuses with the key named and the accepted shape, and hands the rest over in the form the screen sends.

The engine attaches posts_where hooks that stay for the rest of the request, which is one page load on the screen but can be several finds in one JSON-RPC batch. release_filter_hooks() removes them after each MCP query, so one find never inherits another's conditions. Argument types are checked against each tool's inputSchema, so the string 'maybe' is no confirmation.

The preview must show what the write will really produceDry run mirrors the bulk arithmetic and chains a run's operations in order

simulate() copies _process_number_data() literally, and run_chain() passes each result to the next operation, so a sale price set together with a new regular price is checked against the new one. Arithmetic is applied only to the numeric fields the engine calculates on; any other field is shown as a plain replacement.

The preview returns three counts (selection, affected after expanding parents to variations, writable) and warnings: percent of an empty field skipped, variable parents without a price of their own, stock not tracked.

A REST call has the same time limit as an AJAX batchChunks of 25 through do_bulk, 20 s per call, resumed by bulk_key and next_offset

The apply loop slices the queue into chunks of 25 and calls WOOBE_BULK::do_bulk(), the engine the screen uses, until 20 seconds have passed. It returns next_offset and bulk_key; a continuation writes into the same History head, so one rollback undoes the whole job. Caches are cleared once after the writes, not per chunk.

In the free build the chunk shrinks to the remaining write allowance: a token bucket of 100 products refilling continuously over 30 minutes, with fractional tokens and a hard ceiling, so idle time cannot pay for a large run.

Variations carry none of the terms or meta the filter matches onChildren added after the query: all, or only those matching every named attribute

include_variations 'all' adds every child of the matched parents; 'matching' keeps children whose attribute values match all pa_ terms of the filter ('yellow and small' is one variation, not the union), with an empty value counting as 'any'. Categories and tags cannot narrow children.

Attribute keys are built with sanitize_title(), as WooCommerce builds them, because non-Latin attribute names are stored URL-encoded and sanitize_key() would strip them. In matching mode variable parents are removed from the selection so a later variations_only does not expand them back to all variations.

Inside a REST call the plugin and the shop behave unlike the adminOwn boot on rest_api_init, currency price filters stripped, parent terms filled in

WOOBE::init() runs on init, before REST_REQUEST is defined, so on a REST call it sees neither admin nor REST and would do nothing. bootstrap.php hooks rest_api_init itself and brings the plugin up; is_should_init() allows REST only for the plugin's own routes, not the whole site's REST traffic.

Currency switchers filter price getters, so the agent read converted prices; strip_price_filters() removes those filters for the MCP request only. A product put into a subcategory is also put into its ancestors through a set_object_terms listener, since nobody ticks parent boxes in a chat.

Access control and security

shipped · 2026

What it givesOnly the right people can change products, with the optional second factor switched on, a leaked AI access key alone does not open the shop, and access can be limited per area.

Problem
A bulk editor holds write access to every product. Each AJAX handler, the history and the MCP endpoint must check who acts, and an MCP key kept in an assistant's settings can leak.
Solution

One guard for all AJAX handlers with per-product ownership checks; for MCP, header-only keys compared in constant time, a confirmed session token (optional for the shop-wide key, always on for personal keys), personal keys and a per-sector permission map.

Stack
  • WordPress nonces
  • current_user_can / meta capabilities
  • hash_equals
  • random_int
  • WP REST permission_callback
Tasks · 5
Dozens of AJAX handlers each had their own partial checksSingle guard: nonce, capability, then edit_post per product, via parent for variations

After a WPScan report the whole set of AJAX handlers was audited, not only the reported ones, and every handler was moved to WOOBE_HELPER::check_ajax_access(). It checks the nonce, then the capability, then current_user_can('edit_post') for each product ID.

Variations are checked against the parent product: WooCommerce maps edit_post on a variation to plain edit_products, which ignores ownership, and variations have post_author 0. get_request_array() rejects a scalar where an array is expected instead of casting it. Non-admin roles are further limited per field.

A static API key leaks through URLs, logs and timingKey in headers only, hash_equals, every request gated, no route while MCP is off

With MCP off, register_routes() returns before registering anything, so the endpoint does not exist. permission() requires the key on every request, including the MCP handshake, so an unauthenticated caller learns nothing about the endpoint. The key is read from Authorization: Bearer or X-WOOBE-KEY, with the Apache REDIRECT_HTTP_AUTHORIZATION fallback, and compared with hash_equals.

Refusals use 403, not 401: in MCP a 401 starts OAuth discovery and the client reports the server as broken. Download and upload links use their own single-purpose tokens because a browser click cannot send a header.

A leaked key alone should not give access to the shopSecond factor: a session token confirmed in WordPress, stored hashed, 1 h idle expiry

The assistant asks for a token, made of four groups of eight letters and digits from random_int, and it is confirmed by a person logged into WordPress: an administrator for the shop-wide key, the key's owner in his own settings for a personal key. For the shop-wide key this second factor is a setting; for personal keys it is always on. Only its sha256 hash is stored with the confirmation and last-use times. check_connection() compares with hash_equals and drops the connection after 3600 seconds without a call; the last-use time is written at most once a minute.

Connections are per owner: a token confirmed for one personal key is refused with any other key.

Per-user keys must not reveal which users have accessStored key always read and compared, with an equal-length dummy when none exists

A personal key is woobe_<40 hex>-<user id>. personal_key_owner() always reads and compares the stored key with hash_equals, using a dummy of the same length when the user has none, and asks the grant list and capability only after a full match, so response time does not tell a stranger who has access. IDs with leading zeros take the dummy path.

The request then runs as that user through wp_set_current_user(), so his role and field visibility apply exactly as on the screen, and the two-factor connection is always on for personal keys.

Owners need read-only or no access per area of the shopSector map full/read/none per user, narrowed by WP capabilities; closed tools hidden

Every tool declares a sector and annotations.readOnlyHint. The owner returns a map from the woobe_mcp_permissions filter; '*' sets the default, so it works as an allow-list. Invalid entries are logged and ignored, and an invalid '*' falls back to full, the same as no callback. For a personal key each sector is narrowed again by the capability WooCommerce asks for on that screen (edit_products, edit_shop_orders, view_woocommerce_reports and others).

Tools the identity cannot call are not listed at all. A write that lands in another sector, such as a rollback rewriting products, asks require_access() for that sector before writing. The woobe_mcp_tool_allowed filter can only narrow further. No MCP key, shop-wide or personal, may write post_author or ID; those are edited in wp-admin by a person.

Numbers

wordpress.org
  • Active installs 30,000+ wordpress.org counter.
  • Reviews 227 Average 4.7 out of 5.
  • Years supported 8 Since 7 May 2018, last update 13 October 2026.