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

[![npm version](https://img.shields.io/npm/v/sapo-mcp.svg)](https://www.npmjs.com/package/sapo-mcp)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Node 20+](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)

Model Context Protocol server for [Sapo.vn](https://www.sapo.vn) POS & e-commerce platform.

## Features

- **4 modes, 105 unique tools:** `pos-online` (51), `web` (31), `pos-counter` (15), `analytics` (10) — 2 read tools shared between pos-online and web
- **Two transports:** stdio (Claude Desktop, Cursor) and Streamable HTTP (Docker, GoClaw)
- **Safe by default:** destructive ops gated via `SAPO_ALLOW_OPS` (default: none); all destructive calls also require `confirm: true`
- **Single-tenant:** one shop per server instance via Private App credentials
- **Pre-1.0:** tool names and schemas may shift on minor bumps. See [Post-1.0 roadmap](#post-10-roadmap).

## Installation

Requires **Node.js 20 or newer**. Verify with `node --version`.

### Option A — npx (no install)

Recommended for MCP clients (Claude Desktop, Cursor). Always pulls the latest published version:

```bash
npx -y sapo-mcp@latest --version
```

### Option B — global install

```bash
npm install -g sapo-mcp
sapo-mcp --version
sapo-mcp --help
```

### Option C — local project dependency

```bash
npm install sapo-mcp
npx sapo-mcp --mode=pos-online
```

## Quick Start

### 1. Get Credentials

Create a Private App at [developers.sapo.vn](https://developers.sapo.vn):
- Store name: `mystorename` → `mystorename.mysapo.net`
- API Key + Secret

### 2. Configure your MCP client

#### Claude Desktop

Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%/Claude/claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "sapo": {
      "command": "npx",
      "args": ["-y", "sapo-mcp", "--mode=pos-online,web,analytics"],
      "env": {
        "SAPO_STORE": "mystorename",
        "SAPO_API_KEY": "xxx",
        "SAPO_API_SECRET": "yyy"
      }
    }
  }
}
```

Restart Claude Desktop. Tools appear under the 🔌 plug icon.

#### Cursor

Edit `~/.cursor/mcp.json` or per-workspace `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "sapo": {
      "command": "npx",
      "args": ["-y", "sapo-mcp", "--mode=pos-online,web"],
      "env": {
        "SAPO_STORE": "mystorename",
        "SAPO_API_KEY": "xxx",
        "SAPO_API_SECRET": "yyy"
      }
    }
  }
}
```

#### MCP Inspector (test/debug)

```bash
npx @modelcontextprotocol/inspector \
  -e SAPO_STORE=mystorename \
  -e SAPO_API_KEY=xxx \
  -e SAPO_API_SECRET=yyy \
  npx -y sapo-mcp --mode=pos-online
```

Open the printed URL in a browser to inspect/invoke registered tools.

Use `--mode=pos-online,web,analytics` to register multiple modes (union of tools; shared tools registered once).

### 3. CLI Flags

| Flag | Default | Description |
|------|---------|-------------|
| `--mode=<modes>` | `pos-online` | Comma-separated list of modes to activate |
| `--transport=<t>` | `stdio` | Transport type (`stdio` or `http`) |
| `--port=<port>` | `3333` | HTTP port (ignored for stdio) |
| `--help` | — | Print usage and exit |
| `--version` | — | Print version and exit |

## Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `SAPO_STORE` | Yes | — | Store subdomain |
| `SAPO_API_KEY` | Yes | — | Private App API Key |
| `SAPO_API_SECRET` | Yes* | — | Private App API Secret |
| `SAPO_API_SECRET_FILE` | Yes* | — | Path to file containing secret (takes precedence) |
| `SAPO_ALLOW_OPS` | No | `""` | CSV of allowed destructive categories |
| `SAPO_MAX_AUTO_PAGES` | No | `10` | Max auto-pagination pages |
| `SAPO_RETRY_MAX` | No | `3` | HTTP retry attempts |
| `SAPO_LOG_LEVEL` | No | `info` | Log level (error/warn/info/debug/trace) |
| `SAPO_HTTP_HOST` | No† | `127.0.0.1` | HTTP bind host (loopback by default) |
| `SAPO_HTTP_PORT` | No | `3333` | HTTP port |
| `SAPO_HTTP_MAX_SESSIONS` | No | `100` | Max concurrent MCP sessions |
| `SAPO_HTTP_SESSION_IDLE_MS` | No | `1800000` | Idle session GC threshold (30 min) |
| `SAPO_MCP_AUTH_TOKEN` | No† | — | Bearer token. **Required** if host is non-loopback |
| `SAPO_HTTP_CORS_ORIGINS` | No | — | CSV of allowed CORS origins (default: disabled) |

*One of `SAPO_API_SECRET` or `SAPO_API_SECRET_FILE` is required.
†HTTP-only. Token is enforced when `SAPO_HTTP_HOST` is not `127.0.0.1`/`localhost`/`::1`.

## Modes

| Mode | Status | Description |
|------|--------|-------------|
| `pos-online` | 0.5.0 | Online orders, customers, fulfillment (51 tools) |
| `web` | 0.5.0 | Storefront, collections, articles, SEO (31 tools) |
| `pos-counter` | 0.5.0 | POS counter: locations, inventory write, suppliers, shifts, stock transfers (15 tools) |
| `analytics` | 0.5.0 | Composed reports: revenue, top products/customers, LTV, tax, channel breakdown, discount usage, shift report (10 tools) |

> **Note:** `pos-counter` excludes 5 internal-only endpoints (`purchase_orders`, `purchase_returns`, `stock_adjustments`, `cash_transactions`, `cashbook`) — these return HTTP 403 for Private App credentials and require an OAuth Partner App (post-1.0 roadmap).
>
> Analytics tools auto-paginate up to `SAPO_MAX_AUTO_PAGES`; results carry `truncated: true` when the cap is hit.

## Tool Verification Status

Verification levels for the 105 unique tools (last updated 2026-04-30):

| Symbol | Level | What it means |
|---|---|---|
| ✅ | **Canary-monitored** | Endpoint + schema verified live; daily nightly probe via `.github/workflows/canary.yml` |
| 🟢 | **Live-verified** | Endpoint hit during development, schema captured to fixture, not in daily canary |
| 🟡 | **Docs-only** | Schema from `docs/sapo-api-reference.md` (Sapo official); mock tests only, no live probe |
| 🔵 | **Composed** | Not a Sapo endpoint — internal aggregation logic, logic-tested via unit tests |
| 🚨 | **Broken** | Known non-functional, see notes |

### Canary-monitored endpoints (drift detected within 24h)

| Resource | Mode(s) | Endpoint |
|---|---|---|
| store | all | `/admin/store.json` |
| products (read) | pos-online, pos-counter | `/admin/products.json` |
| orders | pos-online, pos-counter | `/admin/orders.json` |
| customers | pos-online, pos-counter | `/admin/customers.json` |
| inventory_levels | pos-online, pos-counter | `/admin/inventory_levels.json` |
| locations | pos-counter | `/admin/locations.json` |
| draft_orders | pos-online | `/admin/draft_orders.json` |
| price_rules | pos-online | `/admin/price_rules.json` |
| pages | web | `/admin/pages.json` |
| suppliers | pos-counter | `/admin/suppliers.json` |
| stock_transfers | pos-counter | `/admin/stock_transfers.json` |
| payment_methods | pos-counter | `/admin/payment_methods.json` |

### 🚨 Broken — needs investigation

| Tool | Resource | Issue |
|---|---|---|
| `list_pos_shifts` | pos_shifts | `/admin/pos_shifts.json` returns `Content-Type: text/html` (Sapo POS web app shell), not JSON. |
| `get_pos_shift` | pos_shifts | Same as above. JSON API endpoint not yet located. |

These 2 tools may fail at runtime. POS shift management currently requires admin UI.

### Per-tool status

Click each section to expand the full tool list with status icons.

<details>
<summary><b>pos-online — 51 tools (with `SAPO_ALLOW_OPS=*`)</b></summary>

**Customers** (7)
- ✅ `list_customers`, `get_customer`, `search_customers`, `count_customers`, `list_customer_orders`
- 🟢 `create_customer`, `update_customer`

**Customer Addresses** (4)
- 🟡 `list_customer_addresses`, `add_customer_address`, `update_customer_address`, `set_default_customer_address`

**Products (read)** (4)
- ✅ `list_products`, `get_product`, `search_products`, `count_products`

**Variants (read)** (2)
- 🟡 `list_variants_for_product`, `get_variant`

**Inventory (read)** (1)
- ✅ `get_inventory_levels`

**Orders** (4)
- ✅ `list_orders`, `get_order`, `count_orders`, `search_orders`

**Order Transactions** (2)
- 🟢 `list_order_transactions`, `create_order_transaction`

**Fulfillments** (4)
- 🟢 `list_fulfillments_for_order`, `get_fulfillment`
- 🟡 `create_fulfillment`, `update_fulfillment_tracking`

**Refunds** (3)
- 🟢 `list_refunds`, `get_refund`, `create_refund` *(create_refund destructive: `SAPO_ALLOW_OPS=refund`)*

**Draft Orders** (7)
- ✅ `list_draft_orders`, `get_draft_order`
- 🟡 `create_draft_order`, `update_draft_order`, `complete_draft_order`, `send_draft_order_invoice`, `delete_draft_order` *(destructive: `delete`)*

**Price Rules** (5)
- ✅ `list_price_rules`, `get_price_rule`
- 🟡 `create_price_rule`, `update_price_rule`, `delete_price_rule` *(destructive: `delete`)*

**Discount Codes** (3)
- 🟡 `list_discount_codes`, `create_discount_code`, `delete_discount_code` *(destructive: `delete`)*

**Destructive (cancel/delete-strict)** (4)
- 🟡 `cancel_order`, `close_order`, `cancel_fulfillment` *(destructive: `cancel`)*
- 🟡 `delete_customer`, `delete_variant` *(destructive: `delete_strict`)*

</details>

<details>
<summary><b>web — 31 tools</b></summary>

**Store** (1)
- ✅ `get_store_info`

**Articles** (5)
- 🟡 `list_articles`, `get_article`, `create_article`, `update_article`, `delete_article` *(destructive: `delete`)*

**Blogs** (5)
- 🟡 `list_blogs`, `get_blog`, `create_blog`, `update_blog`, `delete_blog` *(destructive: `delete`)*

**Pages** (4)
- ✅ `list_pages`, `get_page`
- 🟡 `update_page_seo`, `delete_page` *(destructive: `delete`)*

**Collections** (10)
- 🟡 `list_custom_collections`, `get_custom_collection`, `create_custom_collection`, `update_custom_collection`, `delete_custom_collection` *(destructive: `delete`)*
- 🟡 `list_smart_collections`, `get_smart_collection`
- 🟡 `list_collects`, `create_collect`, `delete_collect` *(destructive: `delete`)*

**Script Tags** (3)
- 🟡 `list_script_tags`, `create_script_tag`, `delete_script_tag` *(destructive: `delete`)*

**Products SEO** (1)
- 🟡 `update_product_seo`

**Variants (read, shared)** (2)
- 🟡 `list_variants_for_product`, `get_variant`

</details>

<details>
<summary><b>pos-counter — 15 tools</b></summary>

**Locations** (2)
- ✅ `list_locations`, `get_location`

**Payment Methods** (1)
- ✅ `list_payment_methods`

**Inventory (write)** (3)
- 🟡 `adjust_inventory_level`, `connect_inventory_level`
- 🟡 `set_inventory_level` *(destructive: `inventory_set`)*

**Variants (write)** (1)
- 🟡 `update_variant`

**POS Orders** (2)
- ✅ `list_pos_orders`, `get_pos_order` *(uses `/admin/orders?source_name=pos`)*

**Suppliers** (2)
- ✅ `list_suppliers`, `get_supplier`

**Stock Transfers** (2)
- ✅ `list_stock_transfers`, `get_stock_transfer`

**POS Shifts** (2)
- 🚨 `list_pos_shifts`, `get_pos_shift` *(see Broken section above — endpoint returns HTML)*

</details>

<details>
<summary><b>analytics — 10 tools</b></summary>

All 10 are 🔵 composed (aggregate from multiple Sapo endpoints, no single endpoint to verify):

`revenue_summary`, `top_products`, `top_customers`, `customer_ltv`, `tax_summary`, `online_vs_counter_breakdown`, `discount_usage_report`, `shift_report`, `inventory_low_stock`, `inventory_value`

</details>

## Destructive Operations

Destructive tools (cancel, delete, bulk-delete) are blocked by default. To enable specific categories:

```bash
# Allow order cancellation and standard deletes only
SAPO_ALLOW_OPS=cancel,delete npx sapo-mcp --mode=pos-online
```

Supported categories: `cancel`, `delete`, `delete_strict`, `inventory_set`, `refund`, `shift_close`, `cashbook_write`.

All destructive tool calls also require `confirm: true` in the tool arguments — this prevents accidental execution when an LLM calls the tool without explicit intent.

## Security

ENV credentials are visible to other processes via `ps` / `/proc/<pid>/environ` on shared hosts. Use `SAPO_API_SECRET_FILE` to pass the secret via a file instead of an env var where possible.

Note: full secret-file isolation (reading the secret only at startup, then clearing) is tracked for the post-1.0 roadmap.

## Probing

Verify which Sapo API endpoints are available on your store before configuring tools.

```bash
# Read-only probe — safe to run against any store (GET only)
SAPO_STORE=mystore SAPO_API_KEY=xxx SAPO_API_SECRET=yyy npm run probe

# Schema drift check — same probe + Zod validation against fixtures
npm run canary
```

Write-probe (`npm run probe:write`) refuses to run unless `SAPO_STORE` contains `test`, `dev`, or `sandbox` — never against production.

## HTTP Transport (Remote / Docker)

Run as an HTTP server for remote MCP clients (e.g. GoClaw) or self-hosted Docker:

```bash
# Local-only (no auth required)
sapo-mcp --mode=pos-online --transport=http --port=3333

# Public bind (auth token required)
SAPO_HTTP_HOST=0.0.0.0 \
SAPO_MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
sapo-mcp --mode=pos-online --transport=http
```

Endpoints:

| Method | Path | Description |
|--------|------|-------------|
| `GET`  | `/health` | Liveness probe — returns `{ status, version, modes, sessions }` |
| `POST` | `/mcp`    | JSON-RPC over Streamable HTTP (creates a session on `initialize`) |
| `GET`  | `/mcp`    | SSE long-poll for an existing session (`mcp-session-id` header) |
| `DELETE` | `/mcp`  | Terminate a session |

**Session model:** Each MCP client gets a UUID session, isolated by an
`McpServer` instance. Idle sessions (`SAPO_HTTP_SESSION_IDLE_MS`) are
evicted automatically. Concurrent sessions cap at `SAPO_HTTP_MAX_SESSIONS`
(503 returned when full).

**Security:**
- Defaults to `127.0.0.1` — loopback only. Token optional.
- Setting `SAPO_HTTP_HOST=0.0.0.0` (or any non-loopback) **requires**
  `SAPO_MCP_AUTH_TOKEN`. The server refuses to start otherwise.
- CORS is **off by default**. Enable per-origin via `SAPO_HTTP_CORS_ORIGINS`
  (CSV of origins, or `*` for all). Only enable when serving browser-based
  agents.

### Docker

See [`examples/Dockerfile`](examples/Dockerfile) and
[`examples/docker-compose.yml`](examples/docker-compose.yml).

```bash
docker build -f examples/Dockerfile -t sapo-mcp:local .
docker run --rm -p 3333:3333 \
  -e SAPO_STORE=mystore \
  -e SAPO_API_KEY=xxx -e SAPO_API_SECRET=yyy \
  -e SAPO_HTTP_HOST=0.0.0.0 \
  -e SAPO_MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
  sapo-mcp:local
```

## Development

```bash
npm install
npm run typecheck   # Type check
npm run lint        # Lint
npm run test        # Run tests
npm run build       # Build for release
```

## Post-1.0 roadmap

The 1.0.0 stable cut is gated on the following items, deferred from this release:

- **MCP Resources** — `sapo://shop/info`, `sapo://orders/today`, `sapo://orders/pending`, `sapo://inventory/low-stock`. Read-heavy data is more efficient as a Resource than repeated tool calls.
- **MCP Prompts** — templated workflows (`respond_to_complaint`, `weekly_report`, `seo_optimize_product`, `customer_followup`).
- **Webhook receiver** — sub-package surfacing Sapo webhooks as MCP events.
- **OAuth 2.0 Partner App** — multi-tenant SaaS deployments. Unlocks the 5 internal-only endpoints currently excluded from `pos-counter` and 4 deferred finance/PO reports (`cashflow_summary`, `pnl_summary`, `supplier_purchase_summary`, `daily_pos_report`).
- **Storefront GraphQL module** — verify Private App access; scope tools if available.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for the per-version history.

## License

MIT — See [LICENSE](LICENSE)

TDQS

A3.8/5.0

Scored across 46 tools

Disambiguation5/5

Each tool targets a distinct resource and action, with clear separation between list, search, count, and get variants. Even similar tools like search_products vs list_products are differentiated by their query parameters.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case, including compound nouns like customer_address and order_transaction. There is no mixing of camelCase or other styles.

Tool Count2/5

With 46 tools, this exceeds the 25-tool threshold for 'too many,' even though the tools are organized by domain. The count is inflated by multiple list/search/count variants and separate address/province helpers, but the sheer volume makes it heavy.

Completeness3/5

The set covers many workflows (customers, orders, fulfillments, draft orders, discounts), but product management lacks create/update/delete, and there are no delete operations for customers or addresses, or create for refunds. These are notable gaps for a commerce platform.

Maintenance

ActivityInactive
ResponsivenessNo issues