Skip to main content
Glama
retail-ops-mcp-kit

retail-ops-mcp-kit

retail-ops-mcp-kit

Four small Python MCP servers — one per enterprise-system class a retail/CPG operations agent integrates (ERP / supplier network / email / spreadsheet). Each server models a real named SaaS on the wire (SAP S/4HANA, SPS Commerce, Microsoft Graph, Google Sheets, …), selectable by env, over one shared synthetic corpus. Every mutation is a staged write: it validates, records a pending_approval record, returns an approval_id, and never executes. A separate approve(approval_id, token) needs a human token that lives only in the server environment and that the agent cannot read — so auto-execution is impossible by construction. Humans stay in control of what runs versus what waits, designed into the connector layer.

  • 4 servers, one runtime dependency (mcp>=1.27.2); everything else is stdlib.

  • FastMCP + hatchling src-layout + uv, Python 3.12, docstring-per-tool.

  • All data is fully synthetic and methodology-only — see PROVENANCE.md.

"I know your stack" — tool → real SaaS endpoint

Each server's tool signature is identical across every dialect it models; only the wire envelope changes (the connector seam). A host cannot tell which system answered.

retail-erp-mcp — the three-way match (PO / goods-receipt / invoice)

MCP tool

Kind

SAP S/4HANA (default)

Oracle NetSuite

MS Dynamics 365 F&O

get_purchase_order

read

GET …/API_PURCHASEORDER_PROCESS_SRV/A_PurchaseOrder('…')?$expand=to_PurchaseOrderItem → {"d":{…}}

GET /services/rest/record/v1/purchaseOrder/{id} → {id,tranId,entity,item:{items[]}}

GET /data/PurchaseOrderHeadersV2(dataAreaId='usmf',PurchaseOrderNumber='…') → {@odata.etag,…}

get_goods_receipt

read

GET …/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader (GM type 101)

GET …/record/v1/itemReceipt/{id}

GET /data/ProductReceiptHeaders

get_invoice

read

GET …/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice('…')

GET …/record/v1/vendorBill/{id}

GET /data/VendorInvoiceHeaders

list_open_pos

read

GET …/A_PurchaseOrder?$filter=… → {"d":{"results":[]}}

GET …/record/v1/purchaseOrder?q=status:open → {items,hasMore,offset}

GET /data/PurchaseOrderHeadersV2?$filter=PurchaseOrderStatus eq 'Backorder'

stage_invoice_payment

staged-write

POST …/API_JOURNALENTRYITEMBASIC_SRV/A_JournalEntryItemBasic (x-csrf-token)

POST …/record/v1/vendorPayment

POST /data/VendorPaymentJournalHeaders

stage_po_amendment

staged-write

PATCH …/A_PurchaseOrder('…') (If-Match / x-csrf)

PATCH …/record/v1/purchaseOrder/{id}

PATCH /data/PurchaseOrderHeadersV2(…) (If-Match etag)

simulate_po_event

webhook seam

Advanced Event Mesh CloudEvent sap/s4/beh/purchaseorder/v1/…/Created/v1

SuiteScript user-event https.post callback

Business Events → Azure Event Grid

Auth handshakes modeled per dialect (documented, never performed): SAP x-csrf-token: Fetch round-trip + sap-client; NetSuite OAuth1.0a account-ID host; D365 @odata.etag + If-Match.

retail-supplier-mcp — supplier network (ASNs / exceptions / deductions)

MCP tool

Kind

SPS Commerce (default)

Coupa

SAP Ariba

get_asn

read

EDI-856 Universal-JSON: hierarchicalLevels[] (S→O→T→I), carrier.scac, sscc18

REST ASN document

cXML ShipNoticeRequest

get_shipment_status

read

214-style status {shipmentIdentification, carrier, trackingNumber, status}

ASN status document

cXML StatusUpdateRequest

get_supplier

read

trading-partner {tradingPartnerId, senderId, …} (derived)

supplier record (derived)

supplier + NetworkID (derived)

list_exceptions

read

exception-queue rows [{id, po_id, evidence_text}] (gold_* withheld)

— same abstraction —

— same abstraction —

stage_credit_note

staged-write

EDI 810-variant

Coupa document-type:"credit_note"

InvoiceDetailRequest purpose="creditMemo"

stage_debit_note

staged-write

SPS debit doc

Coupa negative invoice

InvoiceDetailRequest purpose="debitMemo"

acknowledge_exception

staged-write

EDI 855 PO-ack (itemStatusCode IA/IB/IR) + 997 functional ack

POST /documents/{id}/acknowledge

cXML confirmation

simulate_inbound_asn

webhook seam

