Skip to main content
Glama
himanshuaggarwal04380

Order_Food_MCP

README.md
# Bean & Leaf Café — Food Order MCP Server

A local MCP server for a mock cafe ordering system, built as an internship assignment. Exposes menu browsing, ordering, and order-history tools — backed by a real SQLite database — to any MCP-compatible chat client with a local LLM (developed and demoed against Claude Desktop / Ollama).

Also available as a Docker image: `aghimanshu/foodordermcp`.

---

## Table of Contents

- [Quick Start](#quick-start)
- [Tech Stack & Reasoning](#tech-stack--reasoning)
- [Project Structure](#project-structure)
- [Architecture](#architecture)
- [The Menu](#the-menu)
- [Database Design](#database-design)
- [User Identity Model](#user-identity-model)
- [MCP Tools](#mcp-tools)
- [Running Locally](#running-locally)
- [Running with Docker](#running-with-docker)
- [Connecting to Claude Desktop](#connecting-to-claude-desktop)
- [Testing & Coverage](#testing--coverage)
- [Reference Test Case](#reference-test-case)
- [Known Limitations & Next Steps](#known-limitations--next-steps)

---

## Quick Start

```powershell
uv sync
uv run pytest -v          # confirm everything works
uv run server.py          # run the server (stdio transport)
```

Then point any MCP client at `server.py`, or see [Connecting to Claude Desktop](#connecting-to-claude-desktop).

---

## Tech Stack & Reasoning

| Choice | Why |
|---|---|
| **Python** | Fast iteration; consistent with the local FastMCP/Ollama pipeline built in Week 1. |
| **FastMCP** | Minimal boilerplate; auto-generates strict JSON Schema from Python type hints. |
| **SQLite** | A full SQL database in a single file — no server process, fits a local, no-infrastructure project. Accessed via Python's built-in `sqlite3`, no extra dependency. |
| **pytest + pytest-cov** | Standard, simple test runner; coverage reporting to verify tests actually exercise the code, not just exist. |
| **Docker** | Packages the exact runtime environment so the server runs identically on any machine, not just this one. |
| **Currency: INR (₹)** | Deliberate deviation from the assignment brief's USD example — documented explicitly throughout. |

---

## Project Structure

```
food-order-mcp/
├── menu.csv          # Source-of-truth menu data (edit this, not the DB, to change the menu)
├── db.py               # SQLite connection, schema, seeding, all persistence functions
├── menu.py              # Business logic: menu queries (zero MCP dependency)
├── order.py               # Business logic: order calculation, validation, pricing
├── user.py                 # Local device identity (user_id) + display name
├── server.py                 # MCP wiring: the 4 tools exposed to the client
├── conftest.py                 # pytest fixture: isolates all tests from the real database
├── test_menu.py                  # Tests for menu.py
├── test_order.py                   # Tests for order.py
├── test_server.py                    # Tests for the MCP tool layer
├── inspect_db.py                       # Manual utility: dump full DB contents
├── reset_orders.py                       # Manual utility: clear orders/order_lines
├── Dockerfile
└── README.md
```

**Layering principle, held throughout:** each layer only calls the layer directly below it.

```
server.py  →  order.py / menu.py / user.py  →  db.py  →  cafe.db
```

MCP wiring never touches SQL directly, and business logic never imports FastMCP. This is why migrating from a hardcoded list to SQLite, and later adding order persistence and user identity, only ever required editing one or two files — never `server.py`'s tool definitions themselves (beyond wiring in the new functions).

---

## Architecture

```
 Chat Client (Claude Desktop)
        │  stdio
 MCP Server (server.py, in a container or run directly)
        │
 Business logic (order.py, menu.py, user.py)
        │
 db.py (SQLite access layer)
        │
 cafe.db  →  menu / users / orders / order_lines
```

---

## The Menu

**101 items across 11 categories**: Hot Beverages (14), Cold Beverages (14), Bakery & Pastries (12), Breakfast (10), Sandwiches & Wraps (10), Desserts (8), Burgers (8), Pizza (8), Snacks & Sides (6), Pasta (6), Salads (5).

Source of truth is `menu.csv` — columns: `item_id, name, category, description, price, is_veg`. This is the file to hand-edit if the menu changes; the database re-seeds from it automatically (see below).

---

## Database Design

### Why SQLite over a hardcoded Python list
The original menu was a 7-item in-memory list. Per mentor direction, this was migrated to a real database so the menu can be edited without touching code, and so orders persist across server restarts.

### Why money is stored as integer paise
SQLite has no native `Decimal` type — only `INTEGER`, `REAL`, `TEXT`, `BLOB`. `REAL` reintroduces float rounding errors; `TEXT` prevents numeric querying. The fix used by real payment systems: store the smallest currency unit as an integer. ₹299.00 is stored as `29900`. Conversion to/from `Decimal` happens only at the boundary, in `menu.py` and `order.py`.

### Schema

```sql
CREATE TABLE menu (
    item_id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    description TEXT,
    price_paise INTEGER NOT NULL,
    is_veg TEXT NOT NULL
);

CREATE TABLE users (
    user_id TEXT PRIMARY KEY,
    name TEXT
);

CREATE TABLE orders (
    order_id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL REFERENCES users(user_id),
    order_date TEXT NOT NULL,
    subtotal_paise INTEGER NOT NULL,
    tax_amount_paise INTEGER NOT NULL,
    delivery_fee_paise INTEGER NOT NULL,
    total_paise INTEGER NOT NULL
);

CREATE TABLE order_lines (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id TEXT NOT NULL REFERENCES orders(order_id),
    item_id TEXT NOT NULL,
    item_name TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    unit_price_paise INTEGER NOT NULL,
    line_total_paise INTEGER NOT NULL
);
```

`orders` → `order_lines` is one-to-many, linked by `order_id`, mirroring the in-memory `Invoice` / `list[InvoiceLine]` relationship. `orders` → `users` links each order to whoever placed it.

**Snapshotting, not referencing:** `order_lines` stores its own copy of `item_name` and `unit_price_paise` at the moment of purchase, rather than only storing `item_id` and looking the price up later. If a menu price changes after an order was placed, that order's historical invoice must still reflect what was actually charged — not the current price.

### Seeding strategy: seed-once
On startup, `init_db()` creates the schema if missing, then seeds `menu` from `menu.csv` **only if that table is currently empty**. Every subsequent startup leaves existing data untouched — the database, not the CSV, is the live source of truth after first run. (Rebuilding from CSV on every restart would also risk wiping order history stored in the same database.)

### Order ID generation
Format: `YYYYMMDD-NNN` (e.g. `20260722-001`), sequential per day — generated by counting how many orders already exist for today's date in `orders` and incrementing. This requires real persistence to work correctly; a per-day sequence has no meaning without a durable count of what's already happened.

### Order persistence
`place_order()` calculates the invoice, generates the order ID, and calls `save_order()`, which inserts one row into `orders` and one row per line into `order_lines` — wrapped in a single transaction, so an order is either fully saved or not saved at all, never half-written.

---

## User Identity Model

MCP over stdio has **no login or account concept** — the server never receives any information about who is connecting. To support order history, this project uses a **local device identity** instead:

1. On first run, a random UUID is generated and saved to `user_id.txt` next to the server.
2. Every subsequent run reads the same ID back — same "user" for every order from this installation.
3. A `users` table stores an optional, self-reported display name against that ID (`set_name_tool`).

**This identifies an installation, not an authenticated person.** It is not verified, not secure, and not meant to represent real multi-user identity — a genuine login system would be required for that, which is explicitly out of scope for this project.

---

## MCP Tools

### `get_menu_tool()`
No arguments. Returns all 101 menu items (`id`, `name`, `category`, `description`, `price`, `is_veg`). Triggers on direct menu questions *and* vague ordering intent ("I want to order something") — the tool description explicitly covers both.

### `place_order_tool(items: list[dict])`
`items`: list of `{"item_id": str, "quantity": int}`. Validates, prices, and persists the order under the current local user. Returns a structured invoice (order ID, date, per-line breakdown, subtotal, tax, delivery fee, total) or `{"error": "..."}` for invalid input — errors are returned as normal tool output, not raised exceptions, so the LLM can read and relay them clearly.

### `set_name_tool(name: str)`
Saves a self-reported display name against the current local user ID.

### `get_order_history_tool()`
No arguments. Returns this installation's past orders (most recent first) and saved display name, if any.

---

## Running Locally

```powershell
uv sync
uv run pytest -v
uv run server.py
```

Sanity-check discovery/execution with MCP Inspector:
```powershell
npx @modelcontextprotocol/inspector uv run server.py
```

---

## Running with Docker

```powershell
docker pull aghimanshu/foodordermcp
docker run -i --rm -e DATA_DIR=/app/data -v foodorder_data:/app/data aghimanshu/foodordermcp
```

- **`-i`** — keeps stdin open; required for MCP's stdio transport (no network port is used, so `-p` is never needed).
- **`-v foodorder_data:/app/data`** — a *named* Docker volume, not a local file path, so `cafe.db` and `user_id.txt` persist across container restarts and this command works identically on any machine.

---

## Connecting to Claude Desktop

Add to `claude_desktop_config.json`:

**Local (Python):**
```json
{
  "mcpServers": {
    "food-order": {
      "command": "C:\\path\\to\\food-order-mcp\\.venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\food-order-mcp\\server.py"]
    }
  }
}
```

**Docker:**
```json
{
  "mcpServers": {
    "food-order": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "-e", "DATA_DIR=/app/data", "-v", "foodorder_data:/app/data", "aghimanshu/foodordermcp"]
    }
  }
}
```

Fully quit and relaunch Claude Desktop after editing, then check **"+" → Connectors** for `food-order` and its 4 tools.

---

## Testing & Coverage

15 automated tests across `test_menu.py`, `test_order.py`, `test_server.py` — covering happy paths, edge cases (unknown item, zero/negative quantity, empty order, malformed input), and a hand-verified reference case.

```powershell
uv run pytest --cov=. --cov-report=term-missing
```

| File | Coverage |
|---|---|
| `menu.py` | 100% |
| `order.py` | 100% |
| `user.py` | 100% |
| `db.py` | 95% |
| `server.py` | 91%+ |

**Test isolation (`conftest.py`):** an autouse pytest fixture redirects `db.DB_PATH` and `user.USER_ID_PATH` to a fresh, disposable location (`tmp_path`) for every single test. This was added after discovering that running the test suite was silently writing real rows into the production `cafe.db` and overwriting the real `user_id.txt` — every test now runs against throwaway data instead.

---

## Reference Test Case

Hand-verified order: 2× Margherita Pizza (`PZ01`, ₹249) + 1× Masala Chai (`HB10`, ₹89)

| | |
|---|---|
| Subtotal | ₹587.00 |
| Tax (8%) | ₹46.96 |
| Delivery Fee | ₹100.00 |
| **Total** | **₹733.96** |

Automated in `test_order.py::test_reference_order_case`.

---

## Known Limitations & Next Steps

- **`user_id` identifies a device, not an authenticated person** — no login system exists; a real one would be required for genuine multi-user identity.
- **No fuzzy/typo-tolerant search** — item lookup relies on exact IDs and the LLM's own reasoning over the full menu, not a dedicated search index (Typesense was explored as a possible future addition).
- **No public/remote hosting** — the server runs locally or in a locally-run container; a publicly reachable URL would require switching transport to HTTP and deploying to a real host, both explicitly out of scope for this project without further mentor approval, given the security implications of an unauthenticated public ordering endpoint.
- **Docker image not published to the official MCP Catalog** — only distributed directly via Docker Hub; formal catalog submission involves a review process not pursued here.

TDQS

A4.3/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusion between tools. The tool's purpose is clearly defined.

Naming Consistency5/5

The single tool name 'get_menu_tool' follows a consistent verb_noun pattern with snake_case, which is a reasonable convention.

Tool Count3/5

A single tool for a food ordering service is borderline thin. While the menu retrieval is essential, the absence of any ordering tool makes the count feel insufficient.

Completeness1/5

The tool surface is severely incomplete for a food ordering server. It only provides the menu and lacks critical operations like placing, modifying, or canceling orders.

Maintenance

ActivityMaintained
ResponsivenessNo issues