MCP Server
The MCP server exposes our food search functionality as tools that AI assistants like Claude can use.
Why Build an MCP Server?
With MCP, you can:
- Use your food search in Claude Desktop
- Integrate with VS Code AI extensions
- Build multi-agent workflows
- Create standardized tool interfaces
Step 1: Create the Server
Create app/mcp/server.py:
app/mcp/server.py
import json
from mcp.server import Server
from mcp.types import Tool, TextContent
from app.rag import retriever
# Create the MCP server
server = Server("food-companion")
@server.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [
Tool(
name="search_food",
description="Search for Indian vegetarian dishes by query. Supports filtering by cuisine, spice level, and allergens.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query (e.g., 'spicy breakfast', 'high protein dinner')"
},
"cuisine": {
"type": "string",
"description": "Filter by cuisine type",
"enum": ["south_indian", "north_indian", "gujarati", "bengali", "maharashtrian", "rajasthani"]
},
"spice_level": {
"type": "string",
"description": "Filter by spice level",
"enum": ["mild", "medium", "hot"]
},
"exclude_allergens": {
"type": "array",
"items": {"type": "string"},
"description": "Allergens to exclude (e.g., ['dairy', 'nuts', 'gluten'])"
}
},
"required": ["query"]
}
),
Tool(
name="get_food_details",
description="Get detailed information about a specific dish by name.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Exact name of the dish (e.g., 'Masala Dosa', 'Idli')"
}
},
"required": ["name"]
}
),
Tool(
name="list_cuisines",
description="List all available cuisine types.",
inputSchema={
"type": "object",
"properties": {}
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""
if name == "search_food":
results = retriever.search(
query=arguments["query"],
cuisine=arguments.get("cuisine"),
spice_level=arguments.get("spice_level"),
exclude_allergens=arguments.get("exclude_allergens"),
top_k=5
)
if not results:
return [TextContent(
type="text",
text="No dishes found matching your criteria."
)]
formatted = []
for dish in results:
formatted.append(f"**{dish['name']}** ({dish['cuisine']})")
formatted.append(f"Spice: {dish['spice_level']}")
formatted.append(f"{dish['description']}")
formatted.append("")
return [TextContent(type="text", text="\n".join(formatted))]
elif name == "get_food_details":
dish = retriever.get_by_name(arguments["name"])
if not dish:
return [TextContent(
type="text",
text=f"Dish '{arguments['name']}' not found."
)]
return [TextContent(
type="text",
text=json.dumps(dish, indent=2)
)]
elif name == "list_cuisines":
cuisines = [
"south_indian - Dishes from Tamil Nadu, Karnataka, Kerala, Andhra Pradesh",
"north_indian - Dishes from Punjab, Uttar Pradesh, Delhi",
"gujarati - Dishes from Gujarat, known for sweet-savory balance",
"bengali - Dishes from West Bengal, known for fish and sweets",
"maharashtrian - Dishes from Maharashtra, varied from coastal to inland",
"rajasthani - Dishes from Rajasthan, known for dry preparations"
]
return [TextContent(type="text", text="\n".join(cuisines))]
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
"""Run the MCP server."""
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Understanding the Code
Tool Definition
Each tool has:
- name - Identifier for the tool
- description - What the tool does (AI reads this!)
- inputSchema - JSON Schema for parameters
Tool(
name="search_food",
description="Search for Indian dishes...", # Be descriptive!
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language query"
}
},
"required": ["query"]
}
)
Tool Handler
The call_tool function routes requests to the right logic:
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "search_food":
# Handle search
elif name == "get_food_details":
# Handle details
...
Response Format
Tools return TextContent objects:
return [TextContent(type="text", text="Results here...")]
Step 2: Export the Server
Create app/mcp/__init__.py:
app/mcp/__init__.py
from .server import server
__all__ = ["server"]
Step 3: Configure Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
claude_desktop_config.json
{
"mcpServers": {
"food-companion": {
"command": "uv",
"args": ["run", "python", "-m", "app.mcp.server"],
"cwd": "/absolute/path/to/food-companion/backend",
"env": {
"CHROMA_HOST": "localhost",
"CHROMA_PORT": "8000"
}
}
}
}
Path Must Be Absolute
Replace /absolute/path/to/food-companion/backend with the actual full path to your backend directory.
Step 4: Test the Server
Restart Claude Desktop, then try:
"What South Indian dishes are good for breakfast?"
Claude will:
- See the
search_foodtool is available - Call it with appropriate parameters
- Use the results to answer your question
MCP Server Architecture
┌────────────────────┐ stdio ┌────────────────────┐
│ Claude Desktop │◄──────────────►│ MCP Server │
│ (or other AI) │ │ (food-companion) │
└────────────────────┘ └─────────┬──────────┘
│
┌─────────▼──────────┐
│ ChromaDB │
│ (Food Search) │
└────────────────────┘
Tool Best Practices
Good Descriptions
# Good - tells AI when and how to use it
description="Search for dishes by natural language query. Use when user asks for food recommendations. Supports filtering by cuisine, spice level, and dietary restrictions."
# Bad - too vague
description="Search foods"
Clear Parameter Descriptions
"exclude_allergens": {
"type": "array",
"items": {"type": "string"},
"description": "List of allergens to exclude. Common values: 'dairy', 'nuts', 'gluten', 'soy'"
# Include examples!
}
Helpful Error Messages
if not dish:
return [TextContent(
type="text",
text=f"Dish '{name}' not found. Try searching first with search_food."
)]
Testing Without Claude
You can test the MCP server directly:
cd backend
echo '{"method": "tools/list"}' | uv run python -m app.mcp.server
That's the complete backend! Next, let's build the frontend.