sapo-mcp
# sapo-mcp
[](https://www.npmjs.com/package/sapo-mcp)
[](LICENSE)
[](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
Scored across 46 tools
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.
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.
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.
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.