{eventType:"document.received", transactionType:"856", …} + the 856 payload

{event:"purchase_order.updated", …}

cXML StatusUpdateRequest

Ack/event vocabulary modeled: 850 (inbound PO), 856 (ASN), 810 (invoice), 855 (PO ack), 997 (functional ack); Coupa X-COUPA-API-KEY + scoped OAuth (core.invoice.write); Ariba dual apikey + OAuth and cXML <Credential domain="NetworkID">.

retail-email-mcp — supplier chase (mailbox threads + staged send)

MCP tool

Kind

Microsoft Graph (default)

Gmail API

list_chase_thread

read

GET /me/messages?$filter=conversationId eq '…' → {"value":[…]}

GET /users/me/threads/{id}

get_supplier_reply

read

GET /me/messages/{id}

messages.get?format=full (base64url payload)

stage_chase_email

staged-write

POST /me/sendMail (202, no body)

messages.send {raw, threadId}

stage_escalation_email

staged-write

POST /me/sendMail with ccRecipients

messages.send {raw, threadId}

simulate_change_notification

webhook seam

Graph change-notification + validationToken echo

Pub/Sub {message:{data:<base64>}} + historyId cursor

The chase ladder tone (soft → firm → escalation) is recomputed from prior_chases against policy.chase_ladder — never read from a stored "next step".

retail-sheets-mcp — reconciliation worksheet

MCP tool

Kind

Google Sheets (default)

MS Graph Excel

read_reconciliation_sheet

read

GET /v4/spreadsheets/{id}/values/Reconciliation!A1:I11 → {range,majorDimension,values}

#microsoft.graph.workbookRange (values/text/numberFormat/valueTypes, JSON-number cells)

get_range

read

GET …/values/{range} over Reconciliation / Approvals / Audit

…/worksheets('…')/range(address='…')

stage_cell_update

staged-write

PUT …/values/{range}?valueInputOption=USER_ENTERED

PATCH …/range(address='…')

stage_row_append

staged-write

values:append

…/tables/{name}/rows/add {values:[[…]]}

simulate_change_notification

webhook seam

Drive files.watch push channel (Sheets values have no native webhook)

driveItem change-notification (+ 409/423 stale workbook-session)

Reconciliation is a PO+receipt+invoice+ASN join; Approvals (from approval_actions, with a recomputed Gated column) and Audit (from audit_entries, with a recomputed Complete column over policy.audit_required_fields) are also readable.

Related MCP server: conduit-mcp

The safety model — humans control what runs vs. what waits

  1. Every mutation is a staged write. A mutation tool validates its payload against the corpus, calls staging.stage(...), and returns {result:"pending_approval", approval_id, target_endpoint, payload, validation, status:"pending"}. Nothing executes at stage time.

  2. approve(approval_id, token) is the only path to execution, and it requires the human approval token from the server environment (RETAIL_OPS_MCP_APPROVAL_TOKEN). The supplied token is compared with hmac.compare_digest. That token is never a tool return, never in the corpus, never a default — the agent has no channel to read it, so any token it fabricates fails. Absent a configured token, approval fails closed (refused).

  3. Auto-execution is impossible by construction. Staging never executes; approve needs a secret the agent structurally cannot hold. This is the deliberate inverse of "no creds in argv": here the argv token is the human's proof-of-presence, intentionally withheld from the agent.

  4. Execution is fabrication, not persistence. On a valid human approval, the stored execute(payload) returns a synthetic SaaS response envelope (a SAP journal-entry doc number, a Graph 202, a Sheets updatedCells) recorded on the in-process ledger — it never appends to a JSONL or edits the corpus.

  5. A fingerprint invariant makes it testable. corpus.fingerprint() (sha256 over all fixture bytes) is byte-identical before staging, after staging, and after a full approve+execute. smoke_test.py asserts exactly this.

Shared on every server: approve, list_pending_approvals, get_approval_status, and an active_system() tell.

System selection

Each server picks the modeled SaaS dialect from an env var (validated allowlist, safe local default, no credentials):

Server

Env var

Allowlist

Default

retail-erp-mcp

RETAIL_ERP_SYSTEM

sap_s4hana, netsuite, dynamics365

sap_s4hana

retail-supplier-mcp

RETAIL_SUPPLIER_SYSTEM

sps_commerce, coupa, ariba

sps_commerce

retail-email-mcp

RETAIL_EMAIL_SYSTEM

msgraph, gmail

msgraph

retail-sheets-mcp

RETAIL_SHEETS_SYSTEM

google_sheets, msgraph_excel

google_sheets

active_system() reports the wired dialect without leaking any credential.

Data posture

