Old-Steel-Arsenal

GunStore-POS Admin MCP

Community Old-Steel-Arsenal
Updated

Standalone MCP server wrapping the GunStore-POS Frappe REST API (generic frappe_* + curated firearm/RSR/FastBound/Woo ops)

GunStore-POS Admin MCP

A local MCP server that wraps the GunStore-POSFrappe REST API, so you can read and change a store's settings and content fromClaude or Codex — toggle integration config, edit item pricing/listing, fixrecords, trigger RSR/FastBound/ATF operations, run the Firearms-In-Stock report.

How it works

  • A small generic backbone (frappe_* tools) covers every doctype and everywhitelisted method — present and future. A curated layer makes the frequentsettings/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 thisdomain'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 andAPI Secret (the secret is shown only once).

2. Configure credentials

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).

3. Install

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 isat the repo root). The server loads .env by its own file path, so credentialsnever 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/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 MCPservers 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/gunstore-pos-mcp", "gunstore-mcp"]
startup_timeout_sec = 120

Verify with a quick stdio handshake (initialize + tools/list) or just listtools from within Codex.

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

Tools

Tool reference, organised by task (safety notes and alternatives included): TOOLS.md

115 tools total: 10 generic + 56 curated + 12 distributor + 7 CPA reports + 30 shop-floor; 108 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 26 tools (the write surface is never registered), a per-name read-onlymethod allowlist at the client layer, and the 7 integration Settings doctypesblocked from reads. Default (full) is the whole surface. Register a secondserver 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. ona second machine or for an analyst agent:

git clone [email protected]:Old-Steel-Arsenal/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 withdifferent FRAPPE_BASE_URL / mode combinations (e.g. a dev-site instance).
  • Verify: after connecting, tools/list must show exactly 26 tools andthe server name gunstore-pos-cpa. A misspelled/unknown mode value refusesto start (fail-closed) rather than silently degrading to the writable surface.
  • Security boundary — read before handing this to a third party: thethree-layer gate restricts the MCP call layer only. The API key/secret inthe registration is whatever that Frappe user can do — with a System Managerkey, anyone who extracts it can write via plain REST, MCP gate or not. For athird-party (e.g. an outside accountant's machine), create a dedicatedFrappe 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 | 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); 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)
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

Shop floor: stocktake, cash drawer, storage locations (POS 1.5.0-beta.15)

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)

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
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 a gunstore-pos 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

Remote connector (OAuth, no API key)

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 theconnector URL in claude.ai (Settings → Connectors) or Claude Code(claude mcp add --transport http <name> <url>), signs in to the POS in the browserand approves. No key is typed anywhere, and the server holds none.

  • Every request carries that user's bearer token. The server checks it against thePOS (ffl_core.api.connector.connector_identity, cached 60 s; the token is neverlogged or cached in the clear) and forwards it on every call, so each call runswith the signed-in user's own POS roles.
  • Works with any MCP client that supports OAuth — Claude (claude.aiConnectors, Claude Code claude mcp add --transport http) and ChatGPT (customconnectors / developer mode, or the Responses API mcp tool with the user'stoken). Each client registers itself with the POS (dynamic clientregistration), and the audit shows which one made each call.
  • Switched in the POS: Desk → MCP Settings turns the read-only and fullservers on or off separately (and owns the OAuth settings they sign inthrough); a switched-off server tells the user so on every call.
  • Only connector tokens are accepted: a token the POS issued to a client createdby dynamic registration. Tokens of OAuth apps made in Desk are refused.
  • Every call is audited in the POS (Desk → Connector Audit Log, permanent, rowscannot 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 itcannot be, the tool does not run. Needs a gunstore-pos release carryingffl_core/api/connector.py and the Connector Audit Log doctype.
  • A user with Restrict IP set cannot use the connector: calls reach the POS fromthe 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 107 toolsremotely: upload_attachment reads a path on the server and is never registeredthere. The distributor actions never open remotely (the server refuses to start);the 3 GunBroker writes open on the full connector only, where the POS deploysets GUNSTORE_MCP_GUNBROKER_ACTIONS from that store's own GunBroker switch — astore that lists on GunBroker must be able to end a listing.
  • Env: 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 (listenaddress, default 127.0.0.1) and FRAPPE_INTERNAL_URL (loopback URL of the localPOS frontend — calls then go there directly with the site's Host header). It runson 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.
  • Deployment: a container per surface on each store's POS host, behind that host'sCaddy — image ghcr.io/old-steel-arsenal/gunstore-pos-mcp (built on every push to main),deployed by each POS release (gunstore-pos deploy/prod/mcp, pinnedMCP_TAG) — see deploy/README.md.
  • POS side, once per site (OAuth Settings): Show Auth Server Metadata andEnable Dynamic Client Registration on; Skip Authorization off (every userapproves). Revoke a user's access in Desk under OAuth Bearer Token.

Security notes

  • Local (stdio) server: uses the key's user (Administrator) = full access. Run itlocally only; keep .env out of git (it is, by default). The remote connectorholds no key — see "Remote connector" above.
  • Credentials never transit the MCP: password-type and credential-named fields arestripped 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.

MCP Server · Populars

MCP Server · New