Gunstore Pos Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Gunstore Pos Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A local MCP server that wraps the OpenGunstore Frappe REST API, so you can read and change a store's settings and 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.
frappe_* tools) covers every doctype and everywhitelisted method — present and future. A curated layer makes the frequent settings/sync/report ops one call.
Authorization: token key:secret).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.
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).
cp .env.example .env
# edit .env: FRAPPE_BASE_URL, FRAPPE_API_KEY, FRAPPE_API_SECRET.env is git-ignored. Point at a dev site first (http://dev.localhost:8000) to test, then switch to your live POS URL (e.g. https://pos.example.com).
uv sync # creates .venv and installs deps/path/to/opengunstore-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:
{
"mcpServers": {
"gunstore-pos": {
"command": "uv",
"args": ["run", "--directory", "/path/to/opengunstore-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):
[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/opengunstore-mcp", "gunstore-mcp"]
startup_timeout_sec = 120Verify 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.
Tool reference, organised by task (safety notes and alternatives included): [TOOLS.md](TOOLS.md)
123 tools total: 10 generic + 57 curated + 13 distributor + 7 CPA reports + 30 shop-floor + 6 Range; 110 register by default. Three opt-in sets are held back: the 4 distributor queue actions (GUNSTORE_MCP_DISTRIBUTOR_ACTIONS=1), the 3 GunBroker write actions (GUNSTORE_MCP_GUNBROKER_ACTIONS=1) and the 6 Range tools (GUNSTORE_MCP_RANGE=1, only for a store with the Range extension). None is registered otherwise — an absent tool cannot be talked into firing.
Modes: GUNSTORE_MCP_MODE=cpa starts a read-only accountant surface — exactly 27 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.
To install only the read-only accountant surface (no full server) — e.g. on a second machine or for an analyst agent:
git clone [email protected]:Old-Steel-Arsenal/opengunstore-mcp.git ~/opengunstore-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 ~/opengunstore-mcp gunstore-mcp.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).
tools/list must show exactly 27 tools andthe server name gunstore-pos-cpa. A misspelled/unknown mode value refuses to start (fail-closed) rather than silently degrading to the writable surface.
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.
| 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 |
| Tool | Purpose | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
get_settings / update_settings | ffl \ | fastbound \ | rsr \ | payroc \ | woocommerce \ | shipstation \ | gunbroker \ | sports_south \ | lipseys \ | 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); a paid purchase from a private seller (acquisition_source "Individual", type blank / Purchase / Individual) needs seller_payment_method (Cash / Zelle / Check / ACH) + seller_payment_reference unless Cash (gunstore-pos #705 on) | |||||||||
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) | |||||||||
boundbook_mismatches | dry-run-only bound-book reconcile (read-only; also in cpa mode) | |||||||||
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 (the only store; 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 |
Every write needs confirm=true; there is no registration gate (they are the same counter actions the POS pages offer, role-checked per call and audited on the remote connector). Firearms are never adjusted by a stocktake and storage moves touch no stock or books.
| Tool | Purpose | ||
|---|---|---|---|
inventory_counts / inventory_count_state / inventory_count_variance | list stocktakes / one count's scans + expected rows / counted-vs-system variance (read-only; counts + variance are also on the cpa surface) | ||
inventory_count_create / inventory_count_cancel | start (whole store or scoped) / cancel an open count (confirm) | ||
inventory_count_scan / inventory_count_set_qty / inventory_count_toggle_serial / inventory_count_undo | record a serial or UPC scan / type a quantity / tick a gun by hand / remove an entry (confirm) | ||
inventory_count_finalize | posts ONE Stock Reconciliation for the non-serialized differences, saves the firearm report (confirm; firearms never adjusted) | ||
cash_drawer_today / cash_drawer_preview_close | the drawer page (expected cash + lines) / what a close WOULD do (read-only) | ||
cash_drawer_closes / cash_drawer_entries / cash_drawer_log / cash_drawer_weekly | daily counts / deposits, cash from the bank, expenses, seller payouts / every cash movement with who and the running balance / the Cash Drawer Weekly report (read-only; also on the cpa surface) | ||
cash_drawer_close_day | count the drawer, close POS shifts, book the over/short or first-count entry (confirm) | ||
cash_drawer_record_entry | kind = deposit \ | from_bank \ | expense (confirm); an expense needs expense_account + memo; a receipt is optional (POS 1.8.2+), and when given must be a file the same POS user uploaded within a day — remote has no upload tool, so the user uploads it in the POS and passes the file_url |
cash_drawer_undo / cash_drawer_record_payout | cancel an entry or the latest close / change the method of / re-book an old-way seller payment (first payments go through the receipt) (confirm; managers) | ||
storage_map / storage_location / storage_where / storage_unassigned | the store map (zones_only = zone list) / one location's contents / where a serial or item is / unassigned and to-confirm lists (read-only) | ||
storage_create_zone / storage_add_positions / storage_set_disabled | zone management (confirm; Stock Manager) | ||
storage_scan_move / storage_undo_move / storage_confirm_taken | assign a gun / put units into a location / undo it / say where sold units came from (confirm) |
| 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 |
payroc_transactions | every Payroc card transaction for a date range (≤31 days), read live from the gateway — counter and Woo web orders, sales / refunds / declines, portal refunds and voids included — each matched to the POS with disagreement flags; card type + last 4 only, plus the cardholder name. Needs an OpenGunstore release carrying payroc/ledger.py, and an API user with System Manager / Accounts Manager / Accounts User |
ar_ap_summary | aged AR / AP as of a date (Posting Date basis, 30/60/90/120). AP is only meaningful when purchases are recorded as Purchase Invoices in ERPNext — reference only |
The same server runs as a remote MCP endpoint with GUNSTORE_MCP_TRANSPORT=http. It is an OAuth resource server; the POS itself is the authorization server (Frappe v16's built-in OAuth: dynamic client registration + PKCE). A user adds the connector URL in claude.ai (Settings → Connectors) or Claude Code (claude mcp add --transport http <name> <url>), signs in to the POS in the browser and approves. No key is typed anywhere, and the server holds none.
POS (ffl_core.api.connector.connector_identity, cached 60 s; the token is never logged or cached in the clear) and forwards it on every call, so each call runs with the signed-in user's own POS roles.
Connectors, Claude Code claude mcp add --transport http) and ChatGPT (custom connectors / developer mode, or the Responses API mcp tool with the user's token). Each client registers itself with the POS (dynamic client registration), and the audit shows which one made each call.
servers on or off separately (and owns the OAuth settings they sign in through); a switched-off server tells the user so on every call.
by dynamic registration. Tokens of OAuth apps made in Desk are refused.
cannot be deleted; reads included): who, through which connector, which tool, which arguments (secrets masked by key, in filters and inside JSON strings), Started → Success / Failed. The row is written before the tool runs — if it cannot be, the tool does not run. Needs an OpenGunstore release carrying ffl_core/api/connector.py and the Connector Audit Log doctype.
the connector (and ultimately from Claude's servers), never from the user's IP.
cpa mode keeps all three read-only layers. The full surface is 109 toolsremotely: upload_attachment reads a path on the server and is never registered there. The distributor queue actions never open remotely (the server refuses to start; distributor_create_draft_order is not one of them and is there); the 3 GunBroker writes open on the full connector only, where the POS deploy sets GUNSTORE_MCP_GUNBROKER_ACTIONS from that store's own GunBroker switch — a store that lists on GunBroker must be able to end a listing. The Range tools open where the POS deploy sets MCP_RANGE=1 (a store running the Range extension).
GUNSTORE_MCP_TRANSPORT=http, FRAPPE_BASE_URL (the store's public POS URL,also the OAuth issuer), GUNSTORE_MCP_PUBLIC_URL (https://pos.<domain>/connector/<mode>/mcp, the URL users are given), GUNSTORE_MCP_PORT, GUNSTORE_MCP_MODE; optional GUNSTORE_MCP_HOST (listen address, default 127.0.0.1) and FRAPPE_INTERNAL_URL (loopback URL of the local POS frontend — calls then go there directly with the site's Host header). It runs on the POS host behind that host's reverse proxy, which maps the public path to /mcp and forwards /.well-known/oauth-protected-resource/<public path> unchanged.
Caddy — image ghcr.io/old-steel-arsenal/gunstore-pos-mcp (built on every push to main), deployed by each POS release (opengunstore deploy/prod/mcp, pinned MCP_TAG) — see deploy/README.md.
Enable Dynamic Client Registration on; Skip Authorization off (every user approves). Revoke a user's access in Desk under OAuth Bearer Token.
locally only; keep .env out of git (it is, by default). The remote connector holds no key — see "Remote connector" above.
stripped from every write. Set secrets in Desk directly.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.