← 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.
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