A Model Context Protocol server that lets an AI chatbot connect to real restaurants — browse live menus, place and manage orders through the restaurant's point-of-sale.
Transport: Streamable HTTP Protocol: MCP Auth: OAuth 2.1 · Bearer API key
OrderFlow MCP exposes a set of tools an AI assistant can call to discover onboarded restaurants, read their menus and ordering policies, and create/confirm/track customer orders. All pricing is computed server-side from live inventory, and discounts/deals are only honored when the store's configuration authorizes them — the assistant cannot invent prices or unauthorized promotions.
https://mcp.zappd.ai/mcpPOST /mcp (JSON-RPC messages) · GET /mcp (SSE server→client stream) · DELETE /mcp (tear down session)initialize request opens a session; the server returns an Mcp-Session-Id header that must be sent on every subsequent request.GET /mcp/health (unauthenticated) → { "ok": true }Every request to /mcp carries a bearer token. Two methods are accepted:
Interactive clients such as ChatGPT use OAuth 2.1. Each end-user signs in (via Firebase Auth) and receives an account-scoped access token; tools then only expose that user's own orders. This server is both the Authorization Server and the Resource Server, so discovery is automatic:
/mcp request returns 401 with WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource" (RFC 9728).GET /.well-known/oauth-protected-resource and GET /.well-known/oauth-authorization-server (RFC 8414).POST /oauth/register, RFC 7591) → authorization with PKCE S256 (GET/POST /oauth/authorize) → token (POST /oauth/token, authorization_code + rotating refresh_token).zat_… and are bound to this resource (audience). Each user's token only reaches their own orders.Server-to-server consumers may instead send a pre-issued key:
Authorization: Bearer zmk_your_key_here
zmk_…; a key's orders are isolated to that key.401 Unauthorized — missing, invalid, expired, or revoked token.429 Rate limit exceeded — per-principal request quota or concurrent-session cap reached. A Retry-After header indicates when to retry.Each key carries scopes; a tool call missing its required scope returns INSUFFICIENT_SCOPE.
| Scope | Grants |
|---|---|
stores:read | List restaurants |
menu:read | Read a store's menu |
config:read | Read a store's AI config (hours, policies) and menu ontology |
orders:write | Create, update, confirm, and cancel orders |
orders:self:read | Read orders created by this key |
orders:self:subscribe | Subscribe to live updates for this key's orders |
| Tool | Scope | Input | Description |
|---|---|---|---|
list_all_active_stores | stores:read | — | List active, non-archived stores that have a configured POS (browsable and orderable). |
list_all_stores | stores:read | — | List every store in the directory with active/archived/posType flags. Informational only. |
| Tool | Scope | Input | Description |
|---|---|---|---|
get_menu | menu:read | storeId | Read a store's menu: items, prices, and modifier groups (with minRequired/maxAllowed/singleSelect) plus combination rules to honor when ordering. Ask the customer to choose for every group with minRequired > 0 before ordering, using combinationRules.modGroupQuestions wording when present. |
get_ai_config | config:read | storeId | Customer-facing AI config: interaction rules, hours, promises, and authorized policies (refunds/discounts/deals/guarantees). |
get_ontology | config:read | storeId | Menu ontology mapping customer wording → canonical items/modifiers (itemAliases, modAliases). Consult it to resolve loose or alias wording to catalog IDs when a mention doesn't cleanly match get_menu. |
| Tool | Scope | Input | Description |
|---|---|---|---|
create_order | orders:write | idempotencyKey, storeId, items?, order metadata | Create a virtual order owned by this key. Items use catalog IDs from get_menu; prices are set server-side. Retries with the same idempotencyKey reuse the original order. |
update_order | orders:write | idempotencyKey, orderId, items?, order metadata | Update a virtual order before confirmation. items replaces the whole cart (empty array clears it); omitted fields are unchanged. |
confirm_order | orders:write | idempotencyKey, orderId, customerName?, phoneNumber? | Finalize a non-empty order and hand it to the store; notifies the cashier to set a pickup time and confirm. Does not auto-submit to the POS. |
cancel_order | orders:write | idempotencyKey, orderId | Soft-cancel a virtual order. Refuses orders already submitted (or being submitted) to the POS. |
| Tool | Scope | Input | Description |
|---|---|---|---|
get_order | orders:self:read | orderId | Read one of your own orders. |
list_my_orders | orders:self:read | — | List your orders (those created by this connection), newest first. |
await_order_updates | orders:self:read | orderId, afterCreatedAt? | Long-poll (up to ~2 min) for the restaurant's next lifecycle update on a confirmed order, then relay it to the customer. Call once after confirm_order; pass afterCreatedAt = a prior call's latestCreatedAt to continue. Returns pending: true if nothing new arrived — a polling alternative to the SSE subscription for clients (e.g. ChatGPT) that cannot receive server pushes. |
subscribe_order_updates | orders:self:subscribe | orderId? | Subscribe over the SSE stream to live order updates. With orderId, watches that order; omitted, watches your active orders. Emits notifications/zappd/order_update (status/pickup) and, for a single order, notifications/zappd/order_message (the restaurant's lifecycle messages). Requires the SSE connection. |
unsubscribe_order_updates | orders:self:subscribe | subscriptionId | Cancel an active subscription. |
Each entry in items:
{
"itemId": "catalog-item-id", // from get_menu
"quantity": 1, // integer >= 1
"modifierIds": ["mod-id-1", "..."], // optional; validated against menu rules
"note": "no onions" // optional
}
Order metadata (optional, on create_order/update_order): customerName, phoneNumber, pickupTime, and the policy-claim arrays discounts, deals, refunds, guarantees. Policy claims are only accepted if authorized in get_ai_config.policies; anything else is rejected with POLICY_NOT_AUTHORIZED.
Tool errors ride inside a successful JSON-RPC response with an isError flag and a structured code. Common codes:
| Code | Meaning |
|---|---|
INSUFFICIENT_SCOPE | Key lacks the scope the tool requires. |
STORE_NOT_ONBOARDED | Store is inactive, archived, or has no configured POS. |
ORDER_NOT_FOUND | No order with that id owned by this key. |
ORDER_CART_EMPTY | Tried to confirm an order with no line items. |
INVALID_ITEM_ID / INVALID_MODIFIER_ID | Item or modifier not in the store's catalog. |
MODIFIER_GROUP_MIN / MODIFIER_GROUP_MAX | Modifier selection violates the group's min/max. |
REJECTED_MODIFIER_COMBINATION | Modifier is not allowed on that item per ontology rules. |
POLICY_NOT_AUTHORIZED | Claimed a discount/deal/refund/guarantee the store does not authorize. |
ORDER_ALREADY_IN_POS / POS_MATERIALIZATION_IN_PROGRESS / ORDER_CANCELLED | Order can no longer be edited/cancelled. |
list_all_active_stores → pick a storeId.get_menu (and optionally get_ai_config / get_ontology) to build the cart. Use get_ontology to resolve loose/alias wording, and ask the customer for any required modifier choices (groups with minRequired > 0, using modGroupQuestions wording) before create_order.create_order with items → update_order to adjust.confirm_order with customer name/phone → the store is notified to set a pickup time.get_order / list_my_orders, or subscribe_order_updates for live status.zmk_…) issued by OrderFlow, scoped per the table above.429 + Retry-After when exceeded).