Skip to main content
Glama
francisschmaltz

DoorDash CLI MCP Server

DoorDash CLI MCP server

Streamable HTTP MCP wrapper for DoorDash CLI. It runs on macOS Apple Silicon or Linux amd64, with PostgreSQL storing MCP tokens, submission protection, and the shared DoorDash login credential.

The server exposes the authenticated ordering services in DoorDash CLI v0.2.5. login, export-token, help, and version remain local CLI operations; the doordash_auth MCP tool accepts exported credentials without a restart.

The Linux image and Nomad deployment instructions run the service without macOS or a keyring.

Setup

Requirements:

  • macOS on Apple Silicon or Linux amd64

  • Node.js 24 or newer

  • PostgreSQL (17 is used in CI)

  • DoorDash CLI v0.2.5 for the current platform

Install dependencies and the checksum-verified CLI release. The installer preserves the full distribution, including Linux's _internal directory, under the ignored ./doordash-cli/ directory and updates ./dd-cli:

npm ci
npm run install:cli
./dd-cli --version
cp .env.example .env

Generate a secret with openssl rand -hex 32, paste it into ADMIN_ACCESS_TOKEN in .env, and set DATABASE_URL for the dedicated doordash database. See the database setup and SQLite import before upgrading an existing installation. Start the server; migrations finish before it accepts traffic:

npm start

Open http://127.0.0.1:8787/ and enter the admin secret. Generate an MCP bearer token, copy it immediately, and put it in the MCP client's Authorization/Bearer-token setting. Only the MCP token's SHA-256 hash is stored.

The MCP endpoint is:

http://127.0.0.1:8787/mcp

All MCP requests require a valid bearer token. Then run ./dd-cli export-token on a machine with browser login and provide the exported token through doordash_auth({"access_token":"EXPORTED_TOKEN"}). Alternatively, set DD_CLI_ACCESS_TOKEN in .env before first boot to seed an empty credential store. Later replacements through MCP are saved in PostgreSQL and survive restarts.

Open WebUI in Docker

Listen on the Mac's network interface:

HOST=0.0.0.0

In Open WebUI configure:

  • Type: MCP (Streamable HTTP)

  • URL: http://host.docker.internal:8787/mcp

  • Authentication: Bearer token generated by the local UI

Open WebUI caches tool schemas. After upgrading this service, disconnect and reconnect the MCP integration before starting a new chat; otherwise the model may keep sending an obsolete input shape.

The admin UI, token APIs, and raw activity log require the admin secret. A successful web login receives an HttpOnly session cookie. Scripts can instead send Authorization: Bearer <ADMIN_ACCESS_TOKEN>. The /mcp endpoint keeps its separate MCP bearer tokens.

In the admin's Order preferences, enable Prefer priority / express delivery once to use express automatically for all MCP clients. The setting is saved in PostgreSQL and survives restarts. Eligible ASAP delivery previews include the express fee; orders without express, pickup, and scheduled orders use standard delivery. Order confirmation is still required. Omit priority in preview_order to follow this preference, or pass priority: false for an explicit standard-delivery request. Submission always uses the confirmed submit_context.priority, even if the preference changes afterward.

Related MCP server: AppleScript Automation MCP

Token permissions

Each token has its own Checkout & card details checkbox.

Unchecked tokens receive the complete non-purchase tool set. Checked tokens also receive:

  • list_payment_methods

  • order_submit

Changing the checkbox changes that token immediately. There is no global purchase switch. Revoking a token invalidates it immediately.

Payment output contains only card brand, last four digits, expiry, and default status. DoorDash CLI cannot return a full card number or CVC, and the wrapper does not expose provider/payment IDs.

order_submit is deliberately obnoxious:

  • It requires the exact submit_context returned by preview_order.

  • It requires an explicit tip in dollars, including an explicit zero.

  • It requires the confirmed default card, account default, or work budget.

  • It requires explicit PIN-handoff acknowledgement when the preview says a delivery PIN is required.

  • It re-previews immediately before submission and stops on drift.

  • It records the cart before spawning the purchase command.

  • It never retries a cart with any recorded submission attempt.

  • It polls order_status after DoorDash accepts the submission.

A bearer token authenticates a caller. It is not purchase consent; the tool's confirmation contract is still mandatory.

MCP tools

Every tool advertises an MCP outputSchema with a success/error union. Calls return a readable result plus the same machine payload through both client-compatible channels:

  • content[0].text is a short readable summary.

  • content[1].text is minified JSON, exactly equal to JSON.stringify(structuredContent).

  • structuredContent is the stable, versioned machine result.

The advertised output schemas are intentionally shallow: they name the fields needed for the next tool without repeating the full response grammar for every tool. Every actual response is still checked against the strict internal contract before it is returned.

