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

shop-mcp lets AI agents browse products, look up orders and place orders through five MCP tools. Clients connect to `/mcp` over Streamable HTTP with their own API keys. Order creation reserves stock in a transaction; both write tools accept idempotency keys for retries.

The server uses strict TypeScript, the official MCP SDK, Express, PostgreSQL with Prisma, Redis, Zod, pino and prom-client.

## 60-second quickstart

Start Docker Desktop or Docker Engine with Compose, then run:

```sh
docker compose up --build -d --wait
docker compose exec -T server npm run --silent key:create -- --scope read_write
curl http://localhost:3000/healthz
```

The first build may take longer than a minute. Startup applies migrations and fills a fresh database with 40 products in five categories and 200 orders from the last 90 days. Restarts keep the existing data. Prices are decimal strings in USD.

The key command prints your key once. Save it; Postgres stores only its SHA-256 hash.

Compose works with local defaults. To change them, copy `.env.example` to `.env` and edit it. In PowerShell, use `Copy-Item .env.example .env`. The example's `DATABASE_URL` and `REDIS_URL` connect from your machine; Compose supplies its own URLs using service names.

You can also create a read-only key or set a customer's quota:

```sh
docker compose exec -T server npm run --silent key:create -- --scope read
docker compose exec -T server npm run --silent key:create -- --scope read_write --limit 120
```

## Connect Claude Code

Replace `PASTE_KEY_HERE` with your key:

```sh
claude mcp add --transport http shop http://localhost:3000/mcp --header "Authorization: Bearer PASTE_KEY_HERE"
```

Or add this block to your private `.mcp.json`, which this repo ignores:

```json
{
  "mcpServers": {
    "shop": {
      "type": "http",
      "url": "http://localhost:3000/mcp",
      "headers": { "Authorization": "Bearer ${SHOP_API_KEY}" }
    }
  }
}
```

Set `SHOP_API_KEY` in your shell before starting Claude Code, then check the connection with `/mcp`.

To try all five tools, ask Claude to list products and create an order for one item in stock. Give it a future delivery timestamp and an idempotency key. Then ask it to get the order, search orders and mark the order `paid`. Repeat the create call with the same arguments and key: it should return the same order.

## Connect Claude Desktop

`mcp-remote` forwards Claude Desktop's local MCP traffic to the HTTP server. Install Node.js 20.19+ on the client machine, add this block to `claude_desktop_config.json`, and restart Desktop:

```json
{
  "mcpServers": {
    "shop": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "http://localhost:3000/mcp",
        "--allow-http",
        "--transport", "http-only",
        "--header", "Authorization:${SHOP_AUTH_HEADER}"
      ],
      "env": { "SHOP_AUTH_HEADER": "Bearer PASTE_KEY_HERE" }
    }
  }
}
```

If Desktop cannot launch `npx` on Windows, set `"command": "cmd"` and prepend `"/c", "npx"` to `args`. Keep the header argument as `Authorization:${SHOP_AUTH_HEADER}`, without a space after the colon, so Windows passes it correctly.

