Skip to main content
Glama
README.md
# mcp-alza

A remote MCP connector for [Alza.cz](https://www.alza.cz): the community
[alza-mcp-community](https://github.com/lukabudik/alza-mcp-community) server
behind GitHub OAuth and a tool allowlist, served over Streamable HTTP so it
works as a custom connector in claude.ai — including on the phone.

Unofficial; not affiliated with or endorsed by Alza.cz a.s.

## What an agent can and cannot do

The line is: **browse, fill the basket, watch prices, read orders.** Nothing
that spends money, changes the account, or publishes something under your
name — those stay in the Alza app, where you do them by hand.

| Allowed | Not available |
|---|---|
| Catalog: search, product detail, reviews, comparison, alternatives, deals, categories, pickup points | Placing, paying or cancelling an order |
| Basket: view, add, coupons, gifts, services, pickup point, checkout preview | Account changes: password, 2FA, e-mail, phone, addresses, account deletion |
| Shopping lists | Reviews, discussion posts, feedback (published under your name) |
| Price and stock watchdogs ("Hlídací pes") | Signing in to another account (`auth_start` / `auth_exchange`; only the gateway's own `/alza/login` page calls them) |
| Order and claim history (read-only) | `set_toolset`, the raw `mobile_read` escape hatch |
| PC builder | GDPR export, country / ISIC changes, chat |

The full list is [`src/alza_gateway/policy.py`](src/alza_gateway/policy.py).
It is enforced twice:

1. **The gateway** filters `tools/list` and refuses any call outside the list,
   and for `mutate_list` / `prepare_mutation` also any `action` outside it.
2. **The Alza server** gets the same lists as `ALZA_TOOLS_ALLOW` and
   `ALZA_MUTATION_ACTIONS_ALLOW` (added in the
   alza-mcp-community ≥ 0.6.0):
   other tools are removed, not just hidden, and other mutations never get the
   one-time confirmation token every guarded write needs. The one exception is
   `auth_start` / `auth_exchange`: the Alza server keeps them for the gateway's
   sign-in page, so for those two the gateway's filter is the only layer.

Either layer alone is enough; both together survive a bug in one of them.

The basket is still a write. Set the connector in claude.ai to ask before
running write tools (`add_to_cart`, `mutate_list`, `watchdog_set`, …).

## How it is put together

```
claude.ai ──HTTPS──▶ FastMCP gateway ──stdio──▶ alza-mcp-community (Node)
                     GitHub OAuth                 headless Chromium (catalog)
                     login allowlist              curl_cffi sidecar (account API)
                     tool policy                  /data/tokens.json
```

The Alza server runs as **one long-lived child process** with one MCP session.
A proxy normally opens a fresh backend session per request, which here would
restart the Alza server each time and lose what it keeps in memory: the
signed-in user and the one-time tokens `prepare_mutation` issues for the next
call. The single session is shared by every client, which is fine for a
one-person connector.

GitHub is only the login screen: claude.ai needs OAuth for a remote connector,
and the app requests the `user` scope only. Your `login` is compared against
`ALLOWED_GITHUB_LOGINS`; an empty list refuses to start.

## Setup

1. **GitHub OAuth app** (Settings → Developer settings → OAuth Apps), callback
   URL `<BASE_URL>/auth/callback`.
2. **Configure and start** the container (see below), with `/data` on a volume.
3. **Sign in to Alza once** at `<BASE_URL>/alza/login?token=<SETUP_TOKEN>`.
   Before you click the sign-in link, open the browser's developer tools
   (Chrome: Network, with *Preserve log*). Sign in to Alza; the page ends on an
   `alza://identity?code=…` address the browser cannot open — that is
   expected. Copy it from the Console error or from the last redirect's
   `Location` header, paste it into the form, and submit. Your password goes
   to Alza only. The running Alza server exchanges the code (over the same
   transport as its other account requests) and, with `ALZA_PERSIST_SIGNIN=1`,
   which the gateway sets, writes the tokens to `/data/tokens.json` and keeps
   them refreshed.
4. **Add the connector** in claude.ai: Settings → Connectors → Add custom
   connector → `<BASE_URL>/mcp`.

Without step 3 the catalog works and the basket, watchdogs and orders do not;
`account_status` tells which.

## Configuration

| Variable | |
|---|---|
| `BASE_URL` | Public URL, no trailing slash. |
| `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` | The GitHub OAuth app. |
| `ALLOWED_GITHUB_LOGINS` | Comma-separated GitHub logins allowed in. |
| `JWT_SIGNING_KEY` | Stable key for the tokens issued to clients; without it every restart signs them out. `python -c "import secrets; print(secrets.token_urlsafe(48))"` |
| `SETUP_TOKEN` | Guards `/alza/login`. Any long random string; empty disables the page. |
| `ALZA_BASE_URL` | Storefront, default `https://www.alza.cz`. |
| `ALZA_PROXY_URL` | Optional proxy for Alza traffic, if Cloudflare challenges the host's IP. |
| `DATA_DIR` | Default `/data`: `tokens.json` (Alza) and `fastmcp/` (client registrations). |

Give the container `shm_size: 512m`; Chromium crashes with Docker's default
64 MB `/dev/shm`.

## Notes

- **Cloudflare.** Alza's bot protection sometimes challenges the headless
  browser that reads the catalog (`search_products` then reports a Cloudflare
  challenge, not an empty result). The account stack goes through the curl_cffi
  sidecar and is rarely affected. If it persists, set `ALZA_PROXY_URL`.
- **Upstream version:** the image clones `ALZA_REPO` at `ALZA_REF` (build
  args), pinned to a release tag. Raise it deliberately after reading the
  upstream changelog.

## Tests

```bash
pip install -e '.[test]'
pytest                                   # policy only
ALZA_ENTRYPOINT=../alza-mcp-community/dist/index.js pytest   # + end-to-end through the real server
```

## Licence

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues