# @binaryandthread/mcp

Buy a Binary & Thread heavyweight piece **from inside your AI agent** (Claude, etc.). A small,
deterministic [MCP](https://modelcontextprotocol.io) server (stdio) exposing **10 read-mostly tools**
over the same shared Store API client the `bt` CLI uses, so the agent can browse, stage, and
**hand off** a purchase without ever putting a card in the chat.

> The agent can never complete a purchase or set a shipping address. The card is entered by **you**,
> on the storefront's Stripe page, in your own browser. The MCP just hands you the link.

## The 10 tools

| tool | what it does |
| --- | --- |
| `list_products` | the catalog, **text-only** (no per-item images, keeps the agent's context lean) |
| `get_product` | one piece's specs + its image (rendered **and** the Blob URL as a text floor) |
| `add_to_cart` | create-or-reuse a cart; returns the `cart_id` as the durable handle |
| `get_cart` | the authoritative server cart + total (incl. VAT) |
| `checkout` | mints `{checkout_url, session_id}` **instantly**, never polls, never opens a browser, never sets an address |
| `check_payment` | polls the session **once**; the agent re-calls at its own cadence |
| `get_order_status` | status/timeline by display id + exact email (no address; not-found on a mismatch) |
| `report_issue` | file a bug report, emailed to the maintainer, server-redacted; returns a reference |
| `teapot` | 418, a deadpan easter egg (no input) |
| `fortune` | a deterministic developer aphorism (optional `seed`) |

## Install / run

Distributed via **npx from the verified `@binaryandthread` npm namespace**, published from locked
CI with **npm 2FA (hardware key) + publish provenance** (signed releases). Always install the
scoped package; do not trust a bare `bt-mcp` binary from an unverified source (anti-impersonation,
SECURITY G11/G14/T10/T11).

Listed in the official **MCP Registry** as `com.binaryandthread/store`, under a DNS-verified
namespace we own rather than a personal GitHub account. *(Superseded 2026-09-17: this line used to
say a listing was "deferred until the API is stable". The API has been stable since 0.3.0 and the
sentence was quietly arguing against the cheapest discovery surface available to us.)*

Zero-config, the server ships pointed at the live store (the store URL + publishable key are
baked in; the key is public by design). The minimal config is just:

```jsonc
// Claude Desktop / Claude Code MCP config (claude_desktop_config.json or .mcp.json)
{
  "mcpServers": {
    "binary-and-thread": {
      "command": "npx",
      "args": ["-y", "@binaryandthread/mcp"]
    }
  }
}
```

Override any of it via `env` for local dev / staging, or to attach your customer token:

```jsonc
{
  "mcpServers": {
    "binary-and-thread": {
      "command": "npx",
      "args": ["-y", "@binaryandthread/mcp"],
      "env": {
        "BT_STORE_URL": "http://localhost:9000",  // override the baked-in prod URL (dev/staging)
        "BT_PUBLISHABLE_KEY": "pk_...",            // PUBLIC by design, never a secret key
        "BT_REGION": "se",                          // optional ISO-2 (se · us · any EU country e.g. de/fr/ie); region-resolved, never hardcoded
        "BT_TOKEN": "..."                           // optional customer JWT (own scope), from your keychain, NEVER the chat
      }
    }
  }
}
```

### Config (environment only, all optional; env overrides the baked-in prod defaults)

| var | required | notes |
| --- | --- | --- |
| `BT_STORE_URL` | no | the Medusa Store API base URL (default: production) |
| `BT_PUBLISHABLE_KEY` | no | the **publishable** key, public by design (default: production; a secret `sk_` key is rejected) |
| `BT_TOKEN` | no | the customer JWT (own data only); read from the OS keychain / a configured env, **never from the chat** |
| `BT_REGION` | no | ISO-2 country code to resolve pricing — `se` (SEK), `us` (USD), or any EU country e.g. `de`/`fr`/`ie` (EUR); never a hardcoded currency literal. Sets the **default**; `list_products` / `get_product` / `add_to_cart` also take a per-call `region` arg that overrides it (a country without shipping is refused, not priced in the wrong currency). |

### Onboarding for the agent

A cold agent doesn't have to infer the flow: the server exposes a **`getting-started` resource**
(`bt://getting-started`) describing the `list_products → get_product → add_to_cart → checkout →
check_payment` flow and the money boundary, and a **`buy` prompt** that walks the purchase (respecting
that the card + address are entered by the human in the browser, never in the chat).

## Security posture (the MCP-specific red-team gates)

- **No self-completing purchase (G8 confused-deputy):** `checkout` returns a hand-off URL + session id
  and *says so in its result text*, "I can't complete this for you." The human browser-hop is mandatory.
- **Cards never in the chat:** there is no card / CVC / PAN field anywhere. The card is entered only on
  Stripe's hosted surface.
- **Image path is SSRF / token-DoS safe (G9 / CLAUDE.md rule 6):** images are fetched **only** from the
  allowlisted Vercel Blob host (the same constant the CLI uses, in `@binaryandthread/store-client`),
  over https, with a ≤2MB byte cap + a timeout. A non-allowlisted / `data:` / `http:` URL is **never**
  fetched, it degrades to the Blob-URL text floor. List views are text-only.
- **Untrusted backend strings (G8):** every product title / lore / order note is treated as **data**,
  control-char-stripped, whitespace-collapsed, length-capped, so it can't steer the host agent.
- **No token / PII in output or logs (G10):** all output passes through the shared redactor; the JWT,
  Authorization headers, and full emails never appear. Diagnostics go to **stderr** (stdout is the MCP
  transport). `get_order_status` returns status only, never a shipping address.
- **Nothing to escalate (T1/T10):** the tools are thin, deterministic wrappers over `/store/*` only,
  no admin op, no shell, no free-form execution.

## Why both an image block *and* a URL

Claude renders MCP `ImageContent` natively on claude.ai / the API, but Claude Code has a base64-bloat
bug ([#31208](https://github.com/anthropics/claude-code/issues/31208), ~15–25k tokens/image) and can
fail to render it. So `get_product` always emits the **Blob URL as a text block too**, the reliable
floor. The buy is never blocked on rendering.

## Fits

Every design is sold in one fit today, **regular**, and needs no `fit`. Should a design ever come in two
fits (regular and oversize, priced apart), `list_products` marks it (`fits`, `price_from`), `get_product`
lists sizes and prices per fit (pass `fit` to narrow), and `add_to_cart` **requires** `fit` for it:
without it the tool returns an error naming both fits, and nothing is added. Colours and sizes follow the ship-to region:
`get_product` lists the ones you can buy where the cart ships.

`add_to_cart` by `handle` also **requires** `size` whenever the colour comes in more than one: without it
the tool returns the sizes and asks the agent to put the choice to the buyer, and nothing is added. A
`variant_id` already names its size. `get_cart` and `add_to_cart` list each line's `size` and `colour`,
and `get_cart` on a cart that was already checked out says so (`completed: true`) instead of listing it.
