DataLatte☕
Build an MCP Server for a Local Business in Python
AI & Automation

Build an MCP Server for a Local Business in Python

Part of the guide: Local Marketing Strategy

June 13, 2026·Nataliia· 13 min read All posts
To build an MCP server for a local business, you write a small Python program using the official mcp SDK that exposes your own data (menu, hours, policies, loyalty points) as tools an AI agent can call, then register it with an MCP-compatible client such as Claude Desktop.
It takes a few hours for someone comfortable with basic Python, and the example below builds a complete coffee shop server you can adapt.
Off-the-shelf MCP servers can connect your AI to Google Calendar, Notion, Supabase, and a dozen other tools. They're useful — but they don't know anything about your business.
They don't know that your oat milk latte is $6.50, that your Saturday appointments fill up by Tuesday, that the blue rinse treatment requires a patch test 48 hours in advance, or that your 10% senior discount applies only to services (not retail products).
That's the gap a custom MCP server fills. You build a small server that exposes your unique business data — your menu, your pricing, your policies, your loyalty program — and your AI agent queries it the same way it queries any other tool. Suddenly the AI doesn't just know general things about coffee shops: it knows your coffee shop.
This guide walks through building a complete MCP server for a coffee shop from scratch. The same pattern applies to salons, pet groomers, gyms, or any local business with unique data that an AI needs to access accurately.

Why Build a Custom MCP Server?

Before getting into code, let's be clear about when this is worth doing.
You should build a custom MCP server when:
  • Your AI agent needs to answer questions about your specific products, prices, or policies
  • You have data that changes regularly (menu, staff schedules, promotions) and the AI needs the current version
  • You want the AI to look up or update records in a custom data store (loyalty points, inventory)
  • You need logic specific to your business (discount rules, booking constraints)
You probably don't need a custom server when:
  • A general-purpose MCP server already handles your use case (Supabase, Google Sheets, Notion)
  • You only need one-way information flow from a static document
The investment is a few hours of setup. The payoff is an AI that answers from your real, current data instead of guessing. For a wider view of what agents can do, read our overview of AI agents for local business.

MCP Server Architecture: Three Core Concepts

Before writing code, understand the three primitives the MCP protocol gives you:
Tools — Actions the AI can take. Tools accept parameters and return results. Examples: get_menu(), check_loyalty_points(phone), add_appointment(date, time, client). Tools can read data, write data, call APIs, or run any logic you write.
Resources — Data the AI can read, like documents or structured records. Resources are identified by URIs (e.g., business://hours, business://faq). Unlike tools, resources don't accept parameters — they just expose a snapshot of data.
Prompts — Reusable prompt templates that users or AI clients can invoke. For example, a customer_greeting prompt that always starts with your business name and tone.
For a local business AI agent, you'll primarily use tools (they're the most powerful and flexible).

Prerequisites

  • Python 3.11 or higher
  • pip install mcp (the official MCP Python SDK)
  • Basic Python comfort (you don't need to be an expert)
Check your Python version:
python3 --version  # should show 3.11 or higher
pip install mcp
The mcp library provides the server framework, decorators for registering tools, and handles all the protocol communication with Claude Desktop.

Step-by-Step: Building a Coffee Shop MCP Server

We'll build a server with 4 tools:
  1. get_menu() — current menu with prices from a JSON file
  2. check_loyalty_points(customer_phone) — loyalty balance from a CSV
  3. get_business_hours() — hours including holiday schedule
  4. add_loyalty_points(customer_phone, points) — adds points after purchase
First, create your project structure:
mkdir coffeeshop-mcp
cd coffeeshop-mcp
touch server.py menu.json customers.csv hours.json

Data Files

menu.json — your current menu:
{
  "hot_drinks": [
    { "name": "Espresso", "price": 3.50, "sizes": ["single", "double"] },
    { "name": "Oat Milk Latte", "price": 6.50, "sizes": ["small", "medium", "large"] },
    { "name": "Cappuccino", "price": 5.00, "sizes": ["regular", "large"] },
    { "name": "Pour Over", "price": 5.50, "origin": "Ethiopia Yirgacheffe" }
  ],
  "cold_drinks": [
    { "name": "Cold Brew", "price": 5.00, "sizes": ["12oz", "16oz"] },
    { "name": "Iced Latte", "price": 6.00, "milk_options": ["whole", "oat", "almond", "soy"] }
  ],
  "food": [
    { "name": "Avocado Toast", "price": 9.00, "dietary": ["vegan"] },
    { "name": "Blueberry Muffin", "price": 4.00, "dietary": ["vegetarian"] },
    { "name": "Croissant", "price": 3.50 }
  ],
  "last_updated": "2026-06-13"
}
hours.json — your business hours:
{
  "regular": {
    "monday": "7:00 AM - 6:00 PM",
    "tuesday": "7:00 AM - 6:00 PM",
    "wednesday": "7:00 AM - 6:00 PM",
    "thursday": "7:00 AM - 7:00 PM",
    "friday": "7:00 AM - 7:00 PM",
    "saturday": "8:00 AM - 5:00 PM",
    "sunday": "9:00 AM - 4:00 PM"
  },
  "holidays": {
    "2026-07-04": "Closed",
    "2026-11-26": "10:00 AM - 2:00 PM",
    "2026-12-25": "Closed",
    "2026-01-01": "Closed"
  },
  "timezone": "America/New_York"
}
customers.csv — your loyalty program (one row per customer):
phone,name,points,tier,joined
+15550123,Sample Customer A,340,Gold,2024-03-15
+15550456,Sample Customer B,85,Bronze,2025-11-02
+15550789,Sample Customer C,1240,Platinum,2023-07-20

The MCP Server Code

Now the main file — server.py. Read through the comments; they explain every decision:
import json
import csv
import asyncio
from datetime import date
from pathlib import Path
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.types import Tool, TextContent
import mcp.server.stdio

# Initialize the MCP server with a name your AI client will display
server = Server("coffeeshop-local")

# File paths — adjust to match your project structure
BASE_DIR = Path(__file__).parent
MENU_FILE = BASE_DIR / "menu.json"
HOURS_FILE = BASE_DIR / "hours.json"
CUSTOMERS_FILE = BASE_DIR / "customers.csv"


# ─── TOOL 1: Get Menu ──────────────────────────────────────────────────────────

@server.list_tools()
async def list_tools() -> list[Tool]:
    """Register all tools with the MCP protocol."""
    return [
        Tool(
            name="get_menu",
            description="Get the current coffee shop menu with all items and prices. Use this when a customer asks about menu items, prices, ingredients, or dietary options.",
            inputSchema={
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "enum": ["hot_drinks", "cold_drinks", "food", "all"],
                        "description": "Which menu section to return. Use 'all' for the complete menu."
                    }
                },
                "required": []
            }
        ),
        Tool(
            name="check_loyalty_points",
            description="Look up a customer's loyalty points balance by their phone number.",
            inputSchema={
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string",
                        "description": "Customer phone number in format +15551234567"
                    }
                },
                "required": ["phone"]
            }
        ),
        Tool(
            name="get_business_hours",
            description="Get business hours for a specific day or the full week schedule, including holiday closures.",
            inputSchema={
                "type": "object",
                "properties": {
                    "day": {
                        "type": "string",
                        "description": "Day name (e.g., 'monday') or specific date (e.g., '2026-07-04'). Leave empty for full week."
                    }
                },
                "required": []
            }
        ),
        Tool(
            name="add_loyalty_points",
            description="Add loyalty points to a customer's account after a purchase. Always confirm the transaction amount with the customer first.",
            inputSchema={
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string",
                        "description": "Customer phone number"
                    },
                    "points": {
                        "type": "integer",
                        "description": "Number of points to add (1 point per $1 spent, rounded down)"
                    }
                },
                "required": ["phone", "points"]
            }
        )
    ]


@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    """Route tool calls to the correct handler."""

    if name == "get_menu":
        return await handle_get_menu(arguments)
    elif name == "check_loyalty_points":
        return await handle_check_loyalty(arguments)
    elif name == "get_business_hours":
        return await handle_get_hours(arguments)
    elif name == "add_loyalty_points":
        return await handle_add_points(arguments)
    else:
        return [TextContent(type="text", text=f"Unknown tool: {name}")]


# ─── TOOL HANDLERS ─────────────────────────────────────────────────────────────

async def handle_get_menu(args: dict) -> list[TextContent]:
    with open(MENU_FILE) as f:
        menu = json.load(f)

    category = args.get("category", "all")

    if category == "all":
        result = menu
    elif category in menu:
        result = {category: menu[category]}
    else:
        return [TextContent(type="text", text=f"Category '{category}' not found.")]

    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_check_loyalty(args: dict) -> list[TextContent]:
    phone = args.get("phone", "").strip()

    # Input validation — never trust user-provided data
    if not phone.startswith("+") or not phone[1:].isdigit():
        return [TextContent(type="text", text="Invalid phone format. Use +15551234567")]

    with open(CUSTOMERS_FILE, newline="") as f:
        reader = csv.DictReader(f)
        for row in reader:
            if row["phone"] == phone:
                return [TextContent(type="text", text=json.dumps({
                    "found": True,
                    "name": row["name"],
                    "points": int(row["points"]),
                    "tier": row["tier"],
                    "member_since": row["joined"]
                }, indent=2))]

    return [TextContent(type="text", text=json.dumps({
        "found": False,
        "message": "No loyalty account found for this phone number."
    }))]


