This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.2.0 instead.
Reason given by maintainers: Broken on a fresh install (selectolax 1.0). Use 0.1.2 or later.
au-shopping-mcp
MCP server for shopping at Australian online stores: product search, full product detail,
category browsing and price comparison across 26 sites, from Bunnings, JB Hi-Fi, Officeworks and
Costco to Chemist Warehouse and Supercheap Auto, plus staticice (an Australian price-comparison
site) and aliexpress (the one global marketplace). One process can serve every site
(SITES=all) or a chosen few (SITES=bunnings,costco); tools get the site's prefix
(bunnings_search_products, …), so tool names are the same however the sites are grouped.
It is polite by design: per-site rate limits and caching, public pages only, and member logins
only through a real browser window you sign in to yourself (au-shopping-mcp login <site>).
Retailers change their pages; when an adapter breaks, <site>_probe_page shows what moved.
Check each retailer's terms before relying on it.
Install
uvx au-shopping-mcp --stdio # or: pip install au-shopping-mcp
uvx --from au-shopping-mcp playwright install chromium # only for sites that need the browser tier
Claude Desktop / Claude Code (stdio):
{"mcpServers": {"au-shopping": {"command": "uvx", "args": ["au-shopping-mcp", "--stdio"],
"env": {"SITES": "all"}}}}
Tools (per site)
| Tool | What it does |
|---|---|
<site>_search_products |
Keyword search → shortlist (name, brand, price, rating, id, URL), paginated |
<site>_get_product |
Full detail for one item by id or URL, incl. specs and site-specific extras |
<site>_browse_category |
Products in a category path/URL, same shape as search |
<site>_list_categories |
Category names and paths |
<site>_fetch_page |
Escape hatch: readable text + links (or raw HTML) for any page on the site |
<site>_probe_page |
Diagnostic: which structured-data sources a page exposes |
<site>_site_status |
Fetch tier, last block, request/cache counters, config |
URL-taking tools (get_product, browse_category, list_categories, fetch_page, probe_page) accept ids, site-relative paths, or absolute URLs on the adapter's own domain (and subdomains) only; any other host returns {"error": "offsite_url"}. This stops a prompt-injected agent from using the scraper as a generic fetcher (data exfiltration via query strings, SSRF into the LAN).
All read-only. Errors come back as {"error": ..., "reason": ..., "guidance": ...}.
Architecture
src/au_shopping_mcp/
server.py generic tool surface, bearer auth, request log
adapters/base.py SiteAdapter contract - server.py only talks to this
adapters/bunnings.py Bunnings: URLs, endpoints, extraction order, store scoping
adapters/_template.py copy this for a new site
core/fetch.py escalation ladder HTTP -> BROWSER (Playwright) -> STEALTH (persistent profile)
core/extract.py JSON-LD, __NEXT_DATA__, window state, selectors, links, readable text
core/politeness.py per-domain rate limiter + diskcache
core/config.py env settings, JSON logs to stderr (Loki-friendly)
healthcheck.py loopback liveness probe
Fetch ladder. Each page starts at the lowest tier that last worked for the domain. A block
(403/429/503, Cloudflare markers, cf-mitigated, or a JS shell missing browser_wait_selector)
escalates one tier and retries. After 30 quiet minutes the tier decays back down. MAX_TIER caps it
(MAX_TIER=1 = httpx only, no Chromium). Cookies and the stealth browser profile persist in
DATA_DIR so a Cloudflare clearance survives restarts.
Extraction order. Hydration state → JSON-LD → CSS selectors. Selectors live in one dict per
adapter. Tier 0 (a site's own JSON endpoint) is fetcher.fetch_json(); an adapter opts into it
once you've found the endpoint in DevTools.
Bunnings (verified 2026-09-06). Every page is a fully server-rendered Next.js page whose
__NEXT_DATA__ carries a react-query cache (props.pageProps.dehydratedState.queries). Listings
come from the coveo-search / category-search entry (results[].raw = Coveo product document,
totalCount, 36 per page); product pages from retail-product (catalogue), product-retail-price
(store price), product-fulfilment (store stock, click & collect, delivery) and
product-aisle-item-location. Bunnings emits no JSON-LD. Plain HTTP (tier 1) is enough — the
Chromium tiers are never reached in normal use.
AliExpress (verified 2026-09-21). Listings are server-rendered, but not into any standard
container: the whole result set is a JS object literal window._dida_config_._init_data_= { data: {...} } whose data: key is unquoted, so it is brace-matched with
json.JSONDecoder().raw_decode (extract.window_state can't read it). Items are
data.root.fields.mods.itemList.content[] (60/page) with pageInfo.totalResults and
searchRefineFilters for facets; search URL is /w/wholesale-<query>.html?page=&SortType=
(SortType ∈ total_tranpro_desc, price_asc, price_desc), with selectedSwitches,
shpf_co and pr for filters. Product pages are client-rendered (window.runParams is empty,
no JSON-LD), so get_product renders the page and extracts from the data-pl anchors
(product-title, product-reviewer, product-specs) plus module class prefixes
(price-default--current--<hash> — the hash rotates on every deploy, the prefix doesn't).
Currency, ship-to region and language are the aep_usuc_f cookie
(ALIEXPRESS_CURRENCY/_REGION/_LOCALE, default AUD/AU/en_US); without it the site
prices in whatever the exit IP geolocates to. aliexpress_site_status.locale.effective_currency
shows what the site actually applied. list_categories is unsupported on purpose (the legacy
/category/<id> pages return zero results) — browse_category takes a listing URL instead.
Cutter Electronics (verified 2026-09-21). WooCommerce with the Store API left open, so
this is a tier-0 JSON adapter with no HTML parsing: /wp-json/wc/store/v1/products?search=&page=
for listings, products/<id> (or ?slug=) for detail, products/categories for the tree.
Two traps: paging totals are in the x-wp-total / x-wp-totalpages headers, not the body,
and prices are integer minor units scaled by prices.currency_minor_unit ("5295" → $52.95).
orderby is a fixed WooCommerce enum — anything else is a 400. Category filtering needs the
numeric term id and the categories endpoint ignores ?slug=, so slugs/names are resolved against
the cached full term list. WooCommerce search is literal (title/SKU), so a generic word can return
nothing while the matching category is full — browse_category accepts a category name.
Configuration
| Env | Default | Purpose |
|---|---|---|
SITES |
(unset) | Comma-separated adapter modules under adapters/, or all. One process serves them all, sharing one Playwright browser and stealth profile |
SITE |
bunnings |
Single adapter, used only when SITES is unset (the older one-process-per-site form) |
ALIEXPRESS_CURRENCY / ALIEXPRESS_REGION / ALIEXPRESS_LOCALE |
AUD / AU / en_US |
AliExpress pricing currency, ship-to region and language (the aep_usuc_f cookie) |
PORT |
8347 |
Streamable-HTTP listen port |
MCP_BEARER_TOKEN |
(required for HTTP) | Bearer token; server refuses to start without it |
DATA_DIR |
~/.local/state/au-shopping-mcp (/data in container) |
Cache, cookie jars, browser profile |
MAX_TIER |
3 |
Ceiling on the fetch ladder |
LOG_LEVEL |
INFO |
|
BUNNINGS_STORE_ID |
(unset) | 4-digit store code to scope prices/stock to, e.g. 7313 (Alexandria NSW). It's the name field on bunnings.com.au/stores/<state>/<suburb> |
BUNNINGS_REGION |
(unset) | Pricing region for that store, e.g. NSWMetro. Required together with the store id — the site ignores a bare storeId cookie and falls back to its default (6400 Mentone, VICMetro). <site>_site_status shows the store the site actually applied |
Running
uv sync --extra test # .python-version pins 3.12 (playwright 1.47 / greenlet 3.0 don't build on 3.14)
uv run playwright install chromium # only needed if the fetch ladder ever reaches tiers 2-3
uv run pytest -q
SITES=all uv run au-shopping-mcp --stdio # local, stdio, every site
SITE=bunnings uv run au-shopping-mcp --stdio # one site only
MCP_BEARER_TOKEN=… SITES=all uv run au-shopping-mcp # HTTP on :8347
Client registration (stdio, Claude Desktop / Claude Code): one server, uv run --env-file .env au-shopping-mcp --stdio, with the env file holding SITES=all plus the site knobs (BUNNINGS_STORE_ID, BUNNINGS_REGION,
ALIEXPRESS_*). Knob names are site-prefixed, so they never clash. Registering one server per
site (SITE=<site> in each env file) still works, but costs a process per site per client
session.
Container: build from Containerfile; run with --shm-size=512m and a volume on /data.
MCP clients cache tool schemas, so restart them after an upgrade that changes tools.
Bunnings: what was verified (2026-09-06)
The original PROBE guesses were resolved against the live site:
- Search URL
/search/products?q=<q>&page=<n>&sort=<Key>; category/products/<path>?page=<n>&sort=<Key>. Sort keys:BoostOrder(relevance, default),PriceAscending,PriceDescending,RatingDescending,RatingAscending,NameAscending,NameDescending— exposed asrelevance,price_asc,price_desc,rating_desc,rating_asc,name,name_desc. - Product URL
/<slug>_p<itemNumber>;/_p<itemNumber>returns 200 with the canonical page. - Store scoping = cookies
storeId+regionCode(both). Set viaBUNNINGS_STORE_ID/BUNNINGS_REGION. The fetcher puts them in the cookie jar / browser context (a rawCookie:header gets clobbered by httpx's jar). - Zero-result searches return an empty
itemslist (not an error); a page with no result set at all is anextractionerror pointing atprobe_page. tests/fixtures/bunnings/{search,product}.htmlare real saved pages;tests/test_bunnings_hydration.pycovers the parsing with synthetic pages of the same shape. Re-save the fixtures when the site changes.
If the site changes, run bunnings_probe_page on a search and a product URL and compare
next_data.page_props_keys and the react-query keys against the list above.
Adding a site
Copy adapters/_template.py to adapters/<site>.py, fill the PROBE items using
<site>_probe_page, run it alone with SITE=<site> (or add it to SITES), save fixtures, write tests. With
SITES=all a new adapter appears in every client after its next restart.
Less friendly sites: raise start_tier to BROWSER, lengthen min_delay_s, and consider a
FlareSolverr sidecar as a tier-4 hook in fetch.py if STEALTH isn't enough.
Notes
- Defaults are ~1 req/s, concurrency 2, everything cached. This is for personal lookups, not bulk crawling — check a site's terms before scaling any of that up.
Metadata
Release files for au-shopping-mcp 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| au_shopping_mcp-0.1.1.tar.gz | 1.8 MB | Without provenance | Details for au_shopping_mcp-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| au_shopping_mcp-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for au_shopping_mcp-0.1.1-py3-none-any.whl |
Total release size: 1.9 MB
Release files / au_shopping_mcp-0.1.1.tar.gz
| Download URL | au_shopping_mcp-0.1.1.tar.gz |
|---|---|
| Size | 1.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
acfb95e137bd6048e766ecfd511d005662bc37490ec75794e310ca7f5fa3f7ec
|
|
BLAKE2b-256 checksum How to use checksums |
d3b0c702f7105682740e1855087ef04bab982bddc7fb5bca4cce0dc67c831e09
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / au_shopping_mcp-0.1.1-py3-none-any.whl
| Download URL | au_shopping_mcp-0.1.1-py3-none-any.whl |
|---|---|
| Size | 96.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eb4fcb2a7b162104aa2a2bf2d52789bc816989b12f04c303c003cf52c8d275e1
|
|
BLAKE2b-256 checksum How to use checksums |
e0f81a1d2d9504b4e545bbbbfb51465f3c20dc06635e51bde3489fb2e1ac4484
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for au-shopping-mcp 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.