Typed errors also return the JSON text copy and isError: true. structuredContent.error contains a stable code, retryable: false, and, when recovery is possible, the exact recovery_tool and recovery_arguments. Do not repeat the failed call; perform the one stated recovery action instead.

State-changing tools are serialized inside the service. A cart, address, preview, reorder, or submission cannot slip between purchase revalidation and the purchase command. If a non-idempotent command loses its upstream result, the error names one inspection tool; it never tells the caller to repeat the write.

Cart preflight failures are the one deliberate exception: they return the cart shape with items: [], complete item_errors, and isError: true. That means no cart write occurred. A DoorDash partial-add response instead has both successful items and item_errors; those successful lines are already in the cart and must never be sent again.

The public tools/list schemas use strict snake_case fields. Copy response fields such as address_id, store_id, menu_id, item_id, cart_uuid, cart_item_id, order_uuid, and budget_id directly into the next tool. Unknown fields are rejected. Older camelCase and cents-based inputs remain runtime compatibility aliases, but new callers should not use them. Restaurant stores, items, and cart lines retain upstream menu_id values. A cart also exposes top-level menu_id when every line agrees, so a failed menu fetch does not erase a working restaurant-item handoff.

The contract stays compact. Money is a floating-point dollar number rounded to two decimals. An ETA is one delivery_time string, including a range such as "25-35 min". Distance is one display string, preferring miles when DoorDash provides them. Unavailable optional fields are omitted. Checkout, tracking, and group-cart links appear only when DoorDash supplied them.

Typed tools discard unknown CLI response fields instead of leaking the upstream payload. See MCP response examples for complete wire examples.

Authentication

  • doordash_auth

Call without arguments for login status and renewal instructions. Provide access_token to validate a replacement through a read-only CLI command, save it, and use it immediately. An invalid replacement leaves the stored credential intact. Any active MCP bearer can renew the shared account credential; checkout permission remains separately gated.

The exported token expires and contains no refresh credential. Missing or expired login produces an MCP error directing the assistant to request a new exported token and call doordash_auth. Recovery does not need a Nomad Variable change, an admin UI visit, or a restart. Follow the returned inspection action for any earlier cart/order mutation; renewing login does not authorize a retry.

Addresses

  • list_addresses

  • set_default_address

Address changes are account-wide. DoorDash CLI has no per-cart address override, and checkout URLs do not pin an address. Delivery carts, previews, and submissions use the account-wide default address.

Discovery and catalogs

  • search_restaurants

  • find_nearby_stores

  • get_store_details

  • get_menu

  • find_items

  • get_item_details

  • build_grocery_list

find_items searches grocery and retail catalogs only. A known restaurant store_id is rejected before any retail catalog call and directs the caller to get_menu; never retry find_items for that store.

search_restaurants and find_nearby_stores always call list_addresses and use the coordinates of the account-wide default address. They do not accept a location override. The wrapper returns an error instead of guessing when DoorDash has no marked default or the default has no coordinates.

get_menu requires store_id. It returns the complete restaurant menu and its authoritative menu_id for later item-detail and cart calls. The wrapper makes exactly one read-only dd-cli menu --store-id call and never reads or changes cart state. If DoorDash cannot return that menu, the operation fails instead of fabricating a partial menu from order history. get_item_details routes i_-prefixed IDs, plus bare historical item IDs paired with a restaurant menu_id, through the restaurant modifier endpoint. For an i_ ID without menu_id, the wrapper follows the CLI's documented chain: call menu --store-id, copy its authoritative menu_id, then call restaurant-item-details. It never substitutes store_id for menu_id. Other item IDs use grocery or retail details. On a large modifier tree, pass option_queries such as ["Ranch"] to receive root choices plus compact matching paths instead of dumping the entire tree. When the full-menu endpoint succeeds, get_menu returns every valid item and category supplied by DoorDash, without modifier trees.

Carts

  • list_carts

  • show_cart

  • add_cart_items

  • remove_cart_item

  • delete_cart

The add operation is additive and non-idempotent. Send the complete requested batch once. Every line requires the exact menu item_id and exact menu name; names are labels, not customization. Before one DoorDash cart write, the wrapper fetches current details for every item and validates its ID, name, availability, and required choices. Bare historical restaurant IDs are routed with the supplied restaurant menu_id. A required group with one possible choice is selected automatically. A preference with multiple choices is never guessed.

Put requested choices per line in structured requested_options entries: {"name":"Ranch","quantity":2,"option_id":"o_ranch_sauce"}. name is required; omit quantity for one (maximum 100), and include option_id when the same name appears in more than one branch. If DoorDash reuses one option_id across groups, use the qualified name returned by the error, such as Sauce Ranch. A group name can select its unambiguous Yes option. Omit an optional add-on to decline it; never invent a "No ..." option. An unmatched or unreachable choice blocks the full batch. An ambiguous choice such as Ranch sauce versus Ranch dressing returns both candidates; ask the user, then resend the chosen option_id. Resolution stays within the selected parent branch.