All 11 fixtures are fully synthetic, methodology-only, and vendored verbatim from the agentic-eval-harness retail_ops set — see PROVENANCE.md. policy.json numbers are <<TOKEN>>-tagged invented placeholders; exception rows carry only abstract MARKER_* tokens in benign filler. The suppliers() resource is the one kit-authored derivation (synthesized from the supplier FK, flagged _derived). No network call is ever made; there is no runtime path back to the harness.

Develop

just setup       # uv sync --extra dev
just check       # lint + typecheck + test + smoke  (the gate)
just smoke       # dependency-light staged-write proof → prints SMOKE OK
just run-erp     # run a server on stdio (RETAIL_ERP_SYSTEM overridable)

Wire into an MCP host

Claude Desktop (claude_desktop_config.json) or a project .mcp.json:

{
  "mcpServers": {
    "retail-erp":      { "command": "uv", "args": ["run", "retail-erp-mcp"],
                         "env": { "RETAIL_ERP_SYSTEM": "sap_s4hana",
                                  "RETAIL_OPS_MCP_APPROVAL_TOKEN": "<operator-injected; NOT known to the agent>" } },
    "retail-supplier": { "command": "uv", "args": ["run", "retail-supplier-mcp"],
                         "env": { "RETAIL_SUPPLIER_SYSTEM": "sps_commerce",
                                  "RETAIL_OPS_MCP_APPROVAL_TOKEN": "<operator-injected>" } },
    "retail-email":    { "command": "uv", "args": ["run", "retail-email-mcp"],
                         "env": { "RETAIL_EMAIL_SYSTEM": "msgraph",
                                  "RETAIL_OPS_MCP_APPROVAL_TOKEN": "<operator-injected>" } },
    "retail-sheets":   { "command": "uv", "args": ["run", "retail-sheets-mcp"],
                         "env": { "RETAIL_SHEETS_SYSTEM": "google_sheets",
                                  "RETAIL_OPS_MCP_APPROVAL_TOKEN": "<operator-injected>" } }
  }
}

The approval token belongs in the host/operator's env injection (or a secrets manager), and is deliberately withheld from the agent — that withholding is the entire control. Without it configured, all approvals fail closed.

License

Dual-licensed under Apache-2.0 OR MIT (added at T1). All data synthetic — see PROVENANCE.md.

Available Tools

11 tools
active_systemA

Which ERP dialect is wired (sap_s4hana | netsuite | dynamics365). Lets a host confirm the modeled system without leaking any credential.

Inputs: no named parameters. Returns: object

approveA

Approve a staged write. Requires the human approval token (server env RETAIL_OPS_MCP_APPROVAL_TOKEN) — the agent does not hold it, so an agent-supplied token is refused. On a valid human token the staged action executes (mock) and the record moves pending → executed. Refused if no token is configured (fail closed).

Inputs: token (required), approval_id (required). Returns: object

get_approval_statusA

Return the current record for approval_id (pending | approved | executed | refused).

Inputs: approval_id (required). Returns: object

get_goods_receiptA

Fetch the goods receipt(s) for a PO — the GRN "actual" leg of the match.

- po_id: corpus PO id, e.g. "PO-8801".

Under sap_s4hana returns an API_MATERIAL_DOCUMENT_SRV A_MaterialDocumentHeader
collection with a to_MaterialDocumentItem list (GoodsMovementType 101). `condition`
and `qty_damaged` from the corpus surface as item-level annotations (short-ship /
over-ship / damage / UOM-mismatch traps). Returns an empty collection if nothing was
received.

Inputs: po_id (required). Returns: object

get_invoiceA

Fetch one supplier invoice — the "charged" leg of the match.

- invoice_id: corpus invoice id, e.g. "INV-01".

Under sap_s4hana returns an API_SUPPLIERINVOICE_PROCESS_SRV A_SupplierInvoice with a
to_SuplrInvcItemPurOrdRef line referencing the PO. Carries
SupplierInvoiceIDByInvcgParty for duplicate-invoice detection (the PO-8808 trap:
INV-08a and INV-08b bill the same PO). Under netsuite: a vendorBill; under
dynamics365: a VendorInvoiceHeader. Raises if the invoice is unknown.

Inputs: invoice_id (required). Returns: object

get_purchase_orderA

Fetch one purchase order as the configured ERP would return it.

- po_id: corpus PO id, e.g. "PO-8801". Join key across the whole kit.

Returns the ERP-native PO envelope. Under sap_s4hana: an
API_PURCHASEORDER_PROCESS_SRV `{"d": {A_PurchaseOrder ... to_PurchaseOrderItem}}`
document with stringified decimals and /Date(ms)/ dates. Under netsuite: a REST
Record `purchaseOrder` with JSON numbers and an `item.items[]` sublist. Under
dynamics365: an OData V4 PurchaseOrderHeadersV2 entity with `@odata.etag` and
`dataAreaId`. Raises if the PO is unknown.