The config lives at `%APPDATA%\Claude\claude_desktop_config.json` on Windows and `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS. It contains your key, so keep it out of Git. For managed customer installations, review and pin the bridge version.

Connection options are documented in [Claude Code MCP](https://code.claude.com/docs/en/mcp) and the [mcp-remote README](https://github.com/punkpeye/mcp-remote).

## Verify the running server

These commands create a key and run the smoke test against the container:

```sh
export SHOP_API_KEY="$(docker compose exec -T server npm run --silent key:create -- --scope read_write)"
docker compose exec -T -e SHOP_API_KEY server npm run smoke
curl http://localhost:3000/metrics
docker compose logs server
```

PowerShell equivalent:

```powershell
$env:SHOP_API_KEY = docker compose exec -T server npm run --silent key:create -- --scope read_write
docker compose exec -T -e SHOP_API_KEY server npm run smoke
Invoke-RestMethod http://localhost:3000/healthz
Invoke-WebRequest http://localhost:3000/metrics
```

The smoke test calls all five tools through the SDK HTTP client, repeats a write and checks Prometheus metrics. It leaves one paid order in the demo database. A different response on the repeated call fails the test; the integration suite also checks row counts and stock to confirm that the retry did not write twice.

## Results and errors

The [tool reference](docs/tools.md) lists every argument, result shape and error code. Strict Zod schemas validate inputs. Each result carries a typed JSON object in `structuredContent` and a JSON copy in `content` for older clients. Output schemas cover both successful results and errors with `code`, `message` and optional `details`.

Invalid arguments and rejected shop operations return MCP `isError: true`. Authentication, rate-limit, session and protocol failures return an HTTP error. Its JSON-RPC `error.data` holds the same error object. Client errors omit stack traces, SQL and internal identifiers.

Creating an order reserves stock. Status updates allow exactly these transitions:

```text
created -> paid -> packed -> shipped -> delivered
created -> cancelled
paid    -> refunded
```

Every other transition is rejected, including an update to the current status. Cancellation and refund update the status only; they do not restock items or process payments.

## Keys and rate limits

Each request to `/mcp` checks the Bearer key against its hash in Postgres, even within an existing session. A `read` key can call the three read tools; `read_write` permits all five. To revoke a key, set its `revokedAt` to the current time through a trusted database admin connection.

Missing credentials return `missing_api_key`. Unknown and revoked keys share `invalid_api_key` so the response does not reveal whether a key exists. Server logs distinguish them with `auth_reason`. The creation command prints only the key, without its database id or hash.

The default rate limit is 60 HTTP requests per minute with a burst of 10. Handshakes, notifications and session termination count toward it. Each key's `rateLimitPerMinute` column sets its quota. Redis updates the token bucket in one Lua script using Redis server time; a `429` response includes `Retry-After` and `details.retry_after_seconds`.

## Retrying writes

Both write tools accept `idempotency_key`. Reuse it with the same arguments to get the stored response; changing the arguments returns `idempotency_conflict`. Omitting it allows a retry to write again. Reusing it with another API key or tool is a separate operation.

After validation, the server hashes a canonical form of the arguments, excluding `idempotency_key`. Defaults are applied before hashing, and array order matters. Redis stores the tool, argument hash, response and expiry for 24 hours. A retry keeps the original expiry. Once that window ends, another call can write again. Failed shop operations roll back and are not cached.

There is a gap between committing an order in Postgres and saving its response in Redis. The server commits a `WriteReceipt` in the same transaction as the order, protected by a transaction-scoped advisory lock. If the process dies in that gap, or Redis loses its cache, a retry can recover the response from Postgres. Receipts share the 24-hour expiry and are removed by periodic cleanup.

If Redis is unavailable, MCP requests return `503 dependency_unavailable`, including writes: rate limiting fails closed. A dependency failure after a Postgres commit can still leave a completed write behind an error response. Retry with the original idempotency key to recover its result.

## Logs and health

`/healthz` reports Postgres and Redis separately and remains available when either service fails. Request logs go to stderr as JSON with `request_id`, `key_id`, `tool`, `duration_ms` and `outcome`. Authentication failures add `auth_reason`. Logs omit request bodies, customer details, credentials and raw idempotency keys.

`/metrics` exposes these Prometheus metrics:

- `shop_mcp_requests_total{tool,outcome}`
- `shop_mcp_request_duration_seconds{tool}`
- `shop_mcp_rate_limit_rejections_total`

## Sessions and public hosting

Each session has its own SDK server and transport, bound to the client's key. Session ids are random. By default, an idle session expires after 30 minutes and the server accepts up to 1,000 sessions. Responses use JSON; standalone GET/SSE requests receive `405`, as allowed by [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports). There is no resumable event store. A restart invalidates sessions, so clients must initialize again. Run one instance, or use sticky routing for replicas.

To host it publicly, point a DNS name and a TLS reverse proxy at port 3000. Replace the local database credentials and set `ALLOWED_ORIGINS` to exact trusted browser origins. Native MCP clients can connect without an `Origin` header; browser requests from unlisted origins are rejected.

Expose `/mcp` and use proxy route rules to keep `/metrics` and `/healthz` on an operator network. Compose binds Postgres and Redis ports to loopback. Keep both named volumes and back up Postgres. You will need to provision the public hostname and TLS certificate.

## Development and tests

Use Node.js 20.19+; the Docker image runs Node 22. Prisma 6 is pinned. Dependency overrides select patched releases of `deepmerge-ts` and Vitest's worker dependency.

```sh
npm ci
docker compose up -d postgres redis
# Copy .env.example to .env to connect from your machine.
npm run db:generate
npm run db:migrate
npm run db:seed
npm run check
npm test
npm run test:integration
npm run build
npm start
```

`npm test` checks all 49 pairs of order states. The integration suite uses real Postgres, Redis and SDK HTTP clients. It checks scopes, authentication and revocation, session isolation, idempotency conflicts and recovery, concurrent stock reservations, rate-limit recovery and dependency failures. Tests create their own fixture rows and remove them afterward. Run them against a development database. CI runs both suites and builds the image.

`shop-mcp-stdio` is a second binary for local debugging only. After a build, set `SHOP_API_KEY`, `DATABASE_URL` and `REDIS_URL`, then run `npm run start:stdio`. It uses the same tools and key checks. Production clients connect over HTTP. The project has no REST commerce endpoints, payment integrations or OAuth flows.

License: [MIT](LICENSE).