Use get_item_details when the choices are not already known. Menu results omit modifier trees. Item details keeps large responses bounded with option_queries, while cart preflight resolves the full tree internally. Malformed trees or selectable options without IDs still fail closed before any cart write.

Exact choices may instead be copied into nested_options as {"option_id":"o_...","name":"Chosen option"}. Do not pass modifier group IDs such as e_.... Ordinary selections stay flat. If a selected option exposes another modifier group, put the child selections in that option's options array. The wrapper uses default_handling: "exact" so CLI defaults do not add modifiers the caller did not select.

If preflight returns items: [] plus item_errors, no cart change occurred. Resolve every reported line from the user's stated choices, or ask the user. Then retry the complete batch once. Never repeat unchanged input. An unavailable item explicitly says not to retry; choose another item or use DoorDash checkout. Cart errors return only relevant choices or ambiguity candidates. Call get_item_details with that item_id, menu_id, and focused option_queries when more context is needed.

If DoorDash instead returns successful items plus item_errors, the update was partial. Call show_cart, then add only the failed lines using the returned cart_uuid. Resending an added line or the full original batch duplicates it. When DoorDash reports an error for repeated copies of the same item but omits the modifier variant, the error has ambiguous: true and lists every candidate request line with its selected options. Inspect the cart and add only a variant confirmed missing; never guess which candidate failed.

After a successful add, add_cart_items automatically creates and returns a browser checkout_url. If DoorDash adds the items but link creation fails, the tool preserves the cart result and tells the caller to use create_checkout_link.

One add_cart_items call accepts at most 20 complete request lines and checks at most four distinct item-detail records concurrently.

When cart_uuid is omitted, the wrapper checks for an active cart at that store before adding. An empty cart left by an earlier failed add is reused automatically. A nonempty cart returns ACTIVE_CART_EXISTS with recovery_tool: show_cart and its exact cart_uuid instead of silently duplicating items. Inspect it, then return its checkout link when it already matches, explicitly extend it using its cart_uuid, or ask whether to replace it. delete_cart requires the exact confirmation "DELETE CART" after the user chooses replacement.

remove_cart_item also fails closed. For a replacement, first add and verify the new line, then pass its cart_item_id as replacement_cart_item_id; the tool verifies that both the old and replacement lines exist before removing anything. For a true deletion with no replacement, pass confirm_delete_without_replacement: true instead. Provide exactly one of those proofs. On success, the tool returns a freshly hydrated cart confirming that the old line is gone and, for a replacement, the new line remains. If the mutation or hydration outcome is unknown, follow the returned one-time show_cart action and do not retry the removal.

When extending a cart_uuid, omit fulfillment to preserve its current mode, or copy show_cart.fulfillment when the user explicitly chose delivery or pickup.

list_carts returns at most 25 carts and 10 lines per cart; call show_cart for one cart's detail. Cart detail and add results return at most 100 lines. items_truncation states exactly how many lines were omitted.

Group-cart spend_limit is a dollar amount with at most two decimal places. It requires group_cart: true and cannot be used while extending cart_uuid.

Orders

  • list_orders

  • reorder

  • preview_order

  • create_checkout_link

  • get_receipt

  • order_status

  • order_submit — permission-gated

list_orders returns at most 25 orders with 10 item lines each and marks omitted lines with items_truncation; call get_receipt for one order's itemized detail. reorder first inspects the source order and active same-store carts. It refuses to merge into a nonempty cart, performs the upstream reorder exactly once, then hydrates the result with show_cart and reports item, quantity, and modifier differences from the source. Success always contains the verified hydrated cart. If hydration or the mutation outcome is unknown, follow the one returned inspection action; never issue a second reorder blindly.

Preview supports scheduled orders, delivery/pickup, Priority delivery, credit opt-out, and work-benefit budgets. It returns an exact submit_context:

Omit fulfillment to preserve the cart's current delivery/pickup mode. Passing delivery or pickup explicitly changes that mode.

{
  "cart_uuid": "cart-123",
  "preview_token": "bHy-Nx9_OVxS4mVXGF8TF08f-HmEiEvPl-9mM9pHUaQ.sxoJtkJslOz0zqrsIs4uuss1vX8cPgTWuvHCAScEilk",
  "expected_total_before_tip": 25.01,
  "expected_delivery_address": "123 Main St, Oakland, CA 94611",
  "fulfillment": "delivery",
  "priority": false,
  "apply_credits": true,
  "pin_handoff_required": false
}

preview_token binds the confirmed cart contents, every setting returned in submit_context, and the selected work budget's identity, rules, and remaining balance. Changing any of that requires a new preview; tip, payment confirmation, and expense details are added afterward.

