← Selected work
MCP + Production · Project 4

Restaurant Agent + MCP

Food recommendation agent with all production patterns. First project using MCP — tools exposed as a standalone server any AI can connect to. Streaming SSE, Redis cache, Bottleneck rate limiting.

MCP · Streaming SSE · Upstash Redis · Bottleneck · Zod · Anthropic GitHub ↗
New Concepts This Project
MCP Server — tools in a separate server, any AI connects via SSE URL. Streaming SSE — tokens appear live as Claude generates. Redis cache — repeated searches served instantly, no LLM call. Bottleneck — rate limits Anthropic API under high load. Zod validation — all tool inputs validated before execution.

Architecture — Two Repos

React UI (port 5176)
        │ SSE stream / HTTP
        ▼
Agent Server (port 3004)
        ├── Bottleneck      rate limits Anthropic API calls
        ├── Upstash Redis   caches search results (5 min TTL)
        ├── In-memory queue handles concurrent users
        └── Anthropic Claude reasoning + decisions
        │
        │ HTTP POST /tool/:name
        ▼
MCP Server (port 3010)         ← standalone repo
        ├── search_restaurants  (Zod validated)
        ├── get_restaurant_detail
        ├── get_user_preferences  (Neon DB)
        ├── save_user_preferences (Neon DB)
        └── search_knowledge      (JSON RAG)

System Prompt

You are a friendly restaurant recommendation assistant
for Scottsdale and Phoenix, Arizona.

When a user asks for recommendations:
1. Call get_user_preferences first (check saved prefs)
2. Call search_knowledge if they ask about cuisine types
3. ALWAYS call search_restaurants with their query
4. If dietary needs mentioned → add to filters
5. If price range mentioned → use max_price filter
6. If family/kids → use family_friendly: true
7. Save new preferences with save_user_preferences

Tool Definitions (via MCP)

[
  {
    "name": "search_restaurants",
    "description": "Search restaurants. ALWAYS call this first.",
    "input_schema": {
      "properties": {
        "query":           { "type": "string"  },
        "cuisine":         { "type": "string"  },
        "max_price":       { "type": "number"  },
        "min_rating":      { "type": "number"  },
        "family_friendly": { "type": "boolean" },
        "open_now":        { "type": "boolean" }
      },
      "required": ["query"]
    }
  },
  {
    "name": "get_user_preferences",
    "description": "Load saved user preferences from Neon DB",
    "input_schema": {
      "properties": { "user_id": { "type": "string" } }
    }
  },
  {
    "name": "save_user_preferences",
    "description": "Save user food preferences to Neon DB",
    "input_schema": {
      "properties": {
        "user_id":    { "type": "string" },
        "cuisines":   { "type": "array"  },
        "dietary":    { "type": "array"  },
        "max_budget": { "type": "number" }
      }
    }
  },
  {
    "name": "search_knowledge",
    "description": "RAG search for cuisine info and dietary guidance"
  },
  {
    "name": "get_restaurant_detail",
    "description": "Full details of a specific restaurant by ID"
  }
]

End-to-End Flow

👤 User Input
"Find Tamil food near Scottsdale under $20"
⚡ Cache Check (before LLM)
key = "user_001:find_tamil_food_near_scottsdale_under_$20"
Redis.get(key) → MISS → proceed to LLM

// If HIT → return cached result instantly, no LLM call ✅
🧠 LLM Response — Turn 1
{
  "stop_reason": "tool_use",
  "content": [
    { "type": "text", "text": "Let me check your preferences first." },
    {
      "type": "tool_use", "id": "tool_001",
      "name": "get_user_preferences",
      "input": { "user_id": "user_001" }
    }
  ],
  "usage": { "input_tokens": 380, "output_tokens": 65 }
}
🔌 MCP Tool Call — get_user_preferences
Agent → POST http://localhost:3010/tool/get_user_preferences
// MCP Server validates input with Zod
// Queries Neon DB for saved preferences

Response: { new_user: true, liked: [], cuisines: [], dietary: [], max_budget: 50 }
MCP Server has zero LLM involvement — pure execution. Zod validates before DB query.
🧠 LLM Response — Turn 2
{
  "stop_reason": "tool_use",
  "content": [{
    "type": "tool_use", "id": "tool_002",
    "name": "search_restaurants",
    "input": {
      "query":     "Tamil South Indian food",
      "cuisine":   "South Indian",
      "max_price": 20,
      "open_now":  true
    }
  }],
  "usage": { "input_tokens": 520, "output_tokens": 90 }
}
🔌 MCP Tool Result — search_restaurants
{
  "found": 2,
  "restaurants": [
    { "id": "rest_001", "name": "Pongal South Indian Kitchen",
      "avg_price": 15, "rating": 4.7, "open_now": true },
    { "id": "rest_004", "name": "Dosa Hut",
      "avg_price": 12, "rating": 4.6, "open_now": true }
  ]
}
🧠 LLM Response — Turn 3 (Streaming Final)
{
  "stop_reason": "end_turn",
  "content": [{
    "type": "text",
    "text": "Found 2 Tamil restaurants under $20:\n\n1. Pongal South Indian Kitchen ⭐ 4.7 — ~$15/pp\n   Fresh dosas, idlis, filter coffee. Open now!\n\n2. Dosa Hut ⭐ 4.6 — ~$12/pp\n   Best crispy dosas in Scottsdale. Open now!"
  }],
  "usage": { "input_tokens": 890, "output_tokens": 185 }
}
Tokens streamed to React UI one by one via SSE — text appears live like ChatGPT.
⚡ Cache Save (after response)
Redis.setex(key, 300, result)  // 5 min TTL
// Next same query → instant response, no LLM call ✅
Why MCP Changes Everything

Without MCP (Projects 1-3): executeTool() written in every agent file. Only your agent uses the tools. Business logic mixed with agent code.

With MCP (Project 4): restaurant-mcp-server is standalone. Connect via URL: any agent, Claude Desktop, Cursor all use the same tools. Write once, use everywhere.

Production Patterns Added

Pattern
Tool
Purpose
Where
Streaming
SSE
Tokens appear live
Agent → React
Caching
Upstash Redis
Skip repeat LLM calls
Agent layer
Rate limiting
Bottleneck
Protect Anthropic API
Agent layer
Validation
Zod
Safe tool inputs
MCP server
Memory
Neon DB
User preferences persist
MCP server