Inputs: po_id (required). Returns: object

list_open_posA

List purchase orders with no fully-matching invoice yet — the open three-way-match workqueue.

Takes no args. A PO is "open" when its billed qty (summed across invoices) is less
than the ordered qty. Under sap_s4hana returns an OData collection
`{"d": {"results": [A_PurchaseOrder, ...], "__count": "N"}}`; under netsuite a
`{items, hasMore, offset, totalResults}` page; under dynamics365 an OData V4
`{"value": [...]}`.

Inputs: no named parameters. Returns: object

list_pending_approvalsA

List this server's staged, not-yet-approved writes (status=pending). Read-only. Lets a human see what awaits approval; the agent can see them but cannot approve.

Inputs: no named parameters. Returns: result

simulate_po_eventA

Synthesize an inbound ERP change event for the given PO (read-only; no state change).

- po_id: corpus PO id, e.g. "PO-8801".
- event_type: e.g. "Created" | "Changed".

Under sap_s4hana returns a SAP Advanced Event Mesh CloudEvents envelope on topic
sap/s4/beh/purchaseorder/v1/PurchaseOrder/<event_type>/v1 with payload {PurchaseOrder}.
Under netsuite: a SuiteScript user-event callback; under dynamics365: a Business Events
envelope. Use it to exercise event-driven wiring.

Inputs: po_id (required), event_type. Returns: object

stage_invoice_paymentA

STAGE (do not execute) a supplier-invoice payment for human approval.

- invoice_id: corpus invoice id, e.g. "INV-01".
- amount: payment amount; echoed into the staged payload for the human to confirm.
- currency: ISO code, default "USD".

Validates the invoice exists and the amount is numeric, then records a pending record
and returns {result:"pending_approval", approval_id, target_endpoint, ...}. Models SAP
API_JOURNALENTRYITEMBASIC_SRV vendor payment. THIS DOES NOT PAY ANYTHING — a human must
approve(approval_id, token) to execute. 'pay_invoice' is in policy.gated_actions, so
validation.policy_gated is true.

Inputs: amount (required), invoice_id (required), currency. Returns: object

stage_po_amendmentA

STAGE (do not execute) a PO amendment (qty and/or price) for human approval.

- po_id: corpus PO id, e.g. "PO-8801".
- new_qty: optional new order quantity.
- new_unit_price: optional new net price per unit.

Validates the PO exists and at least one change is present, records a pending record,
returns pending_approval + approval_id. Models a PATCH to API_PURCHASEORDER_PROCESS_SRV
A_PurchaseOrder (If-Match / x-csrf-token write). 'po_amendment' is gated. Executes only
on human approval.

Inputs: po_id (required), new_qty, new_unit_price. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.4/5.0

    Scored across 11 tools

    Disambiguation5/5

    Each tool targets a distinct resource or action: fetching PO, invoice, goods receipt, approval status, listing open POs, staging payment/amendment, simulating events, approving, and listing approvals. Only minor potential confusion exists between get_approval_status and list_pending_approvals, but their inputs (approval_id vs listing) clarify the difference.

    Naming Consistency4/5

    The set mostly follows a verb_noun pattern (get_*, list_*, stage_*, simulate_*), but 'active_system' and 'approve' deviate from that pattern. These deviations are minor and the intent remains readable.

    Tool Count5/5

    With 11 tools, the server is well-scoped for its purpose: covering purchase order, invoice, goods receipt, approval workflow, and simulation. No tool is redundant; each adds a distinct capability.

    Completeness4/5

    The toolkit covers the core three-way match (PO, GR, invoice), open PO listing, staging of payment and PO amendments, and the approval flow. Minor gaps include no explicit reject/cancel for staged actions and no PO/invoice creation, but these may be outside the server's intended scope.

    Maintenance

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      D
      maintenance
      MCP server exposing the StoreLink grocery stocking API as tools for category buyer workflows, including inventory checks and replenishment orders.
      5
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      A governance-layer MCP server that gates AI writes to systems of record, requiring human signatures and providing a tamper-evident audit trail. It offers read, propose, approve, commit, and audit tools, with demo and Xero backends.
      22 npm
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      MCP server that provides mock APIs and deterministic seed data for ERP/OMS, WMS, and CRM systems, enabling supply chain data exploration and integration testing.
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Safe-write Shopify operations MCP server with plan-before-execute writes, out-of-band approval, and tamper-evident audit trail.
      47 npm
      MIT