Skip to main content
Glama
daveed716

Toast MCP Server

by daveed716
README.md
# Toast MCP Server

A read-only [Model Context Protocol](https://modelcontextprotocol.io) server for the
[Toast POS](https://pos.toasttab.com) API. It lets an AI assistant answer questions about your
restaurant and generate sales, labor, and cash reports directly from live Toast data.

It **never writes to Toast**. The HTTP client issues `GET` requests only; the single `POST` in the
codebase is the authentication call Toast requires to mint a token, and it is isolated in
`src/auth.ts`. The smoke test asserts this.

---

## What you can ask

Once connected, questions like these work:

- "How did we do last week compared to the week before?"
- "What were our top 20 items by net sales in July, and what's the average price of each?"
- "Break down sales by hour for last Saturday — when is our real dinner rush?"
- "What's our cash vs card mix this month, and how much did we pay in card processing fees?"
- "Which discounts are getting used most, and by how much?"
- "Show me every void over the last two weeks with the reason and who was working."
- "What was labor as a percentage of net sales last month, by employee?"
- "What are we 86'd on right now?"
- "Find the $340 order from Friday night and show me what was on it."
- "What are our hours on Sundays, and what dining options do we have configured?"

---

## Requirements

- **Node.js 20 or newer** (built and tested on Node 22).
- **Toast API credentials.** For a restaurant reporting on its own data, the right product is
  **Standard API Access**, which is read-only by design and self-serve:

  1. In **Toast Web**, go to **Integrations → Toast API access → Manage credentials**.
  2. Create a credential set, name it (e.g. `mcp-reporting`), and select the read scopes below.
  3. Copy the **client ID** and **client secret** — the secret is shown only once.

  If your account doesn't have that option, it's part of Restaurant Management Essentials; your
  Toast rep can enable it. Partner integrations get credentials from the Toast integrations team
  instead.

### Scopes to enable

| Scope | Needed for |
|---|---|
| `orders:read` | Every sales report — this is the core one |
| `config:read` | Dining options, revenue centers, sales categories, discounts, void reasons, tables |
| `restaurants:read` | Location profile, time zone, closeout hour, hours of service |
| `labor:read` | Time entries, shifts, jobs |
| `labor.employees:read` | Employee names (without it, servers show as short GUIDs) |
| `menus:read` | Published menu, prices, modifiers |
| `cashmgmt:read` | Drawer entries and deposits |
| `stock:read` | Out-of-stock / 86'd items |

Only `orders:read`, `config:read` and `restaurants:read` are needed for core sales reporting. The
server degrades gracefully if a scope is missing — the affected tool reports the denial and the
others keep working. Run `toast_check_connection` to see exactly what's granted.

You also need your **restaurant GUID**. `toast_check_connection` reports it, or find it in the Toast
Web URL when the location is selected, or use `toast_list_restaurants` with a management group GUID.

---

## Install

```bash
npm install && npm run build
```

Then copy the environment template and fill it in:

```bash
cp .env.example .env
```

At minimum set `TOAST_CLIENT_ID`, `TOAST_CLIENT_SECRET`, and `TOAST_RESTAURANT_GUID`. The server
reads this file automatically (via Node's native env-file support), and `.env` is gitignored.

Verify the credentials before wiring anything up:

```bash
npm run check-connection
```

That prints the environment, the granted scopes, the restaurant name, its time zone and closeout
hour, and the current business date.

---

## Connect it to Claude

The server speaks MCP over stdio. You have two options for credentials, and you only need one:

- **Leave them in `.env`.** The server loads `.env` from its own package directory regardless of
  what working directory the client launches it from, so the config below works with no `env` block
  at all — and your secrets stay out of the client's config file.
- **Put them in the client's `env` block**, as shown below. Real environment variables always take
  precedence over `.env`, so this wins if both are present.

### Claude Code

If you filled in `.env`, this is all you need — no credentials in the command:

```bash
claude mcp add toast -- node /absolute/path/to/toast_mcp/dist/index.js
```

To pass credentials explicitly instead:

```bash
claude mcp add toast --env TOAST_CLIENT_ID=your-id --env TOAST_CLIENT_SECRET=your-secret --env TOAST_RESTAURANT_GUID=your-restaurant-guid -- node /absolute/path/to/toast_mcp/dist/index.js
```

### Claude Desktop

Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "toast": {
      "command": "node",
      "args": ["/absolute/path/to/toast_mcp/dist/index.js"],
      "env": {
        "TOAST_CLIENT_ID": "your-client-id",
        "TOAST_CLIENT_SECRET": "your-client-secret",
        "TOAST_RESTAURANT_GUID": "your-restaurant-guid"
      }
    }
  }
}
```

Drop the `env` block entirely if you're using `.env`. On Windows use forward slashes or escaped
backslashes in the path.

---

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `TOAST_CLIENT_ID` | *(required)* | API client ID |
| `TOAST_CLIENT_SECRET` | *(required)* | API client secret |
| `TOAST_ENV_FILE` | — | Load this file instead of searching for `.env`; handy for one credential file per location |
| `TOAST_RESTAURANT_GUID` | — | Default restaurant; every tool can override it per call |
| `TOAST_MANAGEMENT_GROUP_GUID` | — | Enables `toast_list_restaurants` for multi-location groups |
| `TOAST_ENV` | `production` | `production` or `sandbox` |
| `TOAST_HOSTNAME` | — | Full base URL; overrides `TOAST_ENV` |
| `TOAST_CACHE_ENABLED` | `true` | Disk cache for settled business dates |
| `TOAST_CACHE_DIR` | `~/.toast-mcp/cache` | Where cached orders live |
| `TOAST_CACHE_SETTLE_DAYS` | `1` | Days that are always re-fetched live |
| `TOAST_MAX_DAYS` | `92` | Ceiling on business dates per report |
| `TOAST_LOG_LEVEL` | `info` | `debug` logs every request to stderr |

---

## Tools

**Connection and setup**

| Tool | What it does |
|---|---|
| `toast_check_connection` | Verifies credentials, probes each API, shows scopes, time zone, closeout hour, cache status |
| `toast_get_restaurant` | Location profile: address, phone, hours, currency, online ordering and delivery settings |
| `toast_list_restaurants` | Every location in a management group, with GUIDs |
| `toast_clear_cache` | Drops the local cache (touches nothing in Toast) |

**Reporting**

| Tool | What it does |
|---|---|
| `toast_sales_summary` | Headline revenue and volume, optionally vs. the prior period or last year |
| `toast_sales_breakdown` | Net sales grouped by item, sales category, menu group, hour, day of week, date, server, dining option, source, revenue center, service area, or table |
| `toast_payment_summary` | Tender mix, card brands, tips, refunds, processing fees |
| `toast_discount_summary` | Discounts and comps by name, with usage counts |
| `toast_void_report` | Voided orders, checks and items by reason |
| `toast_labor_summary` | Hours, estimated cost, and labor as a percent of net sales |
| `toast_cash_report` | Drawer entries and deposits, reconciled against cash payments |

**Lookup**

| Tool | What it does |
|---|---|
| `toast_search_orders` | Find individual orders by amount, channel, server, or customer/tab text |
| `toast_get_order` | One order in full: line items, modifiers, discounts, payments |
| `toast_list_config` | Any of 24 configuration collections — the way to discover GUIDs for filters |
| `toast_get_menu` | Published menu structure, price list, or one item's modifier detail |
| `toast_get_stock` | Current inventory / 86'd items |
| `toast_list_employees` | Roster and job list with wages |
| `toast_time_entries` | Individual clock-in/out records |
| `toast_list_shifts` | Scheduled shifts |

### Dates

Every report works in **business dates** in the restaurant's own time zone, honouring its configured
**closeout hour** — so a 2am Saturday sale lands on Friday's business date, exactly as it does in
Toast's own reports.

Use `date_range` for a preset (`today`, `yesterday`, `this_week`, `last_week`, `last_7_days`,
`last_14_days`, `last_30_days`, `last_90_days`, `this_month`, `last_month`, `month_to_date`,
`year_to_date`) or `start_date` / `end_date` for anything else. Those accept `2026-08-01`,
`20260801`, `today`, `yesterday`, or relative offsets like `-7d`, `-2w`, `-3m`. The default when
nothing is specified is **yesterday**.

---

## How the numbers are defined

These come from raw order data, so they can differ by small amounts from Toast Web's own reports,
which layer on additional accounting rules. Every report restates its definitions in its output.

| Measure | Definition |
|---|---|
| **Gross sales** | Sum of `preDiscountPrice` on non-voided, non-deferred line items. Excludes tax. |
| **Discounts** | All applied discounts, at both item and check level. |
| **Net sales** | Sum of line-item `price`, which is already net of item *and* check-level discounts. Equals gross minus discounts. Excludes tax, tips, auto-gratuity and service charges. |
| **Service charges** | Applied service charges not flagged as gratuity. Reported separately from net sales. |
| **Auto-gratuity** | Service charges flagged `gratuity`. |
| **Tips** | `tipAmount` on payments that actually collected (voided and denied payments are excluded). |
| **Deferred** | Gift card sales. Money collected, but not revenue — held out of net sales and shown on its own line. |
| **Voids** | Voided and deleted orders, checks and items are excluded from sales entirely and reported in `toast_void_report`. |

**One subtlety worth knowing.** In Toast's data model, a line item's `price` and `preDiscountPrice`
*already include the prices of its nested modifiers*. Summing modifiers on top of their parent
double-counts every upcharge. This server only ever sums top-level selections, and the test suite
asserts the modifier is not counted twice.

Two assumptions are stated wherever they apply: labor cost estimates overtime at **1.5×** the hourly
wage on record (Toast does not report the actual overtime rate; the multiplier is a tool argument),
and time entries with no wage on record contribute hours but no cost.

---

## Rate limits and caching

Toast allows 20 requests/second overall, 5/second for `ordersBulk`, and 1/second for `menus`. The
server runs a token-bucket limiter below each of those ceilings, and retries `429` and `5xx`
responses with exponential backoff, honouring `Retry-After`.

Because a month-long report means pulling every order for 30 business dates, completed dates are
cached to disk as JSON. Today and the previous `TOAST_CACHE_SETTLE_DAYS` days (1 by default) are
always re-fetched, since tips, refunds and closeouts keep changing. Pass `refresh: true` to any
report to bypass the cache, or run `toast_clear_cache` after a correction is made in Toast for an
older date. Every report footer states how many dates came from cache versus live.

---

## Development

```bash
npm run typecheck    # type-check without emitting
npm run build        # compile to dist/
npm test             # build, then run the end-to-end smoke test
```

`npm test` starts a mock Toast API with hand-computed fixture data, launches the compiled server as
a real child process, and drives all 19 tools over stdio as an MCP client would. It asserts the
actual arithmetic (net sales, tax, tips, deferred revenue, labor cost, void totals), that GUIDs
resolve to names, that pagination doesn't truncate, that the cache is used and bypassed correctly,
that errors surface readably — and that nothing but `GET` requests plus the auth `POST` ever reach
the API.

### Layout

```
src/
  index.ts        MCP server entry, tool registration, --check-connection
  env.ts          .env discovery and loading, with environment taking precedence
  config.ts       Environment loading and validation
  auth.ts         Token acquisition, caching, refresh (the only POST)
  client.ts       Read-only HTTP client: retries, rate limiting, pagination
  rateLimiter.ts  Token-bucket limiters matched to Toast's documented limits
  cache.ts        On-disk cache for settled business dates
  service.ts      Data access across Orders, Config, Menus, Labor, Cash, Stock
  dates.ts        Business-date arithmetic in the restaurant's time zone
  aggregate.ts    Revenue definitions and the single-pass fact builder
  grouping.ts     Group-by dimensions
  names.ts        GUID to human name resolution
  money.ts        Integer-cent arithmetic and currency formatting
  format.ts       Text table rendering
  tools/          One module per tool group
test/
  mock-toast.mjs  Fixture Toast API
  config.mjs      Credential loading, .env precedence, error messages
  smoke.mjs       End-to-end assertions
```

---

## Troubleshooting

**"Missing required environment variable(s)"** — the server found no credentials. The message names
the exact `.env` path to create. If it says a `.env` *was* read but didn't define the variable,
check for a typo or a value left blank — a blank value counts as unset.

**A `.env` value seems to be ignored** — something in the real environment is overriding it, since
environment variables take precedence. `toast_check_connection` reports which source the credentials
came from. (A variable exported as empty, e.g. `TOAST_CLIENT_ID=`, is treated as unset and will not
block the `.env` value.)

**`403` on some tools but not others** — a missing scope. Run `toast_check_connection`; the API
access table shows which are denied. Add the scope to your credential set in Toast Web.

**Servers or categories show as `#a1b2c3d4`** — the Configuration or Labor scope isn't granted, so
GUIDs can't be resolved to names. Sales figures are still correct.

**Numbers differ slightly from Toast Web** — expected; see the definitions table above. The most
common causes are Toast's dashboard treating service charges or deferred revenue differently.

**A past date looks stale** — a correction was made in Toast after the date was cached. Pass
`refresh: true`, or run `toast_clear_cache`.

**Reports are slow the first time** — a 90-day report pulls every order for 90 business dates. The
second run is served from cache.