# 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=` | `pos-online` | Comma-separated list of modes to activate | | `--transport=` | `stdio` | Transport type (`stdio` or `http`) | | `--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.
pos-online — 51 tools (with `SAPO_ALLOW_OPS=*`) **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`)*
web — 31 tools **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`
pos-counter — 15 tools **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)*
analytics — 10 tools 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`
## 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//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)