Skip to main content
Glama
xuanji86

GunStore-POS Admin MCP

by xuanji86
README.md
# GunStore-POS Admin MCP

A local [MCP](https://modelcontextprotocol.io) server that wraps the GunStore-POS
Frappe REST API, so you can read and change **production** settings/content from
Claude or Codex — toggle integration config, edit item pricing/listing, fix
records, trigger RSR/FastBound/ATF operations, run the Firearms-In-Stock report.

> **Companion repo:** [`firearm-listing-import`](https://github.com/xuanji86/firearm-listing-import) —
> a Claude Code / Codex skill that uses this MCP (per-gun photos + descriptions → Serial No →
> WooCommerce). Extracted from the gunstore-pos app as a standalone, separately distributable package.

## How it works

- A small **generic backbone** (`frappe_*` tools) covers every doctype and every
  whitelisted method — present and future. A **curated layer** makes the frequent
  settings/sync/report ops one call.
- Talks to Frappe over HTTPS with token auth (`Authorization: token key:secret`).
- **Safety**: delete/cancel/submit and destructive methods — including this
  domain's own high-consequence verbs (dispose / push / charge / consolidate /
  ship / return / receive / sold / settle / onboard) —
  require `confirm=true`;
  password/credential fields are never transmitted (set those in Desk);
  schema/permission doctypes are read-only.

## Setup

### 1. Generate a Frappe API key
In Desk as the user you want to act as (Administrator): top-right avatar →
**My Settings** → **API Access** → **Generate Keys**. Copy the **API Key** and
**API Secret** (the secret is shown only once).

### 2. Configure credentials
```bash
cp .env.example .env
# edit .env: FRAPPE_BASE_URL, FRAPPE_API_KEY, FRAPPE_API_SECRET
```
`.env` is git-ignored. Point at dev first (`http://dev.localhost:8000`) to test,
then switch to prod (`https://pos.oldsteelarsenal.com`).

### 3. Install
```bash
uv sync          # creates .venv and installs deps
```

### 4. Register with your agent

`/path/to/gunstore-pos-mcp` below is wherever you cloned this repo (the package is
at the repo root). The server loads `.env` by its own file path, so credentials
never go in the agent config.

**Claude Code** — add to `.mcp.json` (project) or run `claude mcp add`:
```json
{
  "mcpServers": {
    "gunstore-pos": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/gunstore-pos-mcp", "gunstore-mcp"]
    }
  }
}
```
**Claude Desktop** — the same block in `claude_desktop_config.json`.

**Codex** — add to `~/.codex/config.toml`, then **restart Codex** (it reads MCP
servers at startup):
```toml
[mcp_servers.gunstore-pos]
command = "uv"            # or an absolute path to uv if it isn't on Codex's PATH
args = ["run", "--directory", "/path/to/gunstore-pos-mcp", "gunstore-mcp"]
startup_timeout_sec = 120
```
Verify with a quick stdio handshake (`initialize` + `tools/list`) or just list
tools from within Codex.

Secrets stay in `.env` (loaded by the server), not in the agent config.

## Tools

> **中文速查手册(按"你想干什么"组织,含安全须知与替代路径):[TOOLS.md](TOOLS.md)**

84 tools total: 10 generic + 56 curated + 12 distributor + 6 CPA reports; 77 register by default. Two opt-in sets are held back: the 4 distributor queue actions (`GUNSTORE_MCP_DISTRIBUTOR_ACTIONS=1`) and the 3 GunBroker write actions (`GUNSTORE_MCP_GUNBROKER_ACTIONS=1`). Neither is registered otherwise — an absent tool cannot be talked into firing.

**Modes**: `GUNSTORE_MCP_MODE=cpa` starts a read-only accountant surface —
exactly 19 tools (the write surface is never registered), a per-name read-only
method allowlist at the client layer, and the 7 integration Settings doctypes
blocked from reads. Default (`full`) is the whole surface. Register a second
server entry (e.g. `gunstore-pos-cpa`) with the same command plus
`"env": {"GUNSTORE_MCP_MODE": "cpa"}` to run both side by side.

### Standalone CPA install

To install **only** the read-only accountant surface (no full server) — e.g. on
a second machine or for an analyst agent:

```bash
git clone git@github.com:xuanji86/gunstore-pos-mcp.git ~/gunstore-pos-mcp
claude mcp add gunstore-pos-cpa --scope user \
  --env GUNSTORE_MCP_MODE=cpa \
  --env FRAPPE_BASE_URL=https://pos.example.com \
  --env FRAPPE_API_KEY=<key> \
  --env FRAPPE_API_SECRET=<secret> \
  -- uv run --directory ~/gunstore-pos-mcp gunstore-mcp
```

- **Process env beats `.env`**: the server loads `.env` with `override=False`,
  so env vars set in the registration win. A standalone install needs no
  `.env` file at all — and the same checkout can serve several entries with
  different `FRAPPE_BASE_URL` / mode combinations (e.g. a dev-site instance).
- **Verify**: after connecting, `tools/list` must show exactly **19** tools and
  the server name `gunstore-pos-cpa`. A misspelled/unknown mode value refuses
  to start (fail-closed) rather than silently degrading to the writable surface.
- **Security boundary — read before handing this to a third party**: the
  three-layer gate restricts the *MCP call layer* only. The API key/secret in
  the registration is whatever that Frappe user can do — with a System Manager
  key, anyone who extracts it can write via plain REST, MCP gate or not. For a
  third-party (e.g. an outside accountant's machine), create a dedicated
  Frappe API user with read-only roles and register with *that* key pair.

### Generic backbone
| Tool | Purpose |
|---|---|
| `frappe_list_documents` | list any doctype (filters / fields / limit; `limit=0` = all) |
| `frappe_get_document` | fetch one doc (Single: `name == doctype`) |
| `frappe_describe_doctype` | fields incl. custom fields; flags password fields |
| `frappe_create_document` | create (credential fields stripped) |
| `frappe_update_document` | update (credential fields stripped) |
| `frappe_delete_document` | delete — needs `confirm=true` |
| `frappe_submit_document` | submit a draft — needs `confirm=true` |
| `frappe_cancel_document` | cancel a submitted doc — needs `confirm=true` |
| `frappe_run_method` | call any whitelisted method by dotted path |
| `frappe_run_report` | run a Script/Query report |

### Curated
| Tool | Purpose |
|---|---|
| `get_settings` / `update_settings` | `ffl` \| `fastbound` \| `rsr` \| `payroc` \| `woocommerce` \| `dealer` \| `shipstation` \| `gunbroker` \| `sports_south` \| `data_service` |
| `find_item` / `item_stock` / `available_serials` | typeahead item search / stock per item / in-stock serials + per-gun prices |
| `firearms_in_stock` | the Firearms In Stock report |
| `receive_goods` | Purchase Receipt + FFL acquisitions + FastBound push (`confirm`) |
| `add_stock` / `set_stock` | non-serialized stock add / absolute set (`confirm`) |
| `toggle_service_need` | gunsmith flag on a Serial No (`confirm`) |
| `rsr_catalog_search` | RSR-only catalog search (`distributor_catalog_search` spans every enabled house) |
| `promote_to_item` / `backfill_from_rsr` | RSR catalog row → sellable Item / backfill Item fields (`confirm`) |
| `fastbound_test_connection` / `push_serial_to_fastbound` / `boundbook_reconcile` | FB probe / per-gun correction push (`confirm`) / bound-book reconcile (apply needs `confirm`) |
| `atf_verify_ffl` / `verify_supplier_ffl` / `reverify_all_ffls` | ATF eZ-Check verifies (`confirm`) |
| `woo_test_connection` / `woo_push_item` / `woo_delist_item` / `woo_reconcile` | store probe / list / delist / reconcile an Item — all take `site: retail\|dealer` (writes need `confirm`) |
| `woo_push_serial` / `woo_delist_serial` | list / delist ONE gun (SKU `item_code::serial`; `site`; `confirm`) |
| `set_serial_title` | per-gun Woo listing title (writes `Serial No.item_name`; takes effect on next push) |
| **GunBroker channel** | *the environment, credential and money fields of `GunBroker Settings` are refused on every write path — `update_settings`, `frappe_update_document`, `frappe_create_document` and `frappe_run_method`'s field setters (method names matching `set_value`/`db_set`/`save_doc`-alike, `safety._SETTER_METHOD`), enforced by a test over the whole registered surface — all four tool modules the server registers. Sandbox vs live is a Desk change, not a tool call. The 3 writes below need `GUNSTORE_MCP_GUNBROKER_ACTIONS=1` to be registered at all* |
| `gb_test_connection` / `gb_listing_status` | GunBroker probe — the reply's `sandbox` flag says which marketplace answered (needs SYSTEM_ROLES, as does `gb_pull_orders`; the other three need STOCK_ROLES) / POS-vs-GunBroker view of ONE gun's listing (read-only) |
| `gb_push_serial` / `gb_end_listing` | list ONE gun as a fixed-price Buy Now / end its listing (opt-in via `GUNSTORE_MCP_GUNBROKER_ACTIONS=1`, then `confirm`; both reach GunBroker only via the POS) |
| `gb_pull_orders` | run the order poll now (opt-in + `confirm`; takes no arguments). Importing an order creates POS documents and reserves the gun, which is why it is gated with the writes. The reply `{"queued": true}` is a receipt — the poll runs in the background and reports no counts |
| `pending_orders` / `pending_web_orders` | the Pending Order queue: counter/dealer rows + paid web orders (read-only; consignments live in `consignment_queue`) |
| `dispose_order` / `dispose_web_order` | book the FFL transfer dispositions — stock-out + FastBound push (`confirm`) |
| `record_payment` | Payment Entry against an unpaid submitted invoice; Zelle needs `transaction_number` (`confirm`) |
| `cancel_order` | cancel a submitted counter/dealer order — cascades disposition/stock/FastBound/ShipStation reversal + refund (`confirm` + `reason`) |
| `push_shipment` / `mark_shipped_manually` / `shipstation_test_connection` | ShipStation label push (`confirm`) / no-push escape hatch (`confirm`) / probe |
| `consignment_queue` / `consignment_dealers` / `consignment_serials` / `consignment_dealer_orders` | outbound-consignment reads: At-Dealer queue / shippable FFL dealers / pickable serials / settlement queue |
| `create_consignment_out` / `ship_consignment_out` / `push_consignment_shipment` / `mark_consignment_shipped` | build / dispose+ship / ShipStation push / manual-ship w/ tracking (`confirm`) |
| `retry_consignment_invoice` / `return_consignment_lines` / `cancel_consignment` | settlement-invoice retry / take unsold guns back / cancel line-or-draft (`confirm`; cancel needs `reason`) |
| `start_4473` / `manager_override_4473` / `start_transfer_4473` | 4473 kickoff / stuck-sale manager override / customer transfer (`confirm`) |
| `upload_attachment` | multipart file upload → File doc, optionally attached to a doctype+name / Attach field |

### CPA reports (read-only; registered in both modes)
| Tool | Purpose |
|---|---|
| `sales_report` | the POS Sales Report, payload passed through unchanged (views Order / Order Detail / Product; channel POS/Web/Manual) |
| `inventory_receipts` | the POS Inventory Receipts report — everything that entered stock in a period (per unit / per serial, or a category × receipt-type summary), straight from the stock ledger; classes Purchase / Trade-in / Consignment / Intake / Return / Adjustment / Revaluation (cost correction, 0 units), with No-cost / No-A&D flags |
| `gl_entries` | GL rows for a date range (`is_cancelled=0` always; explicit `truncated:true`) |
| `financial_statement` | P&L / Balance Sheet (Date Range) / Trial Balance (fiscal-year auto-resolved) |
| `tax_liability` | sales-tax liability roll-forward from the GL — accounts resolved from the default sales-tax template, vouchers bucketed fail-closed, cent-exact identity asserted |
| `ar_ap_summary` | aged AR / AP as of a date (Posting Date basis, 30/60/90/120). AP is not maintained in ERPNext (QuickBooks is the book; purchases are prepaid) — reference only |

## Security notes

- Uses the key's user (Administrator) = full access. Run **locally only**; keep
  `.env` out of git (it is, by default).
- Credentials never transit the MCP: password-type and credential-named fields are
  stripped from every write. Set secrets in Desk directly.
- All writes are logged by Frappe under the key's user (audit trail).
- To revoke access, regenerate that user's keys in Desk.

TDQS

B3.4/5.0

Scored across 77 tools

Disambiguation4/5

Most tools target clearly separated resources and workflows, and descriptions explicitly disambiguate near-twins like woo_push_item vs woo_push_serial and mark_shipped_manually vs mark_consignment_shipped. The sheer number of tools creates some selection noise, but an agent can usually determine the correct tool from the name and description.

Naming Consistency3/5

Names are consistently snake_case and often use domain prefixes like woo_, frappe_, consignment_, and distributor_, which is helpful. However, the set mixes verb_noun actions (dispose_order, create_consignment_out), noun-phrase queue names (pending_orders, consignment_queue), and service-style names (test_connection, sync_catalog), so there is no single predictable pattern.

Tool Count2/5

At 77 tools, this is far beyond the typical well-scoped MCP surface and exceeds the 25+ too-many threshold. While the domain is broad, packing generic CRUD wrappers, integration actions, queues, and reports into one server creates a heavy surface that will strain agent context and tool-selection accuracy.

Completeness4/5

The tool set covers nearly the full gun-store administration lifecycle: inventory, FFL/FastBound compliance, orders, consignments, distributor operations, e-commerce integrations, and financial reporting. A few workflow actions are only reachable via generic frappe_run_method or are missing dedicated tools—e.g., GunBroker push/end and explicit distributor-order confirmation—but these are minor gaps.

Maintenance

ActivityActive
ResponsivenessNo issues