SupplySynk MCP server
SupplySynk exposes its inventory domain to AI agents (Claude, ChatGPT, Cursor, custom agents) over the Model Context Protocol.
- Endpoints (Streamable HTTP, stateless JSON responses):
POST https://<app>/api/mcp/<business-slug>— the business's own address (AI-1). Shown in Settings → Connected apps and Developers. Connecting here needs no organization picker: the address names the business.POST https://<app>/api/mcp— the shared address. Still works for existing connections and API keys; OAuth sign-in asks which business.
- Auth (either):
- OAuth 2.1 sign-in — add the address to Claude / ChatGPT / Cursor / VS Code and sign in; no key to copy. The person chooses an access level (below) and which changes to allow.
- API key from Developers → API Keys:
Authorization: Bearer ssk_…(or HTTP BasicclientId:clientSecret) — for server-side agents and automation.
- Protocol versions: 2025-06-18, 2025-03-26, 2024-11-05
Authorization model
OAuth-connected agents act as the signed-in member and can do only what
both that member's role (viewer → owner) and the scopes they approved
allow, further limited by the connection's access level (D-9, D-23). The
business is fixed at consent (org_id claim; with a per-business address the
token is also audience-bound to that address, and a token for business A is
refused at business B's address). Membership is re-checked on every request,
so removing someone or lowering their role takes effect immediately. Actions
are audited under the user with the app's name (via: oauth, clientName),
e.g. "Asha via Claude"; approved actions also record approvedBy.
Access levels (AI-2)
Chosen on the consent screen; changed or revoked in Settings → Connected apps. Connections made before levels existed count as Everyday changes.
| Level | tier | What the app can do |
|---|---|---|
| Look only | read | Read tools only; every write scope is dropped from the connection. |
| Everyday changes | act | Routine changes run (stock adjustments, draft POs, counts, orders). High-impact changes wait for approval: sending a purchase order (submit_purchase_order, create_purchase_order with submit: true), price changes (update_product touching prices or tax), orders with a discount or a price other than the product's (create_order), payment status (update_order with paymentStatus, mark_order_paid, create_counter_sale, mark_counter_sale_paid), emailing a shopper (mark_order_ready_for_pickup), deletes and cancellations (destructive tools), returns, bulk import, publishing the store or a store page (publish_store_page, unpublish_store_page), payment options, and anything that acts on Square (sync_square_stock, refresh_square_payments, catalog/location changes). |
| Ask me first | approval | Every change waits for approval. |
Approval queue (AI-3)
A change that must wait is not run. The tool returns
{ "status": "pending_approval", "approvalId": "…", "summary": "Adjust stock of Club Whites Shirt (M) at Shop floor by +10, reason \"found in back room\"", "expiresAt": "…", "message": "Sent for approval: …" }
Owners and admins get an email (at most one digest an hour per business) and
an item in the bell; an owner, admin or manager approves or rejects it on the
Approvals page (phone-friendly). Approving runs the same tool once, as the
app acting for the member who asked, recorded "approved by"; the approver must
also be allowed to do it. Rejecting records a reason. Requests expire after 7
days. The app checks back with get_pending_action (or list_pending_actions);
result holds what the tool returned, error why it failed. AI apps and API
keys can never approve. REST: GET /api/v1/approvals, GET /api/v1/approvals/{id},
POST /api/v1/approvals/{id}/approve, POST /api/v1/approvals/{id}/reject.
- Statuses:
pending→executing(one approver claimed it and it is running) →approved(ran;result) orfailed(error); orrejected(rejectReason) /expired. A run stillexecutingafter 10 minutes is markedfailed("didn't finish; check and retry"). - The connection must still allow it. Disconnecting the app (by the
person, an owner/admin, or a password reset) or lowering it to Look only
turns its waiting requests down (
rejectReason:connection removed/access lowered). Approving re-checks that the consent still exists, the level isn't Look only and every scope the app held when it asked is still granted. - Summaries never hide money: price, cost, discount, tax and payment changes are listed first with their values; order and PO lines show unit prices, costs and discounts.
- App names are self-reported (dynamic registration). Unless every return address is a known MCP client host (claude.ai, chatgpt.com, …), Approvals, the digest email and the activity log show "Name (unverified, host)".
- Limits: the same call again within 10 minutes returns the waiting
request (
duplicate: true); at most 50 waiting requests per app, person and business (too_many_pending). - Owners and admins see every AI app connected to the business
(
GET /api/v1/ai/connections, Settings → Connected apps) and can lower a member's level (PATCH) or disconnect it (POST /api/v1/ai/connections/disconnect); only the member can raise it. A level that isn't one of the three counts as Ask me first.
OAuth details: discovery via RFC 9728 (/.well-known/oauth-protected-resource)
and RFC 8414 (/.well-known/oauth-authorization-server); dynamic client
registration (RFC 7591, public clients, PKCE required; loopback / custom-scheme
callbacks registered as native per RFC 8252); access tokens are 1-hour JWTs
audience-bound to the address used (/api/mcp or /api/mcp/<slug>; each
business address is its own RFC 8707 resource, created on first use), refresh
tokens 30 days; 1000 requests/hour per user per app. Clients registering
without scopes get read-only access.
Protections: the consent screen labels apps whose return address isn't a known MCP client host as unverified (names are self-reported) and shows where access is sent; change permissions are opt-in per scope; disconnecting an app (Settings → Connected apps) or resetting your password revokes its tokens and cuts it off on the next request; registration is rate limited (10/hour/IP) and unused registrations are pruned daily.
API keys work as below.
| Layer | What it does |
|---|---|
| API key → organization | The key alone decides the tenant. On a tenant host (acme.supplysynk.com / custom domain) the key must belong to that tenant. |
| Scopes → tools | tools/list returns only tools whose scope the key holds; tools/call re-checks, and every service re-checks again. |
| Services | Same code path as the REST API and dashboard: strict input allowlists, tenant-checked references, audit log (api_key:<id>), webhooks, stock invariants. |
| Rate limit | The key's own limit (default 1000/hour), shared across all servers → HTTP 429. |
Create a dedicated key per agent with the narrowest scopes it needs (e.g. the Read Only bundle for a reporting assistant).
Tools
| Area | Tools | Scope |
|---|---|---|
| Products | list_products, get_product, find_product_by_barcode, get_ai_image_allowance / create_product, update_product, delete_product, add_product_variant, add_product_photo_from_url, generate_product_image, describe_product_photo | read:products / write:products |
| Spreadsheet import | get_product_import_format, preview_product_import, import_products | write:products |
| Inventory | list_inventory_levels, list_inventory_movements, list_inventory_lots, trace_lot / adjust_inventory | read:inventory / write:inventory |
| Replenishment | get_reorder_suggestions / create_reorder_purchase_orders | read:reports / write:purchase_orders |
| Insights | get_sales_insights, get_attention_items, get_weekly_summary, list_merch_suggestions | read:reports, read:inventory |
| Merchandising | create_product_reorder_draft / pin_store_product, exclude_store_product | write:purchase_orders / write:products (manager and up) |
| Orders | list_orders, get_order / create_order, update_order, confirm_order, fulfill_order, cancel_order, return_order_items | read:orders / write:orders |
| Counter billing (guide) | get_counter_setup, get_counter_sale / create_counter_sale, mark_counter_sale_paid, cancel_counter_sale | read:orders / write:orders (staff and up) |
| Online orders (pickup) | get_online_order_details / mark_order_paid, mark_order_ready_for_pickup, mark_order_picked_up | read:orders / write:orders (staff and up) |
| Purchasing | list_purchase_orders, get_purchase_order / create_purchase_order, submit_purchase_order, receive_purchase_order, cancel_purchase_order | read:purchase_orders / write:purchase_orders |
| Transfers | list_transfers, get_transfer / create_transfer, ship_transfer, receive_transfer, cancel_transfer | read:inventory / write:inventory |
| Stock counts | list_stock_counts, get_stock_count / create_stock_count, record_stock_count, complete_stock_count, cancel_stock_count | read:inventory / write:inventory |
| Square (details) | get_square_status, get_square_sync_log, get_square_sales_summary / retry_square_event, refresh_square_payments, review_square_catalog, link_square_catalog, sync_square_stock, map_square_locations | read: read:locations, read:orders, read:reports; write: write:orders, write:products, write:inventory, manage:integrations |
| Online store | get_storefront_settings, get_payment_settings, list_storefront_keys / set_storefront_published, update_payment_settings, create_storefront_key, revoke_storefront_key | read:settings / write:settings (owner or admin) |
| Store pages (guide) | list_store_pages, get_store_page / draft_store_page, create_store_page, update_store_page / publish_store_page, unpublish_store_page | read:settings / write:products (drafts; manager and up) / write:settings (publish; owner or admin). Publishing and unpublishing are high impact: they always wait for a person's approval from an "act" connection. Drafts send the version they read. AI apps and API keys can't add or change customer quotes (testimonial sections); only people signed in to SupplySynk can |
| Marketing | list_contacts, get_marketing_results / create_contact, create_buy_link, generate_post_caption, create_post_draft, erase_contact | read:contacts (staff and up; not a default read scope), read:marketing / write:marketing (staff and up), manage:marketing (erase; managers and up). High impact (waits for approval on an "act" connection): erase_contact, and create_buy_link to another website. Drafts only: nothing is published to social networks |
| GST documents (guide) | list_gst_documents / issue_gst_invoice, create_credit_note | read:orders / write:orders |
| Tax (guide) | get_tax_settings, preview_tax / update_tax_settings | read:settings, read:products / write:settings (owner or admin) |
| Master data | list_/get_/create_/update_/delete_ × customer(s), supplier(s), location(s), category/categories | read:* / write:* |
| Task-shaped (AI-5) | find_product_stock, sales_summary / restock_low_items, receive_delivery, count_stock | read:products, read:reports / write:purchase_orders, write:inventory |
| Approvals (AI-3) | list_pending_actions, get_pending_action | read:settings |
Tools carry MCP annotations (readOnlyHint, destructiveHint, idempotentHint)
so clients can ask for confirmation before destructive calls. create_* tools
for orders, purchase orders and transfers accept idempotencyKey for safe retries.
Business-rule failures (insufficient stock, invalid state, not found, missing
scope) come back as tool results with isError: true and a structured
{ error: { code, message, details } }, so the model can explain or recover.
Connecting
With OAuth (recommended for people): add your business's address,
https://<app>/api/mcp/<business-slug> (copy it from Settings → Connected apps),
as a remote MCP server / custom connector in your client and sign in when prompted.
claude mcp add --transport http supplysynk https://<app>/api/mcp/<business-slug>
With an API key — Claude Code
claude mcp add --transport http supplysynk https://<your-workspace>/api/mcp \
--header "Authorization: Bearer ssk_..."
Claude Desktop / other clients (mcp.json style):
{
"mcpServers": {
"supplysynk": {
"type": "http",
"url": "https://<your-workspace>/api/mcp",
"headers": { "Authorization": "Bearer ssk_..." }
}
}
}
Raw HTTP
curl -s https://<your-workspace>/api/mcp \
-H "Authorization: Bearer ssk_..." -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_reorder_suggestions","arguments":{}}}'
Security notes
- Browser requests with a cross-site
Originare rejected (DNS-rebinding protection). - No session state is kept between requests; revoking a key takes effect on the next call.
- Request bodies are capped at 1 MB and 50 JSON-RPC messages.
- OAuth sign-in uses the platform host (
/api/mcpor/api/mcp/<slug>on your main app URL); API keys also work on tenant subdomains and custom domains, and at a business address only for that business. - Not yet supported: Client ID Metadata Documents (AI-6; needs the
@better-auth/cimdplugin, planned in a follow-up), server-initiated notifications, resources/prompts.