Content
# 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)
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
AP2
AP2 provides code samples and demos for the Agent Payments Protocol.
google-meta-ads-ga4-mcp
MCP server for Google Ads, Meta Ads & GA4 — works with ChatGPT, Claude,...
nuwax
Nuwax AI enables easy building and deployment of private Agentic AI solutions.
amazon-sorftime-research-MCP-skill
Amazon Product Selection - Listing Full-Dimension Penetration Analysis...
MakeMoneyWithAI
A curated list of AI tools to monetize open-source projects.
daydreams
Daydreams is an AI agent framework in TypeScript for scalable and composable...