async def handle_get_hours(args: dict) -> list[TextContent]:
    with open(HOURS_FILE) as f:
        hours = json.load(f)

    day = args.get("day", "").lower().strip()

    if not day:
        # Return full week
        return [TextContent(type="text", text=json.dumps(hours["regular"], indent=2))]

    # Check if it's a specific date (holiday lookup)
    if len(day) == 10 and day[4] == "-":  # format: YYYY-MM-DD
        if day in hours["holidays"]:
            return [TextContent(type="text", text=f"Holiday hours for {day}: {hours['holidays'][day]}")]
        # Check if it's a known day of week for that date
        try:
            d = date.fromisoformat(day)
            day_name = d.strftime("%A").lower()
            regular = hours["regular"].get(day_name, "Closed")
            return [TextContent(type="text", text=f"{day} ({d.strftime('%A')}): {regular}")]
        except ValueError:
            return [TextContent(type="text", text="Invalid date format")]

    # Regular day name lookup
    if day in hours["regular"]:
        return [TextContent(type="text", text=f"{day.capitalize()}: {hours['regular'][day]}")]

    return [TextContent(type="text", text=f"Day '{day}' not recognized.")]


async def handle_add_points(args: dict) -> list[TextContent]:
    phone = args.get("phone", "").strip()
    points_to_add = int(args.get("points", 0))

    # Input validation
    if points_to_add <= 0 or points_to_add > 500:
        return [TextContent(type="text", text="Points must be between 1 and 500 per transaction.")]

    if not phone.startswith("+") or not phone[1:].isdigit():
        return [TextContent(type="text", text="Invalid phone format.")]

    # Read all customers, update the matching one
    rows = []
    found = False

    with open(CUSTOMERS_FILE, newline="") as f:
        reader = csv.DictReader(f)
        fieldnames = reader.fieldnames
        for row in reader:
            if row["phone"] == phone:
                row["points"] = str(int(row["points"]) + points_to_add)
                found = True
            rows.append(row)

    if not found:
        return [TextContent(type="text", text="Customer not found. They may need to sign up for the loyalty program first.")]

    # Write back to CSV
    with open(CUSTOMERS_FILE, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)

    updated_customer = next(r for r in rows if r["phone"] == phone)
    return [TextContent(type="text", text=json.dumps({
        "success": True,
        "name": updated_customer["name"],
        "points_added": points_to_add,
        "new_balance": int(updated_customer["points"]),
        "tier": updated_customer["tier"]
    }, indent=2))]


# ─── SERVER ENTRY POINT ────────────────────────────────────────────────────────

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="coffeeshop-local",
                server_version="1.0.0",
                capabilities=server.get_capabilities(
                    notification_options=None,
                    experimental_capabilities={}
                )
            )
        )

if __name__ == "__main__":
    asyncio.run(main())

Register Your Server in Claude Desktop

Add the following to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
  "mcpServers": {
    "coffeeshop": {
      "command": "python3",
      "args": ["/absolute/path/to/coffeeshop-mcp/server.py"]
    }
  }
}
Use the absolute path to server.py. Restart Claude Desktop. In a new conversation, you'll see the coffeeshop tools listed. Ask Claude: "What's the price of an oat milk latte?" — it will call get_menu, read the JSON, and answer from your actual data.

Testing Your Server Locally

Before connecting to Claude Desktop, test your server directly using the MCP inspector:
pip install mcp[cli]
mcp dev server.py
This opens an interactive terminal where you can call each tool manually and see the raw JSON response. Test every tool with valid inputs, invalid inputs, and edge cases (phone not found, requesting a holiday date, adding 0 points).
Sample test session:
# In the mcp dev interface:
> call get_menu {"category": "hot_drinks"}
# Should return hot drinks list from menu.json

> call check_loyalty_points {"phone": "+15550123"}
# Should return Sample Customer A's 340 points

> call check_loyalty_points {"phone": "invalid"}
# Should return validation error, not a crash

> call add_loyalty_points {"phone": "+15550456", "points": 15}
# Should add 15 points to Sample Customer B's account
Fix any errors before connecting to Claude Desktop. A crashing MCP server will silently fail from Claude's perspective.

Deploying Your MCP Server

