sixty60-mcp
# sixty60-mcp
MCP server for Checkers Sixty60: log in, search, build the real cart, and preview checkout totals.
Paying and placing the order happens in the Sixty60 app — the cart built here is the same cart the app shows.
Built on the private Sixty60 mobile-app API, via the API layer of
[checkers60-cli](https://github.com/yashiels/checkers60-cli) (MIT, vendored in `src/vendor/checkers60`
from commit `1e4c5d1`). The API is undocumented and can change or break without notice.
## Hosted: https://mcp.skoonveld.app
A Cloudflare Worker serves the MCP endpoint at `https://mcp.skoonveld.app/mcp` and the setup page at
`https://mcp.skoonveld.app`.
- **Sign-in:** OAuth 2.1 through `@cloudflare/workers-oauth-provider`, using dynamic client registration and
client ID metadata documents. The connect page signs you in with your Sixty60 mobile number and SMS code.
SMS sends are rate-limited per number and per IP address.
- **Per-user state:** each Sixty60 account has its own Durable Object (`Sixty60Account`). It holds that account's
session, device id and stores, and runs the account's tool calls. The OAuth grant carries only the account id
and mobile number.
- **Expiry:** Sixty60 sessions expire after about an hour. The MCP connection stays authorised, and Claude asks
for a new SMS code through `sixty60_login_start`/`sixty60_login_verify`, limited to the connected number.
```bash
npx wrangler dev # local, http://localhost:8787
npx wrangler deploy # Longshots account, custom domain mcp.skoonveld.app
```
## Local (stdio)
```bash
npm install && npm run build
claude mcp add sixty60 -- node "$PWD/dist/local.js" # or use the bundled .mcp.json
```
Ask Claude to log in: it calls `sixty60_login_start` with the mobile number, you read back the SMS code,
and it calls `sixty60_login_verify`. State lives in `~/.config/sixty60-mcp/state.json`
(override with `SIXTY60_STATE_PATH`).
## Stores
Search, prices and stock come from the stores that serve the account's current delivery address
(set it in the app), resolved once per account. With no usable address it falls back to Potchefstroom — override with
`SIXTY60_LATITUDE` / `SIXTY60_LONGITUDE`, or pin exact stores with `CHECKERS60_STORES` (JSON array).
`sixty60_status` shows which address and stores are in use.
## Tools
| Tool | What it does |
|------|--------------|
| `sixty60_login_start` / `sixty60_login_verify` / `sixty60_logout` | SMS one-time-PIN login |
| `sixty60_status` | Session expiry, address and stores in use |
| `sixty60_search`, `sixty60_product` | Catalogue search and product details with deals |
| `sixty60_cart` | Current carts (`sixty-min` and `one-day`) |
| `sixty60_cart_add` / `sixty60_cart_remove` / `sixty60_cart_clear` | Change the real cart |
| `sixty60_checkout_preview`, `sixty60_delivery_slots` | Fees, payable total, slots, payment methods |
| `sixty60_addresses`, `sixty60_orders`, `sixty60_past_purchases` | Account reads |
Prices are in cents (ZAR).
## Layout
- `src/tools.ts`: every tool, shared by both servers
- `src/local.ts`: stdio server, one account saved in a JSON file
- `src/worker/`: hosted server (OAuth, connect pages, `Sixty60Account` Durable Object)
- `src/vendor/checkers60/`: Sixty60 API client from checkers60-cli
## Changes to the vendored code
- `context.ts` (new): per-user state (credentials, device id, stores, cart plans), scoped per call with
`AsyncLocalStorage`. A hosted isolate serves many accounts, so nothing user-specific lives in module globals.
- `config.ts`: `CONFIG` reads and writes the user-specific fields (mobile, stores, location, ids) on the
current user context.
- `creds-store.ts`, `runtime.ts`, `confirm.ts`: credentials, the device id and cart plans live in the user
context instead of files and file locks.
- `cart-mutate.ts`: `runCartMutation` returns a `CartMutationResult` instead of writing to stdout.
- Removed CLI-only modules (`output`, `address-mutate`, `favourites-mutate`, `discovery`) and terminal helpers.
TDQS
Scored across 15 tools
Each tool has a clearly distinct purpose: auth lifecycle, status, search/product lookup, cart operations (view/add/remove/clear), checkout preview, delivery slots, orders, addresses, and past purchases. The only mild overlap is between sixty60_orders and sixty60_past_purchases, but descriptions clearly differentiate active/history orders from frequency-based purchase history.
All tools share the sixty60_ prefix and snake_case, with a consistent naming feel. However, some are verb_noun (login_start, cart_add, checkout_preview) while others are bare nouns acting as getters (cart, product, status, orders, addresses), which is a minor deviation from a pure verb_noun pattern but still very readable.
15 tools is well within the ideal range and each earns its place: authentication, catalogue search, product details, cart management, checkout preview, delivery slots, orders, addresses, and purchase history. No redundant or filler tools are present.
The surface covers the full companion-app workflow: auth lifecycle, search and product lookup, cart CRUD (add/remove/clear/view), checkout preview, delivery slots, order history, addresses, and past purchases. Gaps like placing an order, switching address, and editing addresses are explicitly deferred to the Sixty60 app, so they are intentional rather than missing, though the lack of any address or cart-quantity update beyond add/remove is a minor limitation.