Skip to main content
Glama
himanshuaggarwal04380

Order_Food_MCP

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


Related MCP server: Burger King MCP Server

Quick Start

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.


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

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
);

ordersorder_lines is one-to-many, linked by order_id, mirroring the in-memory Invoice / list[InvoiceLine] relationship. ordersusers 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

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

Sanity-check discovery/execution with MCP Inspector:

npx @modelcontextprotocol/inspector uv run server.py

Running with Docker

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):

{
  "mcpServers": {
    "food-order": {
      "command": "C:\\path\\to\\food-order-mcp\\.venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\food-order-mcp\\server.py"]
    }
  }
}

Docker:

{
  "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.

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.

Available Tools

1 tool
get_menu_toolA

Return the full food menu.

Use this tool whenever the user asks what's available to order, what food items exist, what the prices are, or expresses general intent to order food without specifying particular items yet (e.g. "I want to order something", "I'm hungry", "what can I get"). Showing the menu is the natural first step before an order can be placed. Takes no arguments. Takes no arguments.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. It correctly identifies the tool as a read-only menu retrieval with no side effects, but does not mention any potential latency or data freshness. Still, it's clear and non-misleading.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose and usage guidelines. Slightly repetitive (states 'Takes no arguments' twice), but otherwise concise.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no parameters, an output schema exists, and no sibling tools, the description is complete. It covers purpose, usage context, and even hints at the order flow.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so the description adds no param info beyond the schema. Baseline is 4 for zero parameters, and the description doesn't need to add more.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Return the full food menu' and provides specific user query examples, leaving no ambiguity about the tool's purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly describes when to use the tool (e.g., user asks about available items, prices, general order intent) and states it's the natural first step before placing an order.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedget_menu_tool

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

Related MCP Connectors

Related MCP Servers