duckhub-mcp
Hosted Model Context Protocol (MCP) server for Duckhub — connect Claude, Cursor, ChatGPT and other AI agents to build, translate and publish restaurant menus.
What it can do
- Get Reference: Call this FIRST. Returns everything needed to use the other tools correctly: allowed enum values (units, badge colors, modifier types, card styles, currencies, usable languages), THIS v
- Get Menu: Read the venue’s current (draft) menu. view:"structure" (default) returns a tiny name↔ref map of categories/products — the cheapest overview; use it to learn existing refs before mutating. v
- Build Menu: THE bulk tool: create or update the whole menu in one call — categories, ingredients and products (with composition, modifier groups and inline translations), all keyed by YOUR externalId
What data it sees
Do you need an account
An API key from the service settings is required
Hosted Model Context Protocol (MCP) server for Duckhub — connect Claude, Cursor, ChatGPT and other AI agents to build, translate and publish restaurant menus. 39 tools over a secure API. Docs: docs.duck-hub.com/mcp
How to connect
How to connect duckhub-mcp to Claude
The server address to paste into Claude settings:
https://duckhub-mcp--tykhokhod-d.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect duckhub-mcp to ChatGPT
The server address to paste into ChatGPT settings:
https://duckhub-mcp--tykhokhod-d.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect duckhub-mcp to Cursor
The server address to paste into Cursor settings:
https://duckhub-mcp--tykhokhod-d.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
Server tool list (39)
Raw names from tools/list. Only developers need these.
| get_reference | Call this FIRST. Returns everything needed to use the other tools correctly: allowed enum values (units, badge colors, modifier types, card styles, currencies, usable languages), THIS venue’s plan limits and current usage, rate-limit budgets, order-status transitions, request caps, and the index of all API resources. Also documents conventions: all money amounts are integers in minor currency units. |
| get_menu | Read the venue’s current (draft) menu. view:"structure" (default) returns a tiny name↔ref map of categories/products — the cheapest overview; use it to learn existing refs before mutating. view:"full" returns every product with all fields (heavy — only when field-level detail is needed). Pass locale to resolve all translatable fields into that language. Rows are keyed by `ref` (externalId if set, else id). |
| build_menu | THE bulk tool: create or update the whole menu in one call — categories, ingredients and products (with composition, modifier groups and inline translations), all keyed by YOUR externalId and idempotent (safe to retry; re-running updates in place). For large or first-time payloads call with dryRun:true first — full validation and plan-limit checking with a create/update/skip preview and ZERO writes. CAUTION (replace semantics): when a product includes ingredientExternalIds, modifierGroups or modifierGroupExternalId, that array is treated as the FULL set and REPLACES what exists — anything unlisted is detached. For single-field edits of one product prefer the set_product tool (which never touches those arrays). Changes affect the DRAFT menu only — nothing is guest-visible until publish. Money is in minor units (12.50 → 1250). |
| list_products | Filtered, paginated product list. Default view:"summary" returns sparse rows (ref, name, price, flags — token-cheap); view:"full" returns complete product objects. Filters: category (ref), search (name), withoutDescription:true (only products missing a description — useful for content gap-filling). Pagination: page (1-based) + limit (max 50, default 15); the response envelope carries total/totalPages. Rows are keyed by `ref` (externalId if set, else id) — use refs with get_product/set_product/delete_product. |
| get_product | Full single-product read by ref (externalId or id). The result includes an `etag` field — pass it as ifMatch to set_product so a concurrent change is detected (412) instead of silently overwritten. Pass locale to resolve translated fields. |
| list_categories | Lightweight category index with live product counts, keyed by ref. Pass locale to resolve translated names. |
| set_product | Partial update of ONE product by ref — the safe alternative to build_menu for single-field edits. TRI-STATE semantics on every field: a field you OMIT keeps its current value; explicit null CLEARS it (nullable fields only); a value SETS it. Never resend fields you do not want to change. Composition and modifier groups are deliberately NOT editable here (their full-set replace semantics belong to build_menu). categoryRef: null detaches the product from its category; an unknown categoryRef is a hard 404. Concurrency: pass ifMatch with the etag from get_product — on 412 STALE_RESOURCE re-read and retry with the fresh etag. The result includes the NEW etag for chained edits. Money in minor units (12.50 → 1250). |
| delete_product | Soft-deletes ONE product by ref. Unlike cleanup_menu (which only HIDES products from the menu), this frees plan-limit capacity — deleted products no longer count toward maxProducts. Cannot be undone via the API. |
| list_publications | Publication history (rollback buffer, newest first, ~5 retained snapshots) with ids and timestamps. Use before rollback_publish to pick a snapshot. |
| publish_menu | Publishes the current DRAFT menu to the public guest-facing menu — takes effect immediately. All build_menu/set_product changes stay invisible to guests until this is called. Each publish keeps a snapshot in the rollback history (rollback_publish undoes a bad publish). |
| rollback_publish | Restores a retained snapshot as the LIVE public menu (undo for a bad publish) — replaces what guests currently see, immediately. Omit publicationId to roll back to the previous snapshot; otherwise pick an id from list_publications. |
| cleanup_menu | ⚠️ DANGEROUS BULK OPERATION with KEEP-LIST semantics: each array lists the externalIds to KEEP — everything else of that type is affected (products get HIDDEN from the menu [menuVisible:false, plan capacity NOT freed — use delete_product for that], categories and ingredients get DELETED). An omitted array leaves that type untouched; an EMPTY array wipes ALL items of that type and requires force:true. ALWAYS call with dryRun:true first — it returns counts plus a preview of exactly what WOULD be hidden/deleted, with zero writes. Typical use: after a full build_menu, remove leftovers not present in your source data. |
| get_settings | The complete curated settings view: venue info, working hours, SEO + social links, design, languages, ordering/delivery config, notification wiring — plus read-only context (publicMenuUrl = the live guest menu URL, lastPublishedAt). The result includes an `etag` field; the update_* settings tools handle concurrency automatically, so you normally never need it. |
| update_venue_info | Core venue identity: name, address, phone, Google-Maps link, Wi-Fi password, extra info text, country (ISO alpha-2, uppercase), cuisine type, IANA timezone, default currency. Valid currency codes are in get_reference → currencies. NOTE: renaming (name) changes the DISPLAYED venue name only — the public menu URL slug (yourvenue.duck-hub.com) is fixed at venue creation and never changes (protects printed QR codes and links). Omit = keep current value, null = clear, value = set. — null works only on country and cuisineType; leave other fields out to keep them. Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_working_hours | schedule = the venue’s opening hours shown on the public menu (full 7-day array replaces the current one). orderSchedule = the separate hours during which ONLINE ORDERS are accepted — it is delivery-plan-gated: on a plan without the delivery feature setting it fails with 403 DELIVERY_NOT_AVAILABLE (plain schedule always works). Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_menu_design | Public-menu look: theme (LIGHT/DARK), brandColor (#RRGGBB), menuFontFamily (font id — lowercase letters/digits/hyphens; null = default font), menuCardStyle (venue-wide card design — valid values in get_reference → cardStyles; per-category overrides live on categories via build_menu cardStyle). Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_languages | defaultLanguage = the canonical content language; enabledLanguages = ALL locales the public menu offers (min 1, replaces the current list — include the default). Valid codes are in get_reference → languages. Enable a locale HERE first, then fill content via build_menu inline translations (a locale that is not enabled fails with LOCALE_NOT_ENABLED). Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_seo_and_links | Search/social presence of the public menu page: seoTitle (max 60), seoDescription (max 160), googlePlaceId, and social/review links (Instagram, Telegram, TikTok, Twitter/X, TripAdvisor, Google Reviews). Omit = keep current value, null = clear, value = set. — null works on seoTitle, seoDescription and googlePlaceId. Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_ordering_config | Online-ordering configuration — REQUIRES a plan with the delivery feature (get_reference → planLimits.features.hasDelivery); on other plans every field here fails with 403 DELIVERY_NOT_AVAILABLE. Toggles: deliveryEnabled, pickupEnabled, dineInOrderingEnabled. Money fields are integers in MINOR currency units (12.50 → 1250): minimumOrderAmount, deliveryFee, freeDeliveryThreshold. pickupDiscount is a PERCENT (0-100). Times are minutes. deliveryEmail receives order notifications. (Ordering HOURS live in update_working_hours → orderSchedule.) Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| update_notifications | Staff-notification wiring: notify toggles (waiter call, hookah, bill request, booking, order) and the Telegram chat ids that receive them (telegramChatId for service actions, orderTelegramChatId for orders; null disconnects a chat). NOTE: every change here is recorded in the venue audit log — agents must not silently redirect staff notifications. Returns a COMPACT delta — only this tool’s own fields plus the new etag — not the full settings object; read complete state via get_settings. |
| set_image | Attach an image: target "product" or "banner" (ref required) or "logo" (venue logo, no ref). Source: EXACTLY ONE of sourceUrl (public https URL, server fetches it, ~15 MB cap) or base64 (inline bytes). Aspect rules: product and logo images must be SQUARE 1:1 — pass crop:"square" to have the server center-crop automatically; banner images must be 16:9 (no crop option). The server re-encodes/optimizes everything (WebP). preoptimized:true (product/banner only) uses the softer fast-lane rate bucket but REQUIRES the bytes to already be a target-size WebP — otherwise 400 PREOPTIMIZED_INVALID; when unsure, omit it. |
| set_translations | Cross-entity BULK translation upsert — up to 50 items in ONE call (one rate-limit hit) with per-item results (one bad item fails alone). Each item: model + ref (externalId or id) + translations. Translatable fields per model: product = name/description/badgeLabel; category/ingredient/modifierGroup = name; banner = title/description. translations accepts BOTH equivalent forms: the entries ARRAY [{"locale":"uk","name":"Піца"}] (canonical wire form) or the locale-keyed OBJECT {"uk":{"name":"Піца"}} — the SAME form build_menu inline translations use; the object form is normalized before sending. MERGE semantics per field: omitted = keep, empty string "" = delete that translation, non-empty = set. The locale must be enabled for the venue first (update_languages) — otherwise LOCALE_NOT_ENABLED. TIP: when building a menu from scratch, prefer inline `translations` on build_menu items; use this tool for after-the-fact localization passes. |
| get_translations | statsOnly:true → multilingual completeness in one call: per locale × model counts of translated/untranslated rows (no other params needed) — start here to find gaps. Otherwise a batch translation read: locale AND model are REQUIRED; filter narrows to untranslated/translated/stale rows; fields limits which fields return; refs scopes to specific entities. Rows are keyed by ref. Paginated (limit max 50). |
| list_modifier_groups | The venue’s reusable modifier-group library (restaurant-scoped option groups), keyed by ref, including usedOnProducts (which products each group is attached to). Attach groups to products via build_menu modifierGroupExternalId[]. |
| set_modifier_group | Upsert a REUSABLE modifier group (edit once, applies on every product it is attached to). Pass ref to UPDATE an existing group (only the provided fields change; options — when provided — REPLACE the whole option set); omit ref to CREATE (name + type required; set externalId so you can address it later). type: single_choice | add_ingredients | remove_ingredients — legacy multiple_choice is NOT accepted here (build_menu’s INLINE modifierGroups still accept it for backward compatibility and normalize it to add_ingredients; this tool takes canonical types only). Options reference ingredients by ingredientRef (externalId or id); priceAdjustment is in minor units and may be negative. Attach to products via build_menu modifierGroupExternalId[]; translate via set_translations. |
| delete_modifier_group | Deletes a reusable modifier group by ref. If the group is attached to any product the call fails with 409 MODIFIER_GROUP_IN_USE — pass force:true to detach it from every product and delete. |
| list_promotions | All promotions with their items (each item: productId, salePriceMinor, product summary). Paginated (limit max 100). Keyed by ref (externalId if set, else id). |
| set_promotion | Upsert a promotion in ONE call — pass ref to update, omit to create (name required; set externalId for stable addressing). Optional items[] is the FULL desired item set: the server diffs it against the current items and adds/updates/removes accordingly (item identity = the product; salePriceMinor in minor units). Products not listed are REMOVED from the promotion — omit items entirely to leave them untouched. isActive toggles the promotion AND syncs the products’ live sale prices. schedule = optional weekly activity hours (day/isOpen/HH:MM). NOT atomic: the result reports added/updated/removed/unchanged/errors per item — on partial failure the promotion and successful items ARE saved; re-run set_promotion with the same ref/externalId and the full desired items[] to converge (the diff is idempotent). |
| delete_promotion | Deletes a promotion by ref (externalId or id). Its items are removed with it and the affected products’ live sale prices are un-synced back to normal. Cannot be undone. |
| list_banners | Menu banners keyed by ref. Pass locale to localize title/description. Paginated (limit max 100). |
| set_banner | Upsert a menu banner: pass ref to update, omit to create (title required, max 60 chars; description max 2000; set externalId for stable addressing). linkedProductRef links the banner to a product (null clears the link). isActive shows/hides it. Upload the banner image (16:9) separately via set_image target:"banner". Plan-limited (BANNER_LIMIT_REACHED — check get_reference limits). Note on create with isActive: two internal steps (create, then set isActive) — if the second fails the banner EXISTS without it; re-run with ref (or the same externalId → the conflict hint points you at the existing banner). |
| delete_banner | Deletes a menu banner by ref (externalId or id), including its uploaded image. To just hide a banner temporarily, prefer set_banner with isActive:false. Cannot be undone. |
| list_tables | The venue’s tables keyed by ref — each row carries its guest QR-menu URL (qrUrl). Paginated (limit max 100). |
| set_table | Upsert a table: pass ref to update (name, isActive, floor-plan positionX/positionY, capacity), omit to create (name required, max 50; set externalId for stable addressing). The response includes the table’s guest QR-menu URL (qrUrl). Plan-limited (TABLE_LIMIT_REACHED — check get_reference limits). Note on create with extras (isActive/position/capacity): two internal steps (create, then patch extras) — if the second fails the table EXISTS without them; re-run with ref to finish. |
| delete_table | Deletes a table by ref (externalId or id) — its printed QR code stops resolving immediately. To take a table out of service temporarily, prefer set_table with isActive:false. Cannot be undone. |
| list_orders | Guest orders (paid plans only — 403 PAID_PLAN_REQUIRED on the free plan). Filters: status, since/until/updatedSince (ISO 8601). Paginated (limit max 100). Poll updatedSince for changes. |
| get_order | Full single-order read by order id (paid plans only — 403 PAID_PLAN_REQUIRED on the free plan). |
| update_order_status | Advances an order through its lifecycle (paid plans only). Allowed transitions are in get_reference → orderStatusTransitions (e.g. new → confirmed|cancelled); an invalid transition is rejected. Statuses: new, confirmed, preparing, delivering, completed, cancelled. |
| get_audit_log | What this API key changed and when — every v1 write is recorded (action, entity, timestamp). Filter by exact action (e.g. "menu.sync", "settings.update"); paginated (limit max 100, default 20). Useful to review your own session or debug unexpected state. |