Copy cart_uuid, preview_token, expected_total_before_tip, expected_delivery_address, fulfillment, priority, apply_credits, and pin_handoff_required from submit_context; also copy scheduled_time when present. Add the user's confirmed tip in dollars, tip_confirmed: true, payment confirmation, and confirmation: "PLACE ORDER". For pickup, copy expected_delivery_address: null when the preview returns no delivery address.

When pin_handoff_required is true, ask the user to accept handing the delivery PIN to the Dasher, then add pin_handoff_acknowledged: true. For a work budget, call preview_order again with the selected budget_id, then copy work_benefits.team_id and that budget's budget_id, name, and optional team_account_id; use payment_confirmation: {"type":"work_budget","name":"..."} and include any required expense code or notes. For a personal card, call list_payment_methods and copy brand and last4 from the is_default card after the user confirms it. Call list_payment_methods before asking for final approval so the user can confirm the order, tip, and default card together. payment_confirmation is an object identifying that payment; "PLACE ORDER" belongs in confirmation. Input validation errors submit nothing and leave the cart's submission record untouched. Fix every reported field together before another call; do not repeat unchanged arguments. Use account_default only when that call cannot identify the default, browser checkout was offered, and the user explicitly accepts the unseen account default.

Payments

  • list_payment_methods — permission-gated

Every CLI invocation automatically uses root --json-output and appends:

--intent cli-usage

CLI limitations

The current MCP tool set does not support:

  • Adding or changing saved payment methods

  • Per-cart delivery addresses

  • Creating or deleting saved addresses

  • Setting an existing cart line to an absolute quantity

  • Merchant tips for pickup

  • Agent checkout for restricted or age-gated items

Browser checkout is the fallback for those cases.

Storage and activity

PostgreSQL stores bearer-token hashes, per-token purchase permissions, the duplicate-submission ledger, and the shared DoorDash access token. The environment bootstrap credential never overwrites a credential already stored there. DoorDash owns carts and orders. See the one-time SQLite import to preserve existing bearer tokens and submission history.

The dashboard keeps the last 100 MCP-routed CLI calls in memory. Commands and CLI results include coordinates, addresses, URLs, payment metadata, and error details; login credentials are excluded. Anyone with admin dashboard or raw activity-log access can read them. The log resets on restart. Direct terminal calls do not appear.

Authenticated raw activity JSON:

http://127.0.0.1:8787/activity

Configuration

Variable

Default

Purpose

HOST

127.0.0.1

Listen address

PORT

8787

Listen port

ADMIN_ACCESS_TOKEN

required

Admin UI and API secret; minimum 16 characters

DD_CLI_PATH

./dd-cli

DoorDash executable

DD_CLI_TIMEOUT_MS

120000

Per-command timeout

DATABASE_URL

required

PostgreSQL connection URL

DATABASE_SSL

false

Enable TLS for PostgreSQL

DATABASE_SSL_REJECT_UNAUTHORIZED

true

Validate PostgreSQL certificates; false allows a private self-signed certificate

DD_CLI_ACCESS_TOKEN

unset

Bootstrap an empty credential store; existing stored credentials take precedence

/health/live reports process liveness. /health/ready verifies PostgreSQL and returns 503 during database outages. DoorDash login expiry does not change health checks, leaving MCP authentication recovery reachable.

Container and verification

npm run check
npm test
# Alternatively, include integration tests against a dedicated test database:
TEST_DATABASE_URL=postgres://doordash:TEST_PASSWORD@localhost:5432/doordash_test npm test
# Optional local container smoke check:
docker build --platform linux/amd64 --tag doordash-cli-mcp:test .
npm run test:container

The optional container smoke test creates its own temporary PostgreSQL container and network, verifies the bundled CLI and HTTP/MCP server, and removes those test resources afterward. It does not connect to DoorDash or a live database. GitHub uses one workflow and one syntax/test job, including PostgreSQL integration, for pull requests and pushes to main. Docs-only changes skip CI. After checks pass on main, it builds one cached Linux amd64 image and publishes latest and sha-FULL_COMMIT_SHA tags. Container smoke checks are manual. Deployment commands remain manual in the Nomad guide.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides programmatic access to Grubhub's food delivery platform, enabling restaurant search, menu browsing, cart management, order placement, and delivery tracking through MCP tools.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Order food on DoorDash — search stores, compare real fee-included totals from live quotes, and build carts; placing an order always requires a human approval dialog (fail closed without elicitation). Local macOS server driving DoorDash's official dd-cli.
    29
    93 npm
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables macOS users to securely connect AI assistants such as Notion AI, Claude, and Cursor to their local files and terminal through an MCP server protected by a Bearer token and Cloudflare Tunnel.
    -