Local setup works for Claude Desktop on your personal computer. But if you want the server available 24/7 (for a customer-facing AI on your website), you need to deploy it.
Option 1: Always-on process on a VPS
Get a small VPS (entry-level plans from providers such as DigitalOcean or Linode are typically a few dollars a month; check current pricing). SSH in and run:
# Install dependencies
pip install mcp

# Run as a background service using systemd
sudo nano /etc/systemd/system/coffeeshop-mcp.service
[Unit]
Description=CoffeeShop MCP Server
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/coffeeshop-mcp
ExecStart=/usr/bin/python3 /home/ubuntu/coffeeshop-mcp/server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable coffeeshop-mcp
sudo systemctl start coffeeshop-mcp
Option 2: Serverless via Cloudflare Workers
For serverless deployment, you'd port the server to use the MCP HTTP transport instead of stdio. This is more advanced — see the MCP documentation on HTTP transport for details. Serverless platforms often have free or low-cost tiers that can suit small customer-facing deployments; check current limits and pricing.

Security: What Not to Expose

Custom MCP servers are powerful, which means you need to think carefully about what you expose.
Rules to follow:
  1. Never expose credentials through MCP tools. Don't create a tool that returns your Stripe secret key, WiFi password, or staff passwords — even if only you use the MCP server. AI context windows can be logged.
  2. Validate all inputs. See how the loyalty point handler checks points_to_add > 0 and points_to_add <= 500? That prevents someone from passing -1000 to drain points or 999999 to overflow the field. Never trust inputs.
  3. Limit write operations. If the AI only needs to read your menu, don't build a update_menu_price() tool unless you specifically need it. Fewer write tools = smaller attack surface.
  4. Use rate limiting for write tools. For any tool that modifies data, add a simple in-memory rate limiter. For example, limit add_loyalty_points to 10 calls per minute — a bot can't silently inflate every customer's balance.
  5. Log every write operation. Append to a simple log file every time add_loyalty_points or any write tool is called: timestamp, inputs, result. This gives you an audit trail.

Comparison: Custom MCP Server vs Alternatives

ApproachSetup TimeFlexibilityCostMaintenanceBest For
Custom MCP serverA few hoursMaximum — your logicFree locally; small hosting fee if deployedLow (update JSON/CSV files)Unique business data
n8n workflowHoursHigh — visual builderPaid cloud plans or self-hostLowComplex multi-step flows
ZapierUnder an hourModerate — preset connectorsFree tier, then paid plansVery lowSimple trigger-action automations
Direct API integrationHoursHigh — any APIVariesMedium (API changes)Well-documented external services
General MCP server (Supabase, Sheets)HoursModerate — generic DB opsFree tiers, then paidVery lowStandard data storage
The custom MCP server wins when your data or logic is unique — which it always is for a local business. n8n is a good middle ground if you want more flexibility than Zapier but don't want to write Python.

FAQ

Q: Do I need to be a developer to build an MCP server? A: You need basic Python skills: reading and modifying code, running commands in a terminal and understanding what a function does. If you have never written Python, plan extra time to learn the basics first. The example here is deliberately straightforward.
Q: Can my MCP server connect to the internet? A: Yes. Tool handlers can call external APIs, for example to fetch today's specials from your website or check the weather. Keep calls fast, because tool calls can time out.
Q: How do I update the data my MCP server exposes? A: Edit the data files. Because the example reads menu.json, hours.json and customers.csv on every tool call, changes show up without code changes. For frequently changing data like inventory, replace the files with a database query.
Q: Can multiple AI clients use my MCP server? A: Yes, if you run it with an HTTP-based transport rather than stdio. In stdio mode one client launches the server directly. Check the current MCP documentation for transport options, since the SDK evolves.
Q: Is my MCP server secure? A: In stdio mode the server is not network accessible. If you deploy it over HTTP you must add authentication, for example a long random bearer token, and never expose credentials or unrestricted write tools. Review the Security section above.

Next Step

If you would rather have this built and maintained for you, see our AI agents and automation service or contact us to talk through your use case.

Free for local businesses

Want this applied to your business?

I'll review your Google presence, local SEO, and ad accounts — and send you a specific action plan within 48 hours. No pitch, no pressure.

Want hands-on help?

See how DataLatte handles AI Agents & Automation for local businesses.

Learn more
Nataliia — local marketing expert
Nataliia

Local marketing strategist with 10+ years at global agencies — OMD, Dentsu, GroupM, and BBDO. Now helping small businesses get the same data-driven edge. Based in Europe, working with clients in the US, UK, Australia, and beyond.

About Nataliia

Want this applied to your business?

Let's review your current marketing setup together — free, no obligations.

Get Your Free Marketing Audit