hktvmall-mcp
An MCP server for HKTVmall, Hong Kong's online mall and supermarket. It lets an AI assistant (Claude, ChatGPT, Cursor, ...) search HKTVmall for a whole shopping list, compare prices and unit prices, show product cards with photos, read your order history and set quantities in your real basket. You pay yourself, on HKTVmall: the server cannot place, pay for or change an order.
Unofficial. Not affiliated with or endorsed by HKTVmall or Hong Kong Television Network Limited. HKTVmall has no public API; this server uses the same endpoints HKTVmall's own website uses, gently and only when you ask. Use it with your own account for your own shopping; HKTVmall's terms and conditions apply to you.
Requirements
- A connection from Hong Kong: HKTVmall answers connections from elsewhere with errors.
- An HKTVmall account for the basket and order history (search works without one).
- uv (it fetches Python 3.12 if needed), or Docker for a server.
- Google Chrome for signing in (or Chromium, see Signing in).
Quick start
-
Sign in once. A browser window opens on HKTVmall's own login page; log in as usual (HKTVmall's own checks included). It closes once you are signed in, and the session is saved for the server.
uvx --from "hktvmall-mcp[login]" hktvmall-mcp login
-
Add the server to your client. It runs over stdio, started by the client:
-
Claude Code:
claude mcp add hktvmall -- uvx hktvmall-mcp -
Claude Desktop, Cursor and most other clients: add to the client's MCP configuration
{ "mcpServers": { "hktvmall": { "command": "uvx", "args": ["hktvmall-mcp"] } } }
-
-
Ask. "Put milk, a dozen eggs and some bananas in my HKTVmall basket", "What did I order last month? Add the same toilet paper again", "Which of these olive oils is cheapest per 100 ml?".
These commands run the latest release from PyPI (hktvmall-mcp); write
hktvmall-mcp==X.Y.Z to stay on a release, or --from git+https://github.com/benjamin-di/hktvmall-mcp-server for the
latest code.
Tools
| Tool | What it does |
|---|---|
hktv_search(queries[1..25], zone, per_query) |
The whole shopping list in one HKTVmall request; a few lean candidates per item (id, title, title_zh, brand, pack, price, availability, seller, delivery label such as "Next Day Delivery", price_per_unit per 100 ml, 100 g, roll or piece when the pack size is unambiguous, review_count, url). max_price_cents is sent to HKTVmall as a price range. Supermarket zone by default, widened per item when it has nothing; at most 100 products per call. |
hktv_get_products(ids) |
Current price, stock, delivery, price_per_unit, review_count, summary (shelf life, packing), origin, image and url; not_found lists ids HKTVmall does not sell right now. |
hktv_show_products(items[{id, note}] 1..12, title) |
The assistant's picks as product cards (photo gallery, price, unit price, rating, delivery, seller, the assistant's note, a link), drawn by clients that support MCP Apps (claude.ai, ChatGPT); other clients get the same data as text. |
hktv_delivery_slots() |
HKTVmall's next delivery slots for your first address, the fee, free_from and the slot already chosen. You pick a slot at checkout. |
hktv_reviews(id, limit 1..5, rating) |
Public reviews: average, count, breakdown by rating and recent comments; nothing about the reviewers. |
hktv_get_cart() |
The basket as HKTVmall shows it: lines, delivery groups with fees and amount_to_free_delivery, totals (as HKTVmall's order summary computes them), checkout_url, and HKTVmall's checkout warnings in notes. |
hktv_update_cart(changes[{id, quantity}], confirmed) |
Exact quantities (0 removes); each change's outcome (applied, adjusted with the reason such as stock_limit, or rejected with the reason such as out_of_stock or store_closed) is read back from HKTVmall. Raising a line above 15 or adding over HK$1,500 in one call changes nothing (requires_buyer_review) unless the person agreed (confirmed). |
hktv_recent_orders(limit 1..50, offset, items) |
Your order history, newest first: number, date, status, total, deliveries and the items (id, title, quantity, line total). Pages back through the whole history with offset. Read-only: never the address, payment or vouchers. Re-ordering goes through hktv_update_cart, line by line. |
hktv_status(check) |
Sign-in state, today's budget, any pause and the latest unresolved error. check=true runs a cached health check against HKTVmall (for monitoring). |
Money is {"cents": 12990, "display": "HK$129.90"}. Errors are {"error": {code, next_step, retryable}}, and the
next_step text tells the assistant what to do. A call that changed anything never returns an error: it reports each
change's outcome.
Safety
Enforced in code and pinned by tests/test_safety.py:
- No money moves. No endpoint for placing, paying for, changing or cancelling an order, and none for vouchers, credit, addresses, profiles, wish lists or delivery-slot changes, exists in the code. You check out on HKTVmall.
- Only HKTVmall. Requests go only to
www.hktvmall.com, its search service and its public review service, to an allowlist of paths; redirects that leave those hosts are refused. The product cards load photos from HKTVmall's image servers only and connect nowhere. - The truth is the basket. HKTVmall's replies to basket changes are not always true (an item silently dropped, a quantity silently capped), so every change is read back and reported as HKTVmall then shows it.
- Gentle. One request at a time, at most about three a second, a daily request budget, and a pause of 5 minutes (30 when it repeats) whenever HKTVmall pushes back.
- Your account only. Each call acts for the account its connection belongs to, never one named in a tool argument. Product names, descriptions and reviews are treated as data, not instructions.
Privacy
Everything stays on the computer that runs the server, in its state folder (~/.local/state/hktvmall-mcp on Linux,
~/Library/Application Support/hktvmall-mcp on macOS, %LOCALAPPDATA%\hktvmall-mcp on Windows, /state in the
container; HKTVMALL_STATE_DIR overrides it):
| File | What it holds |
|---|---|
session.json |
Your HKTVmall sign-in (cookies) and the browser identity it was made with. Treat it like a password. |
browser/ |
The sign-in window's browser profile, so HKTVmall recognises the browser next time. |
state.json, shared.json |
Today's request budget, any pause, unresolved errors and health-check results. |
orders.json |
The item lines of completed orders already read (id, title, quantity, line total), so the history loads quickly. |
Nothing is sent anywhere except HKTVmall. Logs (stderr) never contain cookies, search terms or HKTVmall's error text;
a basket change HKTVmall did not apply as asked is logged with its product id and quantities, for diagnosis. The
server never reads your address or payment details (the delivery-slot lookup reads only the delivery zone of your
first address).
hktvmall-mcp logout deletes the session and the browser profile.
Signing in
hktvmall-mcp login opens HKTVmall's login page in your installed Google Chrome, using a separate profile in the
state folder. You do every step yourself; nothing is clicked or typed for you. When HKTVmall's own home page says you
are signed in, the session is saved and the window closes. A running server picks up the new session with its next
call.
- No Chrome? Run
patchright install chromium, thenhktvmall-mcp login --browser chromium, both the same way as in the Quick start (uvx --from "hktvmall-mcp[login]" ...). - HKTVmall decides how long a session lasts. When it ends, tools return
session_expired: sign in again. hktvmall-mcp statusshows whether the saved session is signed in, without asking HKTVmall anything.
Running it as a server
For remote clients (claude.ai, ChatGPT and the Claude and ChatGPT apps reach connectors over HTTPS) or a shared home server, run it over streamable HTTP. It refuses to start without a bearer token:
docker run -d --name hktvmall-mcp -p 127.0.0.1:8000:8000 \
-e MCP_BEARER_TOKEN="$(openssl rand -hex 32)" \
-v hktvmall-state:/state \
ghcr.io/benjamin-di/hktvmall-mcp-server:latest
or MCP_BEARER_TOKEN=... uvx hktvmall-mcp serve --port 8000.
The MCP endpoint is /mcp;
/health is open for health checks (the image's HEALTHCHECK runs hktvmall-mcp health). Put it behind your own
HTTPS reverse proxy to reach it from outside, and give clients the bearer (Authorization: Bearer <token>).
The server still needs a signed-in session in its state folder. Sign in with hktvmall-mcp login on a computer with
a screen in the same network and copy that computer's session.json into the server's state folder (the volume
above); the server adopts it on its next call. HKTVmall decides how long a session lasts; if it ends sooner on the
server, sign in again the same way.
Hosting several people (experimental)
One server can also hold other people's HKTVmall accounts, each person acting only on their own. They add the
server as a connector in Claude or ChatGPT and press Connect: the server's own OAuth sign-in asks for an invite code
you gave them, then shows them HKTVmall's real login page through a browser running next to the server
(ghcr.io/benjamin-di/hktvmall-mcp-login-browser, viewed through noVNC; nothing on the login is automated). Their
sessions are stored encrypted (HKTVMALL_SESSION_KEY). Invites and accounts are managed with
hktvmall-mcp admin invite LABEL | invites | revoke PREFIX | accounts | disconnect ID. This needs a public HTTPS
address (HKTVMALL_PUBLIC_URL) and the login browser service (HKTVMALL_LOGIN_URL, HKTVMALL_LOGIN_TOKEN). All
accounts share the server's IP address, so requests are paced and budgeted for all of them together. Wrong invite
codes are rate-limited per client address from X-Forwarded-For: the reverse proxy in front must overwrite that
header with the client's address, not append to it.
Run the login browser on a private network next to the server, never published to the internet: it listens on port
8080, refuses to start without HKTVMALL_LOGIN_TOKEN (at least 32 characters, the same value the server gets) and
holds at most HKTVMALL_LOGIN_SLOTS (2) logins at a time, each ended after HKTVMALL_LOGIN_MINUTES (15)
unfinished. The server's HKTVMALL_LOGIN_URL names it by host and port, such as http://hktvmall-mcp-login:8080.
Session snapshot (for operators)
Instead of its own session.json, the default account can read a session that another program keeps signed in and
publishes as a file: set HKTVMALL_SESSION_SNAPSHOT (an absolute path, mounted read-only) and
HKTVMALL_EXPECTED_EMAIL (the account it must belong to). The server never writes it; it adopts a file only when
its generation is greater than the one it holds, and checks HKTVmall's home page before the first basket call of
each generation. The file is JSON, at most 512 KiB:
{"schema": 1, "target": "hktvmall", "generation": 3, "email": "you@example.com",
"user_agent": "Mozilla/5.0 ...", "verified_at": "2030-01-01T00:00:00+00:00",
"cookies": [{"name": "JSESSIONID", "value": "...", "domain": "www.hktvmall.com", "path": "/",
"secure": true, "expires": null}]}
expires is Unix seconds or null; it must contain a JSESSIONID for www.hktvmall.com. Symlinks, files that are
not regular files and timestamps in the future are rejected. session.json uses the same format.
Configuration
| Variable | Default | |
|---|---|---|
MCP_BEARER_TOKEN |
required for serve |
one or more tokens (comma-separated, at least 16 characters each), all for the default account |
HKTVMALL_STATE_DIR |
per-user folder (see Privacy); /state in the image |
sessions, budgets, pause, order cache |
HKTVMALL_DAILY_BUDGET |
2000 | the default account's HKTVmall requests per day (resets 00:00 Hong Kong time) |
HKTVMALL_KEEPALIVE_SECONDS |
21600 | how often an idle session is refreshed (re-mints its token and reads the basket) |
HKTVMALL_SESSION_SNAPSHOT, HKTVMALL_EXPECTED_EMAIL |
unset | read the default account's session from a published file (above) |
HKTVMALL_SESSION_KEY |
unset | 32 random bytes, base64: enables connected accounts and encrypts their sessions |
HKTVMALL_ACCOUNT_DAILY_BUDGET |
500 | each connected account's requests per day |
HKTVMALL_TOTAL_DAILY_BUDGET |
4000 | all accounts together per day |
HKTVMALL_PUBLIC_URL |
unset | the server's public HTTPS address, for connected accounts' OAuth sign-in |
HKTVMALL_LOGIN_URL, HKTVMALL_LOGIN_TOKEN |
unset | the login browser service (http://host:port on a private network) and its bearer |
LOG_LEVEL |
INFO |
Troubleshooting
| Code | What to do |
|---|---|
not_signed_in |
Run hktvmall-mcp login. |
session_expired |
HKTVmall ended the sign-in: run hktvmall-mcp login again (connected people reconnect; a snapshot needs a newer generation). |
account_mismatch |
HKTVmall shows a different account than the saved one: sign in again. |
session_source_missing / session_source_invalid |
The session snapshot is missing, unreadable, for another account or malformed: check its publisher, mount and HKTVMALL_EXPECTED_EMAIL. A malformed session.json: sign in again. |
blocked |
HKTVmall pushed back; calls resume after the pause (hktv_status.paused_until). |
budget_exhausted |
Today's budget is spent; it resets at 00:00 Hong Kong time. |
upstream_error |
HKTVmall or the network failed; retry in a minute. From outside Hong Kong, every call fails this way. |
schema_changed |
HKTVmall changed a page or API the server reads. Check for a newer release, or open an issue. |
basket_unreadable |
HKTVmall cannot show the basket (for example after a zero-quantity line added elsewhere): open the basket in HKTVmall's app or site and remove the broken line. |
outcome_unknown |
A change was sent but could not be read back: check the basket before changing anything again. |
How it works
HKTVmall has no public API, so the server speaks to the endpoints its website uses, over plain HTTP, with the
cookies of a browser session you signed in to. docs/hktvmall-site.md records what is known
about those endpoints and their quirks; docs/DESIGN.md explains the decisions. When HKTVmall
changes its site, calls fail with schema_changed until the server is updated.
Development
See CONTRIBUTING.md. Tests never contact HKTVmall.
License
MIT.
Metadata
Release files for hktvmall-mcp 0.11.6
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 | |
|---|---|---|---|---|
| hktvmall_mcp-0.11.6.tar.gz | 289.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hktvmall_mcp-0.11.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 387.9 kB
Release files / hktvmall_mcp-0.11.6.tar.gz
| Download URL | hktvmall_mcp-0.11.6.tar.gz |
|---|---|
| Size | 289.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4828538716803a3733e16131aa152c50bd20f7430afd0209c51360717268db12
|
|
BLAKE2b-256 checksum How to use checksums |
cec901faa17f02603fef21eb159a74ca7b3a9ac35639aa75c0fb922baab2af07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","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 / hktvmall_mcp-0.11.6-py3-none-any.whl
| Download URL | hktvmall_mcp-0.11.6-py3-none-any.whl |
|---|---|
| Size | 98.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3a613d187ae98caaf8ccb66f914062041772cdc82a449dd37c068e926ffa7f24
|
|
BLAKE2b-256 checksum How to use checksums |
f2a2aede9cccc8c292514539081d448066faf3c788fda38ad1d00974d767d29b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","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 hktvmall-mcp 0.11.6
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.